PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.28
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.28
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
fluent-cart / app / Services / ShortCodeParser / Parsers / OrderParser.php

OrderParser.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.28, at app/Services/ShortCodeParser/Parsers/OrderParser.php

647 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\ShortCodeParser\Parsers;
4
5 use FluentCart\Api\CurrencySettings;
6 use FluentCart\Api\StoreSettings;
7 use FluentCart\App\Helpers\Helper;
8 use FluentCart\App\Helpers\Status;
9 use FluentCart\App\Models\Order;
10 use FluentCart\App\Models\OrderMeta;
11 use FluentCart\App\Models\Subscription;
12 use FluentCart\App\Modules\PaymentMethods\Core\GatewayManager;
13 use FluentCart\App\Services\DateTime\DateTime;
14 use FluentCart\App\Services\Payments\PaymentReceipt;
15 use FluentCart\App\Services\TemplateService;
16 use FluentCart\Framework\Support\Arr;
17 use FluentCart\Framework\Support\Str;
18 use FluentCartPro\App\Modules\Licensing\Models\License;
19
20 class OrderParser extends BaseParser
21 {
22 private StoreSettings $storeSettings;
23 private $order;
24 private $orderTz;
25
26 private $licenses;
27
28 private bool $licenseLoaded = false;
29
30 private $subscriptions;
31
32 private bool $subscriptionLoaded = false;
33
34 public function __construct($data)
35 {
36 $this->storeSettings = new StoreSettings();
37 $this->order = Arr::get($data, 'order');
38 $config = Arr::wrap(
39 Arr::get($this->order, 'config')
40 );
41 $rawTz = Arr::get($config, 'user_tz', 'UTC');
42 $this->orderTz = (@timezone_open($rawTz) !== false) ? $rawTz : 'UTC';
43 $orderId = Arr::get($this->order, 'id');
44
45
46 parent::__construct($data);
47 }
48
49 // protected array $methodMap = [
50 // 'customer_dashboard_link' => 'getCustomerDashboardLink',
51 // 'payment_summary' => 'getPaymentSummary',
52 // 'payment_receipt' => 'getPaymentReceipt',
53 // ];
54
55 protected array $methodMap = [
56 'item_count' => 'getItemCount',
57 'is_digital' => 'getIsDigital',
58 'store_vat_display' => 'getStoreVatDisplay',
59 'buyer_vat_display' => 'getBuyerVatDisplay',
60 'buyer_company_name' => 'getBuyerCompanyName',
61 ];
62
63 protected array $attributeMap = [
64 'id' => 'order.id',
65 'status' => 'order.status',
66 'created_at' => 'order.created_at',
67 'updated_at' => 'order.updated_at',
68 ];
69
70 protected array $centColumns = [
71 'total_amount',
72 'subtotal',
73 'discount_tax',
74 'manual_discount_total',
75 'coupon_discount_total',
76 'shipping_tax',
77 'shipping_total',
78 'fee_total',
79 'tax_total',
80 'total_paid',
81 'total_refund'
82 ];
83
84 public function parse($accessor = '', $code = '', $transformer = null): ?string
85 {
86
87 if ($this->shouldParseAddress($accessor)) {
88 return $this->parseAddressFields($accessor);
89 }
90
91 if (in_array($accessor, ['updated_at', 'created_at'])) {
92 $date = Arr::get($this->data, $this->attributeMap[$accessor]);
93 $timestamp = DateTime::anyTimeToGmt($date)->getTimestamp();
94
95 $date = wp_date(
96 get_option('date_format'),
97 $timestamp,
98 new \DateTimeZone($this->orderTz)
99 );
100
101 return Helper::translateNumber($date);
102 }
103
104 // Handle _formatted suffix for cent columns (e.g. total_amount_formatted)
105 $formattedSuffix = '_formatted';
106 if (Str::endsWith($accessor, $formattedSuffix)) {
107 $baseAccessor = substr($accessor, 0, -strlen($formattedSuffix));
108 if (in_array($baseAccessor, $this->centColumns)) {
109 $amount = Arr::get($this->order, $baseAccessor);
110 if (!is_numeric($amount)) {
111 return (string) $amount;
112 }
113 return CurrencySettings::getPriceHtml($amount, $this->order['currency']);
114 }
115 }
116
117 if (in_array($accessor, $this->centColumns)) {
118 $amount = Arr::get($this->order, $accessor);
119 if (!is_numeric($amount)) {
120 return (string) $amount;
121 }
122 return (string) ($amount / 100);
123 }
124
125 // $html parsers
126 $htmlParsers = [
127 'order.download_details',
128 'order.items_table',
129 'order.payment_summary',
130 'order.payment_receipt',
131 'order.subscription_details',
132 'order.license_details',
133 'order.address_details',
134 ];
135
136 if (in_array($code, $htmlParsers)) {
137
138 $order = $this->order;
139
140 if ($code == 'order.items_table') {
141 return \FluentCart\App\App::make('view')->make('emails.parts.items_table', [
142 'order' => $order,
143 'formattedItems' => $order->order_items,
144 'heading' => __('Order Summary', 'fluent-cart'),
145 ]);
146 }
147
148 if ($code === 'order.subscription_details') {
149 if ($order->subscriptions && $order->subscriptions->count() > 0) {
150 return \FluentCart\App\App::make('view')->make('invoice.parts.subscription_items', [
151 'subscriptions' => $order->subscriptions,
152 'order' => $order
153 ]);
154 }
155 return '';
156 }
157
158 if ($code === 'order.license_details') {
159 $licenses = $order->getLicenses();
160 if ($licenses && $licenses->count() > 0) {
161 return \FluentCart\App\App::make('view')->make('emails.parts.licenses', [
162 'licenses' => $licenses,
163 'heading' => _n('License', 'Licenses', $licenses->count(), 'fluent-cart'),
164 'show_notice' => false
165 ]);
166 }
167 return '';
168 }
169
170 if ($code === 'order.download_details') {
171 $downloads = $order->getDownloads();
172 if ($downloads) {
173 return \FluentCart\App\App::make('view')->make('emails.parts.downloads', [
174 'order' => $order,
175 'heading' => _n('Download', 'Downloads', count($downloads), 'fluent-cart'),
176 'downloadItems' => $downloads,
177 ]);
178 }
179 return '';
180 }
181
182 if ($code === 'order.address_details') {
183 return \FluentCart\App\App::make('view')->make('emails.parts.addresses', [
184 'order' => $order,
185 ]);
186 }
187
188 if ($code == 'order.payment_summary') {
189 return $this->getPaymentSummary();
190 }
191 if ($code == 'order.payment_receipt') {
192 return $this->getPaymentReceipt();
193 }
194 }
195
196
197 return $this->get($accessor, $code);
198 }
199
200 public function shouldParseAddress($accessor): bool
201 {
202 return Str::startsWith($accessor, 'billing.') || Str::startsWith($accessor, 'shipping.');
203 }
204
205 public function parseAddressFields($accessor)
206 {
207 list($addressType, $accessorsKey) = $this->resolveAddressFieldKeys($accessor);
208 return $this->getAddressData($addressType, $accessorsKey);
209 }
210
211 public function resolveAddressFieldKeys($accessor): array
212 {
213 $exploded = explode('.', $accessor);
214 $addressType = $exploded[0];
215 $accessorsKey = implode('.', array_slice($exploded, 1));
216 return [$addressType, $accessorsKey];
217 }
218
219 public function getAddressData($addressAccessor, $accessor = null)
220 {
221 $address = Arr::get($this->order, $addressAccessor . '_address');
222
223 if (empty($address)) {
224 return "";
225 }
226
227 $formattedFields = ['city', 'state', 'country'];
228 if (in_array($accessor, $formattedFields) && method_exists($address, 'getFormattedAddress')) {
229 $formatted = $address->getFormattedAddress();
230 return Arr::get($formatted, $accessor) ?: '';
231 }
232
233 return Arr::get($address, $accessor) ?: '';
234 }
235
236 public function getPaymentSummary()
237 {
238 $order = $this->order;
239
240 return \FluentCart\App\App::make('view')->make('emails.parts.items_table', [
241 'order' => $order,
242 'formattedItems' => $order->order_items,
243 'heading' => '',
244 ]);
245 }
246
247 public function getPaymentReceipt()
248 {
249 $order = $this->order;
250
251 ob_start();
252
253 \FluentCart\App\App::make('view')->render('emails.parts.items_table', [
254 'order' => $order,
255 'formattedItems' => $order->order_items,
256 'heading' => __('Order Summary', 'fluent-cart'),
257 ]);
258
259
260 if ($order->subscriptions && $order->subscriptions->count() > 0) {
261 \FluentCart\App\App::make('view')->render('invoice.parts.subscription_items', [
262 'subscriptions' => $order->subscriptions,
263 'order' => $order
264 ]);
265 }
266
267 $licenses = $order->getLicenses();
268 if ($licenses && $licenses->count() > 0) {
269 \FluentCart\App\App::make('view')->render('emails.parts.licenses', [
270 'licenses' => $licenses,
271 'heading' => __('Licenses', 'fluent-cart'),
272 'show_notice' => false
273 ]);
274 }
275
276 $downloads = $order->getDownloads();
277 if ($downloads) {
278 \FluentCart\App\App::make('view')->render('emails.parts.downloads', [
279 'order' => $order,
280 'heading' => __('Downloads', 'fluent-cart'),
281 'downloadItems' => $downloads,
282 ]);
283 }
284
285 echo '<hr />';
286
287 \FluentCart\App\App::make('view')->render('emails.parts.addresses', [
288 'order' => $order,
289 ]);
290
291 return ob_get_clean();
292
293
294 }
295
296 public function getDiscountTotal(): string
297 {
298 return (string) ($this->getDiscountTotalInCents() / 100);
299 }
300
301 public function getDiscountTotalFormatted(): string
302 {
303 return CurrencySettings::getPriceHtml($this->getDiscountTotalInCents(), $this->order['currency']);
304 }
305
306 private function getDiscountTotalInCents(): int
307 {
308 return (int) Arr::get($this->order, 'coupon_discount_total', 0)
309 + (int) Arr::get($this->order, 'manual_discount_total', 0);
310 }
311
312 public function getOrderRef(): string
313 {
314 $invoiceNo = Arr::get($this->order, 'invoice_no');
315
316 if (!empty($invoiceNo)) {
317 return (string) $invoiceNo;
318 }
319
320 return (string) Arr::get($this->order, 'id');
321 }
322
323 public function getCustomerDashboardAnchorLink($accessor, $code = null, $conditions = [])
324 {
325 $defaultValue = Arr::get($conditions, 'default_value') ?? Arr::get($this->order, 'invoice_no');
326 if (empty($this->order)) {
327 return $code;
328 }
329
330 $profilePage = $this->storeSettings->getCustomerProfilePage();
331
332
333 if (!empty($profilePage)) {
334 return "<a style='color: #017EF3; text-decoration: none;' href='" . "$profilePage#/order/" . Arr::get($this->order, 'uuid') . "'>" . $defaultValue . "</a>";
335 } else {
336 return Arr::get($this->order, 'invoice_no');
337 }
338
339 }
340
341 public function getCustomerDashboardLink($accessor, $code = null)
342 {
343 if (empty($this->order)) {
344 return $code;
345 }
346
347 $orderLink = TemplateService::getCustomerProfileUrl('order/' . Arr::get($this->order, 'uuid'));
348
349 return is_user_logged_in() ? $orderLink : wp_login_url($orderLink);
350 }
351
352 public function getAdminOrderLink($accessor, $code = null)
353 {
354 if (empty($this->order)) {
355 return $code;
356 }
357 return admin_url('admin.php?page=fluent-cart#/orders/' . Arr::get($this->order, 'id') . '/view');
358 }
359
360 public function getAdminOrderAnchorLink($accessor, $code = null, $conditions = [])
361 {
362 $defaultValue = Arr::get($conditions, 'default_value');
363 if (empty($this->order)) {
364 return $code;
365 }
366
367 $url = admin_url('admin.php?page=fluent-cart#/orders/' . Arr::get($this->order, 'id') . '/view');
368
369 if (!empty($defaultValue)) {
370 return "<a style='color: #017EF3; text-decoration: none;' href='" . $url . "'>" . $defaultValue . "</a>";
371 }
372
373 return $url;
374 }
375
376 public function getCustomerOrderLink($accessor, $code = null)
377 {
378 if (empty($this->order)) {
379 return $code;
380 }
381
382 $customerProfilePage = $this->storeSettings->getCustomerProfilePage();
383 $orderLink = $customerProfilePage . '#/order/' . Arr::get($this->order, 'uuid');
384
385 return is_user_logged_in() ? $orderLink : wp_login_url($orderLink);
386 }
387
388 public function getTotalAmount()
389 {
390 $total = ($this->order['total_amount'] / 100);
391 $currency_sign = $this->order['currency'];
392 return $total . $currency_sign;
393 }
394
395 public function getDownloads()
396 {
397 $order = $this->order;
398
399 $downloads = $order->getDownloads();
400 if ($downloads) {
401 return (string)\FluentCart\App\App::make('view')->make('emails.parts.downloads', [
402 'order' => $order,
403 'heading' => '',
404 'downloadItems' => $downloads
405 ]);
406 }
407
408 return '';
409
410 }
411
412 public function getLicenses()
413 {
414 $order = $this->order;
415 $licenses = $order->getLicenses();
416 if ($licenses && $licenses->count() > 0) {
417 return (string)\FluentCart\App\App::make('view')->make('emails.parts.licenses', [
418 'licenses' => $licenses,
419 'heading' => __('Licenses', 'fluent-cart'),
420 'show_notice' => false
421 ]);
422 }
423
424 return '';
425 }
426
427 public function getLicenseCount(): string
428 {
429 return (string)$this->licenses->count();
430 }
431
432 public function getIsDigital(): string
433 {
434 if (!$this->order) {
435 return 'no';
436 }
437
438 $fulfillmentType = Arr::get($this->order, 'fulfillment_type');
439
440 return $fulfillmentType === 'digital' ? 'yes' : 'no';
441 }
442
443 public function getItemCount(): string
444 {
445 $orderItems = $this->order ? $this->order->order_items : null;
446
447 if ($orderItems) {
448 return (string)$orderItems->count();
449 }
450
451 return '0';
452 }
453
454 public function getPaymentMethodTitle(): string
455 {
456 if (!$this->order) {
457 return '';
458 }
459
460 $title = (string) Arr::get($this->order, 'payment_method_title', '');
461 if ($title !== '') {
462 return $title;
463 }
464
465 $slug = (string) Arr::get($this->order, 'payment_method', '');
466 if ($slug === '') {
467 return '';
468 }
469
470 if (class_exists(GatewayManager::class)) {
471 $gateway = GatewayManager::getInstance($slug);
472 if ($gateway) {
473 $gatewayTitle = (string) $gateway->getMeta('title');
474 if ($gatewayTitle !== '') {
475 return $gatewayTitle;
476 }
477 }
478 }
479
480 return ucwords(str_replace(['_', '-'], ' ', $slug));
481 }
482
483 public function getTaxBreakdown(): string
484 {
485 if (!$this->order) {
486 return '';
487 }
488
489 $currency = (string) Arr::get($this->order, 'currency', '');
490 $taxTotal = (int) Arr::get($this->order, 'tax_total', 0);
491 $shippingTax = (int) Arr::get($this->order, 'shipping_tax', 0);
492 $rowStyle = 'padding:5px 0;font-size:11px;';
493 $labelStyle = $rowStyle . 'color:#525866;';
494 $valueStyle = $rowStyle . 'color:#0E121B;text-align:right;';
495
496 $rows = '';
497 $rates = $this->order->orderTaxRates ?? null;
498
499 if ($rates && $rates->count() > 0) {
500 foreach ($rates as $rate) {
501 $label = $this->resolveTaxRateLabel($rate);
502 $amount = (int) Arr::get($rate, 'total_tax', (int) Arr::get($rate, 'order_tax', 0) + (int) Arr::get($rate, 'shipping_tax', 0));
503 $rows .= '<tr>'
504 . '<td style="' . $labelStyle . '">' . esc_html($label) . '</td>'
505 . '<td style="' . $valueStyle . '">' . CurrencySettings::getPriceHtml($amount, $currency) . '</td>'
506 . '</tr>';
507 }
508
509 return $rows;
510 }
511
512 if ($taxTotal > 0 || $shippingTax > 0) {
513 if ($shippingTax > 0 && $taxTotal > $shippingTax) {
514 $rows .= '<tr>'
515 . '<td style="' . $labelStyle . '">' . esc_html__('Tax', 'fluent-cart') . '</td>'
516 . '<td style="' . $valueStyle . '">' . CurrencySettings::getPriceHtml($taxTotal - $shippingTax, $currency) . '</td>'
517 . '</tr>';
518 $rows .= '<tr>'
519 . '<td style="' . $labelStyle . '">' . esc_html__('Shipping Tax', 'fluent-cart') . '</td>'
520 . '<td style="' . $valueStyle . '">' . CurrencySettings::getPriceHtml($shippingTax, $currency) . '</td>'
521 . '</tr>';
522 } else {
523 $rows .= '<tr>'
524 . '<td style="' . $labelStyle . '">' . esc_html__('Tax', 'fluent-cart') . '</td>'
525 . '<td style="' . $valueStyle . '">' . CurrencySettings::getPriceHtml($taxTotal, $currency) . '</td>'
526 . '</tr>';
527 }
528 }
529
530 return $rows;
531 }
532
533 private function resolveTaxRateLabel($orderTaxRate): string
534 {
535 $taxRate = $orderTaxRate->tax_rate ?? null;
536 if ($taxRate) {
537 $name = (string) Arr::get($taxRate, 'name', '');
538 $rate = Arr::get($taxRate, 'rate');
539 if ($name !== '' && is_numeric($rate)) {
540 $rate = (string) $rate;
541
542 if (strpos($rate, '.') !== false) {
543 $rate = rtrim(rtrim($rate, '0'), '.');
544 }
545
546 return $name . ' (' . $rate . '%)';
547 }
548 if ($name !== '') {
549 return $name;
550 }
551 }
552
553 return __('Tax', 'fluent-cart');
554 }
555
556 public function getSubscriptions()
557 {
558 return '';
559 }
560
561 /**
562 * Returns formatted store VAT display string, e.g. "VAT: NL123456789B01".
563 * Returns empty string if no store VAT is configured for this order.
564 */
565 public function getStoreVatDisplay(): string
566 {
567 if (!$this->order) {
568 return '';
569 }
570
571 $orderTaxRate = $this->order->orderTaxRates ? $this->order->orderTaxRates->first() : null;
572
573 if (!$orderTaxRate) {
574 return '';
575 }
576
577 $storeVatNumber = Arr::get($orderTaxRate->meta ?? [], 'store_vat_number', '');
578
579 if (empty($storeVatNumber)) {
580 return '';
581 }
582
583 $taxCountry = Arr::get($orderTaxRate->meta ?? [], 'tax_country', '');
584 $label = \FluentCart\App\Modules\Tax\TaxModule::getCountryTaxTitle($taxCountry);
585
586 return esc_html($label) . ': ' . esc_html($storeVatNumber);
587 }
588
589 /**
590 * Returns formatted buyer VAT display string, e.g. "VAT/Tax ID: XX123456".
591 * Checks OrderMeta vat_tax_id first, then falls back to EU VAT reverse charge number.
592 */
593 public function getBuyerVatDisplay(): string
594 {
595 if (!$this->order) {
596 return '';
597 }
598
599 // Check OrderMeta vat_tax_id first (simple VAT/Tax ID)
600 $vatMeta = OrderMeta::query()
601 ->where('order_id', $this->order->id)
602 ->where('meta_key', 'vat_tax_id')
603 ->orderBy('id', 'DESC')
604 ->first();
605
606 if ($vatMeta && !empty($vatMeta->meta_value)) {
607 return esc_html(__('VAT/Tax ID', 'fluent-cart')) . ': ' . esc_html($vatMeta->meta_value);
608 }
609
610 // Fall back to EU VAT reverse charge number
611 $orderTaxRate = $this->order->orderTaxRates ? $this->order->orderTaxRates->first() : null;
612 $vatNumber = Arr::get($orderTaxRate->meta ?? [], 'vat_reverse.vat_number', '');
613
614 if (!empty($vatNumber)) {
615 return esc_html(__('EU VAT', 'fluent-cart')) . ': ' . esc_html($vatNumber);
616 }
617
618 return '';
619 }
620
621 /**
622 * Returns buyer company name from billing address meta or VAT reverse charge data.
623 */
624 public function getBuyerCompanyName(): string
625 {
626 if (!$this->order) {
627 return '';
628 }
629
630 // Check billing address meta first
631 if ($this->order->billing_address) {
632 $companyName = Arr::get($this->order->billing_address->meta ?? [], 'other_data.company_name', '');
633 if (!empty($companyName)) {
634 return esc_html($companyName);
635 }
636 }
637
638 // Fall back to VAT reverse charge name
639 $orderTaxRate = $this->order->orderTaxRates ? $this->order->orderTaxRates->first() : null;
640
641 return esc_html(Arr::get($orderTaxRate->meta ?? [], 'vat_reverse.name', ''));
642 }
643
644 }
645
646
647