@fleetbase/storefront-engine 0.4.7 → 0.4.9

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 (43) 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/index.js +0 -10
  34. package/package.json +4 -4
  35. package/server/src/Http/Controllers/ActionController.php +61 -0
  36. package/server/src/Http/Controllers/v1/CheckoutController.php +25 -9
  37. package/server/src/Models/Cart.php +2 -1
  38. package/server/src/Models/Network.php +25 -0
  39. package/server/src/Models/Store.php +25 -0
  40. package/server/src/Notifications/PromotionalPushNotification.php +166 -0
  41. package/server/src/Support/QPay.php +475 -3
  42. package/server/src/routes.php +1 -0
  43. package/translations/en-us.yaml +28 -0
@@ -39,7 +39,11 @@
39
39
  />
40
40
  </InputGroup>
41
41
 
42
- <ContentPanel @title={{t "storefront.networks.index.network.index.general-network-settings-form.contact-social-panel.panel-title"}} @open={{false}} @pad={{false}} @wrapperClass="bordered-top">
42
+ <ContentPanel
43
+ @title={{t "storefront.networks.index.network.index.general-network-settings-form.contact-social-panel.panel-title"}}
44
+ @open={{false}}
45
+ @wrapperClass="bordered-classic"
46
+ >
43
47
  <InputGroup @name={{t "storefront.networks.index.network.index.general-network-settings-form.contact-social-panel.phone"}}>
44
48
  <PhoneInput @value={{@model.phone}} @onInput={{fn (mut @model.phone)}} class="form-input w-full" />
45
49
  </InputGroup>
@@ -75,7 +79,11 @@
75
79
  />
76
80
  </ContentPanel>
77
81
 
78
- <ContentPanel @title={{t "storefront.networks.index.network.index.general-network-settings-form.logo-backdrop-panel.panel-title"}} @open={{false}} @pad={{false}} @wrapperClass="bordered-top">
82
+ <ContentPanel
83
+ @title={{t "storefront.networks.index.network.index.general-network-settings-form.logo-backdrop-panel.panel-title"}}
84
+ @open={{false}}
85
+ @wrapperClass="bordered-classic"
86
+ >
79
87
  <InputGroup
80
88
  @name={{t "storefront.networks.index.network.index.general-network-settings-form.logo-backdrop-panel.logo-label"}}
81
89
  @helpText={{t "storefront.networks.index.network.index.general-network-settings-form.logo-backdrop-panel.logo-help-text"}}
@@ -125,12 +133,7 @@
125
133
  </InputGroup>
126
134
  </ContentPanel>
127
135
 
128
- <ContentPanel
129
- @title={{t "storefront.networks.index.network.index.general-network-settings-form.alert-panel.panel-title"}}
130
- @open={{false}}
131
- @pad={{true}}
132
- @panelBodyClass="bg-gray-800"
133
- >
136
+ <ContentPanel @title={{t "storefront.networks.index.network.index.general-network-settings-form.alert-panel.panel-title"}} @open={{false}} @wrapperClass="bordered-classic">
134
137
  <div>
135
138
  <p class="dark:text-gray-100 mb-4">{{t "storefront.networks.index.network.index.general-network-settings-form.alert-panel.panel-description"}}</p>
136
139
  <InputGroup @name={{t "storefront.networks.index.network.index.general-network-settings-form.alert-panel.new-order-alert-label"}} @wrapperClass="mb-0">
@@ -175,6 +178,25 @@
175
178
  />
176
179
  {{/if}}
177
180
  </div>
