# easy-invoice/2.3.7/includes/Models/Quote.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.3.7. 903 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.3.7/code/includes/Models/Quote.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.3.7/raw/includes/Models/Quote.php
- Modified: 2026-05-21T08:29:24+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.3.7/code/includes/Models/Quote.php#L10-L20`.

```php
<?php
/**
 * Quote Model
 *
 * @package     EasyInvoice
 * @author      Your Name
 * @copyright   Copyright (c) 2023, Your Company
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       1.0.0
 */

namespace EasyInvoice\Models;

use EasyInvoice\Constants\PostTypes;
use EasyInvoice\Models\QuoteItem;

/**
 * Quote Model
 *
 * Represents a quote in the system with extensible architecture for pro features.
 *
 * @since 1.0.0
 */
class Quote {
    /**
     * Quote ID
     *
     * @var int
     */
    private $id;

    /**
     * Dynamic data storage for all fields
     *
     * @var array
     */
    private $data = [];

    /**
     * Items
     *
     * @var array
     */
    private $items = [];

    /**
     * Modified flag
     *
     * @var bool
     */
    private $is_modified = false;

    /**
     * Magic method to get dynamic properties
     *
     * @since 1.0.0
     * @param string $name Property name
     * @return mixed Property value
     */
    public function __get($name) {
        // Handle special properties
        if ($name === 'id') {
            return $this->id;
        }
        
        // Return from dynamic data array
        return $this->data[$name] ?? null;
    }

    /**
     * Magic method to set dynamic properties
     *
     * @since 1.0.0
     * @param string $name Property name
     * @param mixed $value Property value
     */
    public function __set($name, $value) {
        // Handle special properties
        if ($name === 'id') {
            $this->id = $value;
            return;
        }
        
        // Store in dynamic data array
        $this->data[$name] = $value;
        $this->is_modified = true;
    }

    /**
     * Magic method to check if property exists
     *
     * @since 1.0.0
     * @param string $name Property name
     * @return bool
     */
    public function __isset($name) {
        if ($name === 'id') {
            return isset($this->id);
        }
        
        return isset($this->data[$name]);
    }

    /**
     * Dynamic getter method
     *
     * @since 1.0.0
     * @param string $name Method name
     * @param array $arguments Method arguments
     * @return mixed
     */
    public function __call($name, $arguments) {
        // Handle getter methods (getFieldName)
        if (strpos($name, 'get') === 0) {
            $field_name = $this->camelCaseToSnakeCase(substr($name, 3)); // Remove 'get' prefix
            return $this->__get($field_name);
        }
        
        // Handle setter methods (setFieldName)
        if (strpos($name, 'set') === 0) {
            $field_name = $this->camelCaseToSnakeCase(substr($name, 3)); // Remove 'set' prefix
            $value = $arguments[0] ?? null;
            $this->__set($field_name, $value);
            return null;
        }
        
        // Handle isset methods (isFieldName)
        if (strpos($name, 'is') === 0) {
            $field_name = $this->camelCaseToSnakeCase(substr($name, 2)); // Remove 'is' prefix
            return (bool) $this->__get($field_name);
        }
        
        // Handle has methods (hasFieldName)
        if (strpos($name, 'has') === 0) {
            $field_name = $this->camelCaseToSnakeCase(substr($name, 3)); // Remove 'has' prefix
            return !empty($this->__get($field_name));
        }
        
        throw new \BadMethodCallException("Method $name does not exist");
    }

    /**
     * Convert camelCase to snake_case
     *
     * @since 1.0.0
     * @param string $camelCase
     * @return string
     */
    private function camelCaseToSnakeCase($camelCase) {
        return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $camelCase));
    }

    /**
     * Constructor
     *
     * @since 1.0.0
     * @param \WP_Post|int $quote Quote post object or ID
     */
    public function __construct($quote = null) {
        // Initialize data array dynamically from field configuration
        $this->data = $this->getDefaultValuesFromConfiguration();

        if ($quote instanceof \WP_Post) {
            $this->loadFromPost($quote);
        } elseif (is_numeric($quote)) {
            $post = get_post($quote);
            if ($post && $post->post_type === PostTypes::EASY_INVOICE_QUOTE_POST_TYPE) {
                $this->loadFromPost($post);
            }
        }

        do_action('easy_invoice_quote_model_constructed', $this);
    }

    /**
     * Get default values from field configuration
     *
     * @since 1.0.0
     * @return array
     */
    private function getDefaultValuesFromConfiguration(): array {
        $default_values = [];
        
        // Get field definitions to determine default values
        $field_registration = new \EasyInvoice\Forms\Quote\QuoteFieldRegistration();
        
        // Initialize the field registration to ensure fields are registered
        $field_registration->registerDefaultTabs();
        $field_registration->registerDefaultFields();
        
        $field_definitions = [];
        $tabs = $field_registration->getTabs();
        foreach ($tabs as $tab_id => $tab) {
            $tab_fields = $field_registration->getFields($tab_id);
            foreach ($tab_fields as $field) {
                $field_definitions[] = $field;
            }
        }

        // Extract default values from field configuration
        foreach ($field_definitions as $field) {
            $field_name = $field['name'] ?? '';
            if (empty($field_name)) {
                continue;
            }
            
            $default_value = $field['default_value'] ?? null;
            
            // Handle callable default values
            if (is_callable($default_value)) {
                $default_value = $default_value();
            }
            
            // Set appropriate default based on field type
            if ($default_value !== null) {
                $default_values[$field_name] = $default_value;
            } else {
                // Set type-appropriate defaults
                $field_type = $field['type'] ?? 'text';
                switch ($field_type) {
                    case 'number':
                        $default_values[$field_name] = 0.0;
                        break;
                    case 'checkbox':
                        $default_values[$field_name] = false;
                        break;
                    case 'select':
                        $default_values[$field_name] = '';
                        break;
                    case 'array':
                        $default_values[$field_name] = [];
                        break;
                    default:
                        $default_values[$field_name] = '';
                        break;
                }
            }
        }

        return $default_values;
    }

    /**
     * Load quote data from WP_Post object
     *
     * @since 1.0.0
     * @param \WP_Post $post Post object
     */
    private function loadFromPost(\WP_Post $post): void {
        $this->id = $post->ID;
        $this->data['title'] = $post->post_title ?: '';
        $this->data['created_date'] = $post->post_date ?: '';
        $this->data['modified_date'] = $post->post_modified ?: '';

        // Load items first so they're available for total calculations
        $this->loadItems();

        // Load meta data using configuration-driven approach
        $this->loadMetaData();
        
        // Allow plugins to load additional data
        do_action('easy_invoice_quote_loaded_from_post', $this, $post);

        // Ensure totals are calculated
        $this->calculateTotals();
    }

    /**
     * Load quote items
     *
     * @since 1.0.0
     */
    private function loadItems(): void {
        $items_data = get_post_meta($this->id, '_easy_invoice_quote_items', true);
        if (is_array($items_data)) {
            $this->items = []; // Clear existing items
            foreach ($items_data as $item_data) {
                if (is_array($item_data)) {
                    // Ensure required fields have default values
                    // Ensure taxable field is properly set
                    $taxable = isset($item_data['taxable']) ? $item_data['taxable'] : true;
                    if (is_string($taxable)) {
                        $taxable = strtolower($taxable);
                        $taxable = $taxable === '1' || $taxable === 'true' || $taxable === 'yes' || $taxable === 'on';
                    }
                    $taxable = (bool) $taxable;

                    $item_data = array_merge([
                        'quantity' => 0,
                        'price' => 0,
                        'adjust_percentage' => 0,
                        'taxable' => $taxable,
                        'name' => '',
                        'description' => ''
                    ], $item_data);
                    $this->items[] = new QuoteItem($item_data);
                }
            }
        }
    }

    /**
     * Load meta data using configuration-driven approach
     *
     * @since 1.0.0
     */
    private function loadMetaData(): void {
        if (!$this->id) {
            return;
        }

        // Get field definitions to determine which meta keys to load
        $field_registration = new \EasyInvoice\Forms\Quote\QuoteFieldRegistration();
        
        // Initialize the field registration to ensure fields are registered
        $field_registration->registerDefaultTabs();
        $field_registration->registerDefaultFields();
        
        $field_definitions = [];
        $tabs = $field_registration->getTabs();
        foreach ($tabs as $tab_id => $tab) {
            $tab_fields = $field_registration->getFields($tab_id);
            foreach ($tab_fields as $field) {
                $field_definitions[] = $field;
            }
        }

        // Load meta data for each field definition
        foreach ($field_definitions as $field) {
            $field_name = $field['name'] ?? '';
            if (empty($field_name)) {
                continue;
            }
            
            $meta_key = '_easy_invoice_quote_' . $field_name;
            $value = get_post_meta($this->id, $meta_key, true);
            
            // Handle special cases for certain fields
            if ($field_name === 'prices_include_tax') {
                $this->data[$field_name] = $value === '1' || $value === 'yes' ? 'yes' : 'no';
            } else if ($field_name === 'discount_type' && empty($value)) {
                $this->data[$field_name] = 'none';
            } else if ($field_name === 'discount_calculation_method' && empty($value)) {
                $this->data[$field_name] = 'before_tax';
            } else if ($field_name === 'tax_rate' && empty($value)) {
                $this->data[$field_name] = 0;
            } else if ($field_name === 'discount_value' && empty($value)) {
                $this->data[$field_name] = 0;
            } else if ($value !== '') {
                // Store in dynamic data array
                $this->data[$field_name] = $value;
            }
        }
        


        // Auto-calculate totals if they're 0 or if we have items but no totals
        if (((isset($this->data['total']) ? $this->data['total'] : 0) == 0 && !empty($this->items)) || 
            ((isset($this->data['subtotal']) ? $this->data['subtotal'] : 0) == 0 && !empty($this->items))) {
            $this->calculateTotals();
        }

        // Populate client information if we have a client_id but no customer data
        if (($this->data['client_id'] ?? 0) > 0 && (empty($this->data['customer_name']) || empty($this->data['customer_email']))) {
            $this->populateClientInfo();
        }
    }

    /**
     * Ensure quote has proper post_name (slug) for pretty URLs
     * 
     * @since 1.0.0
     * @return bool
     */
    public function ensureProperSlug(): bool {
        if (!$this->id) {
            return false;
        }
        
        $post = get_post($this->id);
        if (!$post || empty($post->post_name)) {
            // Generate a proper slug for this quote
            $post_title = $this->data['title'] ?: $this->data['number'] ?: 'Untitled Quote';
            $post_name = sanitize_title($post_title);
            
            // Ensure uniqueness
            $original_slug = $post_name;
            $counter = 1;
            while (get_page_by_path($post_name, OBJECT, \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE)) {
                $post_name = $original_slug . '-' . $counter;
                $counter++;
            }
            
            // Update the post with the new slug
            $result = wp_update_post([
                'ID' => $this->id,
                'post_name' => $post_name
            ]);
            
            return $result !== 0;
        }
        
        return true;
    }

    /**
     * Save quote to database
     *
     * @since 1.0.0
     * @return bool True if successful, false otherwise
     */
    public function save(): bool {
        // Prepare post data
        $post_data = [
            'post_title' => $this->data['title'] ?? '',
            'post_content' => $this->data['description'] ?? '',
            'post_type' => PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
            'post_status' => 'publish'
        ];
        
        if ($this->id) {
            $post_data['ID'] = $this->id;
            $post_id = wp_update_post($post_data);
        } else {
            $post_id = wp_insert_post($post_data);
        }
        
        if (is_wp_error($post_id)) {
            return false;
        }
        
        // Update the ID if this was a new post
        if (!$this->id) {
            $this->id = $post_id;
        }
        
        // Calculate totals from items before saving
        $this->calculateTotals();
        
        // Save meta data (including quote status)
        $this->saveMetaData();
        
        // Allow plugins to perform actions after saving
        do_action('easy_invoice_quote_after_save', $this);
        
        $this->is_modified = false;
        return true;
    }

    /**
     * Save quote meta data
     *
     * @since 1.0.0
     */
    private function saveMetaData(): void {
        if (!$this->id) {
            return;
        }



        // Get field definitions to determine which meta keys to save
        $field_registration = new \EasyInvoice\Forms\Quote\QuoteFieldRegistration();
        
        // Initialize the field registration to ensure fields are registered
        $field_registration->registerDefaultTabs();
        $field_registration->registerDefaultFields();
        
        $field_definitions = [];
        $tabs = $field_registration->getTabs();
        foreach ($tabs as $tab_id => $tab) {
            $tab_fields = $field_registration->getFields($tab_id);
            foreach ($tab_fields as $field) {
                $field_definitions[] = $field;
            }
        }

        // Save meta data for each field definition
        foreach ($field_definitions as $field) {
            $field_name = $field['name'] ?? '';
            if (empty($field_name)) {
                continue;
            }
            
            $meta_key = '_easy_invoice_quote_' . $field_name;
            
            // Get the value from dynamic data array
            // Save all fields that exist in the data array (including empty strings to allow clearing fields)
            // If a field exists in $this->data, it means it was explicitly set, so we should save it
            if (array_key_exists($field_name, $this->data)) {
                $value = $this->data[$field_name];
                update_post_meta($this->id, $meta_key, $value);
            }
        }
        
        // Save items
        $this->saveItems();
        
        // Allow plugins to save additional meta data
        do_action('easy_invoice_quote_save_meta_data', $this);
    }

    /**
     * Save quote items
     *
     * @since 1.0.0
     */
    private function saveItems(): void {
        $items_data = [];
        
        // Process items for saving
        
        foreach ($this->items as $item) {
            if (is_object($item) && method_exists($item, 'toArray')) {
                $item_data = $item->toArray();
                // Ensure taxable field is properly set as a string '1' or '0'
                $item_data['taxable'] = $item->isTaxable() ? '1' : '0';
                $items_data[] = $item_data;
            } elseif (is_array($item)) {
                // Convert array to QuoteItem object for proper saving
                $quote_item = new QuoteItem($item);
                $item_data = $quote_item->toArray();
                // Ensure taxable field is properly set as a string '1' or '0'
                $item_data['taxable'] = $quote_item->isTaxable() ? '1' : '0';
                $items_data[] = $item_data;
            }
        }
        
        // Save items to meta
        
        update_post_meta($this->id, '_easy_invoice_quote_items', $items_data);
    }

    /**
     * Calculate totals from items
     *
     * @since 1.0.0
     */
    public function calculateTotals(): void {
        // Per-quote tax_enabled override. Same semantics as the
        // Invoice model — see Invoice::calculateTotals() for rationale.
        $tax_enabled_meta = $this->data['tax_enabled'] ?? null;
        if ($tax_enabled_meta === null || $tax_enabled_meta === '') {
            $tax_enabled = get_option('easy_invoice_tax_enabled', 'no') === 'yes';
        } else {
            $tax_enabled = ($tax_enabled_meta === 'yes' || $tax_enabled_meta === '1'
                            || $tax_enabled_meta === 1   || $tax_enabled_meta === true);
        }

        $prices_include_tax = ($this->data['prices_include_tax'] ?? 'no') === 'yes';
        $tax_rate = $tax_enabled ? floatval($this->data['tax_rate'] ?? 0) : 0;
        
        // Initialize totals
        $subtotal = 0;
        $taxable_subtotal = 0;
        $this->data['tax_amount'] = 0;
        $this->data['discount_amount'] = 0;
        
        // First pass: Calculate raw totals
        foreach ($this->items as $item) {
            $quantity = 0;
            $price = 0;
            $adjust_percentage = 0;
            $is_taxable = true;
            
            if (is_object($item) && method_exists($item, 'getAmount')) {
                $quantity = $item->getQuantity();
                $price = $item->getPrice();
                $adjust_percentage = $item->getAdjustPercentage();
                $is_taxable = $item->isTaxable();
            } elseif (is_array($item)) {
                $quantity = isset($item['quantity']) ? (float) $item['quantity'] : 0;
                $price = isset($item['price']) ? (float) $item['price'] : 0;
                $adjust_percentage = isset($item['adjust_percentage']) ? (float) $item['adjust_percentage'] : 0;
                $is_taxable = isset($item['taxable']) ? (bool) $item['taxable'] : true;
            }
            
            // If prices include tax and item is taxable, remove tax from price
            if ($prices_include_tax && $is_taxable && $tax_rate > 0) {
                $price = $price / (1 + ($tax_rate / 100));
            }
            
            // Calculate item total
            $item_total = $quantity * $price;
            // Only apply adjust percentage if the adjust field is enabled
            if ($adjust_percentage != 0 && \EasyInvoice\Controllers\SettingsController::shouldShowQuoteAdjustField()) {
                $item_total = $item_total * (1 + $adjust_percentage / 100);
            }
            
            $subtotal += $item_total;
            if ($is_taxable) {
                $taxable_subtotal += $item_total;
            }
        }
        
        $this->data['subtotal'] = $subtotal;
        
        // Get discount calculation method (default to before_tax)
        $discount_calculation_method = $this->data['discount_calculation_method'] ?? 'before_tax';
        
        // Calculate initial discount amount
        if (($this->data['discount_type'] ?? '') === 'percentage' && ($this->data['discount_value'] ?? 0) > 0) {
            $this->data['discount_amount'] = ($subtotal * $this->data['discount_value']) / 100;
        } elseif (($this->data['discount_type'] ?? '') === 'fixed' && ($this->data['discount_value'] ?? 0) > 0) {
            $this->data['discount_amount'] = $this->data['discount_value'];
        }
        
        // Calculate tax and total based on discount calculation method
        if ($discount_calculation_method === 'before_tax') {
            // For before_tax: Apply discount first, then calculate tax on remaining taxable amount
            if ($subtotal > 0) {
                $discount_ratio = $this->data['discount_amount'] / $subtotal;
                $taxable_amount = $taxable_subtotal * (1 - $discount_ratio);
            } else {
                $taxable_amount = 0;
            }
            
            if ($tax_rate > 0) {
                $this->data['tax_amount'] = ($taxable_amount * $tax_rate) / 100;
            }
            
            $this->data['total'] = $subtotal - $this->data['discount_amount'] + $this->data['tax_amount'];
        } else {
            // For after_tax: Calculate tax first, then apply discount
            if ($tax_rate > 0) {
                $this->data['tax_amount'] = ($taxable_subtotal * $tax_rate) / 100;
            }
            
            $total_before_discount = $subtotal + $this->data['tax_amount'];
            
            // Recalculate percentage discount based on total including tax
            if (($this->data['discount_type'] ?? '') === 'percentage' && ($this->data['discount_value'] ?? 0) > 0 && $total_before_discount > 0) {
                $this->data['discount_amount'] = ($total_before_discount * $this->data['discount_value']) / 100;
            }
            
            $this->data['total'] = $total_before_discount - $this->data['discount_amount'];
        }
    }

    /**
     * Populate client information from client_id
     *
     * @since 1.0.0
     */
    public function populateClientInfo(): void {
        if (($this->data['client_id'] ?? 0) > 0) {
            $client_repository = new \EasyInvoice\Repositories\ClientRepository();
            $client = $client_repository->find($this->data['client_id']);
            
            if ($client) {
                $this->data['customer_name'] = $client->getBusinessClientName() ?: '';
                $this->data['customer_email'] = $client->getEmail() ?: '';
                $this->data['customer_address'] = $client->getAddress() ?: '';
            }
        }
    }

    /**
     * Recalculate totals and save (for existing quotes)
     *
     * @since 1.0.0
     * @return bool
     */
    public function recalculateAndSave(): bool {
        // Populate client information if we have a client_id
        $this->populateClientInfo();
        
        // Calculate totals from items
        $this->calculateTotals();
        
        // Save the updated quote
        return $this->save();
    }

    /**
     * Convert to array
     *
     * @since 1.0.0
     * @return array
     */
    public function toArray(): array {
        $items_array = [];
        foreach ($this->items as $item) {
            if (is_object($item) && method_exists($item, 'toArray')) {
                $items_array[] = $item->toArray();
            } else {
                $items_array[] = $item;
            }
        }

        // Start with dynamic data
        $data = $this->data;
        
        // Add special properties
        $data['id'] = $this->id;
        $data['items'] = $items_array;

        // Allow plugins to modify the array data
        return apply_filters('easy_invoice_quote_model_to_array', $data, $this);
    }

    // Essential methods only
    public function getId(): int { return $this->id ?? 0; }
    public function setId(int $id): void { $this->id = $id; }
    public function getItems(): array { return $this->items; }
    public function setItems(array $items): void { 
        $this->items = [];
        foreach ($items as $item) {
            if (is_array($item)) {
                $this->items[] = new QuoteItem($item);
            } elseif (is_object($item) && $item instanceof QuoteItem) {
                $this->items[] = $item;
            }
        }
        $this->is_modified = true;
    }
    public function isModified(): bool { return $this->is_modified; }
    
    /**
     * Set meta data for the quote
     *
     * @since 1.0.0
     * @param string $key Meta key
     * @param mixed $value Meta value
     */
    public function setMetaData(string $key, $value): void {
        // Store in dynamic data array without the _easy_invoice_ prefix
        $field_name = easy_invoice_str_replace('_easy_invoice_', '', $key);
        $this->data[$field_name] = $value;
        $this->is_modified = true;
        

    }
    
    /**
     * Get currency code
     *
     * @since 1.0.0
     * @return string Currency code
     */
    public function getCurrencyCode(): string {
        $currency_code = $this->data['currency_code'] ?? 'USD';
        
        // If currency is set to 'global', resolve to actual global setting
        if ($currency_code === 'global') {
            $currency_code = get_option('easy_invoice_currency_code', 'USD');
        }
        
        // Ensure currency code is uppercase for consistency
        return strtoupper($currency_code);
    }
    
    /**
     * Set currency code
     *
     * @since 1.0.0
     * @param string $currency_code Currency code
     */
    public function setCurrencyCode(string $currency_code): void {
        $this->data['currency_code'] = $currency_code;
        $this->is_modified = true;
    }
    
    /**
     * Get currency position
     *
     * @since 1.0.0
     * @return string Currency position
     */
    public function getCurrencyPosition(): string {
        $currency_position = $this->data['currency_position'] ?? 'left';
        
        // If currency position is set to 'global', resolve to actual global setting
        if ($currency_position === 'global') {
            $currency_position = get_option('easy_invoice_currency_position', 'left');
        }
        
        return $currency_position;
    }
    
    /**
     * Set currency position
     *
     * @since 1.0.0
     * @param string $currency_position Currency position
     */
    public function setCurrencyPosition(string $currency_position): void {
        $this->data['currency_position'] = $currency_position;
        $this->is_modified = true;
    }

    /**
     * Get raw currency code (without resolving global)
     *
     * @since 1.0.0
     * @return string Raw currency code
     */
    public function getRawCurrencyCode(): string {
        return $this->data['currency_code'] ?? 'global';
    }

    /**
     * Get raw currency position (without resolving global)
     *
     * @since 1.0.0
     * @return string Raw currency position
     */
    public function getRawCurrencyPosition(): string {
        return $this->data['currency_position'] ?? 'global';
    }

    public function getDescription(): string {
        return $this->data['description'] ?? '';
    }

    public function setDescription(string $description): void {
        $this->data['description'] = $description;
        $this->is_modified = true;
    }

    public function getTemplate(): string {
        return $this->data['quote_template'] ?? 'standard';
    }

    public function setTemplate(string $template): void {
        $this->data['quote_template'] = $template;
        $this->is_modified = true;
    }

    /**
     * Get discount type (percentage, fixed, none)
     *
     * @since 1.0.0
     * @return string
     */
    public function getDiscountType(): string {
        return $this->data['discount_type'] ?? 'none';
    }

    /**
     * Get discount value (percentage or fixed amount)
     *
     * @since 1.0.0
     * @return float
     */
    public function getDiscountValue(): float {
        return floatval($this->data['discount_value'] ?? 0);
    }

    /**
     * Get discount amount (calculated value)
     *
     * @since 1.0.0
     * @return float
     */
    public function getDiscountAmount(): float {
        $this->calculateTotals();
        return floatval($this->data['discount_amount'] ?? 0);
    }

    /**
     * Get tax rate percentage
     *
     * @since 1.0.0
     * @return float
     */
    public function getTaxRate(): float {
        return floatval($this->data['tax_rate'] ?? 0);
    }

    /**
     * Get tax amount (calculated value)
     *
     * @since 1.0.0
     * @return float
     */
    public function getTaxAmount(): float {
        $this->calculateTotals();
        return floatval($this->data['tax_amount'] ?? 0);
    }

    /**
     * Get subtotal (sum of all items)
     *
     * @since 1.0.0
     * @return float
     */
    public function getSubtotal(): float {
        $this->calculateTotals();
        return floatval($this->data['subtotal'] ?? 0);
    }

    /**
     * Get total (final amount including tax and discount)
     *
     * @since 1.0.0
     * @return float
     */
    public function getTotal(): float {
        $this->calculateTotals();
        return floatval($this->data['total'] ?? 0);
    }
} 
```
