@fleetbase/storefront-engine 0.4.0 → 0.4.1

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 (57) hide show
  1. package/addon/components/customer-panel/orders.hbs +2 -2
  2. package/addon/components/modals/create-gateway.hbs +33 -12
  3. package/addon/components/network-category-picker.js +1 -1
  4. package/addon/components/order-panel.hbs +1 -1
  5. package/addon/components/widget/customers.hbs +5 -2
  6. package/addon/components/widget/customers.js +14 -6
  7. package/addon/components/widget/orders.hbs +30 -9
  8. package/addon/components/widget/orders.js +7 -1
  9. package/addon/components/widget/storefront-key-metrics.js +2 -2
  10. package/addon/components/widget/storefront-metrics.hbs +11 -1
  11. package/addon/components/widget/storefront-metrics.js +103 -1
  12. package/addon/controllers/networks/index/network/index.js +2 -0
  13. package/addon/controllers/networks/index/network/stores.js +0 -1
  14. package/addon/controllers/settings/gateways.js +6 -1
  15. package/addon/controllers/settings/index.js +1 -0
  16. package/addon/controllers/settings/notifications.js +3 -5
  17. package/addon/models/network.js +1 -0
  18. package/addon/models/store.js +1 -0
  19. package/addon/routes/networks/index/network/index.js +5 -0
  20. package/addon/routes/settings/index.js +5 -0
  21. package/addon/services/order-actions.js +31 -0
  22. package/addon/styles/storefront-engine.css +22 -0
  23. package/addon/templates/networks/index/network/index.hbs +13 -0
  24. package/addon/templates/networks/index/network/stores.hbs +8 -1
  25. package/addon/templates/settings/gateways.hbs +15 -4
  26. package/addon/templates/settings/index.hbs +13 -0
  27. package/addon/templates/settings/notifications.hbs +2 -2
  28. package/addon/utils/commerce-date-ranges.js +263 -0
  29. package/app/utils/commerce-date-ranges.js +1 -0
  30. package/composer.json +1 -1
  31. package/extension.json +1 -1
  32. package/package.json +1 -1
  33. package/server/migrations/2025_09_01_041353_add_default_order_config_column.php +110 -0
  34. package/server/src/Console/Commands/MigrateStripeSandboxCustomers.php +150 -0
  35. package/server/src/Expansions/OrderExpansion.php +43 -0
  36. package/server/src/Http/Controllers/NetworkController.php +1 -1
  37. package/server/src/Http/Controllers/OrderController.php +62 -9
  38. package/server/src/Http/Controllers/v1/CheckoutController.php +45 -48
  39. package/server/src/Http/Controllers/v1/ServiceQuoteController.php +19 -3
  40. package/server/src/Http/Middleware/SetStorefrontSession.php +8 -6
  41. package/server/src/Http/Resources/Customer.php +41 -17
  42. package/server/src/Http/Resources/Gateway.php +16 -11
  43. package/server/src/Http/Resources/Network.php +35 -34
  44. package/server/src/Http/Resources/NotificationChannel.php +17 -10
  45. package/server/src/Http/Resources/Store.php +36 -35
  46. package/server/src/Models/Customer.php +18 -2
  47. package/server/src/Models/FoodTruck.php +15 -0
  48. package/server/src/Models/Network.php +63 -1
  49. package/server/src/Models/Store.php +71 -9
  50. package/server/src/Notifications/StorefrontOrderAccepted.php +154 -0
  51. package/server/src/Observers/OrderObserver.php +6 -4
  52. package/server/src/Providers/StorefrontServiceProvider.php +1 -0
  53. package/server/src/Rules/IsValidLocation.php +12 -0
  54. package/server/src/Support/PushNotification.php +13 -4
  55. package/server/src/Support/Storefront.php +199 -37
  56. package/server/src/routes.php +1 -0
  57. package/translations/en-us.yaml +5 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\Storefront\Support;
4
4
 
5
+ use Fleetbase\FleetOps\Flow\Activity;
5
6
  use Fleetbase\FleetOps\Models\Contact;