181
+ <div class="input-group">
182
+ <Toggle
183
+ @isToggled={{@model.options.required_checkout_min}}
184
+ @onToggle={{fn (mut @model.options.required_checkout_min)}}
185
+ @helpText={{t "storefront.networks.index.network.index.general-network-settings-form.config-switches.minimum-order-amount-input-help-text"}}
186
+ >
187
+ <FaIcon @icon="dollar-sign" class="text-gray-600 dark:text-gray-400 mx-2" /><span class="dark:text-gray-100 text-sm">{{t
188
+ "storefront.networks.index.network.index.general-network-settings-form.config-switches.enable-minimum-order-amount"
189
+ }}</span>
190
+ </Toggle>
191
+ {{#if @model.options.required_checkout_min}}
192
+ <MoneyInput
193
+ @wrapperClass="mb-0 mt-2"
194
+ @name={{t "storefront.networks.index.network.index.general-network-settings-form.config-switches.minimum-order-amount-input-label"}}
195
+ @value={{@model.options.required_checkout_min_amount}}
196
+ @currency={{@model.currency}}
197
+ />
198
+ {{/if}}
199
+ </div>
178
200
  <div class="input-group">
179
201
  <Toggle @isToggled={{@model.options.auto_accept_orders}} @onToggle={{fn (mut @model.options.auto_accept_orders)}}>
180
202
  <FaIcon @icon="robot" class="text-gray-600 dark:text-gray-400 mx-2" /><span class="dark:text-gray-100 text-sm">{{t
@@ -2,37 +2,16 @@
2
2
  <Overlay::Header @title={{t "storefront.networks.index.network.manage-network"}} @onPressCancel={{this.transitionBack}} />
3
3
  <Overlay::Body class="p-0i" @increaseInnerBodyHeightBy="0" @wrapperClass="space-y-4 pt-4">
4
4
  <div class="flex flex-col">
5
- <div class="px-4 flex items-center section-header-title mb-4">
5
+ <div class="px-4 flex items-center section-header-title">
6
6
  <FaIcon @icon="network-wired" @size="lg" class="text-sky-500 mr-3" />
7
7
  <h3 class="text-lg font-semibold dark:text-white">
8
8
  {{@model.name}}
9
9
  </h3>
10
10
  </div>
11
- <div class="section-header-actions w-full overflow-x-scroll lg:overflow-x-auto">
12
- <div class="ui-tabs">
13
- <nav>
14
- <LinkTo @route="networks.index.network.index" class="ui-tab">
15
- <FaIcon @icon="cogs" class="mr-2" />
16
- <span>{{t "storefront.networks.index.network.settings"}}</span>
17
- </LinkTo>
18
- <LinkTo @route="networks.index.network.stores" class="ui-tab">
19
- <FaIcon @icon="store" class="mr-2" />
20
- <span>{{t "storefront.networks.index.network.stores.store"}}</span>
21
- </LinkTo>
22
- <LinkTo @route="networks.index.network.orders" class="ui-tab">
23
- <FaIcon @icon="file-invoice-dollar" class="mr-2" />
24
- <span>{{t "storefront.networks.index.network.orders"}}</span>
25
- </LinkTo>
26
- <LinkTo @route="networks.index.network.customers" class="ui-tab">
27
- <FaIcon @icon="users" class="mr-2" />
28
- <span>{{t "storefront.networks.index.network.customers"}}</span>
29
- </LinkTo>
30
- </nav>
31
- </div>
32
- </div>
33
11
  </div>
34
-
35
- <div>{{outlet}}</div>
36
- <Spacer @height="300px" />
12
+ <TabNavigation @tabs={{this.tabs}}>
13
+ {{outlet}}
14
+ <Spacer @height="400px" />
15
+ </TabNavigation>
37
16
  </Overlay::Body>
38
17
  </Overlay>
@@ -0,0 +1,85 @@
1
+ <div class="overflow-y-scroll h-screen">
2
+ <div class="h-screen">
3
+ <main class="h-screen max-w-xl mx-auto pt-4">
4
+ <form class="h-screen" {{on "submit" this.sendPushNotification}}>
5
+ <div class="space-y-4 h-screen">
6
+ <div>
7
+ <h1 class="text-lg leading-6 font-bold text-gray-900 dark:text-gray-100">
8
+ {{t "storefront.promotions.push-notifications.header"}}
9
+ </h1>
10
+ <p class="mt-1 text-sm text-gray-500">
11
+ {{t "storefront.promotions.push-notifications.description"}}
12
+ </p>
13
+ </div>
14
+
15
+ <InputGroup
16
+ @name={{t "storefront.promotions.push-notifications.title-label"}}
17
+ @value={{this.title}}
18
+ @placeholder={{t "storefront.promotions.push-notifications.title-placeholder"}}
19
+ @helpText={{t "storefront.promotions.push-notifications.title-help-text"}}
20
+ />
21
+
22
+ <InputGroup @name={{t "storefront.promotions.push-notifications.body-label"}} @helpText={{t "storefront.promotions.push-notifications.body-help-text"}}>
23
+ <Textarea @value={{this.body}} placeholder={{t "storefront.promotions.push-notifications.body-placeholder"}} rows="4" class="form-input w-full" />
24
+ </InputGroup>
25
+
26
+ <div class="mb-4">
27
+ <Toggle
28
+ @isToggled={{this.selectAllCustomers}}
29
+ @onToggle={{fn (mut this.selectAllCustomers)}}
30
+ @wrapperClass="justify-start"
31
+ @label={{t "storefront.promotions.push-notifications.select-all-customers"}}
32
+ @helpText={{t "storefront.promotions.push-notifications.select-all-customers-help-text"}}
33
+ />
34
+ </div>
35
+
36
+ {{#unless this.selectAllCustomers}}
37
+ <InputGroup @name={{t "storefront.promotions.push-notifications.customers-label"}}>
38
+ <ModelSelectMultiple
39
+ @modelName="contact"
40
+ @query={{hash type="customer"}}
41
+ @selectedModel={{this.selectedCustomers}}
42
+ @placeholder={{t "storefront.promotions.push-notifications.customers-placeholder"}}
43
+ @infiniteScroll={{true}}
44
+ @renderInPlace={{true}}
45
+ @onChange={{fn (mut this.selectedCustomers)}}
46
+ @triggerClass="form-select form-input w-full"
47
+ as |model|
48
+ >
49
+ <div class="flex flex-row space-x-2">
50
+ <div class="hide-from-trigger">
51
+ <Image
52
+ src={{model.photo_url}}
53
+ @fallbackSrc={{config "defaultValues.customerImage"}}
54
+ alt={{model.name}}
55
+ height="40"
56
+ width="40"
57
+ class="h-10 w-10 rounded-lg shadow-sm"
58
+ />
59
+ </div>
60
+ <div>
61
+ <div class="font-semibold normalize-in-trigger">{{model.name}}</div>
62
+ <div class="text-xs hide-from-trigger">{{n-a model.email}}</div>
63
+ <div class="text-xs hide-from-trigger">{{n-a model.phone}}</div>
64
+ </div>
65
+ </div>
66
+ </ModelSelectMultiple>
67
+ </InputGroup>
68
+ {{/unless}}
69
+
70
+ <div class="flex justify-end">
71
+ <Button
72
+ @type="primary"
73
+ @icon={{if this.isLoading "spinner" "paper-plane"}}
74
+ @iconPrefix={{if this.isLoading "fas fa-spin" "fas"}}
75
+ @text={{t "storefront.promotions.push-notifications.send-button"}}
76
+ @isLoading={{this.isLoading}}
77
+ @disabled={{this.isLoading}}
78
+ @onClick={{this.sendPushNotification}}
79
+ />
80
+ </div>
81
+ </div>
82
+ </form>
83
+ </main>
84
+ </div>
85
+ </div>
@@ -0,0 +1,8 @@
1
+ <TabNavigation @tabs={{this.tabs}} @contentClass="scrollable" @tablistClass="px-4 flex-row-reverse">
2
+ <:actions>
3
+ <h3 class="uppercase text-xs tracking-wide text-gray-700 dark:text-gray-400 font-semibold">{{t "storefront.promotions.title"}}</h3>
4
+ </:actions>
5
+ <:default>
6
+ {{outlet}}
7
+ </:default>
8
+ </TabNavigation>
@@ -254,6 +254,25 @@
254
254
  />
255
255
  {{/if}}
256
256
  </div>
257
+ <div class="input-group">
258
+ <Toggle
259
+ @isToggled={{@model.options.required_checkout_min}}
260
+ @onToggle={{fn (mut @model.options.required_checkout_min)}}
261
+ @helpText={{t "storefront.settings.index.minimum-order-amount-help-text"}}
262
+ >
263
+ <FaIcon @icon="dollar-sign" class="text-gray-600 dark:text-gray-400 mx-2" /><span class="dark:text-gray-100 text-sm">{{t
264
+ "storefront.settings.index.enable-minimum-order-amount"
265
+ }}</span>
266
+ </Toggle>
267
+ {{#if @model.options.required_checkout_min}}
268
+ <MoneyInput
269
+ @wrapperClass="mb-0 mt-2"
270
+ @name={{t "storefront.settings.index.minimum-order-amount"}}
271
+ @value={{@model.options.required_checkout_min_amount}}
272
+ @currency={{@model.currency}}
273
+ />
274
+ {{/if}}
275
+ </div>
257
276
  <div class="input-group">
258
277
  <Toggle @isToggled={{@model.options.auto_accept_orders}} @onToggle={{fn (mut @model.options.auto_accept_orders)}}>
259
278
  <FaIcon @icon="robot" class="text-gray-600 dark:text-gray-400 mx-2" /><span class="dark:text-gray-100 text-sm">{{t
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/storefront-engine/controllers/promotions/push-notifications';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/storefront-engine/controllers/promotions';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/storefront-engine/routes/promotions/push-notifications';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/storefront-engine/routes/promotions';
@@ -0,0 +1 @@
1
+ {{outlet}}
@@ -0,0 +1 @@
1
+ {{outlet}}
package/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/storefront-api",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Headless Commerce & Marketplace Extension for Fleetbase",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
@@ -29,7 +29,7 @@ function getMountedEngineRoutePrefix() {
29
29
  mountedEngineRoutePrefix = fleetbase.route;
30
30
  }
31
31
 
32
- return `console.${mountedEngineRoutePrefix}.`;
32
+ return `console.${mountedEngineRoutePrefix}`;
33
33
  }
34
34
 
35
35
  function getenv(variable, defaultValue = null) {
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Storefront",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Headless Commerce & Marketplace Extension for Fleetbase",
5
5
  "repository": "https://github.com/fleetbase/storefront",
6
6
  "license": "AGPL-3.0-or-later",
package/index.js CHANGED
@@ -8,16 +8,6 @@ const path = require('path');
8
8
  module.exports = buildEngine({
9
9
  name,
10
10
 
11
- postprocessTree(type, tree) {
12
- if (type === 'css') {
13
- tree = new Funnel(tree, {
14
- exclude: ['**/@fleetbase/ember-ui/**/*.css'],
15
- });
16
- }
17
-
18
- return tree;
19
- },
20
-
21
11
  lazyLoading: {
22
12
  enabled: true,
23
13
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/storefront-engine",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Headless Commerce & Marketplace Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "storefront",
@@ -44,9 +44,9 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@babel/core": "^7.23.2",
47
- "@fleetbase/ember-core": "^0.3.6",
48
- "@fleetbase/ember-ui": "^0.3.9",
49
- "@fleetbase/fleetops-data": "^0.1.20",
47
+ "@fleetbase/ember-core": "^0.3.8",
48
+ "@fleetbase/ember-ui": "^0.3.14",
49
+ "@fleetbase/fleetops-data": "^0.1.23",
50
50
  "@fortawesome/ember-fontawesome": "^2.0.0",
51
51
  "@fortawesome/fontawesome-svg-core": "6.4.0",
52
52
  "@fortawesome/free-brands-svg-icons": "6.4.0",
@@ -95,4 +95,65 @@ class ActionController extends Controller
95
95
 
96
96
  return response()->json($metrics);
97
97
  }
98
+
99
+ /**
100
+ * Send promotional push notification to selected customers.
101
+ *
102
+ * @return \Illuminate\Http\Response
103
+ */
104
+ public function sendPushNotification(Request $request)
105
+ {
106
+ $title = $request->input('title');
107
+ $body = $request->input('body');
108
+ $customerIds = $request->input('customers', []);
109
+ $storeId = $request->input('store');
110
+ $selectAll = $request->boolean('select_all', false);
111
+
112
+ // Validate inputs
113
+ if (!$title || !$body) {
114
+ return response()->json(['error' => 'Title and body are required'], 400);
115
+ }
116
+
117
+ if (!$selectAll && empty($customerIds)) {
118
+ return response()->json(['error' => 'At least one customer must be selected'], 400);
119
+ }
120
+
121
+ // Get the store
122
+ $store = Store::where('public_id', $storeId)->first();
123
+ if (!$store) {
124
+ return response()->json(['error' => 'Store not found'], 404);
125
+ }
126
+
127
+ // Get customers
128
+ if ($selectAll) {
129
+ // Get all customers for this store's company
130
+ $customers = Contact::where('company_uuid', session('company'))
131
+ ->where('type', 'customer')
132
+ ->get();
133
+ } else {
134
+ // Get only selected customers
135
+ $customers = Contact::whereIn('uuid', $customerIds)
136
+ ->where('company_uuid', session('company'))
137
+ ->where('type', 'customer')
138
+ ->get();
139
+ }
140
+
141
+ // Send notifications
142
+ $sentCount = 0;
143
+ foreach ($customers as $customer) {
144
+ try {
145
+ $customer->notify(new \Fleetbase\Storefront\Notifications\PromotionalPushNotification($title, $body, $store));
146
+ $sentCount++;
147
+ } catch (\Exception $e) {
148
+ // Log error but continue with other customers
149
+ \Log::error('Failed to send push notification to customer: ' . $customer->uuid, ['error' => $e->getMessage()]);
150
+ }
151
+ }
152
+
153
+ return response()->json([
154
+ 'status' => 'OK',
155
+ 'sent_count' => $sentCount,
156
+ 'total' => count($customers),
157
+ ]);
158
+ }
98
159
  }
@@ -63,7 +63,7 @@ class CheckoutController extends Controller
63
63
 
64
64
  // handle cash orders
65
65
  if ($isCashOnDelivery) {
66
- return static::initializeCashCheckout($customer, $gateway, $serviceQuote, $cart, $checkoutOptions);
66
+ return static::initializeCashCheckout($customer, $gateway, $serviceQuote, $cart, $checkoutOptions, $request);
67
67
  }
68
68
 
69
69
  if (!$gateway) {
@@ -72,18 +72,18 @@ class CheckoutController extends Controller
72
72
 
73
73
  // handle checkout initialization based on gateway
74
74
  if ($gateway->isStripeGateway) {
75
- return static::initializeStripeCheckout($customer, $gateway, $serviceQuote, $cart, $checkoutOptions);
75
+ return static::initializeStripeCheckout($customer, $gateway, $serviceQuote, $cart, $checkoutOptions, $request);
76
76
  }
77
77
 
78
78
  // handle checkout initialization based on gateway
79
79
  if ($gateway->isQPayGateway) {
80
- return static::initializeQPayCheckout($customer, $gateway, $serviceQuote, $cart, $checkoutOptions);
80
+ return static::initializeQPayCheckout($customer, $gateway, $serviceQuote, $cart, $checkoutOptions, $request);
81
81
  }
82
82
 
83
83
  return response()->apiError('Unable to initialize checkout!');
84
84
  }
85
85
 
86
- public static function initializeCashCheckout(Contact $customer, Gateway $gateway, ServiceQuote $serviceQuote, Cart $cart, $checkoutOptions)
86
+ public static function initializeCashCheckout(Contact $customer, Gateway $gateway, ServiceQuote $serviceQuote, Cart $cart, $checkoutOptions, $request)
87
87
  {
88
88
  // check if pickup order
89
89
  $isPickup = $checkoutOptions->is_pickup;
@@ -132,7 +132,7 @@ class CheckoutController extends Controller
132
132
  ]);
133
133
  }
134
134
 
135
- public static function initializeStripeCheckout(Contact $customer, Gateway $gateway, ?ServiceQuote $serviceQuote, Cart $cart, $checkoutOptions)
135
+ public static function initializeStripeCheckout(Contact $customer, Gateway $gateway, ?ServiceQuote $serviceQuote, Cart $cart, $checkoutOptions, $request)
136
136
  {
137
137
  // check if pickup order
138
138
  $isPickup = $checkoutOptions->is_pickup;
@@ -416,7 +416,7 @@ class CheckoutController extends Controller
416
416
  ]);
417
417
  }
418
418
 
419
- public static function initializeQPayCheckout(Contact $customer, Gateway $gateway, ?ServiceQuote $serviceQuote, Cart $cart, $checkoutOptions)
419
+ public static function initializeQPayCheckout(Contact $customer, Gateway $gateway, ?ServiceQuote $serviceQuote, Cart $cart, $checkoutOptions, $request)
420
420
  {
421
421
  // Get store info
422
422
  $about = Storefront::about();
@@ -424,6 +424,12 @@ class CheckoutController extends Controller
424
424
  // check if pickup order
425
425
  $isPickup = $checkoutOptions->is_pickup;
426
426
 
427
+ // Get ebarimt company registration number if any
428
+ $ebarimtRegistationNumber = $request->ebarimt_registration_no ?? '';
429
+ if ($ebarimtRegistationNumber) {
430
+ $customer->updateMeta('ebarimt_registration_no', $ebarimtRegistationNumber);
431
+ }
432
+
427
433
  // get amount/subtotal
428
434
  $amount = static::calculateCheckoutAmount($cart, $serviceQuote, $checkoutOptions);
429
435
  $currency = $cart->getCurrency();
@@ -470,6 +476,7 @@ class CheckoutController extends Controller
470
476
  $callbackUrl = Utils::apiUrl('storefront/v1/checkouts/capture-qpay', $callbackParams);
471
477
 
472
478
  // Create invoice description
479
+ $taxType = '1'; // Start with VAT required
473
480
  $ebarimtInvoiceCode = $gateway->sandbox ? 'TEST_INVOICE' : $gateway->config?->ebarimt_invoice_id ?? null;
474
481
  $invoiceAmount = $amount;
475
482
  $invoiceCode = $gateway->sandbox ? 'TEST_INVOICE' : $gateway->config?->invoice_id ?? null;
@@ -478,18 +485,25 @@ class CheckoutController extends Controller
478
485
  $senderInvoiceNo = $checkout->public_id;
479
486
  $districtCode = $gateway->config?->district_code ?? null;
480
487
  $invoiceReceiverData = Utils::filterArray([
488
+ 'register' => $ebarimtRegistationNumber,
481
489
  'name' => $customer->name,
482
490
  'email' => $customer->email ?? null,
483
491
  'phone' => $customer->phone ?? null,
484
492
  ]);
493
+
494
+ // Create QPay line items
485
495
  $lines = QPay::createQpayInitialLines($cart, $serviceQuote, $checkoutOptions);
486
496
  foreach ($cart->items as $item) {
487
- $lines[] = [
497
+ $classificationCode = QPay::getCartItemClassificationCode($item);
498
+ $isVatExempt = QPay::isTaxFreeClassificationCode($classificationCode);
499
+
500
+ $line = [
488
501
  'line_description' => $item->name,
489
502
  'line_quantity' => number_format($item->quantity ?? 1, 2, '.', ''),
490
503
  'line_unit_price' => number_format($item->price, 2, '.', ''),
491
504
  'note' => $checkout->public_id,
492
- 'classification_code' => '6511100',
505
+ 'classification_code' => $classificationCode,
506
+ 'tax_product_code' => QPay::getCartItemTaxProductCode($item),
493
507
  'taxes' => [
494
508
  [
495
509
  'tax_code' => 'VAT',
@@ -499,12 +513,14 @@ class CheckoutController extends Controller
499
513
  ],
500
514
  ],
501
515
  ];
516
+
517
+ $lines[] = $line;
502
518
  }
503
519
 
504
520
  // Create qpay invoice
505
521
  $invoice = null;
506
522
  if ($ebarimtInvoiceCode) {
507
- $invoice = $qpay->createEbarimtInvoice($ebarimtInvoiceCode, $senderInvoiceNo, $invoiceReceiverCode, $invoiceReceiverData, $invoiceDescription, '1', $districtCode, $lines);
523
+ $invoice = $qpay->createEbarimtInvoice($ebarimtInvoiceCode, $senderInvoiceNo, $invoiceReceiverCode, $invoiceReceiverData, $invoiceDescription, $taxType, $districtCode, $lines);
508
524
  } else {
509
525
  $invoice = $qpay->createSimpleInvoice($invoiceAmount, $invoiceCode, $invoiceDescription, $invoiceReceiverCode, $senderInvoiceNo, $callbackUrl);
510
526
  }
@@ -332,6 +332,7 @@ class Cart extends StorefrontModel
332
332
  'subtotal' => $subtotal,
333
333
  'variants' => $variants,
334
334
  'addons' => $addons,
335
+ 'meta' => $product->meta ?? [],
335
336
  ];
336
337
 
337
338
  // If item was added from a food truck
@@ -677,7 +678,7 @@ class Cart extends StorefrontModel
677
678
  */
678
679
  public static function findProduct(string $id): ?Product
679
680
  {
680
- return Product::select(['uuid', 'store_uuid', 'public_id', 'name', 'description', 'price', 'currency', 'sale_price', 'is_on_sale'])->where(['public_id' => $id])->with([])->first();
681
+ return Product::select(['uuid', 'store_uuid', 'public_id', 'name', 'description', 'price', 'currency', 'sale_price', 'is_on_sale', 'meta'])->where(['public_id' => $id])->with([])->first();
681
682
  }
682
683
 
683
684
  public function getCurrency(?string $fallbackCurrency = null): ?string
@@ -3,6 +3,7 @@
3
3
  namespace Fleetbase\Storefront\Models;
4
4
 
5
5
  use Fleetbase\Casts\Json;
6
+ use Fleetbase\Casts\Money;
6
7
  use Fleetbase\FleetOps\Models\OrderConfig;
7
8
  use Fleetbase\FleetOps\Support\Utils;
8
9
  use Fleetbase\Models\Category;
@@ -228,6 +229,30 @@ class Network extends StorefrontModel
228
229
  return $this->stores()->count();
229
230
  }
230
231
 
232
+ /**
233
+ * Mutator for the `options` attribute.
234
+ * Allows special handling for nested keys while keeping JSON casting intact.
235
+ */
236
+ public function setOptionsAttribute($value)
237
+ {
238
+ // Ensure we operate on a PHP array
239
+ if (is_string($value)) {
240
+ $value = json_decode($value, true) ?? [];
241
+ }
242
+
243
+ if (!is_array($value)) {
244
+ $value = [];
245
+ }
246
+
247
+ // Special handling for `required_checkout_min_amount`
248
+ if (array_key_exists('required_checkout_min_amount', $value)) {
249
+ $value['required_checkout_min_amount'] = Money::apply($value['required_checkout_min_amount']);
250
+ }
251
+
252
+ // Assign the array back — JSON cast handles encoding
253
+ $this->attributes['options'] = $value;
254
+ }
255
+
231
256
  /**
232
257
  * Adds a new store to the network.
233
258
  */
@@ -3,6 +3,7 @@
3
3
  namespace Fleetbase\Storefront\Models;
4
4
 
5
5
  use Fleetbase\Casts\Json;
6
+ use Fleetbase\Casts\Money;
6
7
  use Fleetbase\FleetOps\Models\OrderConfig;
7
8
  use Fleetbase\FleetOps\Models\Place;
8
9
  use Fleetbase\Models\Category;
@@ -292,6 +293,30 @@ class Store extends StorefrontModel
292
293
  ->wherePivotNull('deleted_at');
293
294
  }
294
295
 
296
+ /**
297
+ * Mutator for the `options` attribute.
298
+ * Allows special handling for nested keys while keeping JSON casting intact.
299
+ */
300
+ public function setOptionsAttribute($value)
301
+ {
302
+ // Ensure we operate on a PHP array
303
+ if (is_string($value)) {
304
+ $value = json_decode($value, true) ?? [];
305
+ }
306
+
307
+ if (!is_array($value)) {
308
+ $value = [];
309
+ }
310
+
311
+ // Special handling for `required_checkout_min_amount`
312
+ if (array_key_exists('required_checkout_min_amount', $value)) {
313
+ $value['required_checkout_min_amount'] = Money::apply($value['required_checkout_min_amount']);
314
+ }
315
+
316
+ // Assign the array back — JSON cast handles encoding
317
+ $this->attributes['options'] = $value;
318
+ }
319
+
295
320
  /**
296
321
  * @var string
297
322
  */