PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.1.10
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.1.10
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.1.10, at includes/Models/Quote.php

892 lines 28.0 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 $prices_include_tax = ($this->data['prices_include_tax'] ?? 'no') === 'yes';
541 $tax_rate = floatval($this->data['tax_rate'] ?? 0);
542
543 // Initialize totals
544 $subtotal = 0;
545 $taxable_subtotal = 0;
546 $this->data['tax_amount'] = 0;
547 $this->data['discount_amount'] = 0;
548
549 // First pass: Calculate raw totals
550 foreach ($this->items as $item) {
551 $quantity = 0;
552 $price = 0;
553 $adjust_percentage = 0;
554 $is_taxable = true;
555
556 if (is_object($item) && method_exists($item, 'getAmount')) {
557 $quantity = $item->getQuantity();
558 $price = $item->getPrice();
559 $adjust_percentage = $item->getAdjustPercentage();
560 $is_taxable = $item->isTaxable();
561 } elseif (is_array($item)) {
562 $quantity = isset($item['quantity']) ? (float) $item['quantity'] : 0;
563 $price = isset($item['price']) ? (float) $item['price'] : 0;
564 $adjust_percentage = isset($item['adjust_percentage']) ? (float) $item['adjust_percentage'] : 0;
565 $is_taxable = isset($item['taxable']) ? (bool) $item['taxable'] : true;
566 }
567
568 // If prices include tax and item is taxable, remove tax from price
569 if ($prices_include_tax && $is_taxable && $tax_rate > 0) {
570 $price = $price / (1 + ($tax_rate / 100));
571 }
572
573 // Calculate item total
574 $item_total = $quantity * $price;
575 if ($adjust_percentage != 0) {
576 $item_total = $item_total * (1 + $adjust_percentage / 100);
577 }
578
579 $subtotal += $item_total;
580 if ($is_taxable) {
581 $taxable_subtotal += $item_total;
582 }
583 }
584
585 $this->data['subtotal'] = $subtotal;
586
587 // Get discount calculation method (default to before_tax)
588 $discount_calculation_method = $this->data['discount_calculation_method'] ?? 'before_tax';
589
590 // Calculate initial discount amount
591 if (($this->data['discount_type'] ?? '') === 'percentage' && ($this->data['discount_value'] ?? 0) > 0) {
592 $this->data['discount_amount'] = ($subtotal * $this->data['discount_value']) / 100;
593 } elseif (($this->data['discount_type'] ?? '') === 'fixed' && ($this->data['discount_value'] ?? 0) > 0) {
594 $this->data['discount_amount'] = $this->data['discount_value'];
595 }
596
597 // Calculate tax and total based on discount calculation method
598 if ($discount_calculation_method === 'before_tax') {
599 // For before_tax: Apply discount first, then calculate tax on remaining taxable amount
600 if ($subtotal > 0) {
601 $discount_ratio = $this->data['discount_amount'] / $subtotal;
602 $taxable_amount = $taxable_subtotal * (1 - $discount_ratio);
603 } else {
604 $taxable_amount = 0;
605 }
606
607 if ($tax_rate > 0) {
608 $this->data['tax_amount'] = ($taxable_amount * $tax_rate) / 100;
609 }
610
611 $this->data['total'] = $subtotal - $this->data['discount_amount'] + $this->data['tax_amount'];
612 } else {
613 // For after_tax: Calculate tax first, then apply discount
614 if ($tax_rate > 0) {
615 $this->data['tax_amount'] = ($taxable_subtotal * $tax_rate) / 100;
616 }
617
618 $total_before_discount = $subtotal + $this->data['tax_amount'];
619
620 // Recalculate percentage discount based on total including tax
621 if (($this->data['discount_type'] ?? '') === 'percentage' && ($this->data['discount_value'] ?? 0) > 0 && $total_before_discount > 0) {
622 $this->data['discount_amount'] = ($total_before_discount * $this->data['discount_value']) / 100;
623 }
624
625 $this->data['total'] = $total_before_discount - $this->data['discount_amount'];
626 }
627 }
628
629 /**
630 * Populate client information from client_id
631 *
632 * @since 1.0.0
633 */
634 public function populateClientInfo(): void {
635 if (($this->data['client_id'] ?? 0) > 0) {
636 $client_repository = new \EasyInvoice\Repositories\ClientRepository();
637 $client = $client_repository->find($this->data['client_id']);
638
639 if ($client) {
640 $this->data['customer_name'] = $client->getBusinessClientName() ?: '';
641 $this->data['customer_email'] = $client->getEmail() ?: '';
642 $this->data['customer_address'] = $client->getAddress() ?: '';
643 }
644 }
645 }
646
647 /**
648 * Recalculate totals and save (for existing quotes)
649 *
650 * @since 1.0.0
651 * @return bool
652 */
653 public function recalculateAndSave(): bool {
654 // Populate client information if we have a client_id
655 $this->populateClientInfo();
656
657 // Calculate totals from items
658 $this->calculateTotals();
659
660 // Save the updated quote
661 return $this->save();
662 }
663
664 /**
665 * Convert to array
666 *
667 * @since 1.0.0
668 * @return array
669 */
670 public function toArray(): array {
671 $items_array = [];
672 foreach ($this->items as $item) {
673 if (is_object($item) && method_exists($item, 'toArray')) {
674 $items_array[] = $item->toArray();
675 } else {
676 $items_array[] = $item;
677 }
678 }
679
680 // Start with dynamic data
681 $data = $this->data;
682
683 // Add special properties
684 $data['id'] = $this->id;
685 $data['items'] = $items_array;
686
687 // Allow plugins to modify the array data
688 return apply_filters('easy_invoice_quote_model_to_array', $data, $this);
689 }
690
691 // Essential methods only
692 public function getId(): int { return $this->id ?? 0; }
693 public function setId(int $id): void { $this->id = $id; }
694 public function getItems(): array { return $this->items; }
695 public function setItems(array $items): void {
696 $this->items = [];
697 foreach ($items as $item) {
698 if (is_array($item)) {
699 $this->items[] = new QuoteItem($item);
700 } elseif (is_object($item) && $item instanceof QuoteItem) {
701 $this->items[] = $item;
702 }
703 }
704 $this->is_modified = true;
705 }
706 public function isModified(): bool { return $this->is_modified; }
707
708 /**
709 * Set meta data for the quote
710 *
711 * @since 1.0.0
712 * @param string $key Meta key
713 * @param mixed $value Meta value
714 */
715 public function setMetaData(string $key, $value): void {
716 // Store in dynamic data array without the _easy_invoice_ prefix
717 $field_name = easy_invoice_str_replace('_easy_invoice_', '', $key);
718 $this->data[$field_name] = $value;
719 $this->is_modified = true;
720
721
722 }
723
724 /**
725 * Get currency code
726 *
727 * @since 1.0.0
728 * @return string Currency code
729 */
730 public function getCurrencyCode(): string {
731 $currency_code = $this->data['currency_code'] ?? 'USD';
732
733 // If currency is set to 'global', resolve to actual global setting
734 if ($currency_code === 'global') {
735 $currency_code = get_option('easy_invoice_currency_code', 'USD');
736 }
737
738 // Ensure currency code is uppercase for consistency
739 return strtoupper($currency_code);
740 }
741
742 /**
743 * Set currency code
744 *
745 * @since 1.0.0
746 * @param string $currency_code Currency code
747 */
748 public function setCurrencyCode(string $currency_code): void {
749 $this->data['currency_code'] = $currency_code;
750 $this->is_modified = true;
751 }
752
753 /**
754 * Get currency position
755 *
756 * @since 1.0.0
757 * @return string Currency position
758 */
759 public function getCurrencyPosition(): string {
760 $currency_position = $this->data['currency_position'] ?? 'left';
761
762 // If currency position is set to 'global', resolve to actual global setting
763 if ($currency_position === 'global') {
764 $currency_position = get_option('easy_invoice_currency_position', 'left');
765 }
766
767 return $currency_position;
768 }
769
770 /**
771 * Set currency position
772 *
773 * @since 1.0.0
774 * @param string $currency_position Currency position
775 */
776 public function setCurrencyPosition(string $currency_position): void {
777 $this->data['currency_position'] = $currency_position;
778 $this->is_modified = true;
779 }
780
781 /**
782 * Get raw currency code (without resolving global)
783 *
784 * @since 1.0.0
785 * @return string Raw currency code
786 */
787 public function getRawCurrencyCode(): string {
788 return $this->data['currency_code'] ?? 'global';
789 }
790
791 /**
792 * Get raw currency position (without resolving global)
793 *
794 * @since 1.0.0
795 * @return string Raw currency position
796 */
797 public function getRawCurrencyPosition(): string {
798 return $this->data['currency_position'] ?? 'global';
799 }
800
801 public function getDescription(): string {
802 return $this->data['description'] ?? '';
803 }
804
805 public function setDescription(string $description): void {
806 $this->data['description'] = $description;
807 $this->is_modified = true;
808 }
809
810 public function getTemplate(): string {
811 return $this->data['quote_template'] ?? 'standard';
812 }
813
814 public function setTemplate(string $template): void {
815 $this->data['quote_template'] = $template;
816 $this->is_modified = true;
817 }
818
819 /**
820 * Get discount type (percentage, fixed, none)
821 *
822 * @since 1.0.0
823 * @return string
824 */
825 public function getDiscountType(): string {
826 return $this->data['discount_type'] ?? 'none';
827 }
828
829 /**
830 * Get discount value (percentage or fixed amount)
831 *
832 * @since 1.0.0
833 * @return float
834 */
835 public function getDiscountValue(): float {
836 return floatval($this->data['discount_value'] ?? 0);
837 }
838
839 /**
840 * Get discount amount (calculated value)
841 *
842 * @since 1.0.0
843 * @return float
844 */
845 public function getDiscountAmount(): float {
846 $this->calculateTotals();
847 return floatval($this->data['discount_amount'] ?? 0);
848 }
849
850 /**
851 * Get tax rate percentage
852 *
853 * @since 1.0.0
854 * @return float
855 */
856 public function getTaxRate(): float {
857 return floatval($this->data['tax_rate'] ?? 0);
858 }
859
860 /**
861 * Get tax amount (calculated value)
862 *
863 * @since 1.0.0
864 * @return float
865 */
866 public function getTaxAmount(): float {
867 $this->calculateTotals();
868 return floatval($this->data['tax_amount'] ?? 0);
869 }
870
871 /**
872 * Get subtotal (sum of all items)
873 *
874 * @since 1.0.0
875 * @return float
876 */
877 public function getSubtotal(): float {
878 $this->calculateTotals();
879 return floatval($this->data['subtotal'] ?? 0);
880 }
881
882 /**
883 * Get total (final amount including tax and discount)
884 *
885 * @since 1.0.0
886 * @return float
887 */
888 public function getTotal(): float {
889 $this->calculateTotals();
890 return floatval($this->data['total'] ?? 0);
891 }
892 }