@fleetbase/storefront-engine 0.3.31 → 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.
- package/addon/components/customer-panel/orders.hbs +2 -2
- package/addon/components/customer-panel/orders.js +1 -1
- package/addon/components/modals/create-gateway.hbs +33 -12
- package/addon/components/modals/share-network.hbs +1 -1
- package/addon/components/network-category-picker.hbs +2 -1
- package/addon/components/network-category-picker.js +52 -22
- package/addon/components/order-panel.hbs +1 -1
- package/addon/components/widget/customers.hbs +5 -2
- package/addon/components/widget/customers.js +14 -6
- package/addon/components/widget/orders.hbs +30 -9
- package/addon/components/widget/orders.js +7 -1
- package/addon/components/widget/storefront-key-metrics.js +3 -3
- package/addon/components/widget/storefront-metrics.hbs +11 -1
- package/addon/components/widget/storefront-metrics.js +103 -1
- package/addon/controllers/customers/index.js +2 -2
- package/addon/controllers/networks/index/network/index.js +2 -0
- package/addon/controllers/networks/index/network/orders.js +1 -2
- package/addon/controllers/networks/index/network/stores.js +68 -64
- package/addon/controllers/networks/index.js +1 -2
- package/addon/controllers/products/index/category.js +1 -2
- package/addon/controllers/products/index/index.js +1 -2
- package/addon/controllers/settings/gateways.js +6 -1
- package/addon/controllers/settings/index.js +1 -0
- package/addon/controllers/settings/notifications.js +3 -5
- package/addon/models/network.js +1 -0
- package/addon/models/store.js +1 -0
- package/addon/routes/networks/index/network/index.js +5 -0
- package/addon/routes/networks/index/network/stores.js +6 -5
- package/addon/routes/settings/index.js +5 -0
- package/addon/services/order-actions.js +31 -0
- package/addon/styles/storefront-engine.css +29 -0
- package/addon/templates/networks/index/network/index.hbs +13 -0
- package/addon/templates/networks/index/network/stores.hbs +15 -1
- package/addon/templates/networks/index.hbs +1 -1
- package/addon/templates/settings/gateways.hbs +15 -4
- package/addon/templates/settings/index.hbs +13 -0
- package/addon/templates/settings/notifications.hbs +2 -2
- package/addon/utils/commerce-date-ranges.js +263 -0
- package/app/utils/commerce-date-ranges.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +2 -3
- package/server/migrations/2025_09_01_041353_add_default_order_config_column.php +110 -0
- package/server/src/Console/Commands/MigrateStripeSandboxCustomers.php +150 -0
- package/server/src/Expansions/OrderExpansion.php +43 -0
- package/server/src/Http/Controllers/NetworkController.php +20 -3
- package/server/src/Http/Controllers/OrderController.php +62 -9
- package/server/src/Http/Controllers/v1/CheckoutController.php +45 -48
- package/server/src/Http/Controllers/v1/ServiceQuoteController.php +19 -3
- package/server/src/Http/Middleware/SetStorefrontSession.php +8 -6
- package/server/src/Http/Resources/Customer.php +41 -17
- package/server/src/Http/Resources/Gateway.php +16 -11
- package/server/src/Http/Resources/Network.php +35 -34
- package/server/src/Http/Resources/NotificationChannel.php +17 -10
- package/server/src/Http/Resources/Store.php +36 -35
- package/server/src/Models/Customer.php +18 -2
- package/server/src/Models/FoodTruck.php +15 -0
- package/server/src/Models/Network.php +65 -2
- package/server/src/Models/Store.php +73 -10
- package/server/src/Notifications/StorefrontOrderAccepted.php +154 -0
- package/server/src/Observers/OrderObserver.php +6 -4
- package/server/src/Providers/StorefrontServiceProvider.php +1 -0
- package/server/src/Rules/IsValidLocation.php +12 -0
- package/server/src/Support/PushNotification.php +13 -4
- package/server/src/Support/Storefront.php +199 -37
- package/server/src/routes.php +2 -0
- package/translations/en-us.yaml +8 -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
package/extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetbase/storefront-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Headless Commerce & Marketplace Extension for Fleetbase",
|
|
5
5
|
"fleetbase": {
|
|
6
6
|
"route": "storefront",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@babel/core": "^7.23.2",
|
|
47
47
|
"@fleetbase/ember-core": "latest",
|
|
48
48
|
"@fleetbase/ember-ui": "latest",
|
|
49
|
-
"@fleetbase/fleetops-data": "
|
|
49
|
+
"@fleetbase/fleetops-data": "^0.1.19",
|
|
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",
|
|
@@ -79,7 +79,6 @@
|
|
|
79
79
|
"ember-cli-sri": "^2.1.1",
|
|
80
80
|
"ember-cli-terser": "^4.0.2",
|
|
81
81
|
"ember-concurrency": "^3.1.1",
|
|
82
|
-
"ember-concurrency-decorators": "^2.0.3",
|
|
83
82
|
"ember-data": "^4.12.5",
|
|
84
83
|
"ember-engines": "^0.9.0",
|
|
85
84
|
"ember-load-initializers": "^2.1.2",
|
|
@@ -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,150 @@
|
|
|
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 Stripe\Customer as StripeCustomer;
|
|
10
|
+
use Stripe\Exception\InvalidRequestException;
|
|
11
|
+
use Stripe\Stripe;
|
|
12
|
+
|
|
13
|
+
class MigrateStripeSandboxCustomers extends Command
|
|
14
|
+
{
|
|
15
|
+
/**
|
|
16
|
+
* The name and signature of the console command.
|
|
17
|
+
*
|
|
18
|
+
* Added a --store option to allow running the migration for a single store by UUID or public_id.
|
|
19
|
+
*/
|
|
20
|
+
protected $signature = 'storefront:migrate-stripe-customers
|
|
21
|
+
{--dry-run : Don\'t actually create or update anything}
|
|
22
|
+
{--store= : Specify a store UUID or public_id to migrate only that store}';
|
|
23
|
+
|
|
24
|
+
/** The console command description. */
|
|
25
|
+
protected $description = 'Migrates Stripe customers created in test mode to live mode and updates contact metadata.';
|
|
26
|
+
|
|
27
|
+
/** Execute the console command. */
|
|
28
|
+
public function handle(): int
|
|
29
|
+
{
|
|
30
|
+
$dryRun = $this->option('dry-run');
|
|
31
|
+
$storeInput = $this->option('store');
|
|
32
|
+
|
|
33
|
+
$this->info('Starting Stripe customer migration...');
|
|
34
|
+
|
|
35
|
+
// If a single store is specified, load that store by UUID or public_id.
|
|
36
|
+
if ($storeInput) {
|
|
37
|
+
$store = Store::where('uuid', $storeInput)
|
|
38
|
+
->orWhere('public_id', $storeInput)
|
|
39
|
+
->first();
|
|
40
|
+
|
|
41
|
+
if (!$store) {
|
|
42
|
+
$this->error("Store '{$storeInput}' not found.");
|
|
43
|
+
|
|
44
|
+
return Command::FAILURE;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
$this->migrateCustomers($store, $dryRun);
|
|
48
|
+
} else {
|
|
49
|
+
// Otherwise, load all stores.
|
|
50
|
+
$stores = Store::all();
|
|
51
|
+
foreach ($stores as $store) {
|
|
52
|
+
$this->migrateCustomers($store, $dryRun);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
$this->info('Stripe customer migration complete.');
|
|
57
|
+
|
|
58
|
+
return Command::SUCCESS;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Migrates all customers for a given store whose stripe_id is still pointing at test mode.
|
|
63
|
+
*
|
|
64
|
+
* Only runs when the store's gateway is configured for live mode; skips migration for sandbox gateways.
|
|
65
|
+
*/
|
|
66
|
+
public function migrateCustomers(Store $store, bool $dryRun): int
|
|
67
|
+
{
|
|
68
|
+
// Find the Stripe gateway for this store (there is only one gateway per store).
|
|
69
|
+
$gateway = Gateway::where([
|
|
70
|
+
'code' => 'stripe',
|
|
71
|
+
'owner_uuid'=> $store->uuid,
|
|
72
|
+
])->first();
|
|
73
|
+
|
|
74
|
+
if (!$gateway) {
|
|
75
|
+
$this->warn("Store {$store->name}: no Stripe gateway configured.");
|
|
76
|
+
|
|
77
|
+
return Command::SUCCESS;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// If the gateway is still sandbox, skip migration because we can't create live customers yet.
|
|
81
|
+
if ($gateway->sandbox) {
|
|
82
|
+
$this->warn("Store {$store->name} is using a sandbox gateway. Migration will only run when the gateway is switched to live.");
|
|
83
|
+
|
|
84
|
+
return Command::SUCCESS;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Inform the store being migrated
|
|
88
|
+
$this->info("Store {$store->name}: Will have sandbox customers migrated...");
|
|
89
|
+
|
|
90
|
+
$secretKey = $gateway->config->secret_key;
|
|
91
|
+
|
|
92
|
+
// Process customers in chunks to conserve memory.
|
|
93
|
+
Customer::where('company_uuid', $store->company_uuid)->chunk(50, function ($customers) use ($dryRun, $secretKey, $store) {
|
|
94
|
+
foreach ($customers as $customer) {
|
|
95
|
+
$stripeId = $customer->getMeta('stripe_id');
|
|
96
|
+
if (!$stripeId) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Set the Stripe API key to the store's live secret key.
|
|
101
|
+
Stripe::setApiKey($secretKey);
|
|
102
|
+
|
|
103
|
+
$current = null;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Attempt to retrieve the customer using the current key.
|
|
107
|
+
// If found and livemode is true, no migration needed.
|
|
108
|
+
$current = StripeCustomer::retrieve($stripeId);
|
|
109
|
+
if ($current && $current->livemode === true) {
|
|
110
|
+
$this->line("Customer {$customer->id}: Stripe ID {$stripeId} is already a live customer.");
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
} catch (InvalidRequestException $e) {
|
|
114
|
+
// The existing ID was not found with the live key, so it likely belongs to the test environment.
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If we're doing a dry run, just report what would happen.
|
|
118
|
+
if ($dryRun) {
|
|
119
|
+
$this->info("Customer {$customer->id}: Would migrate test Stripe ID {$stripeId} to live.");
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Create a new live customer using the contact details.
|
|
124
|
+
$metadata = [
|
|
125
|
+
'contact_id' => $customer->public_id,
|
|
126
|
+
'storefront_id' => $store->public_id,
|
|
127
|
+
'company_id' => $store->company_uuid,
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
$params = [
|
|
131
|
+
'description' => 'Customer migrated from sandbox',
|
|
132
|
+
'email' => $customer->email,
|
|
133
|
+
'name' => $customer->name,
|
|
134
|
+
'phone' => $customer->phone,
|
|
135
|
+
'metadata' => $metadata,
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
$newCustomer = StripeCustomer::create($params);
|
|
139
|
+
|
|
140
|
+
// Save the old test ID and update the live ID.
|
|
141
|
+
$customer->updateMeta('stripe_id_sandbox', $stripeId);
|
|
142
|
+
$customer->updateMeta('stripe_id', $newCustomer->id);
|
|
143
|
+
|
|
144
|
+
$this->info("Customer {$customer->id}: Migrated test ID {$stripeId} to live Stripe ID {$newCustomer->id}.");
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return Command::SUCCESS;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -110,7 +110,7 @@ class NetworkController extends StorefrontController
|
|
|
110
110
|
*/
|
|
111
111
|
public function removeStores(string $id, NetworkActionRequest $request)
|
|
112
112
|
{
|
|
113
|
-
$stores =
|
|
113
|
+
$stores = $request->array('stores');
|
|
114
114
|
|
|
115
115
|
// delete each
|
|
116
116
|
foreach ($stores as $storeId) {
|
|
@@ -121,7 +121,7 @@ class NetworkController extends StorefrontController
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
|
-
*
|
|
124
|
+
* Add a store to a network category.
|
|
125
125
|
*
|
|
126
126
|
* @return \Illuminate\Http\Response
|
|
127
127
|
*/
|
|
@@ -132,7 +132,6 @@ class NetworkController extends StorefrontController
|
|
|
132
132
|
|
|
133
133
|
// get network store instance
|
|
134
134
|
$networkStore = NetworkStore::where(['network_uuid' => $id, 'store_uuid' => $store])->first();
|
|
135
|
-
|
|
136
135
|
if ($networkStore) {
|
|
137
136
|
$networkStore->update(['category_uuid' => $category]);
|
|
138
137
|
}
|
|
@@ -140,6 +139,24 @@ class NetworkController extends StorefrontController
|
|
|
140
139
|
return response()->json(['status' => 'ok']);
|
|
141
140
|
}
|
|
142
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Remove stores to a network.
|
|
144
|
+
*
|
|
145
|
+
* @return \Illuminate\Http\Response
|
|
146
|
+
*/
|
|
147
|
+
public function removeStoreCategory(string $id, NetworkActionRequest $request)
|
|
148
|
+
{
|
|
149
|
+
$store = $request->input('store');
|
|
150
|
+
|
|
151
|
+
// get network store instance
|
|
152
|
+
$networkStore = NetworkStore::where(['network_uuid' => $id, 'store_uuid' => $store])->first();
|
|
153
|
+
if ($networkStore) {
|
|
154
|
+
$networkStore->update(['category_uuid' => null]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return response()->json(['status' => 'ok']);
|
|
158
|
+
}
|
|
159
|
+
|
|
143
160
|
/**
|
|
144
161
|
* Remove stores to a network.
|
|
145
162
|
*
|