PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.27
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.27
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.27, at app/Services/ShortCodeParser/Parsers/OrderParser.php

641 lines 20.4 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 return $name . ' (' . rtrim(rtrim((string) $rate, '0'), '.') . '%)';
541 }
542 if ($name !== '') {
543 return $name;
544 }
545 }
546
547 return __('Tax', 'fluent-cart');
548 }
549
550 public function getSubscriptions()
551 {
552 return '';
553 }
554
555 /**
556 * Returns formatted store VAT display string, e.g. "VAT: NL123456789B01".
557 * Returns empty string if no store VAT is configured for this order.
558 */
559 public function getStoreVatDisplay(): string
560 {
561 if (!$this->order) {
562 return '';
563 }
564
565 $orderTaxRate = $this->order->orderTaxRates ? $this->order->orderTaxRates->first() : null;
566
567 if (!$orderTaxRate) {
568 return '';
569 }
570
571 $storeVatNumber = Arr::get($orderTaxRate->meta ?? [], 'store_vat_number', '');
572
573 if (empty($storeVatNumber)) {
574 return '';
575 }
576
577 $taxCountry = Arr::get($orderTaxRate->meta ?? [], 'tax_country', '');
578 $label = \FluentCart\App\Modules\Tax\TaxModule::getCountryTaxTitle($taxCountry);
579
580 return esc_html($label) . ': ' . esc_html($storeVatNumber);
581 }
582
583 /**
584 * Returns formatted buyer VAT display string, e.g. "VAT/Tax ID: XX123456".
585 * Checks OrderMeta vat_tax_id first, then falls back to EU VAT reverse charge number.
586 */
587 public function getBuyerVatDisplay(): string
588 {
589 if (!$this->order) {
590 return '';
591 }
592
593 // Check OrderMeta vat_tax_id first (simple VAT/Tax ID)
594 $vatMeta = OrderMeta::query()
595 ->where('order_id', $this->order->id)
596 ->where('meta_key', 'vat_tax_id')
597 ->orderBy('id', 'DESC')
598 ->first();
599
600 if ($vatMeta && !empty($vatMeta->meta_value)) {
601 return esc_html(__('VAT/Tax ID', 'fluent-cart')) . ': ' . esc_html($vatMeta->meta_value);
602 }
603
604 // Fall back to EU VAT reverse charge number
605 $orderTaxRate = $this->order->orderTaxRates ? $this->order->orderTaxRates->first() : null;
606 $vatNumber = Arr::get($orderTaxRate->meta ?? [], 'vat_reverse.vat_number', '');
607
608 if (!empty($vatNumber)) {
609 return esc_html(__('EU VAT', 'fluent-cart')) . ': ' . esc_html($vatNumber);
610 }
611
612 return '';
613 }
614
615 /**
616 * Returns buyer company name from billing address meta or VAT reverse charge data.
617 */
618 public function getBuyerCompanyName(): string
619 {
620 if (!$this->order) {
621 return '';
622 }
623
624 // Check billing address meta first
625 if ($this->order->billing_address) {
626 $companyName = Arr::get($this->order->billing_address->meta ?? [], 'other_data.company_name', '');
627 if (!empty($companyName)) {
628 return esc_html($companyName);
629 }
630 }
631
632 // Fall back to VAT reverse charge name
633 $orderTaxRate = $this->order->orderTaxRates ? $this->order->orderTaxRates->first() : null;
634
635 return esc_html(Arr::get($orderTaxRate->meta ?? [], 'vat_reverse.name', ''));
636 }
637
638 }
639
640
641