@fleetbase/storefront-engine 0.4.0 → 0.4.2

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 (77) 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 +31 -10
  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/products/index/category/new.js +13 -2
  15. package/addon/controllers/settings/gateways.js +6 -1
  16. package/addon/controllers/settings/index.js +1 -0
  17. package/addon/controllers/settings/notifications.js +4 -5
  18. package/addon/models/network.js +1 -0
  19. package/addon/models/store.js +1 -0
  20. package/addon/routes/networks/index/network/index.js +5 -0
  21. package/addon/routes/settings/index.js +5 -0
  22. package/addon/services/order-actions.js +31 -0
  23. package/addon/styles/storefront-engine.css +22 -0
  24. package/addon/templates/home.hbs +2 -2
  25. package/addon/templates/networks/index/network/index.hbs +21 -8
  26. package/addon/templates/networks/index/network/stores.hbs +8 -1
  27. package/addon/templates/products/index/category/new.hbs +149 -134
  28. package/addon/templates/settings/gateways.hbs +15 -4
  29. package/addon/templates/settings/index.hbs +13 -0
  30. package/addon/templates/settings/notifications.hbs +2 -2
  31. package/addon/utils/commerce-date-ranges.js +263 -0
  32. package/app/utils/commerce-date-ranges.js +1 -0
  33. package/composer.json +1 -1
  34. package/extension.json +1 -1
  35. package/package.json +4 -4
  36. package/server/migrations/2025_09_01_041353_add_default_order_config_column.php +110 -0
  37. package/server/src/Console/Commands/MigrateStripeSandboxCustomers.php +165 -0
  38. package/server/src/Expansions/OrderExpansion.php +43 -0
  39. package/server/src/Http/Controllers/NetworkController.php +1 -1
  40. package/server/src/Http/Controllers/OrderController.php +62 -9
  41. package/server/src/Http/Controllers/v1/CheckoutController.php +57 -48
  42. package/server/src/Http/Controllers/v1/ServiceQuoteController.php +19 -3
  43. package/server/src/Http/Middleware/SetStorefrontSession.php +8 -6
  44. package/server/src/Http/Resources/Customer.php +41 -17
  45. package/server/src/Http/Resources/Gateway.php +16 -11
  46. package/server/src/Http/Resources/Network.php +35 -34
  47. package/server/src/Http/Resources/NotificationChannel.php +17 -10
  48. package/server/src/Http/Resources/Product.php +1 -0
  49. package/server/src/Http/Resources/Store.php +36 -35
  50. package/server/src/Models/Customer.php +18 -2
  51. package/server/src/Models/FoodTruck.php +29 -2
  52. package/server/src/Models/Network.php +63 -1
  53. package/server/src/Models/Store.php +71 -9
  54. package/server/src/Notifications/StorefrontOrderAccepted.php +164 -0
  55. package/server/src/Notifications/StorefrontOrderCanceled.php +11 -1
  56. package/server/src/Notifications/StorefrontOrderCompleted.php +11 -1
  57. package/server/src/Notifications/StorefrontOrderDriverAssigned.php +12 -4
  58. package/server/src/Notifications/StorefrontOrderEnroute.php +12 -4
  59. package/server/src/Notifications/StorefrontOrderNearby.php +12 -4
  60. package/server/src/Notifications/StorefrontOrderPreparing.php +12 -4
  61. package/server/src/Notifications/StorefrontOrderReadyForPickup.php +12 -4
  62. package/server/src/Observers/OrderObserver.php +6 -4
  63. package/server/src/Providers/StorefrontServiceProvider.php +1 -0
  64. package/server/src/Rules/IsValidLocation.php +12 -0
  65. package/server/src/Support/PushNotification.php +20 -4
  66. package/server/src/Support/Storefront.php +283 -37
  67. package/server/src/routes.php +1 -0
  68. package/translations/{ar-ae.yml → ar-ae.yaml} +313 -253
  69. package/translations/bg-bg.yaml +734 -0
  70. package/translations/en-us.yaml +5 -0
  71. package/translations/es-es.yaml +732 -0
  72. package/translations/fr-fr.yaml +748 -0
  73. package/translations/mn-mn.yaml +725 -0
  74. package/translations/pt-br.yaml +732 -0
  75. package/translations/ru-ru.yaml +726 -0
  76. package/translations/vi-vn.yaml +412 -338
  77. package/translations/zh-cn.yaml +659 -0
