@fleetbase/storefront-engine 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/addon/components/storefront-order-summary.hbs +69 -68
  2. package/addon/components/storefront-order-summary.js +10 -1
  3. package/addon/controllers/base-controller.js +2 -22
  4. package/addon/controllers/networks/index/network.js +25 -0
  5. package/addon/controllers/promotions/push-notifications.js +64 -0
  6. package/addon/controllers/promotions.js +16 -0
  7. package/addon/engine.js +1 -31
  8. package/addon/extension.js +35 -0
  9. package/addon/routes/customers/index.js +0 -9
  10. package/addon/routes/networks/index/network/customers.js +0 -9
  11. package/addon/routes/networks/index.js +0 -9
  12. package/addon/routes/orders/index.js +0 -9
  13. package/addon/routes/products/index.js +2 -8
  14. package/addon/routes/promotions/push-notifications.js +19 -0
  15. package/addon/routes/promotions.js +16 -0
  16. package/addon/routes.js +3 -1
  17. package/addon/styles/storefront-engine.css +4 -0
  18. package/addon/templates/application.hbs +7 -0
  19. package/addon/templates/networks/index/network/index.hbs +30 -8
  20. package/addon/templates/networks/index/network.hbs +5 -26
  21. package/addon/templates/promotions/push-notifications.hbs +85 -0
  22. package/addon/templates/promotions.hbs +8 -0
  23. package/addon/templates/settings/index.hbs +19 -0
  24. package/app/controllers/promotions/push-notifications.js +1 -0
  25. package/app/controllers/promotions.js +1 -0
  26. package/app/routes/promotions/push-notifications.js +1 -0
  27. package/app/routes/promotions.js +1 -0
  28. package/app/templates/promotions/push-notifications.hbs +1 -0
  29. package/app/templates/promotions.hbs +1 -0
  30. package/composer.json +1 -1
  31. package/config/environment.js +1 -1
  32. package/extension.json +1 -1
  33. package/package.json +4 -4
  34. package/server/config/storefront.php +1 -1
  35. package/server/src/Http/Controllers/ActionController.php +61 -0
  36. package/server/src/Http/Controllers/v1/CheckoutController.php +26 -10
  37. package/server/src/Http/Controllers/v1/CustomerController.php +22 -16
  38. package/server/src/Models/Cart.php +2 -1
  39. package/server/src/Models/Network.php +25 -0
  40. package/server/src/Models/Store.php +25 -0
  41. package/server/src/Notifications/PromotionalPushNotification.php +166 -0
  42. package/server/src/Support/QPay.php +550 -2
  43. package/server/src/Support/Storefront.php +14 -0
  44. package/server/src/routes.php +1 -0
  45. package/translations/en-us.yaml +28 -0
@@ -2,19 +2,110 @@
2
2
 
3
3
  namespace Fleetbase\Storefront\Support;
4
4
 
5
+ use Fleetbase\FleetOps\Models\ServiceQuote;
6
+ use Fleetbase\Storefront\Models\Cart;
5
7
  use Fleetbase\Storefront\Models\Checkout;
8
+ use Fleetbase\Storefront\Models\Product;
6
9
  use Fleetbase\Support\Utils;
7
10
  use GuzzleHttp\Client;
8
11
  use Illuminate\Support\Str;
9
12
 
13
+ /**
14
+ * QPay Payment Gateway Integration Class.
15
+ *
16
+ * This class provides a comprehensive interface for integrating with the QPay payment gateway,
17
+ * supporting both simple and eBarimt (Mongolian electronic receipt) invoice creation, payment
18
+ * processing, and transaction management. It handles authentication, API communication, and
19
+ * provides utilities for tax calculations and classification code validation.
20
+ *
21
+ * @author Fleetbase Pte Ltd
22
+ */
10
23
  class QPay
