@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
@@ -0,0 +1,263 @@
1
+ import {
2
+ startOfDay,
3
+ endOfDay,
4
+ startOfWeek,
5
+ endOfWeek,
6
+ startOfMonth,
7
+ endOfMonth,
8
+ startOfQuarter,
9
+ endOfQuarter,
10
+ startOfYear,
11
+ endOfYear,
12
+ subDays,
13
+ subWeeks,
14
+ subMonths,
15
+ subQuarters,
16
+ subYears,
17
+ format,
18
+ } from 'date-fns';
19
+
20
+ /**
21
+ * Predefined date range buttons for ecommerce analytics dashboard
22
+ * Each button contains a label and a function that returns [startDate, endDate]
23
+ */
24
+ export const predefinedDateRanges = [
25
+ // Recent periods - most commonly used for daily monitoring
26
+ {
27
+ label: 'Today',
28
+ getValue: () => {
29
+ const today = new Date();
30
+ return [startOfDay(today), endOfDay(today)];
31
+ },
32
+ },
33
+ {
34
+ label: 'Yesterday',
35
+ getValue: () => {
36
+ const yesterday = subDays(new Date(), 1);
37
+ return [startOfDay(yesterday), endOfDay(yesterday)];
38
+ },
39
+ },
40
+ {
41
+ label: 'Last 7 Days',
42
+ getValue: () => {
43
+ const today = new Date();
44
+ const sevenDaysAgo = subDays(today, 6); // 6 days ago + today = 7 days
45
+ return [startOfDay(sevenDaysAgo), endOfDay(today)];
46
+ },
47
+ },
48
+ {
49
+ label: 'Last 14 Days',
50
+ getValue: () => {
51
+ const today = new Date();
52
+ const fourteenDaysAgo = subDays(today, 13);
53
+ return [startOfDay(fourteenDaysAgo), endOfDay(today)];
54
+ },
55
+ },
56
+ {
57
+ label: 'Last 30 Days',
58
+ getValue: () => {
59
+ const today = new Date();
60
+ const thirtyDaysAgo = subDays(today, 29);
61
+ return [startOfDay(thirtyDaysAgo), endOfDay(today)];
62
+ },
63
+ },
64
+
65
+ // Weekly periods
66
+ {
67
+ label: 'This Week',
68
+ getValue: () => {
69
+ const today = new Date();
70
+ return [startOfWeek(today, { weekStartsOn: 1 }), endOfWeek(today, { weekStartsOn: 1 })]; // Monday start
71
+ },
72
+ },
73
+ {
74
+ label: 'Last Week',
75
+ getValue: () => {
76
+ const lastWeek = subWeeks(new Date(), 1);
77
+ return [startOfWeek(lastWeek, { weekStartsOn: 1 }), endOfWeek(lastWeek, { weekStartsOn: 1 })];
78
+ },
79
+ },
80
+
81
+ // Monthly periods - crucial for monthly reporting
82
+ {
83
+ label: 'This Month',
84
+ getValue: () => {
85
+ const today = new Date();
86
+ return [startOfMonth(today), endOfMonth(today)];
87
+ },
88
+ },
89
+ {
90
+ label: 'Last Month',
91
+ getValue: () => {
92
+ const lastMonth = subMonths(new Date(), 1);
93
+ return [startOfMonth(lastMonth), endOfMonth(lastMonth)];
94
+ },
95
+ },
96
+ {
97
+ label: 'Last 3 Months',
98
+ getValue: () => {
99
+ const today = new Date();
100
+ const threeMonthsAgo = subMonths(today, 3);
101
+ return [startOfMonth(threeMonthsAgo), endOfMonth(today)];
102
+ },
103
+ },
104
+ {
105
+ label: 'Last 6 Months',
106
+ getValue: () => {
107
+ const today = new Date();
108
+ const sixMonthsAgo = subMonths(today, 6);
109
+ return [startOfMonth(sixMonthsAgo), endOfMonth(today)];
110
+ },
111
+ },
112
+
113
+ // Quarterly periods - important for business reporting
114
+ {
115
+ label: 'This Quarter',
116
+ getValue: () => {
117
+ const today = new Date();
118
+ return [startOfQuarter(today), endOfQuarter(today)];
119
+ },
120
+ },
121
+ {
122
+ label: 'Last Quarter',
123
+ getValue: () => {
124
+ const lastQuarter = subQuarters(new Date(), 1);
125
+ return [startOfQuarter(lastQuarter), endOfQuarter(lastQuarter)];
126
+ },
127
+ },
128
+ {
129
+ label: 'Q1 2024',
130
+ getValue: () => {
131
+ const q1Start = new Date(2024, 0, 1); // January 1, 2024
132
+ return [startOfQuarter(q1Start), endOfQuarter(q1Start)];
133
+ },
134
+ },
135
+ {
136
+ label: 'Q2 2024',
137
+ getValue: () => {
138
+ const q2Start = new Date(2024, 3, 1); // April 1, 2024
139
+ return [startOfQuarter(q2Start), endOfQuarter(q2Start)];
140
+ },
141
+ },
142
+ {
143
+ label: 'Q3 2024',
144
+ getValue: () => {
145
+ const q3Start = new Date(2024, 6, 1); // July 1, 2024
146
+ return [startOfQuarter(q3Start), endOfQuarter(q3Start)];
147
+ },
148
+ },
149
+ {
150
+ label: 'Q4 2024',
151
+ getValue: () => {
152
+ const q4Start = new Date(2024, 9, 1); // October 1, 2024
153
+ return [startOfQuarter(q4Start), endOfQuarter(q4Start)];
154
+ },
155
+ },
156
+
157
+ // Yearly periods - essential for annual analysis
158
+ {
159
+ label: 'This Year',
160
+ getValue: () => {
161
+ const today = new Date();
162
+ return [startOfYear(today), endOfYear(today)];
163
+ },
164
+ },
165
+ {
166
+ label: 'Last Year',
167
+ getValue: () => {
168
+ const lastYear = subYears(new Date(), 1);
169
+ return [startOfYear(lastYear), endOfYear(lastYear)];
170
+ },
171
+ },
172
+ {
173
+ label: '2024',
174
+ getValue: () => {
175
+ const year2024 = new Date(2024, 0, 1);
176
+ return [startOfYear(year2024), endOfYear(year2024)];
177
+ },
178
+ },
179
+ {
180
+ label: '2023',
181
+ getValue: () => {
182
+ const year2023 = new Date(2023, 0, 1);
183
+ return [startOfYear(year2023), endOfYear(year2023)];
184
+ },
185
+ },
186
+
187
+ // Special ecommerce periods
188
+ {
189
+ label: 'Black Friday Week',
190
+ getValue: () => {
191
+ // Assuming Black Friday 2024 is November 29th
192
+ const blackFriday = new Date(2024, 10, 29); // November 29, 2024
193
+ const weekStart = subDays(blackFriday, 3); // Tuesday before
194
+ const weekEnd = subDays(blackFriday, -3); // Monday after
195
+ return [startOfDay(weekStart), endOfDay(weekEnd)];
196
+ },
197
+ },
198
+ {
199
+ label: 'Holiday Season 2024',
200
+ getValue: () => {
201
+ // November 1st to December 31st
202
+ const seasonStart = new Date(2024, 10, 1); // November 1, 2024
203
+ const seasonEnd = new Date(2024, 11, 31); // December 31, 2024
204
+ return [startOfDay(seasonStart), endOfDay(seasonEnd)];
205
+ },
206
+ },
207
+ {
208
+ label: 'Back to School 2024',
209
+ getValue: () => {
210
+ // August 1st to September 15th
211
+ const seasonStart = new Date(2024, 7, 1); // August 1, 2024
212
+ const seasonEnd = new Date(2024, 8, 15); // September 15, 2024
213
+ return [startOfDay(seasonStart), endOfDay(seasonEnd)];
214
+ },
215
+ },
216
+ ];
217
+
218
+ /**
219
+ * Convert predefined date ranges to AirDatepicker buttons format
220
+ * @param {Function} onRangeSelect - Callback function when a range is selected
221
+ * @returns {Array} Array of button objects for AirDatepicker
222
+ */
223
+ export function createDateRangeButtons(onRangeSelect) {
224
+ return predefinedDateRanges.map((range) => ({
225
+ content: range.label,
226
+ className: 'custom-date-range-btn',
227
+ onClick: (datepicker) => {
228
+ const [startDate, endDate] = range.getValue();
229
+ datepicker.selectDate([startDate, endDate]);
230
+ datepicker.hide();
231
+
232
+ // Call the callback if provided
233
+ if (typeof onRangeSelect === 'function') {
234
+ onRangeSelect({
235
+ label: range.label,
236
+ startDate,
237
+ endDate,
238
+ formattedRange: `${format(startDate, 'MMM dd, yyyy')} - ${format(endDate, 'MMM dd, yyyy')}`,
239
+ });
240
+ }
241
+ },
242
+ }));
243
+ }
244
+
245
+ /**
246
+ * Get a specific date range by label
247
+ * @param {string} label - The label of the date range
248
+ * @returns {Array|null} [startDate, endDate] or null if not found
249
+ */
250
+ export function getDateRangeByLabel(label) {
251
+ const range = predefinedDateRanges.find((r) => r.label === label);
252
+ return range ? range.getValue() : null;
253
+ }
254
+
255
+ /**
256
+ * Format date range for display
257
+ * @param {Date} startDate
258
+ * @param {Date} endDate
259
+ * @returns {string} Formatted date range string
260
+ */
261
+ export function formatDateRange(startDate, endDate) {
262
+ return `${format(startDate, 'MMM dd, yyyy')} - ${format(endDate, 'MMM dd, yyyy')}`;
263
+ }
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/storefront-engine/utils/commerce-date-ranges';
package/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/storefront-api",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Headless Commerce & Marketplace Extension for Fleetbase",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Storefront",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/storefront-engine",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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": "latest",
48
- "@fleetbase/ember-ui": "latest",
49
- "@fleetbase/fleetops-data": "^0.1.19",
47
+ "@fleetbase/ember-core": "^0.3.3",
48
+ "@fleetbase/ember-ui": "^0.3.5",
49
+ "@fleetbase/fleetops-data": "^0.1.20",
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",
@@ -0,0 +1,110 @@
1
+ <?php
2
+
3
+ use Fleetbase\Storefront\Support\Storefront;
4
+ use Fleetbase\Support\Utils;
5
+ use Illuminate\Database\Migrations\Migration;
6
+ use Illuminate\Database\Query\Expression;
7
+ use Illuminate\Database\Schema\Blueprint;
8
+ use Illuminate\Support\Facades\DB;
9
+ use Illuminate\Support\Facades\Schema;
10
+
11
+ return new class extends Migration {
12
+ /**
13
+ * Run the migrations.
14
+ */
15
+ public function up(): void
16
+ {
17
+ $databaseName = Utils::getFleetbaseDatabaseName();
18
+ $sfConnection = config('storefront.connection.db');
19
+
20
+ Schema::connection($sfConnection)->table('stores', function (Blueprint $table) use ($databaseName) {
21
+ // nullable because we’ll backfill after
22
+ $table->foreignUuid('order_config_uuid')
23
+ ->nullable()
24
+ ->after('backdrop_uuid')
25
+ ->constrained(new Expression($databaseName . '.order_configs'), 'uuid')
26
+ ->nullOnDelete();
27
+ });
28
+
29
+ Schema::connection($sfConnection)->table('networks', function (Blueprint $table) use ($databaseName) {
30
+ $table->foreignUuid('order_config_uuid')
31
+ ->nullable()
32
+ ->after('backdrop_uuid')
33
+ ->constrained(new Expression($databaseName . '.order_configs'), 'uuid')
34
+ ->nullOnDelete();
35
+ });
36
+
37
+ DB::connection($sfConnection)->transaction(function () use ($sfConnection) {
38
+ // Pull valid company UUIDs from core.companies
39
+ $validCompanyUuids = DB::connection(config('database.default'))
40
+ ->table('companies')
41
+ ->pluck('uuid')
42
+ ->all();
43
+
44
+ // STORES: distinct companies missing an order_config_uuid
45
+ $storeCompanyIds = DB::connection($sfConnection)
46
+ ->table('stores')
47
+ ->whereNull('order_config_uuid')
48
+ ->whereNotNull('company_uuid')
49
+ ->distinct()
50
+ ->pluck('company_uuid')
51
+ ->filter(fn ($uuid) => in_array($uuid, $validCompanyUuids, true))
52
+ ->values();
53
+
54
+ foreach ($storeCompanyIds as $companyUuid) {
55
+ // Resolve company-scoped default config
56
+ $config = Storefront::getOrderConfig($companyUuid);
57
+ if ($config) {
58
+ DB::connection($sfConnection)
59
+ ->table('stores')
60
+ ->where('company_uuid', $companyUuid)
61
+ ->whereNull('order_config_uuid')
62
+ ->update(['order_config_uuid' => $config->uuid]);
63
+ }
64
+ }
65
+
66
+ // NETWORKS: distinct companies missing an order_config_uuid
67
+ $networkCompanyIds = DB::connection($sfConnection)
68
+ ->table('networks')
69
+ ->whereNull('order_config_uuid')
70
+ ->whereNotNull('company_uuid')
71
+ ->distinct()
72
+ ->pluck('company_uuid')
73
+ ->filter(fn ($uuid) => in_array($uuid, $validCompanyUuids, true))
74
+ ->values();
75
+
76
+ foreach ($networkCompanyIds as $companyUuid) {
77
+ $config = Storefront::getOrderConfig($companyUuid);
78
+ if ($config) {
79
+ DB::connection($sfConnection)
80
+ ->table('networks')
81
+ ->where('company_uuid', $companyUuid)
82
+ ->whereNull('order_config_uuid')
83
+ ->update(['order_config_uuid' => $config->uuid]);
84
+ }
85
+ }
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Reverse the migrations.
91
+ */
92
+ public function down(): void
93
+ {
94
+ $sfConnection = config('storefront.connection.db');
95
+
96
+ // Drop FK and column for stores
97
+ Schema::connection($sfConnection)->table('stores', function (Blueprint $table) {
98
+ // Drop the foreign key constraint first, then the column
99
+ // Default FK name pattern: {table}_{column}_foreign
100
+ $table->dropForeign(['order_config_uuid']);
101
+ $table->dropColumn('order_config_uuid');
102
+ });
103
+
104
+ // Drop FK and column for networks
105
+ Schema::connection($sfConnection)->table('networks', function (Blueprint $table) {
106
+ $table->dropForeign(['order_config_uuid']);
107
+ $table->dropColumn('order_config_uuid');
108
+ });
109
+ }
110
+ };
@@ -0,0 +1,165 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\Storefront\Console\Commands;
4
+
5
+ use Fleetbase\Storefront\Models\Customer;
6
+ use Fleetbase\Storefront\Models\Gateway;
7
+ use Fleetbase\Storefront\Models\Store;
8
+ use Illuminate\Console\Command;
9
+ use Illuminate\Http\Request;
10
+ use Illuminate\Support\Facades\App;
11
+ use Illuminate\Support\Facades\Session;
12
+ use Stripe\Customer as StripeCustomer;
13
+ use Stripe\Exception\InvalidRequestException;
14
+ use Stripe\Stripe;
15
+
16
+ class MigrateStripeSandboxCustomers extends Command
17
+ {
18
+ /**
19
+ * The name and signature of the console command.
20
+ *
21
+ * Added a --store option to allow running the migration for a single store by UUID or public_id.
22
+ */
23
+ protected $signature = 'storefront:migrate-stripe-customers
24
+ {--dry-run : Don\'t actually create or update anything}
25
+ {--store= : Specify a store UUID or public_id to migrate only that store}';
26
+
27
+ /** The console command description. */
28
+ protected $description = 'Migrates Stripe customers created in test mode to live mode and updates contact metadata.';
29
+
30
+ /** Execute the console command. */
31
+ public function handle(): int
32
+ {
33
+ $dryRun = $this->option('dry-run');
34
+ $storeInput = $this->option('store');
35
+
36
+ $this->startSession();
37
+ $this->info('Starting Stripe customer migration...');
38
+
39
+ // If a single store is specified, load that store by UUID or public_id.
40
+ if ($storeInput) {
41
+ $store = Store::where('uuid', $storeInput)
42
+ ->orWhere('public_id', $storeInput)
43
+ ->first();
44
+
45
+ if (!$store) {
46
+ $this->error("Store '{$storeInput}' not found.");
47
+
48
+ return Command::FAILURE;
49
+ }
50
+
51
+ $this->migrateCustomers($store, $dryRun);
52
+ } else {
53
+ // Otherwise, load all stores.
54
+ $stores = Store::all();
55
+ foreach ($stores as $store) {
56
+ $this->migrateCustomers($store, $dryRun);
57
+ }
58
+ }
59
+
60
+ $this->info('Stripe customer migration complete.');
61
+
62
+ return Command::SUCCESS;
63
+ }
64
+
65
+ /**
66
+ * Migrates all customers for a given store whose stripe_id is still pointing at test mode.
67
+ *
68
+ * Only runs when the store's gateway is configured for live mode; skips migration for sandbox gateways.
69
+ */
70
+ public function migrateCustomers(Store $store, bool $dryRun): int
71
+ {
72
+ // Find the Stripe gateway for this store (there is only one gateway per store).
73
+ $gateway = Gateway::where([
74
+ 'code' => 'stripe',
75
+ 'owner_uuid'=> $store->uuid,
76
+ ])->first();
77
+
78
+ if (!$gateway) {
79
+ $this->warn("Store {$store->name}: no Stripe gateway configured.");
80
+
81
+ return Command::SUCCESS;
82
+ }
83
+
84
+ // If the gateway is still sandbox, skip migration because we can't create live customers yet.
85
+ if ($gateway->sandbox) {
86
+ $this->warn("Store {$store->name} is using a sandbox gateway. Migration will only run when the gateway is switched to live.");
87
+
88
+ return Command::SUCCESS;
89
+ }
90
+
91
+ // Inform the store being migrated
92
+ $this->info("Store {$store->name}: Will have sandbox customers migrated...");
93
+
94
+ $secretKey = $gateway->config->secret_key;
95
+
96
+ // Process customers in chunks to conserve memory.
97
+ Customer::where('company_uuid', $store->company_uuid)->chunk(50, function ($customers) use ($dryRun, $secretKey, $store) {
98
+ foreach ($customers as $customer) {
99
+ $stripeId = $customer->getMeta('stripe_id');
100
+ if (!$stripeId) {
101
+ continue;
102
+ }
103
+
104
+ // Set the Stripe API key to the store's live secret key.
105
+ Stripe::setApiKey($secretKey);
106
+
107
+ $current = null;
108
+
109
+ try {
110
+ // Attempt to retrieve the customer using the current key.
111
+ // If found and livemode is true, no migration needed.
112
+ $current = StripeCustomer::retrieve($stripeId);
113
+ if ($current && $current->livemode === true) {
114
+ $this->line("Customer {$customer->id}: Stripe ID {$stripeId} is already a live customer.");
115
+ continue;
116
+ }
117
+ } catch (InvalidRequestException $e) {
118
+ // The existing ID was not found with the live key, so it likely belongs to the test environment.
119
+ }
120
+
121
+ // If we're doing a dry run, just report what would happen.
122
+ if ($dryRun) {
123
+ $this->info("Customer {$customer->id}: Would migrate test Stripe ID {$stripeId} to live.");
124
+ continue;
125
+ }
126
+
127
+ // Create a new live customer using the contact details.
128
+ $metadata = [
129
+ 'contact_id' => $customer->public_id,
130
+ 'storefront_id' => $store->public_id,
131
+ 'company_id' => $store->company_uuid,
132
+ ];
133
+
134
+ $params = [
135
+ 'description' => 'Customer migrated from sandbox',
136
+ 'email' => $customer->email,
137
+ 'name' => $customer->name,
138
+ 'phone' => $customer->phone,
139
+ 'metadata' => $metadata,
140
+ ];
141
+
142
+ $newCustomer = StripeCustomer::create($params);
143
+
144
+ // Save the old test ID and update the live ID.
145
+ $customer->updateMeta('stripe_id_sandbox', $stripeId);
146
+ $customer->updateMeta('stripe_id', $newCustomer->id);
147
+
148
+ $this->info("Customer {$customer->id}: Migrated test ID {$stripeId} to live Stripe ID {$newCustomer->id}.");
149
+ }
150
+ });
151
+
152
+ return Command::SUCCESS;
153
+ }
154
+
155
+ private function startSession()
156
+ {
157
+ if (!App::bound('request')) {
158
+ App::instance('request', Request::create('/'));
159
+ }
160
+ if (!Session::isStarted()) {
161
+ Session::start();
162
+ request()->setLaravelSession(Session::getFacadeRoot());
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,43 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\Storefront\Expansions;
4
+
5
+ use Fleetbase\Build\Expansion;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Fleetbase\Storefront\Models\Network;
8
+ use Fleetbase\Storefront\Models\Store;
9
+
10
+ class OrderExpansion implements Expansion
11
+ {
12
+ /**
13
+ * Get the target class to expand.
14
+ *
15
+ * @return string|Class
16
+ */
17
+ public static function target()
18
+ {
19
+ return Order::class;
20
+ }
21
+
22
+ /**
23
+ * Get the current storefront for the order created.
24
+ */
25
+ public static function getStorefrontAttribute(): \Closure
26
+ {
27
+ return function (): Store|Network|null {
28
+ /** @var Order $this */
29
+ $storefrontId = $this->getMeta('storefront_id');
30
+ $storefrontNetworkId = $this->getMeta('storefront_network_id');
31
+
32
+ if ($storefrontId) {
33
+ return Store::where('public_id', $storefrontId)->first();
34
+ }
35
+
36
+ if ($storefrontNetworkId) {
37
+ return Network::where('public_id', $storefrontNetworkId)->first();
38
+ }
39
+
40
+ return null;
41
+ };
42
+ }
43
+ }
@@ -121,7 +121,7 @@ class NetworkController extends StorefrontController
121
121
  }
122
122
 
123
123
  /**
124
- * Add a store to a network category
124
+ * Add a store to a network category.
125
125
  *
126
126
  * @return \Illuminate\Http\Response
127
127
  */