@@ -71,12 +71,20 @@ class StorefrontOrderPreparing extends Notification
71
71
 
72
72
  /**
73
73
  * Get the notification's delivery channels.
74
- *
75
- * @return array
76
74
  */
77
- public function via($notifiable)
75
+ public function via($notifiable): array
78
76
  {
79
- return ['mail', 'database', FcmChannel::class, ApnChannel::class];
77
+ $channels = ['mail', 'database'];
78
+
79
+ if (Storefront::hasNotificationChannelConfigured($this->storefront, 'apn')) {
80
+ $channels[] = ApnChannel::class;
81
+ }
82
+
83
+ if (Storefront::hasNotificationChannelConfigured($this->storefront, 'fcm')) {
84
+ $channels[] = FcmChannel::class;
85
+ }
86
+
87
+ return $channels;
80
88
  }
81
89
 
82
90
  /**
@@ -71,12 +71,20 @@ class StorefrontOrderReadyForPickup extends Notification
71
71
 
72
72
  /**
73
73
  * Get the notification's delivery channels.
74
- *
75
- * @return array
76
74
  */
77
- public function via($notifiable)
75
+ public function via($notifiable): array
78
76
  {
79
- return ['mail', 'database', FcmChannel::class, ApnChannel::class];
77
+ $channels = ['mail', 'database'];
78
+
79
+ if (Storefront::hasNotificationChannelConfigured($this->storefront, 'apn')) {
80
+ $channels[] = ApnChannel::class;
81
+ }
82
+
83
+ if (Storefront::hasNotificationChannelConfigured($this->storefront, 'fcm')) {
84
+ $channels[] = FcmChannel::class;
85
+ }
86
+
87
+ return $channels;
80
88
  }
81
89
 
82
90
  /**
@@ -14,10 +14,12 @@ class OrderObserver
14
14
  */
15
15
  public function creating(Order $order)
16
16
  {
17
- // Set the storefront order config
18
- $orderConfig = Storefront::getDefaultOrderConfig();
19
- if ($orderConfig) {
20
- $order->order_config_uuid = $orderConfig->uuid;
17
+ // Set the storefront order config if none is set already
18
+ if (!$order->order_config_uuid) {
19
+ $orderConfig = Storefront::getDefaultOrderConfig();
20
+ if ($orderConfig) {
21
+ $order->order_config_uuid = $orderConfig->uuid;
22
+ }
21
23
  }
22
24
  }
23
25
  }
@@ -58,6 +58,7 @@ class StorefrontServiceProvider extends CoreServiceProvider
58
58
  \Fleetbase\Storefront\Console\Commands\NotifyStorefrontOrderNearby::class,
59
59
  \Fleetbase\Storefront\Console\Commands\SendOrderNotification::class,
60
60
  \Fleetbase\Storefront\Console\Commands\PurgeExpiredCarts::class,
61
+ \Fleetbase\Storefront\Console\Commands\MigrateStripeSandboxCustomers::class,
61
62
  ];
62
63
 
63
64
  /**
@@ -3,7 +3,9 @@
3
3
  namespace Fleetbase\Storefront\Rules;
4
4
 
5
5
  use Fleetbase\FleetOps\Models\Place;
6
+ use Fleetbase\FleetOps\Models\Vehicle;
6
7
  use Fleetbase\FleetOps\Support\Utils;
8
+ use Fleetbase\Storefront\Models\FoodTruck;
7
9
  use Fleetbase\Storefront\Models\StoreLocation;
8
10
  use Illuminate\Contracts\Validation\Rule;
9
11
  use Illuminate\Support\Str;
@@ -29,6 +31,16 @@ class IsValidLocation implements Rule
29
31
  return StoreLocation::where('public_id', $value)->exists();
30
32
  }
31
33
 
34
+ // Validate Vehicle id
35
+ if (is_string($value) && Str::startsWith($value, 'vehicle_')) {
36
+ return Vehicle::where('public_id', $value)->exists();
37
+ }
38
+
39
+ // Validate FoodTruck id
40
+ if (is_string($value) && Str::startsWith($value, 'food_truck_')) {
41
+ return FoodTruck::where('public_id', $value)->exists();
42
+ }
43
+
32
44
  // Validate object with coordinates
33
45
  if (isset($value->coordinates)) {
34
46
  return Utils::isCoordinates($value->coordinates);
@@ -17,10 +17,14 @@ use Pushok\Client as PushOkClient;
17
17
 
18
18
  class PushNotification
19
19
  {
20
- public static function createApnMessage(Order $order, string $title, string $body, string $status, $notifiable = null): ApnMessage
20
+ public static function createApnMessage(Order $order, string $title, string $body, string $status, $notifiable = null): ?ApnMessage
21
21
  {
22
22
  $storefront = static::getStorefrontFromOrder($order);
23
23
  $client = static::getApnClient($storefront, $order);
24
+ if (!$client) {
25
+ // create apn message anyway
26
+ return new ApnMessage($title, $body);
27
+ }
24
28
 
25
29
  return ApnMessage::create()
26
30
  ->badge(1)
@@ -33,10 +37,19 @@ class PushNotification
33
37
  ->via($client);
34
38
  }
35
39
 
36
- public static function createFcmMessage(Order $order, string $title, string $body, string $status, $notifiable = null): FcmMessage
40
+ public static function createFcmMessage(Order $order, string $title, string $body, string $status, $notifiable = null): ?FcmMessage
37
41
  {
38
42
  $storefront = static::getStorefrontFromOrder($order);
39
43
  $notificationChannel = static::getNotificationChannel('apn', $storefront, $order);
44
+ if (!$notificationChannel) {
45
+ // create fcm message anyway
46
+ return new FcmMessage(
47
+ notification: new FcmNotification(
48
+ title: $title,
49
+ body: $body
50
+ )
51
+ );
52
+ }
40
53
 
41
54
  // Configure FCM
42
55
  static::configureFcm($notificationChannel);
@@ -97,7 +110,7 @@ class PushNotification
97
110
  return $firebaseConfig;
98
111
  }
99
112
 
100
- public static function getNotificationChannel(string $scheme, Network|Store $storefront, ?Order $order = null): NotificationChannel
113
+ public static function getNotificationChannel(string $scheme, Network|Store $storefront, ?Order $order = null): ?NotificationChannel
101
114
  {
102
115
  if ($order && $order->hasMeta('storefront_notification_channel')) {
103
116
  return NotificationChannel::where([
@@ -113,9 +126,12 @@ class PushNotification
113
126
  ])->first();
114
127
  }
115
128
 
116
- public static function getApnClient(Network|Store $storefront, ?Order $order = null): PushOkClient
129
+ public static function getApnClient(Network|Store $storefront, ?Order $order = null): ?PushOkClient
117
130
  {
118
131
  $notificationChannel = static::getNotificationChannel('apn', $storefront, $order);
132
+ if (!$notificationChannel) {
133
+ return null;
134
+ }
119
135
  $config = (array) $notificationChannel->config;
120
136
 
121
137
  $isProductionEnv = Utils::castBoolean(data_get($config, 'production', app()->isProduction()));
@@ -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;
@@ -9,11 +10,15 @@ use Fleetbase\Models\Company;
9
10
  use Fleetbase\Models\User;
10
11
  use Fleetbase\Storefront\Models\Gateway;
11
12
  use Fleetbase\Storefront\Models\Network;
13
+ use Fleetbase\Storefront\Models\NotificationChannel;
12
14
  use Fleetbase\Storefront\Models\Product;
13
15
  use Fleetbase\Storefront\Models\Store;
16
+ use Fleetbase\Storefront\Notifications\StorefrontOrderAccepted;
14
17
  use Fleetbase\Storefront\Notifications\StorefrontOrderCreated;
15
18
  use Fleetbase\Support\Auth;
16
19
  use Fleetbase\Support\Utils;
20
+ use Illuminate\Support\Facades\DB;
21
+ use Illuminate\Support\Facades\Log;
17
22
  use Illuminate\Support\Facades\Notification;
18
23
  use Illuminate\Support\Facades\Redis;
19
24
  use Illuminate\Support\Str;
@@ -21,15 +26,20 @@ use Laravel\Sanctum\PersonalAccessToken;
21
26
 
22
27
  class Storefront
23
28
  {
29
+ /** @var string */
30
+ private const CONFIG_KEY = 'storefront';
31
+ /** @var string */
32
+ private const CONFIG_NS = 'system:order-config:storefront';
33
+ /** @var array<string,OrderConfig> In-memory cache keyed by company UUID */
34
+ private static array $configCache = [];
35
+
24
36
  /**
25
37
  * Returns current store or network based on session `storefront_key`
26
38
  * with bare minimum columns, but can optionally pass in more columns to receive.
27
39
  *
28
40
  * @param array $columns
29
- *
30
- * @return \Fleetbase\Models\Storefront\Network|\Fleetbase\Models\Storefront\Store
31
41
  */