11
24
  {
12
- private string $host = 'https://merchant.qpay.mn/';
25
+ /**
26
+ * The base URL for the QPay merchant API.
27
+ */
28
+ private string $host = 'https://merchant.qpay.mn/';
29
+
30
+ /**
31
+ * The API version namespace.
32
+ */
13
33
  private string $namespace = 'v2';
34
+
35
+ /**
36
+ * The callback URL for payment notifications.
37
+ */
14
38
  private ?string $callbackUrl;
39
+
40
+ /**
41
+ * HTTP request options for the Guzzle client.
42
+ */
15
43
  private array $requestOptions = [];
44
+
45
+ /**
46
+ * The Guzzle HTTP client instance.
47
+ */
16
48
  private Client $client;
17
49
 
50
+ /**
51
+ * Classification codes that are exempt from tax in Mongolia.
52
+ *
53
+ * These codes represent various categories of goods and services that are
54
+ * subject to zero tax rate under Mongolian tax regulations.
55
+ */
56
+ public static array $zeroTaxClassificationCodes = [
57
+ '2111100',
58
+ '2111300',
59
+ '2111500',
60
+ '2111600',
61
+ '2112100',
62
+ '2112200',
63
+ '2112300',
64
+ '2113100',
65
+ '2113300',
66
+ '2113500',
67
+ '2113600',
68
+ '2113700',
69
+ '2113800',
70
+ '2113900',
71
+ '2114100',
72
+ '2114200',
73
+ '2114300',
74
+ '2114400',
75
+ '2115100',
76
+ '2115200',
77
+ '2115300',
78
+ '2115500',
79
+ '2115600',
80
+ '2115910',
81
+ '2115920',
82
+ '2115930',
83
+ '2115940',
84
+ '2115990',
85
+ '2116000',
86
+ '2117100',
87
+ '2117210',
88
+ '2117290',
89
+ '2117300',
90
+ '2117410',
91
+ '2117490',
92
+ '2117500',
93
+ '2117600',
94
+ '2117900',
95
+ '2118000',
96
+ '2119000',
97
+ ];
98
+
99
+ /**
100
+ * QPay constructor.
101
+ *
102
+ * Initializes the QPay client with authentication credentials and sets up
103
+ * the HTTP client with appropriate headers and base URI.
104
+ *
105
+ * @param string|null $username QPay merchant username
106
+ * @param string|null $password QPay merchant password
107
+ * @param string|null $callbackUrl URL to receive payment notifications
108
+ */
18
109
  public function __construct(?string $username = null, ?string $password = null, ?string $callbackUrl = null)
19
110
  {
20
111
  $this->callbackUrl = $callbackUrl ?? Utils::apiUrl('storefront/v1/checkouts/process-qpay');
@@ -29,17 +120,37 @@ class QPay
29
120
  $this->client = new Client($this->requestOptions);
30
121
  }
31
122
 
123
+ /**
124
+ * Update a specific request option and reinitialize the HTTP client.
125
+ *
126
+ * @param string $key The option key to update
127
+ * @param mixed $value The new value for the option
128
+ *
129
+ * @return void
130
+ */
32
131
  public function updateRequestOption($key, $value)
33
132
  {
34
133
  $this->requestOptions[$key] = $value;
35
134
  $this->client = new Client($this->requestOptions);
36
135
  }
37
136
 
137
+ /**
138
+ * Get the current Guzzle HTTP client instance.
139
+ *
140
+ * @return Client The HTTP client instance
141
+ */
38
142
  public function getClient()
39
143
  {
40
144
  return $this->client;
41
145
  }
42
146
 
147
+ /**
148
+ * Set the callback URL for payment notifications.
149
+ *
150
+ * @param string $url The callback URL
151
+ *
152
+ * @return $this
153
+ */
43
154
  public function setCallback(string $url)
44
155
  {
45
156
  $this->callbackUrl = $url;
@@ -47,6 +158,11 @@ class QPay
47
158
  return $this;
48
159
  }
49
160
 
161
+ /**
162
+ * Set the API namespace/version and update the base URI.
163
+ *
164
+ * @param string $namespace The API version namespace (e.g., 'v2')
165
+ */
50
166
  public function setNamespace(string $namespace): ?QPay
51
167
  {
52
168
  $this->namespace = $namespace;
@@ -55,6 +171,13 @@ class QPay
55
171
  return $this;
56
172
  }
57
173
 
174
+ /**
175
+ * Build the complete request URL from host, namespace, and path.
176
+ *
177
+ * @param string $path Optional path to append to the base URL
178
+ *
179
+ * @return string The complete request URL
180
+ */
58
181
  private function buildRequestUrl(string $path = ''): string
59
182
  {
60
183
  $url = trim($this->host . $this->namespace . '/' . $path);
@@ -62,6 +185,11 @@ class QPay
62
185
  return $url;
63
186
  }
64
187
 
188
+ /**
189
+ * Switch to using the QPay sandbox environment for testing.
190
+ *
191
+ * @return $this
192
+ */
65
193
  public function useSandbox()
66
194
  {
67
195
  $this->host = 'https://merchant-sandbox.qpay.mn/';
@@ -70,16 +198,39 @@ class QPay
70
198
  return $this;
71
199
  }
72
200
 
201
+ /**
202
+ * Create a new QPay instance (static factory method).
203
+ *
204
+ * @param string|null $username QPay merchant username
205
+ * @param string|null $password QPay merchant password
206
+ * @param string|null $callbackUrl URL to receive payment notifications
207
+ *
208
+ * @return QPay A new QPay instance
209
+ */
73
210
  public static function instance(?string $username = null, ?string $password = null, ?string $callbackUrl = null): QPay
74
211
  {
75
212
  return new static($username, $password, $callbackUrl);
76
213
  }
77
214
 
215
+ /**
216
+ * Check if a callback URL has been set.
217
+ *
218
+ * @return bool True if callback URL is set, false otherwise
219
+ */
78
220
  private function hasCallbackUrl()
79
221
  {
80
222
  return isset($this->callbackUrl);
81
223
  }
82
224
 
225
+ /**
226
+ * Make an HTTP request to the QPay API.
227
+ *
228
+ * @param string $method HTTP method (GET, POST, DELETE, etc.)
229
+ * @param string $path API endpoint path
230
+ * @param array $options Additional request options
231
+ *
232
+ * @return object|null Decoded JSON response
233
+ */
83
234
  private function request(string $method, string $path, array $options = [])
84
235
  {
85
236
  $options['http_errors'] = false;
@@ -92,6 +243,15 @@ class QPay
92
243
  return $json;
93
244
  }
94
245
 
246
+ /**
247
+ * Make a POST request to the QPay API.
248
+ *
249
+ * @param string $path API endpoint path
250
+ * @param array $params Request parameters
251
+ * @param array $options Additional request options
252
+ *
253
+ * @return object|null Decoded JSON response
254
+ */
95
255
  public function post(string $path, array $params = [], array $options = [])
96
256
  {
97
257
  $options = ['json' => $params];
@@ -99,6 +259,15 @@ class QPay
99
259
  return $this->request('POST', $path, $options);
100
260
  }
101
261
 
262
+ /**
263
+ * Make a DELETE request to the QPay API.
264
+ *
265
+ * @param string $path API endpoint path
266
+ * @param array $params Request parameters
267
+ * @param array $options Additional request options
268
+ *
269
+ * @return object|null Decoded JSON response
270
+ */
102
271
  public function delete(string $path, array $params = [], array $options = [])
103
272
  {
104
273
  $options = ['json' => $params];
@@ -106,21 +275,44 @@ class QPay
106
275
  return $this->request('DELETE', $path, $options);
107
276
  }
108
277
 
278
+ /**
279
+ * Make a GET request to the QPay API.
280
+ *
281
+ * @param string $path API endpoint path
282
+ * @param array $options Additional request options
283
+ *
284
+ * @return object|null Decoded JSON response
285
+ */
109
286
  public function get(string $path, array $options = [])
110
287
  {
111
288
  return $this->request('GET', $path, $options);
112
289
  }
113
290
 
291
+ /**
292
+ * Obtain an authentication token from QPay.
293
+ *
294
+ * @return object|null Response containing access token
295
+ */
114
296
  public function getAuthToken()
115
297
  {
116
298
  return $this->post('auth/token');
117
299
  }
118
300
 
301
+ /**
302
+ * Refresh the current authentication token.
303
+ *
304
+ * @return object|null Response containing refreshed access token
305
+ */
119
306
  public function refreshAuthToken()
120
307
  {
121
308
  return $this->post('auth/refresh');
122
309
  }
123
310
 
311
+ /**
312
+ * Configure the client to use Bearer token authentication.
313
+ *
314
+ * @param string $token The Bearer token
315
+ */
124
316
  private function useBearerToken(string $token): QPay
125
317
  {
126
318
  unset($this->requestOptions['auth']);
@@ -130,6 +322,13 @@ class QPay
130
322
  return $this;
131
323
  }
132
324
 
325
+ /**
326
+ * Set the authentication token for API requests.
327
+ *
328
+ * If no token is provided, automatically obtains one using credentials.
329
+ *
330
+ * @param string|null $accessToken Optional access token to use
331
+ */
133
332
  public function setAuthToken(?string $accessToken = null): QPay
134
333
  {
135
334
  if ($accessToken) {
@@ -146,6 +345,18 @@ class QPay
146
345
  return $this;
147
346
  }
148
347
 
348
+ /**
349
+ * Create a simple invoice without eBarimt (electronic receipt).
350
+ *
351
+ * @param int $amount Invoice amount
352
+ * @param string|null $invoiceCode Unique invoice code
353
+ * @param string|null $invoiceDescription Description of the invoice
354
+ * @param string|null $invoiceReceiverCode Receiver identification code
355
+ * @param string|null $senderInvoiceNo Sender's invoice number
356
+ * @param string|null $callbackUrl URL for payment notifications
357
+ *
358
+ * @return object|null Created invoice response
359
+ */
149
360
  public function createSimpleInvoice(int $amount, ?string $invoiceCode = '', ?string $invoiceDescription = '', ?string $invoiceReceiverCode = '', ?string $senderInvoiceNo = '', ?string $callbackUrl = null)
150
361
  {
151
362
  if (!$callbackUrl && $this->hasCallbackUrl()) {
@@ -164,6 +375,21 @@ class QPay
164
375
  return $this->createQPayInvoice($params);
165
376
  }
166
377
 
378
+ /**
379
+ * Create an eBarimt invoice (Mongolian electronic receipt).
380
+ *
381
+ * @param string|null $invoiceCode Unique invoice code
382
+ * @param string|null $senderInvoiceNo Sender's invoice number
383
+ * @param string|null $invoiceReceiverCode Receiver identification code
384
+ * @param array $invoiceReceiverData Receiver details (name, register number, etc.)
385
+ * @param string|null $invoiceDescription Description of the invoice
386
+ * @param string|null $taxType Tax type code (default: '1')
387
+ * @param string|null $districtCode District code for tax purposes
388
+ * @param array $lines Invoice line items with classification codes and taxes
389
+ * @param string|null $callbackUrl URL for payment notifications
390
+ *
391
+ * @return object|null Created invoice response
392
+ */
167
393
  public function createEbarimtInvoice(?string $invoiceCode = '', ?string $senderInvoiceNo = '', ?string $invoiceReceiverCode = '', array $invoiceReceiverData = [], ?string $invoiceDescription = '', ?string $taxType = '1', ?string $districtCode = '', array $lines = [], ?string $callbackUrl = null)
168
394
  {
169
395
  if (!$callbackUrl && $this->hasCallbackUrl()) {
@@ -185,6 +411,14 @@ class QPay
185
411
  return $this->createQPayInvoice($params);
186
412
  }
187
413
 
414
+ /**
415
+ * Create a QPay invoice with custom parameters.
416
+ *
417
+ * @param array $params Invoice parameters
418
+ * @param array $options Additional request options
419
+ *
420
+ * @return object|null Created invoice response
421
+ */
188
422
  public function createQPayInvoice(array $params = [], $options = [])
189
423
  {
190
424
  if (!isset($params['callback_url']) && $this->hasCallbackUrl()) {
@@ -194,11 +428,26 @@ class QPay
194
428
  return $this->post('invoice', $params, $options);
195
429
  }
196
430
 
431
+ /**
432
+ * Get payment information by payment ID.
433
+ *
434
+ * @param string $paymentId The payment ID
435
+ *
436
+ * @return object|null Payment information
437
+ */
197
438
  public function paymentGet(string $paymentId)
198
439
  {
199
440
  return $this->get('payment/' . $paymentId);
200
441
  }
201
442
 
443
+ /**
444
+ * Check the payment status for an invoice.
445
+ *
446
+ * @param string $invoiceId The invoice ID
447
+ * @param array $options Additional request options
448
+ *
449
+ * @return object|null Payment check response
450
+ */
202
451
  public function paymentCheck(string $invoiceId, $options = [])
203
452
  {
204
453
  $params = [
@@ -209,6 +458,13 @@ class QPay
209
458
  return $this->post('payment/check', $params, $options);
210
459
  }
211
460
 
461
+ /**
462
+ * Get the first payment record for an invoice.
463
+ *
464
+ * @param string $invoiceId The invoice ID
465
+ *
466
+ * @return object|null First payment record or null if none found
467
+ */
212
468
  public function getPayment(string $invoiceId)
213
469
  {
214
470
  $paymentCheck = $this->paymentCheck($invoiceId);
@@ -217,6 +473,14 @@ class QPay
217
473
  return $paymentCheck && is_array($rows) && count($rows) ? $rows[0] : null;
218
474
  }
219
475
 
476
+ /**
477
+ * Cancel a payment for an invoice.
478
+ *
479
+ * @param string $invoiceId The invoice ID
480
+ * @param array $options Additional request options
481
+ *
482
+ * @return object|null Cancellation response
483
+ */
220
484
  public function paymentCancel(string $invoiceId, $options = [])
221
485
  {
222
486
  $params = [
@@ -226,6 +490,14 @@ class QPay
226
490
  return $this->delete('payment/cancel', $params, $options);
227
491
  }
228
492
 
493
+ /**
494
+ * Refund a payment for an invoice.
495
+ *
496
+ * @param string $invoiceId The invoice ID
497
+ * @param array $options Additional request options
498
+ *
499
+ * @return object|null Refund response
500
+ */
229
501
  public function paymentRefund(string $invoiceId, $options = [])
230
502
  {
231
503
  $params = [
@@ -235,16 +507,39 @@ class QPay
235
507
  return $this->delete('payment/refund', $params, $options);
236
508
  }
237
509
 
510
+ /**
511
+ * Create an invoice using static method (convenience wrapper).
512
+ *
513
+ * @param string $username QPay merchant username
514
+ * @param string $password QPay merchant password
515
+ * @param array $invoiceParams Invoice parameters
516
+ *
517
+ * @return object|null Created invoice response
518
+ */
238
519
  public static function createInvoice(string $username, string $password, array $invoiceParams = [])
239
520
  {
240
521
  return static::instance($username, $password)->setAuthToken()->createQPayInvoice($invoiceParams);
241
522
  }
242
523
 
524
+ /**
525
+ * Generate a unique code based on current date and ID.
526
+ *
527
+ * @param string $id Identifier to append to date
528
+ *
529
+ * @return string Generated code in format YYYYMMDD{id}
530
+ */
243
531
  public static function generateCode(string $id)
244
532
  {
245
533
  return date('Ymd') . $id;
246
534
  }
247
535
 
536
+ /**
537
+ * Clean a code by removing spaces and special characters.
538
+ *
539
+ * @param string $code The code to clean
540
+ *
541
+ * @return string Cleaned code containing only alphanumeric characters and hyphens
542
+ */
248
543
  public static function cleanCode(string $code)
249
544
  {
250
545
  $code = str_replace(' ', '-', $code);
@@ -252,6 +547,13 @@ class QPay
252
547
  return preg_replace('/[^A-Za-z0-9\-]/', '', $code);
253
548
  }
254
549
 
550
+ /**
551
+ * Create test payment data from a checkout object for testing purposes.
552
+ *
553
+ * @param Checkout $checkout The checkout object
554
+ *
555
+ * @return array Mock payment data structure
556
+ */
255
557
  public static function createTestPaymentDataFromCheckout(Checkout $checkout): array
256
558
  {
257
559
  return [
@@ -271,6 +573,11 @@ class QPay
271
573
  ];
272
574
  }
273
575
 
576
+ /**
577
+ * Generate a mock eBarimt response for testing purposes.
578
+ *
579
+ * @return array Mock eBarimt response data structure
580
+ */
274
581
  public static function mockEbarimtResponse(): array
275
582
  {
276
583
  return [
@@ -281,7 +588,6 @@ class QPay
281
588
  'ebarimt_receiver_type' => 'CITIZEN',
282
589
  'ebarimt_receiver' => '88614450',
283
590
  'ebarimt_district_code' => '3505',
284
- 'ebarimt_bill_type' => '1',
285
591
  'g_merchant_id' => 'KKTT',
286
592
  'merchant_branch_code' => 'BRANCH1',
287
593
  'merchant_terminal_code' => null,
@@ -312,6 +618,16 @@ class QPay
312
618
  ];
313
619
  }
314
620
 
621
+ /**
622
+ * Calculate VAT (Value Added Tax) from a total amount.
623
+ *
624
+ * Assumes 10% VAT rate is included in the amount. Calculates the VAT portion
625
+ * by dividing by 1.1 and multiplying by 0.10, then truncates to 4 decimal places.
626
+ *
627
+ * @param float|int $amount The total amount including VAT
628
+ *
629
+ * @return float The calculated VAT amount truncated to 4 decimal places
630
+ */
315
631
  public static function calculateTax($amount): float
316
632
  {
317
633
  $result = ((float) $amount / 1.1) * 0.10;
@@ -319,4 +635,236 @@ class QPay
319
635
 
320
636
  return $truncated;
321
637
  }
638
+
639
+ /**
640
+ * Create initial invoice lines for QPay from cart and service quote.
641
+ *
642
+ * Generates line items for tips, delivery tips, and delivery fees with proper
643
+ * classification codes and tax calculations for eBarimt invoices.
644
+ *
645
+ * @param Cart $cart The shopping cart
646
+ * @param ServiceQuote|null $serviceQuote The delivery service quote
647
+ * @param array|object $checkoutOptions Checkout options including tips and pickup flag
648
+ *
649
+ * @return array Array of line items with descriptions, quantities, prices, and taxes
650
+ */
651
+ public static function createQpayInitialLines(Cart $cart, ?ServiceQuote $serviceQuote, $checkoutOptions): array
652
+ {
653
+ // Prepare dependencies
654
+ $checkoutOptions = (object) $checkoutOptions;
655
+ $subtotal = (int) $cart->subtotal;
656
+ $total = $subtotal;
657
+ $tip = $checkoutOptions->tip ?? false;
658
+ $deliveryTip = $checkoutOptions->delivery_tip ?? false;
659
+ $isPickup = $checkoutOptions->is_pickup ?? false;
660
+
661
+ // Initialize lines
662
+ $lines = [];
663
+
664
+ if ($tip) {
665
+ $tipAmount = Storefront::calculateTipAmount($tip, $subtotal);
666
+ $lines[] = [
667
+ 'line_description' => 'Tip',
668
+ 'line_quantity' => number_format(1, 2, '.', ''),
669
+ 'line_unit_price' => number_format($tipAmount, 2, '.', ''),
670
+ 'note' => 'Tip',
671
+ 'classification_code' => '6511100',
672
+ 'tax_product_code' => '319',
673
+ 'taxes' => [
674
+ [
675
+ 'tax_code' => 'VAT',
676
+ 'description' => 'VAT',
677
+ 'amount' => QPay::calculateTax($tipAmount),
678
+ 'note' => 'Tip',
679
+ ],
680
+ ],
681
+ ];
682
+ }
683
+
684
+ if ($deliveryTip && !$isPickup) {
685
+ $deliveryTipAmount = Storefront::calculateTipAmount($deliveryTip, $subtotal);
686
+ $lines[] = [
687
+ 'line_description' => 'Delivery Tip',
688
+ 'line_quantity' => number_format(1, 2, '.', ''),
689
+ 'line_unit_price' => number_format($deliveryTipAmount, 2, '.', ''),
690
+ 'note' => 'Delivery Tip',
691
+ 'classification_code' => '6511100',
692
+ 'tax_product_code' => '319',
693
+ 'taxes' => [
694
+ [
695
+ 'tax_code' => 'VAT',
696
+ 'description' => 'VAT',
697
+ 'amount' => QPay::calculateTax($deliveryTipAmount),
698
+ 'note' => 'Delivery Tip',
699
+ ],
700
+ ],
701
+ ];
702
+ }
703
+
704
+ if (!$isPickup) {
705
+ $serviceQuoteAmount = Utils::numbersOnly($serviceQuote->amount);
706
+ $lines[] = [
707
+ 'line_description' => 'Delivery Fee',
708
+ 'line_quantity' => number_format(1, 2, '.', ''),
709
+ 'line_unit_price' => number_format($serviceQuoteAmount, 2, '.', ''),
710
+ 'note' => 'Delivery Fee',
711
+ 'classification_code' => '6511100',
712
+ 'tax_product_code' => '319',
713
+ 'taxes' => [
714
+ [
715
+ 'tax_code' => 'VAT',
716
+ 'description' => 'VAT',
717
+ 'amount' => QPay::calculateTax($serviceQuoteAmount),
718
+ 'note' => 'Delivery Fee',
719
+ ],
720
+ ],
721
+ ];
722
+ }
723
+
724
+ return $lines;
725
+ }
726
+
727
+ /**
728
+ * Validate if a classification code is in the correct format.
729
+ *
730
+ * Classification codes must be exactly 7 digits for Mongolian tax purposes.
731
+ *
732
+ * @param mixed $classificationCode The code to validate
733
+ *
734
+ * @return bool True if valid (exactly 7 digits), false otherwise
735
+ */
736
+ public static function isValidClassificationCode($classificationCode): bool
737
+ {
738
+ if ($classificationCode === null) {
739
+ return false;
740
+ }
741
+
742
+ $classificationCode = (string) $classificationCode;
743
+
744
+ // Must be exactly 7 digits
745
+ return preg_match('/^\d{7}$/', $classificationCode) === 1;
746
+ }
747
+
748
+ /**
749
+ * Validate if a tax product code is in the correct format.
750
+ *
751
+ * Tax product codes must be exactly 7 digits for Mongolian tax purposes.
752
+ *
753
+ * @param mixed $taxProductCode The code to validate
754
+ *
755
+ * @return bool True if valid (exactly 3 digits), false otherwise
756
+ */
757
+ public static function isValidTaxProductCode($taxProductCode): bool
758
+ {
759
+ if ($taxProductCode === null) {
760
+ return false;
761
+ }
762
+
763
+ $taxProductCode = (string) $taxProductCode;
764
+
765
+ // Must be exactly 3 digits
766
+ return preg_match('/^\d{3}$/', $taxProductCode) === 1;
767
+ }
768
+
769
+ /**
770
+ * Check if a classification code is tax-free (zero tax rate).
771
+ *
772
+ * @param mixed $classificationCode The code to check
773
+ *
774
+ * @return bool True if the code is in the zero tax list, false otherwise
775
+ */
776
+ public static function isTaxFreeClassificationCode($classificationCode): bool
777
+ {
778
+ if (!self::isValidClassificationCode($classificationCode)) {
779
+ return false;
780
+ }
781
+
782
+ return in_array((string) $classificationCode, self::$zeroTaxClassificationCodes, true);
783
+ }
784
+
785
+ /**
786
+ * Get the classification code for a cart item.
787
+ *
788
+ * Attempts to retrieve the classification code from the item's meta data first,
789
+ * then falls back to the product's meta data, and finally uses a default code.
790
+ *
791
+ * @param object $item The cart item
792
+ *
793
+ * @return string The classification code (7 digits)
794
+ */
795
+ public static function getCartItemClassificationCode($item): string
796
+ {
797
+ $classificationCode = '6511100';
798
+
799
+ // Try item meta
800
+ if (isset($item->meta)) {
801
+ $meta = is_string($item->meta)
802
+ ? json_decode($item->meta, true) // decode into array
803
+ : (array) $item->meta;
804
+
805
+ $metaCode = data_get($meta, 'classification_code');
806
+
807
+ if (self::isValidClassificationCode($metaCode)) {
808
+ return $metaCode;
809
+ }
810
+ }
811
+
812
+ // Try product meta fallback
813
+ if ($item->product_id) {
814
+ $product = Product::where('public_id', $item->product_id)->first();
815
+
816
+ if ($product && $product->hasMeta('classification_code')) {
817
+ $productCode = $product->getMeta('classification_code');
818
+ if (self::isValidClassificationCode($productCode)) {
819
+ return $productCode;
820
+ }
821
+ }
822
+ }
823
+
824
+ // Final fallback
825
+ return $classificationCode;
826
+ }
827
+
828
+ /**
829
+ * Get the tax_product_code for a cart item.
830
+ *
831
+ * Attempts to retrieve the tax product code from the item's meta data first,
832
+ * then falls back to the product's meta data, and finally uses a default code.
833
+ *
834
+ * @param object $item The cart item
835
+ *
836
+ * @return string The tax_product_code code (7 digits)
837
+ */
838
+ public static function getCartItemTaxProductCode($item): string
839
+ {
840
+ $taxProductCode = '319';
841
+
842
+ // Try item meta
843
+ if (isset($item->meta)) {
844
+ $meta = is_string($item->meta)
845
+ ? json_decode($item->meta, true) // decode into array
846
+ : (array) $item->meta;
847
+
848
+ $metaCode = data_get($meta, 'tax_product_code');
849
+
850
+ if (self::isValidTaxProductCode($metaCode)) {
851
+ return $metaCode;
852
+ }
853
+ }
854
+
855
+ // Try product meta fallback
856
+ if ($item->product_id) {
857
+ $product = Product::where('public_id', $item->product_id)->first();
858
+
859
+ if ($product && $product->hasMeta('tax_product_code')) {
860
+ $productCode = $product->getMeta('tax_product_code');
861
+ if (self::isValidTaxProductCode($productCode)) {
862
+ return $productCode;
863
+ }
864
+ }
865
+ }
866
+
867
+ // Final fallback
868
+ return $taxProductCode;
869
+ }
322
870
  }