PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.2
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Models / Quote.php

Quote.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.3.2, at includes/Models/Quote.php

903 lines 28.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Quote Model
4 *
5 * @package EasyInvoice
6 * @author Your Name
7 * @copyright Copyright (c) 2023, Your Company
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.0.0
10 */
11
12 namespace EasyInvoice\Models;
13
14 use EasyInvoice\Constants\PostTypes;
15 use EasyInvoice\Models\QuoteItem;
16
17 /**
18 * Quote Model
19 *
20 * Represents a quote in the system with extensible architecture for pro features.
21 *
22 * @since 1.0.0
23 */
24 class Quote {
25 /**
26 * Quote ID
27 *
28 * @var int
29 */
30 private $id;
31
32 /**
33 * Dynamic data storage for all fields
34 *
35 * @var array
36 */
37 private $data = [];
38
39 /**
40 * Items
41 *
42 * @var array
43 */
44 private $items = [];
45
46 /**
47 * Modified flag
48 *
49 * @var bool
50 */
51 private $is_modified = false;
52
53 /**
54 * Magic method to get dynamic properties
55 *
56 * @since 1.0.0
57 * @param string $name Property name
58 * @return mixed Property value
59 */
60 public function __get($name) {
61 // Handle special properties
62 if ($name === 'id') {
63 return $this->id;
64 }
65
66 // Return from dynamic data array
67 return $this->data[$name] ?? null;
68 }
69
70 /**
71 * Magic method to set dynamic properties
72 *
73 * @since 1.0.0
74 * @param string $name Property name
75 * @param mixed $value Property value
76 */
77 public function __set($name, $value) {
78 // Handle special properties
79 if ($name === 'id') {
80 $this->id = $value;
81 return;
82 }
83
84 // Store in dynamic data array
85 $this->data[$name] = $value;
86 $this->is_modified = true;
87 }
88
89 /**
90 * Magic method to check if property exists
91 *
92 * @since 1.0.0
93 * @param string $name Property name
94 * @return bool
95 */
96 public function __isset($name) {
97 if ($name === 'id') {
98 return isset($this->id);
99 }
100
101 return isset($this->data[$name]);
102 }
103
104 /**
105 * Dynamic getter method
106 *
107 * @since 1.0.0
108 * @param string $name Method name
109 * @param array $arguments Method arguments
110 * @return mixed
111 */
112 public function __call($name, $arguments) {
113 // Handle getter methods (getFieldName)
114 if (strpos($name, 'get') === 0) {
115 $field_name = $this->camelCaseToSnakeCase(substr($name, 3)); // Remove 'get' prefix
116 return $this->__get($field_name);
117 }
118
119 // Handle setter methods (setFieldName)
120 if (strpos($name, 'set') === 0) {
121 $field_name = $this->camelCaseToSnakeCase(substr($name, 3)); // Remove 'set' prefix
122 $value = $arguments[0] ?? null;
123 $this->__set($field_name, $value);
124 return null;
125 }
126
127 // Handle isset methods (isFieldName)
128 if (strpos($name, 'is') === 0) {
129 $field_name = $this->camelCaseToSnakeCase(substr($name, 2)); // Remove 'is' prefix
130 return (bool) $this->__get($field_name);
131 }
132
133 // Handle has methods (hasFieldName)
134 if (strpos($name, 'has') === 0) {
135 $field_name = $this->camelCaseToSnakeCase(substr($name, 3)); // Remove 'has' prefix
136 return !empty($this->__get($field_name));
137 }
138
139 throw new \BadMethodCallException("Method $name does not exist");
140 }
141
142 /**
143 * Convert camelCase to snake_case
144 *
145 * @since 1.0.0
146 * @param string $camelCase
147 * @return string
148 */
149 private function camelCaseToSnakeCase($camelCase) {
150 return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $camelCase));
151 }
152
153 /**
154 * Constructor
155 *
156 * @since 1.0.0
157 * @param \WP_Post|int $quote Quote post object or ID
158 */
159 public function __construct($quote = null) {
160 // Initialize data array dynamically from field configuration
161 $this->data = $this->getDefaultValuesFromConfiguration();
162
163 if ($quote instanceof \WP_Post) {
164 $this->loadFromPost($quote);
165 } elseif (is_numeric($quote)) {
166 $post = get_post($quote);
167 if ($post && $post->post_type === PostTypes::EASY_INVOICE_QUOTE_POST_TYPE) {
168 $this->loadFromPost($post);
169 }
170 }
171
172 do_action('easy_invoice_quote_model_constructed', $this);
173 }
174
175 /**
176 * Get default values from field configuration
177 *
178 * @since 1.0.0
179 * @return array
180 */
181 private function getDefaultValuesFromConfiguration(): array {
182 $default_values = [];
183
184 // Get field definitions to determine default values
185 $field_registration = new \EasyInvoice\Forms\Quote\QuoteFieldRegistration();
186
187 // Initialize the field registration to ensure fields are registered
188 $field_registration->registerDefaultTabs();
189 $field_registration->registerDefaultFields();
190
191 $field_definitions = [];
192 $tabs = $field_registration->getTabs();
193 foreach ($tabs as $tab_id => $tab) {
194 $tab_fields = $field_registration->getFields($tab_id);
195 foreach ($tab_fields as $field) {
196 $field_definitions[] = $field;
197 }
198 }
199
200 // Extract default values from field configuration
201 foreach ($field_definitions as $field) {
202 $field_name = $field['name'] ?? '';
203 if (empty($field_name)) {
204 continue;
205 }
206
207 $default_value = $field['default_value'] ?? null;
208
209 // Handle callable default values
210 if (is_callable($default_value)) {
211 $default_value = $default_value();
212 }
213
214 // Set appropriate default based on field type
215 if ($default_value !== null) {
216 $default_values[$field_name] = $default_value;
217 } else {
218 // Set type-appropriate defaults
219 $field_type = $field['type'] ?? 'text';
220 switch ($field_type) {
221 case 'number':
222 $default_values[$field_name] = 0.0;
223 break;
224 case 'checkbox':
225 $default_values[$field_name] = false;
226 break;
227 case 'select':
228 $default_values[$field_name] = '';
229 break;
230 case 'array':
231 $default_values[$field_name] = [];
232 break;
233 default:
234 $default_values[$field_name] = '';
235 break;
236 }
237 }
238 }
239
240 return $default_values;
241 }
242
243 /**
244 * Load quote data from WP_Post object
245 *
246 * @since 1.0.0
247 * @param \WP_Post $post Post object
248 */
249 private function loadFromPost(\WP_Post $post): void {
250 $this->id = $post->ID;
251 $this->data['title'] = $post->post_title ?: '';
252 $this->data['created_date'] = $post->post_date ?: '';
253 $this->data['modified_date'] = $post->post_modified ?: '';
254
255 // Load items first so they're available for total calculations
256 $this->loadItems();
257
258 // Load meta data using configuration-driven approach
259 $this->loadMetaData();
260
261 // Allow plugins to load additional data
262 do_action('easy_invoice_quote_loaded_from_post', $this, $post);
263
264 // Ensure totals are calculated
265 $this->calculateTotals();
266 }
267
268 /**
269 * Load quote items
270 *
271 * @since 1.0.0
272 */
273 private function loadItems(): void {
274 $items_data = get_post_meta($this->id, '_easy_invoice_quote_items', true);
275 if (is_array($items_data)) {
276 $this->items = []; // Clear existing items
277 foreach ($items_data as $item_data) {
278 if (is_array($item_data)) {
279 // Ensure required fields have default values
280 // Ensure taxable field is properly set
281 $taxable = isset($item_data['taxable']) ? $item_data['taxable'] : true;
282 if (is_string($taxable)) {
283 $taxable = strtolower($taxable);
284 $taxable = $taxable === '1' || $taxable === 'true' || $taxable === 'yes' || $taxable === 'on';
285 }
286 $taxable = (bool) $taxable;
287
288 $item_data = array_merge([
289 'quantity' => 0,
290 'price' => 0,
291 'adjust_percentage' => 0,
292 'taxable' => $taxable,
293 'name' => '',
294 'description' => ''
295 ], $item_data);
296 $this->items[] = new QuoteItem($item_data);
297 }
298 }
299 }
300 }
301
302 /**
303 * Load meta data using configuration-driven approach
304 *
305 * @since 1.0.0
306 */
307 private function loadMetaData(): void {
308 if (!$this->id) {
309 return;
310 }
311
312 // Get field definitions to determine which meta keys to load
313 $field_registration = new \EasyInvoice\Forms\Quote\QuoteFieldRegistration();
314
315 // Initialize the field registration to ensure fields are registered
316 $field_registration->registerDefaultTabs();
317 $field_registration->registerDefaultFields();
318
319 $field_definitions = [];
320 $tabs = $field_registration->getTabs();
321 foreach ($tabs as $tab_id => $tab) {
322 $tab_fields = $field_registration->getFields($tab_id);
323 foreach ($tab_fields as $field) {
324 $field_definitions[] = $field;
325 }
326 }
327
328 // Load meta data for each field definition
329 foreach ($field_definitions as $field) {
330 $field_name = $field['name'] ?? '';
331 if (empty($field_name)) {
332 continue;
333 }
334
335 $meta_key = '_easy_invoice_quote_' . $field_name;
336 $value = get_post_meta($this->id, $meta_key, true);
337
338 // Handle special cases for certain fields
339 if ($field_name === 'prices_include_tax') {
340 $this->data[$field_name] = $value === '1' || $value === 'yes' ? 'yes' : 'no';
341 } else if ($field_name === 'discount_type' && empty($value)) {
342 $this->data[$field_name] = 'none';
343 } else if ($field_name === 'discount_calculation_method' && empty($value)) {
344 $this->data[$field_name] = 'before_tax';
345 } else if ($field_name === 'tax_rate' && empty($value)) {
346 $this->data[$field_name] = 0;
347 } else if ($field_name === 'discount_value' && empty($value)) {
348 $this->data[$field_name] = 0;
349 } else if ($value !== '') {
350 // Store in dynamic data array
351 $this->data[$field_name] = $value;
352 }
353 }
354
355
356
357 // Auto-calculate totals if they're 0 or if we have items but no totals
358 if (((isset($this->data['total']) ? $this->data['total'] : 0) == 0 && !empty($this->items)) ||
359 ((isset($this->data['subtotal']) ? $this->data['subtotal'] : 0) == 0 && !empty($this->items))) {
360 $this->calculateTotals();
361 }
362
363 // Populate client information if we have a client_id but no customer data
364 if (($this->data['client_id'] ?? 0) > 0 && (empty($this->data['customer_name']) || empty($this->data['customer_email']))) {
365 $this->populateClientInfo();
366 }
367 }
368
369 /**
370 * Ensure quote has proper post_name (slug) for pretty URLs
371 *
372 * @since 1.0.0
373 * @return bool
374 */
375 public function ensureProperSlug(): bool {
376 if (!$this->id) {
377 return false;
378 }
379
380 $post = get_post($this->id);
381 if (!$post || empty($post->post_name)) {
382 // Generate a proper slug for this quote
383 $post_title = $this->data['title'] ?: $this->data['number'] ?: 'Untitled Quote';
384 $post_name = sanitize_title($post_title);
385
386 // Ensure uniqueness
387 $original_slug = $post_name;
388 $counter = 1;
389 while (get_page_by_path($post_name, OBJECT, \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE)) {
390 $post_name = $original_slug . '-' . $counter;
391 $counter++;
392 }
393
394 // Update the post with the new slug
395 $result = wp_update_post([
396 'ID' => $this->id,
397 'post_name' => $post_name
398 ]);
399
400 return $result !== 0;
401 }
402
403 return true;
404 }
405
406 /**
407 * Save quote to database
408 *
409 * @since 1.0.0
410 * @return bool True if successful, false otherwise
411 */
412 public function save(): bool {
413 // Prepare post data
414 $post_data = [
415 'post_title' => $this->data['title'] ?? '',
416 'post_content' => $this->data['description'] ?? '',
417 'post_type' => PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
418 'post_status' => 'publish'
419 ];
420
421 if ($this->id) {
422 $post_data['ID'] = $this->id;
423 $post_id = wp_update_post($post_data);
424 } else {
425 $post_id = wp_insert_post($post_data);
426 }
427
428 if (is_wp_error($post_id)) {
429 return false;
430 }
431
432 // Update the ID if this was a new post
433 if (!$this->id) {
434 $this->id = $post_id;
435 }
436
437 // Calculate totals from items before saving
438 $this->calculateTotals();
439
440 // Save meta data (including quote status)
441 $this->saveMetaData();
442
443 // Allow plugins to perform actions after saving
444 do_action('easy_invoice_quote_after_save', $this);
445
446 $this->is_modified = false;
447 return true;
448 }
449
450 /**
451 * Save quote meta data
452 *
453 * @since 1.0.0
454 */
455 private function saveMetaData(): void {
456 if (!$this->id) {
457 return;
458 }
459
460
461
462 // Get field definitions to determine which meta keys to save
463 $field_registration = new \EasyInvoice\Forms\Quote\QuoteFieldRegistration();
464
465 // Initialize the field registration to ensure fields are registered
466 $field_registration->registerDefaultTabs();
467 $field_registration->registerDefaultFields();
468
469 $field_definitions = [];
470 $tabs = $field_registration->getTabs();
471 foreach ($tabs as $tab_id => $tab) {
472 $tab_fields = $field_registration->getFields($tab_id);
473 foreach ($tab_fields as $field) {
474 $field_definitions[] = $field;
475 }
476 }
477
478 // Save meta data for each field definition
479 foreach ($field_definitions as $field) {
480 $field_name = $field['name'] ?? '';
481 if (empty($field_name)) {
482 continue;
483 }
484
485 $meta_key = '_easy_invoice_quote_' . $field_name;
486
487 // Get the value from dynamic data array
488 // Save all fields that exist in the data array (including empty strings to allow clearing fields)
489 // If a field exists in $this->data, it means it was explicitly set, so we should save it
490 if (array_key_exists($field_name, $this->data)) {
491 $value = $this->data[$field_name];
492 update_post_meta($this->id, $meta_key, $value);
493 }
494 }
495
496 // Save items
497 $this->saveItems();
498
499 // Allow plugins to save additional meta data
500 do_action('easy_invoice_quote_save_meta_data', $this);
501 }
502
503 /**
504 * Save quote items
505 *
506 * @since 1.0.0
507 */
508 private function saveItems(): void {
509 $items_data = [];
510
511 // Process items for saving
512
513 foreach ($this->items as $item) {
514 if (is_object($item) && method_exists($item, 'toArray')) {
515 $item_data = $item->toArray();
516 // Ensure taxable field is properly set as a string '1' or '0'
517 $item_data['taxable'] = $item->isTaxable() ? '1' : '0';
518 $items_data[] = $item_data;
519 } elseif (is_array($item)) {
520 // Convert array to QuoteItem object for proper saving
521 $quote_item = new QuoteItem($item);
522 $item_data = $quote_item->toArray();
523 // Ensure taxable field is properly set as a string '1' or '0'
524 $item_data['taxable'] = $quote_item->isTaxable() ? '1' : '0';
525 $items_data[] = $item_data;
526 }
527 }
528
529 // Save items to meta
530
531 update_post_meta($this->id, '_easy_invoice_quote_items', $items_data);
532 }
533
534 /**
535 * Calculate totals from items
536 *
537 * @since 1.0.0
538 */
539 public function calculateTotals(): void {
540 // Per-quote tax_enabled override. Same semantics as the
541 // Invoice model — see Invoice::calculateTotals() for rationale.
542 $tax_enabled_meta = $this->data['tax_enabled'] ?? null;
543 if ($tax_enabled_meta === null || $tax_enabled_meta === '') {
544 $tax_enabled = get_option('easy_invoice_tax_enabled', 'no') === 'yes';
545 } else {
546 $tax_enabled = ($tax_enabled_meta === 'yes' || $tax_enabled_meta === '1'
547 || $tax_enabled_meta === 1 || $tax_enabled_meta === true);
548 }
549
550 $prices_include_tax = ($this->data['prices_include_tax'] ?? 'no') === 'yes';
551 $tax_rate = $tax_enabled ? floatval($this->data['tax_rate'] ?? 0) : 0;
552
553 // Initialize totals
554 $subtotal = 0;
555 $taxable_subtotal = 0;
556 $this->data['tax_amount'] = 0;
557 $this->data['discount_amount'] = 0;
558
559 // First pass: Calculate raw totals
560 foreach ($this->items as $item) {
561 $quantity = 0;
562 $price = 0;
563 $adjust_percentage = 0;
564 $is_taxable = true;
565
566 if (is_object($item) && method_exists($item, 'getAmount')) {
567 $quantity = $item->getQuantity();
568 $price = $item->getPrice();
569 $adjust_percentage = $item->getAdjustPercentage();
570 $is_taxable = $item->isTaxable();
571 } elseif (is_array($item)) {
572 $quantity = isset($item['quantity']) ? (float) $item['quantity'] : 0;
573 $price = isset($item['price']) ? (float) $item['price'] : 0;
574 $adjust_percentage = isset($item['adjust_percentage']) ? (float) $item['adjust_percentage'] : 0;
575 $is_taxable = isset($item['taxable']) ? (bool) $item['taxable'] : true;
576 }
577
578 // If prices include tax and item is taxable, remove tax from price
579 if ($prices_include_tax && $is_taxable && $tax_rate > 0) {
580 $price = $price / (1 + ($tax_rate / 100));
581 }
582
583 // Calculate item total
584 $item_total = $quantity * $price;
585 // Only apply adjust percentage if the adjust field is enabled
586 if ($adjust_percentage != 0 && \EasyInvoice\Controllers\SettingsController::shouldShowQuoteAdjustField()) {
587 $item_total = $item_total * (1 + $adjust_percentage / 100);
588 }
589
590 $subtotal += $item_total;
591 if ($is_taxable) {
592 $taxable_subtotal += $item_total;
593 }
594 }
595
596 $this->data['subtotal'] = $subtotal;
597
598 // Get discount calculation method (default to before_tax)
599 $discount_calculation_method = $this->data['discount_calculation_method'] ?? 'before_tax';
600
601 // Calculate initial discount amount
602 if (($this->data['discount_type'] ?? '') === 'percentage' && ($this->data['discount_value'] ?? 0) > 0) {
603 $this->data['discount_amount'] = ($subtotal * $this->data['discount_value']) / 100;
604 } elseif (($this->data['discount_type'] ?? '') === 'fixed' && ($this->data['discount_value'] ?? 0) > 0) {
605 $this->data['discount_amount'] = $this->data['discount_value'];
606 }
607
608 // Calculate tax and total based on discount calculation method
609 if ($discount_calculation_method === 'before_tax') {
610 // For before_tax: Apply discount first, then calculate tax on remaining taxable amount
611 if ($subtotal > 0) {
612 $discount_ratio = $this->data['discount_amount'] / $subtotal;
613 $taxable_amount = $taxable_subtotal * (1 - $discount_ratio);
614 } else {
615 $taxable_amount = 0;
616 }
617
618 if ($tax_rate > 0) {
619 $this->data['tax_amount'] = ($taxable_amount * $tax_rate) / 100;
620 }
621
622 $this->data['total'] = $subtotal - $this->data['discount_amount'] + $this->data['tax_amount'];
623 } else {
624 // For after_tax: Calculate tax first, then apply discount
625 if ($tax_rate > 0) {
626 $this->data['tax_amount'] = ($taxable_subtotal * $tax_rate) / 100;
627 }
628
629 $total_before_discount = $subtotal + $this->data['tax_amount'];
630
631 // Recalculate percentage discount based on total including tax
632 if (($this->data['discount_type'] ?? '') === 'percentage' && ($this->data['discount_value'] ?? 0) > 0 && $total_before_discount > 0) {
633 $this->data['discount_amount'] = ($total_before_discount * $this->data['discount_value']) / 100;
634 }
635
636 $this->data['total'] = $total_before_discount - $this->data['discount_amount'];
637 }
638 }
639
640 /**
641 * Populate client information from client_id
642 *
643 * @since 1.0.0
644 */
645 public function populateClientInfo(): void {
646 if (($this->data['client_id'] ?? 0) > 0) {
647 $client_repository = new \EasyInvoice\Repositories\ClientRepository();
648 $client = $client_repository->find($this->data['client_id']);
649
650 if ($client) {
651 $this->data['customer_name'] = $client->getBusinessClientName() ?: '';
652 $this->data['customer_email'] = $client->getEmail() ?: '';
653 $this->data['customer_address'] = $client->getAddress() ?: '';
654 }
655 }
656 }
657
658 /**
659 * Recalculate totals and save (for existing quotes)
660 *
661 * @since 1.0.0
662 * @return bool
663 */
664 public function recalculateAndSave(): bool {
665 // Populate client information if we have a client_id
666 $this->populateClientInfo();
667
668 // Calculate totals from items
669 $this->calculateTotals();
670
671 // Save the updated quote
672 return $this->save();
673 }
674
675 /**
676 * Convert to array
677 *
678 * @since 1.0.0
679 * @return array
680 */
681 public function toArray(): array {
682 $items_array = [];
683 foreach ($this->items as $item) {
684 if (is_object($item) && method_exists($item, 'toArray')) {
685 $items_array[] = $item->toArray();
686 } else {
687 $items_array[] = $item;
688 }
689 }
690
691 // Start with dynamic data
692 $data = $this->data;
693
694 // Add special properties
695 $data['id'] = $this->id;
696 $data['items'] = $items_array;
697
698 // Allow plugins to modify the array data
699 return apply_filters('easy_invoice_quote_model_to_array', $data, $this);
700 }
701
702 // Essential methods only
703 public function getId(): int { return $this->id ?? 0; }
704 public function setId(int $id): void { $this->id = $id; }
705 public function getItems(): array { return $this->items; }
706 public function setItems(array $items): void {
707 $this->items = [];
708 foreach ($items as $item) {
709 if (is_array($item)) {
710 $this->items[] = new QuoteItem($item);
711 } elseif (is_object($item) && $item instanceof QuoteItem) {
712 $this->items[] = $item;
713 }
714 }
715 $this->is_modified = true;
716 }
717 public function isModified(): bool { return $this->is_modified; }
718
719 /**
720 * Set meta data for the quote
721 *
722 * @since 1.0.0
723 * @param string $key Meta key
724 * @param mixed $value Meta value
725 */
726 public function setMetaData(string $key, $value): void {
727 // Store in dynamic data array without the _easy_invoice_ prefix
728 $field_name = easy_invoice_str_replace('_easy_invoice_', '', $key);
729 $this->data[$field_name] = $value;
730 $this->is_modified = true;
731
732
733 }
734
735 /**
736 * Get currency code
737 *
738 * @since 1.0.0
739 * @return string Currency code
740 */
741 public function getCurrencyCode(): string {
742 $currency_code = $this->data['currency_code'] ?? 'USD';
743
744 // If currency is set to 'global', resolve to actual global setting
745 if ($currency_code === 'global') {
746 $currency_code = get_option('easy_invoice_currency_code', 'USD');
747 }
748
749 // Ensure currency code is uppercase for consistency
750 return strtoupper($currency_code);
751 }
752
753 /**
754 * Set currency code
755 *
756 * @since 1.0.0
757 * @param string $currency_code Currency code
758 */
759 public function setCurrencyCode(string $currency_code): void {
760 $this->data['currency_code'] = $currency_code;
761 $this->is_modified = true;
762 }
763
764 /**
765 * Get currency position
766 *
767 * @since 1.0.0
768 * @return string Currency position
769 */
770 public function getCurrencyPosition(): string {
771 $currency_position = $this->data['currency_position'] ?? 'left';
772
773 // If currency position is set to 'global', resolve to actual global setting
774 if ($currency_position === 'global') {
775 $currency_position = get_option('easy_invoice_currency_position', 'left');
776 }
777
778 return $currency_position;
779 }
780
781 /**
782 * Set currency position
783 *
784 * @since 1.0.0
785 * @param string $currency_position Currency position
786 */
787 public function setCurrencyPosition(string $currency_position): void {
788 $this->data['currency_position'] = $currency_position;
789 $this->is_modified = true;
790 }
791
792 /**
793 * Get raw currency code (without resolving global)
794 *
795 * @since 1.0.0
796 * @return string Raw currency code
797 */
798 public function getRawCurrencyCode(): string {
799 return $this->data['currency_code'] ?? 'global';
800 }
801
802 /**
803 * Get raw currency position (without resolving global)
804 *
805 * @since 1.0.0
806 * @return string Raw currency position
807 */
808 public function getRawCurrencyPosition(): string {
809 return $this->data['currency_position'] ?? 'global';
810 }
811
812 public function getDescription(): string {
813 return $this->data['description'] ?? '';
814 }
815
816 public function setDescription(string $description): void {
817 $this->data['description'] = $description;
818 $this->is_modified = true;
819 }
820
821 public function getTemplate(): string {
822 return $this->data['quote_template'] ?? 'standard';
823 }
824
825 public function setTemplate(string $template): void {
826 $this->data['quote_template'] = $template;
827 $this->is_modified = true;
828 }
829
830 /**
831 * Get discount type (percentage, fixed, none)
832 *
833 * @since 1.0.0
834 * @return string
835 */
836 public function getDiscountType(): string {
837 return $this->data['discount_type'] ?? 'none';
838 }
839
840 /**
841 * Get discount value (percentage or fixed amount)
842 *
843 * @since 1.0.0
844 * @return float
845 */
846 public function getDiscountValue(): float {
847 return floatval($this->data['discount_value'] ?? 0);
848 }
849
850 /**
851 * Get discount amount (calculated value)
852 *
853 * @since 1.0.0
854 * @return float
855 */
856 public function getDiscountAmount(): float {
857 $this->calculateTotals();
858 return floatval($this->data['discount_amount'] ?? 0);
859 }
860
861 /**
862 * Get tax rate percentage
863 *
864 * @since 1.0.0
865 * @return float
866 */
867 public function getTaxRate(): float {
868 return floatval($this->data['tax_rate'] ?? 0);
869 }
870
871 /**
872 * Get tax amount (calculated value)
873 *
874 * @since 1.0.0
875 * @return float
876 */
877 public function getTaxAmount(): float {
878 $this->calculateTotals();
879 return floatval($this->data['tax_amount'] ?? 0);
880 }
881
882 /**
883 * Get subtotal (sum of all items)
884 *
885 * @since 1.0.0
886 * @return float
887 */
888 public function getSubtotal(): float {
889 $this->calculateTotals();
890 return floatval($this->data['subtotal'] ?? 0);
891 }
892
893 /**
894 * Get total (final amount including tax and discount)
895 *
896 * @since 1.0.0
897 * @return float
898 */
899 public function getTotal(): float {
900 $this->calculateTotals();
901 return floatval($this->data['total'] ?? 0);
902 }
903 }