32
- public static function about($columns = [], $with = [])
42
+ public static function about($columns = [], $with = []): Store|Network|null
33
43
  {
34
44
  $key = session('storefront_key');
35
45
 
@@ -38,7 +48,7 @@ class Storefront
38
48
  }
39
49
 
40
50
  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);
51
+ $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
52
  }
43
53
 
44
54
  if (Str::startsWith($key, 'store')) {
@@ -53,10 +63,16 @@ class Storefront
53
63
  return $about;
54
64
  }
55
65
 
56
- public static function findAbout($id, $columns = [], $with = [])
66
+ /**
67
+ * Returns current store or network based ID param passed
68
+ * with bare minimum columns, but can optionally pass in more columns to receive.
69
+ *
70
+ * @param array $columns
71
+ */
72
+ public static function findAbout($id, $columns = [], $with = []): Store|Network|null
57
73
  {
58
74
  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);
75
+ $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
76
  }
61
77
 
62
78
  if (Str::startsWith($id, 'store')) {
@@ -204,57 +220,106 @@ class Storefront
204
220
  return $stripeCustomer;
205
221
  }
206
222
 
207
- public static function patchOrderConfig(Order $order)
223
+ /**
224
+ * Ensure an Order has an effective OrderConfig.
225
+ * - If already set (FK or relation), return it.
226
+ * - Else resolve default for the order's company and patch the FK quietly.
227
+ *
228
+ * @param \Fleetbase\Storefront\Models\Order $order
229
+ *
230
+ * @return \Fleetbase\Storefront\Models\OrderConfig|null
231
+ */
232
+ public static function patchOrderConfig($order): ?OrderConfig
208
233
  {
209
- $orderConfig = $order->config();
210
- if (!$orderConfig) {
211
- $orderConfig = Storefront::getDefaultOrderConfig();
212
- if ($orderConfig) {
213
- $order->update(['order_config_uuid' => $orderConfig->uuid]);
214
- }
234
+ // FK fast-path
235
+ if (!empty($order->order_config_uuid)) {
236
+ return OrderConfig::where('uuid', $order->order_config_uuid)->first();
237
+ }
238
+
239
+ // Relation fast-path (if your Order has relation 'orderConfig')
240
+ $order->loadMissing('orderConfig');
241
+ if ($order->orderConfig instanceof OrderConfig) {
242
+ return $order->orderConfig;
243
+ }
244
+
245
+ // Resolve by company
246
+ $companyUuid = $order->company_uuid ?? ($order->company->uuid ?? null);
247
+ $orderConfig = static::getOrderConfig($companyUuid);
248
+
249
+ if ($orderConfig) {
250
+ // Quiet write to avoid events, if preferred
251
+ $order->forceFill(['order_config_uuid' => $orderConfig->uuid]);
252
+ method_exists($order, 'saveQuietly') ? $order->saveQuietly() : $order->save();
215
253
  }
216
254
 
217
255
  return $orderConfig;
218
256
  }
219
257
 
220
- public static function getDefaultOrderConfig()
258
+ /**
259
+ * Get the default Storefront OrderConfig for the "current" company context.
260
+ *
261
+ * @return \Fleetbase\Storefront\Models\OrderConfig|null
262
+ */
263
+ public static function getDefaultOrderConfig(): ?OrderConfig
221
264
  {
222
- $company = Auth::getCompany();
223
- if ($company) {
224
- return static::getOrderConfig($company);
225
- }
265
+ $company = session('company') ?? (method_exists(Auth::class, 'getCompany') ? Auth::getCompany() : null);
226
266
 
227
- return null;
267
+ return static::getOrderConfig($company);
228
268
  }
229
269
 
230
- public static function getOrderConfig(Company $company)
270
+ /**
271
+ * Get (or lazily create) the Storefront OrderConfig for a company.
272
+ * Accepts Company model, UUID string, or null (falls back to session company).
273
+ *
274
+ * @return \Fleetbase\Storefront\Models\OrderConfig|null
275
+ */
276
+ public static function getOrderConfig(Company|string|null $company): ?OrderConfig
231
277
  {
232
- $orderConfig = OrderConfig::where(['company_uuid' => $company->uuid, 'key' => 'storefront', 'namespace' => 'system:order-config:storefront'])->first();
233
- if (!$orderConfig) {
234
- $orderConfig = static::createStorefrontConfig($company);
278
+ $companyUuid = static::resolveCompanyUuid($company);
279
+ if (!$companyUuid) {
280
+ return null;
235
281
  }
236
282
 
237
- return $orderConfig;
283
+ // Request-lifetime cache
284
+ if (isset(static::$configCache[$companyUuid])) {
285
+ return static::$configCache[$companyUuid];
286
+ }
287
+
288
+ $attrs = [
289
+ 'company_uuid' => $companyUuid,
290
+ 'key' => self::CONFIG_KEY,
291
+ 'namespace' => self::CONFIG_NS,
292
+ ];
293
+
294
+ $config = OrderConfig::where($attrs)->first();
295
+ if (!$config) {
296
+ // Short transaction to avoid dupes under race
297
+ $config = DB::transaction(function () use ($attrs, $companyUuid) {
298
+ $existing = OrderConfig::where($attrs)->first();
299
+ if ($existing) {
300
+ return $existing;
301
+ }
302
+
303
+ return static::createStorefrontConfig($companyUuid);
304
+ });
305
+ }
306
+
307
+ return static::$configCache[$companyUuid] = $config;
238
308
  }
239
309
 
240
310
  /**
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.
311
+ * Create (or retrieve) the Storefront config for a company.
312
+ * Accepts Company model or UUID string.
248
313
  *
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
314
+ * @return \Fleetbase\Storefront\Models\OrderConfig
252
315
  */
253
- public static function createStorefrontConfig(Company $company): OrderConfig
316
+ public static function createStorefrontConfig(Company|string $company): OrderConfig
254
317
  {
318
+ $companyUuid = $company instanceof Company ? $company->uuid : $company;
319
+
255
320
  return OrderConfig::firstOrCreate(
256
321
  [
257
- 'company_uuid' => $company->uuid,
322
+ 'company_uuid' => $companyUuid,
258
323
  'key' => 'storefront',
259
324
  'namespace' => 'system:order-config:storefront',
260
325
  ],
@@ -343,7 +408,7 @@ class Storefront
343
408
  'require_pod' => false,
344
409
  ],
345
410
  'picked_up' => [
346
- 'key' => 'completed',
411
+ 'key' => 'picked_up',
347
412
  'code' => 'picked_up',
348
413
  'color' => '#1f2937',
349
414
  'logic' => [],
@@ -483,4 +548,185 @@ class Storefront
483
548
  ]
484
549
  );
485
550
  }
551
+
552
+ /**
553
+ * Normalize a Company|UUID|null to a UUID string (or null if unresolved).
554
+ */
555
+ protected static function resolveCompanyUuid(Company|string|null $company): ?string
556
+ {
557
+ if ($company instanceof Company && !empty($company->uuid)) {
558
+ return (string) $company->uuid;
559
+ }
560
+ if (is_string($company) && $company !== '' && (method_exists(Str::class, 'isUuid') ? Str::isUuid($company) : true)) {
561
+ return $company;
562
+ }
563
+ $sessionCompany = session('company');
564
+
565
+ return !empty($sessionCompany) ? (string) $sessionCompany : null;
566
+ }
567
+
568
+ public static function createAcceptedActivity(?OrderConfig $orderConfig = null): Activity
569
+ {
570
+ return new Activity([
571
+ 'key' => 'accepted',
572
+ 'code' => 'accepted',
573
+ 'color' => '#1f2937',
574
+ 'logic' => [],
575
+ 'events' => [],
576
+ 'status' => 'Order has been accepted',
577
+ 'actions' => [],
578
+ 'details' => 'Order has been accepted by {storefront.name}',
579
+ 'options' => [],
580
+ 'complete' => false,
581
+ 'entities' => [],
582
+ 'sequence' => 0,
583
+ 'activities' => ['dispatched'],
584
+ 'internalId' => Str::uuid(),
585
+ 'pod_method' => 'scan',
586
+ 'require_pod' => false,
587
+ ], $orderConfig ? $orderConfig->activities()->toArray() : []);
588
+ }
589
+
590
+ public static function autoAcceptOrder(Order $order)
591
+ {
592
+ // Patch order config
593
+ $orderConfig = static::patchOrderConfig($order);
594
+ $activity = static::createAcceptedActivity($orderConfig);
595
+
596
+ // Dispatch already if order is a pickup
597
+ if ($order->isMeta('is_pickup')) {
598
+ $order->firstDispatchWithActivity();
599
+ }
600
+
601
+ // Set order as accepted
602
+ try {
603
+ $order->setStatus($activity->code);
604
+ $order->insertActivity($activity, $order->getLastLocation());
605
+ } catch (\Exception $e) {
606
+ Log::debug('[Storefront] was unable to accept an order.', ['order' => $order, 'activity' => $activity]);
607
+
608
+ return response()->error('Unable to accept order.');
609
+ }
610
+
611
+ // Notify customer order was accepted
612
+ try {
613
+ $order->customer->notify(new StorefrontOrderAccepted($order));
614
+ } catch (\Exception $e) {
615
+ }
616
+
617
+ return $order;
618
+ }
619
+
620
+ public static function autoDispatchOrder(Order $order, bool $adhoc = true)
621
+ {
622
+ // Patch order config
623
+ Storefront::patchOrderConfig($order);
624
+
625
+ if ($order->isMeta('is_pickup')) {
626
+ $order->updateStatus('pickup_ready');
627
+
628
+ return $order;
629
+ }
630
+
631
+ // toggle order to adhoc
632
+ if ($adhoc === true) {
633
+ $order->update(['adhoc' => true]);
634
+ } else {
635
+ // Find nearest driver and assign
636
+ $driver = $order->findClosestDrivers()->first();
637
+ if ($driver) {
638
+ $order->assignDriver($driver);
639
+ } else {
640
+ // no driver available to make adhoc
641
+ $order->update(['adhoc' => true]);
642
+ }
643
+ }
644
+
645
+ $order->dispatchWithActivity();
646
+
647
+ return $order;
648
+ }
649
+
650
+ /**
651
+ * Determine whether the given Store or Network has a configured notification channel.
652
+ *
653
+ * Accepts either a model instance (Store|Network) or an identifier string.
654
+ * When a string is provided, this method attempts to resolve it in the following order:
655
+ * 1) UUID → `where('uuid', $id)`
656
+ * 2) public_id → `where('public_id', $id)` (if your models use a public_id column)
657
+ *
658
+ * If resolution fails, the method returns false.
659
+ *
660
+ * @param Store|\Fleetbase\FleetOps\Models\Network|string|null $subject
661
+ * Store/Network model instance or identifier (uuid/public_id)
662
+ * @param string $channel Channel scheme/key (e.g., "email", "sms", "fcm").
663
+ *
664
+ * @return bool true if a NotificationChannel exists for the subject and scheme; otherwise false
665
+ *
666
+ * @example
667
+ * YourClass::hasNotificationChannelConfigures($store, 'email');
668
+ * YourClass::hasNotificationChannelConfigures($networkUuid, 'fcm');
669
+ * YourClass::hasNotificationChannelConfigures('STO-12345', 'sms'); // public_id example
670
+ *
671
+ * @note The method name appears to have a typo; consider renaming to:
672
+ * hasNotificationChannelConfigured() and keeping this as a BC alias.
673
+ */
674
+ public static function hasNotificationChannelConfigured(Store|Network|string|null $subject, string $channel): bool
675
+ {
676
+ $model = self::resolveSubjectToModel($subject);
677
+
678
+ if (!$model) {
679
+ return false;
680
+ }
681
+
682
+ return NotificationChannel::query()
683
+ ->where('owner_uuid', $model->uuid)
684
+ ->where('scheme', $channel)
685
+ ->exists();
686
+ }
687
+
688
+ /**
689
+ * Resolve the provided subject into a Store or Network model.
690
+ *
691
+ * Attempts resolution by uuid first, then by public_id (if present).
692
+ * Returns null if no matching model can be found.
693
+ *
694
+ * @param Store|\Fleetbase\FleetOps\Models\Network|string|null $subject
695
+ *
696
+ * @return Store|\Fleetbase\FleetOps\Models\Network|null
697
+ */
698
+ protected static function resolveSubjectToModel(Store|Network|string|null $subject): Store|Network|null
699
+ {
700
+ if ($subject instanceof Store || $subject instanceof Network) {
701
+ return $subject;
702
+ }
703
+
704
+ if (!is_string($subject) || $subject === '') {
705
+ return null;
706
+ }
707
+
708
+ // Try UUID
709
+ if (Str::isUuid($subject)) {
710
+ if ($found = Store::query()->where('uuid', $subject)->first()) {
711
+ return $found;
712
+ }
713
+ if ($found = Network::query()->where('uuid', $subject)->first()) {
714
+ return $found;
715
+ }
716
+ }
717
+
718
+ // Try public_id (optional; remove if you don't use it)
719
+ if (property_exists(Store::class, 'public_id') || Schema::hasColumn((new Store())->getTable(), 'public_id')) {
720
+ if ($found = Store::query()->where('public_id', $subject)->first()) {
721
+ return $found;
722
+ }
723
+ }
724
+ if (property_exists(Network::class, 'public_id') || Schema::hasColumn((new Network())->getTable(), 'public_id')) {
725
+ if ($found = Network::query()->where('public_id', $subject)->first()) {
726
+ return $found;
727
+ }
728
+ }
729
+
730
+ return null;
731
+ }
486
732
  }
@@ -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
  );