6
7
  use Fleetbase\FleetOps\Models\Order;
7
8
  use Fleetbase\FleetOps\Models\OrderConfig;
@@ -11,9 +12,12 @@ use Fleetbase\Storefront\Models\Gateway;
11
12
  use Fleetbase\Storefront\Models\Network;
12
13
  use Fleetbase\Storefront\Models\Product;
13
14
  use Fleetbase\Storefront\Models\Store;
15
+ use Fleetbase\Storefront\Notifications\StorefrontOrderAccepted;
14
16
  use Fleetbase\Storefront\Notifications\StorefrontOrderCreated;
15
17
  use Fleetbase\Support\Auth;
16
18
  use Fleetbase\Support\Utils;
19
+ use Illuminate\Support\Facades\DB;
20
+ use Illuminate\Support\Facades\Log;
17
21
  use Illuminate\Support\Facades\Notification;
18
22
  use Illuminate\Support\Facades\Redis;
19
23
  use Illuminate\Support\Str;
@@ -21,15 +25,20 @@ use Laravel\Sanctum\PersonalAccessToken;
21
25
 
22
26
  class Storefront
23
27
  {
28
+ /** @var string */
29
+ private const CONFIG_KEY = 'storefront';
30
+ /** @var string */
31
+ private const CONFIG_NS = 'system:order-config:storefront';
32
+ /** @var array<string,OrderConfig> In-memory cache keyed by company UUID */
33
+ private static array $configCache = [];
34
+
24
35
  /**
25
36
  * Returns current store or network based on session `storefront_key`
26
37
  * with bare minimum columns, but can optionally pass in more columns to receive.
27
38
  *
28
39
  * @param array $columns
29
- *
30
- * @return \Fleetbase\Models\Storefront\Network|\Fleetbase\Models\Storefront\Store
31
40
  */
32
- public static function about($columns = [], $with = [])
41
+ public static function about($columns = [], $with = []): Store|Network|null
33
42
  {
34
43
  $key = session('storefront_key');
35
44
 
@@ -38,7 +47,7 @@ class Storefront
38
47
  }
39
48
 
40
49
  if (is_array($columns)) {
41
- $columns = array_merge(['uuid', 'public_id', 'company_uuid', 'backdrop_uuid', 'logo_uuid', 'name', 'description', 'translations', 'website', 'facebook', 'instagram', 'twitter', 'email', 'phone', 'tags', 'currency', 'timezone', 'pod_method', 'options'], $columns);
50
+ $columns = array_merge(['uuid', 'public_id', 'company_uuid', 'backdrop_uuid', 'logo_uuid', 'order_config_uuid', 'name', 'description', 'translations', 'website', 'facebook', 'instagram', 'twitter', 'email', 'phone', 'tags', 'currency', 'timezone', 'pod_method', 'options'], $columns);
42
51
  }
43
52
 
44
53
  if (Str::startsWith($key, 'store')) {
@@ -53,10 +62,16 @@ class Storefront
53
62
  return $about;
54
63
  }
55
64
 
56
- public static function findAbout($id, $columns = [], $with = [])
65
+ /**
66
+ * Returns current store or network based ID param passed
67
+ * with bare minimum columns, but can optionally pass in more columns to receive.
68
+ *
69
+ * @param array $columns
70
+ */
71
+ public static function findAbout($id, $columns = [], $with = []): Store|Network|null
57
72
  {
58
73
  if (is_array($columns)) {
59
- $columns = array_merge(['uuid', 'public_id', 'company_uuid', 'backdrop_uuid', 'logo_uuid', 'name', 'description', 'translations', 'website', 'facebook', 'instagram', 'twitter', 'email', 'phone', 'tags', 'currency', 'timezone', 'pod_method', 'options'], $columns);
74
+ $columns = array_merge(['uuid', 'public_id', 'company_uuid', 'backdrop_uuid', 'logo_uuid', 'order_config_uuid', 'name', 'description', 'translations', 'website', 'facebook', 'instagram', 'twitter', 'email', 'phone', 'tags', 'currency', 'timezone', 'pod_method', 'options'], $columns);
60
75
  }
61
76
 
62
77
  if (Str::startsWith($id, 'store')) {
@@ -204,57 +219,106 @@ class Storefront
204
219
  return $stripeCustomer;
205
220
  }
206
221
 
207
- public static function patchOrderConfig(Order $order)
222
+ /**
223
+ * Ensure an Order has an effective OrderConfig.
224
+ * - If already set (FK or relation), return it.
225
+ * - Else resolve default for the order's company and patch the FK quietly.
226
+ *
227
+ * @param \Fleetbase\Storefront\Models\Order $order
228
+ *
229
+ * @return \Fleetbase\Storefront\Models\OrderConfig|null
230
+ */
231
+ public static function patchOrderConfig($order): ?OrderConfig
208
232
  {
209
- $orderConfig = $order->config();
210
- if (!$orderConfig) {
211
- $orderConfig = Storefront::getDefaultOrderConfig();
212
- if ($orderConfig) {
213
- $order->update(['order_config_uuid' => $orderConfig->uuid]);
214
- }
233
+ // FK fast-path
234
+ if (!empty($order->order_config_uuid)) {
235
+ return OrderConfig::where('uuid', $order->order_config_uuid)->first();
236
+ }
237
+
238
+ // Relation fast-path (if your Order has relation 'orderConfig')
239
+ $order->loadMissing('orderConfig');
240
+ if ($order->orderConfig instanceof OrderConfig) {
241
+ return $order->orderConfig;
242
+ }
243
+
244
+ // Resolve by company
245
+ $companyUuid = $order->company_uuid ?? ($order->company->uuid ?? null);
246
+ $orderConfig = static::getOrderConfig($companyUuid);
247
+
248
+ if ($orderConfig) {
249
+ // Quiet write to avoid events, if preferred
250
+ $order->forceFill(['order_config_uuid' => $orderConfig->uuid]);
251
+ method_exists($order, 'saveQuietly') ? $order->saveQuietly() : $order->save();
215
252
  }
216
253
 
217
254
  return $orderConfig;
218
255
  }
219
256
 
220
- public static function getDefaultOrderConfig()
257
+ /**
258
+ * Get the default Storefront OrderConfig for the "current" company context.
259
+ *
260
+ * @return \Fleetbase\Storefront\Models\OrderConfig|null
261
+ */
262
+ public static function getDefaultOrderConfig(): ?OrderConfig
221
263
  {
222
- $company = Auth::getCompany();
223
- if ($company) {
224
- return static::getOrderConfig($company);
225
- }
264
+ $company = session('company') ?? (method_exists(Auth::class, 'getCompany') ? Auth::getCompany() : null);
226
265
 
227
- return null;
266
+ return static::getOrderConfig($company);
228
267
  }
229
268
 
230
- public static function getOrderConfig(Company $company)
269
+ /**
270
+ * Get (or lazily create) the Storefront OrderConfig for a company.
271
+ * Accepts Company model, UUID string, or null (falls back to session company).
272
+ *
273
+ * @return \Fleetbase\Storefront\Models\OrderConfig|null
274
+ */
275
+ public static function getOrderConfig(Company|string|null $company): ?OrderConfig
231
276
  {
232
- $orderConfig = OrderConfig::where(['company_uuid' => $company->uuid, 'key' => 'storefront', 'namespace' => 'system:order-config:storefront'])->first();
233
- if (!$orderConfig) {
234
- $orderConfig = static::createStorefrontConfig($company);
277
+ $companyUuid = static::resolveCompanyUuid($company);
278
+ if (!$companyUuid) {
279
+ return null;
235
280
  }
236
281
 
237
- return $orderConfig;
282
+ // Request-lifetime cache
283
+ if (isset(static::$configCache[$companyUuid])) {
284
+ return static::$configCache[$companyUuid];
285
+ }
286
+
287
+ $attrs = [
288
+ 'company_uuid' => $companyUuid,
289
+ 'key' => self::CONFIG_KEY,
290
+ 'namespace' => self::CONFIG_NS,
291
+ ];
292
+
293
+ $config = OrderConfig::where($attrs)->first();
294
+ if (!$config) {
295
+ // Short transaction to avoid dupes under race
296
+ $config = DB::transaction(function () use ($attrs, $companyUuid) {
297
+ $existing = OrderConfig::where($attrs)->first();
298
+ if ($existing) {
299
+ return $existing;
300
+ }
301
+
302
+ return static::createStorefrontConfig($companyUuid);
303
+ });
304
+ }
305
+
306
+ return static::$configCache[$companyUuid] = $config;
238
307
  }
239
308
 
240
309
  /**
241
- * Creates or retrieves an existing storefront configuration for a given company.
242
- *
243
- * This method checks if a storefront configuration (OrderConfig) already exists for the given company.
244
- * If it exists, the method returns the existing configuration. Otherwise, it creates a new configuration with
245
- * predefined settings for a storefront order process. The configuration includes various stages like 'created',
246
- * 'started', 'canceled', 'completed', etc., each defined with specific attributes like key, code, color, logic,
247
- * events, status, actions, details, and more. These stages help manage the order lifecycle in a storefront context.
310
+ * Create (or retrieve) the Storefront config for a company.
311
+ * Accepts Company model or UUID string.
248
312
  *
249
- * @param Company $company the company for which the storefront configuration is being created or retrieved
250
- *
251
- * @return OrderConfig the storefront order configuration associated with the specified company
313
+ * @return \Fleetbase\Storefront\Models\OrderConfig
252
314
  */
253
- public static function createStorefrontConfig(Company $company): OrderConfig
315
+ public static function createStorefrontConfig(Company|string $company): OrderConfig
254
316
  {
317
+ $companyUuid = $company instanceof Company ? $company->uuid : $company;
318
+
255
319
  return OrderConfig::firstOrCreate(
256
320
  [
257
- 'company_uuid' => $company->uuid,
321
+ 'company_uuid' => $companyUuid,
258
322
  'key' => 'storefront',
259
323
  'namespace' => 'system:order-config:storefront',
260
324
  ],
@@ -343,7 +407,7 @@ class Storefront
343
407
  'require_pod' => false,
344
408
  ],
345
409
  'picked_up' => [
346
- 'key' => 'completed',
410
+ 'key' => 'picked_up',
347
411
  'code' => 'picked_up',
348
412
  'color' => '#1f2937',
349
413
  'logic' => [],
@@ -483,4 +547,102 @@ class Storefront
483
547
  ]
484
548
  );
485
549
  }
550
+
551
+ /**
552
+ * Normalize a Company|UUID|null to a UUID string (or null if unresolved).
553
+ */
554
+ protected static function resolveCompanyUuid(Company|string|null $company): ?string
555
+ {
556
+ if ($company instanceof Company && !empty($company->uuid)) {
557
+ return (string) $company->uuid;
558
+ }
559
+ if (is_string($company) && $company !== '' && (method_exists(Str::class, 'isUuid') ? Str::isUuid($company) : true)) {
560
+ return $company;
561
+ }
562
+ $sessionCompany = session('company');
563
+
564
+ return !empty($sessionCompany) ? (string) $sessionCompany : null;
565
+ }
566
+
567
+ public static function createAcceptedActivity(?OrderConfig $orderConfig = null): Activity
568
+ {
569
+ return new Activity([
570
+ 'key' => 'accepted',
571
+ 'code' => 'accepted',
572
+ 'color' => '#1f2937',
573
+ 'logic' => [],
574
+ 'events' => [],
575
+ 'status' => 'Order has been accepted',
576
+ 'actions' => [],
577
+ 'details' => 'Order has been accepted by {storefront.name}',
578
+ 'options' => [],
579
+ 'complete' => false,
580
+ 'entities' => [],
581
+ 'sequence' => 0,
582
+ 'activities' => ['dispatched'],
583
+ 'internalId' => Str::uuid(),
584
+ 'pod_method' => 'scan',
585
+ 'require_pod' => false,
586
+ ], $orderConfig ? $orderConfig->activities()->toArray() : []);
587
+ }
588
+
589
+ public static function autoAcceptOrder(Order $order)
590
+ {
591
+ // Patch order config
592
+ $orderConfig = static::patchOrderConfig($order);
593
+ $activity = static::createAcceptedActivity($orderConfig);
594
+
595
+ // Dispatch already if order is a pickup
596
+ if ($order->isMeta('is_pickup')) {
597
+ $order->firstDispatchWithActivity();
598
+ }
599
+
600
+ // Set order as accepted
601
+ try {
602
+ $order->setStatus($activity->code);
603
+ $order->insertActivity($activity, $order->getLastLocation());
604
+ } catch (\Exception $e) {
605
+ Log::debug('[Storefront] was able to accept an order.', ['order' => $order, 'activity' => $activity]);
606
+
607
+ return response()->error('Unable to accept order.');
608
+ }
609
+
610
+ // Notify customer order was accepted
611
+ try {
612
+ $order->customer->notify(new StorefrontOrderAccepted($order));
613
+ } catch (\Exception $e) {
614
+ }
615
+
616
+ return $order;
617
+ }
618
+
619
+ public static function autoDispatchOrder(Order $order, bool $adhoc = true)
620
+ {
621
+ // Patch order config
622
+ Storefront::patchOrderConfig($order);
623
+
624
+ if ($order->isMeta('is_pickup')) {
625
+ $order->updateStatus('pickup_ready');
626
+
627
+ return $order;
628
+ }
629
+
630
+ // toggle order to adhoc
631
+ if ($adhoc === true) {
632
+ $order->update(['adhoc' => true]);
633
+ } else {
634
+ // Find nearest driver and assign
635
+ $driver = $order->findClosestDrivers()->first();
636
+ if ($driver) {
637
+ $order->assignDriver($driver);
638
+ } else {
639
+ // no driver available to make adhoc
640
+ $order->update(['adhoc' => true]);
641
+ }
642
+ }
643
+
644
+ $order->dispatchWithActivity();
645
+
646
+ return $order;
647
+ }
486
648
  }
@@ -161,6 +161,7 @@ Route::prefix(config('storefront.api.routing.prefix', 'storefront'))->namespace(
161
161
  function ($router, $controller) {
162
162
  $router->post('accept', $controller('acceptOrder'));
163
163
  $router->post('ready', $controller('markOrderAsReady'));
164
+ $router->post('preparing', $controller('markOrderAsPreparing'));
164
165
  $router->post('completed', $controller('markOrderAsCompleted'));
165
166
  }
166
167
  );
@@ -234,6 +234,8 @@ storefront:
234
234
  cancel-order: Cancel Order
235
235
  accept-order: Accept Order!
236
236
  mark-as-ready: Mark as Ready!
237
+ dispatch: Dispatch
238
+ mark-as-preparing: Mark as Preparing
237
239
  mark-as-completed: Mark as Completed
238
240
  more-details: More Details
239
241
  customer: Customer
@@ -258,6 +260,9 @@ storefront:
258
260
  mark-as-completed-modal-title: Are you sure you want to mark order as completed?
259
261
  mark-as-completed-modal-body: Marking the order as completed is a confirmation that the customer has picked up the order and the order is completed.
260
262
  mark-as-completed-accept-button-text: Order Completed!
263
+ mark-as-preparing-modal-title: Are you sure you want to mark order as preparing?
264
+ mark-as-preparing-modal-body: Marking the order as preparing is a confirmation that the order is being prepared.
265
+ mark-as-preparing-accept-button-text: Order Preparing
261
266
  assign-driver-modal-title: Assign Driver
262
267
  assign-driver-modal-accept-button-text: Assign Driver
263
268
  storefront-metrics: