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

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