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

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