PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.1.13
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.1.13
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 / Invoice.php

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

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