@fleetbase/storefront-engine 0.3.17 → 0.3.20

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 (102) hide show
  1. package/addon/components/customer-panel/orders.hbs +107 -104
  2. package/addon/components/customer-panel/orders.js +52 -45
  3. package/addon/components/modals/incoming-order.hbs +208 -199
  4. package/addon/components/modals/manage-addons.js +6 -2
  5. package/addon/components/modals/order-ready-assign-driver.hbs +1 -1
  6. package/addon/components/order-panel/details.js +0 -2
  7. package/addon/components/order-panel.hbs +314 -1
  8. package/addon/components/order-panel.js +51 -3
  9. package/addon/components/widget/customers.hbs +75 -51
  10. package/addon/components/widget/customers.js +29 -41
  11. package/addon/components/widget/orders.hbs +278 -119
  12. package/addon/components/widget/orders.js +75 -80
  13. package/addon/components/widget/storefront-metrics.hbs +3 -6
  14. package/addon/components/widget/storefront-metrics.js +25 -41
  15. package/addon/controllers/orders/index.js +214 -105
  16. package/addon/controllers/products/index/category/new.js +2 -1
  17. package/addon/controllers/products/index/index.js +0 -23
  18. package/addon/controllers/settings/gateways.js +14 -18
  19. package/addon/helpers/get-tip-amount.js +13 -2
  20. package/addon/models/product-addon-category.js +3 -0
  21. package/addon/routes/application.js +2 -4
  22. package/addon/services/order-actions.js +248 -0
  23. package/addon/services/storefront.js +2 -0
  24. package/addon/styles/storefront-engine.css +55 -0
  25. package/addon/templates/home.hbs +2 -1
  26. package/addon/templates/orders/index/view.hbs +1 -1
  27. package/addon/templates/orders/index.hbs +26 -3
  28. package/addon/templates/products/index/category/new.hbs +4 -1
  29. package/addon/templates/products/index/index.hbs +28 -28
  30. package/addon/templates/settings/gateways.hbs +1 -1
  31. package/addon/templates/settings/index.hbs +2 -2
  32. package/addon/templates/settings.hbs +1 -1
  33. package/app/services/order-actions.js +1 -0
  34. package/composer.json +1 -1
  35. package/extension.json +1 -1
  36. package/package.json +1 -1
  37. package/server/migrations/2023_05_03_025307_create_carts_table.php +1 -1
  38. package/server/migrations/2023_05_03_025307_create_checkouts_table.php +1 -1
  39. package/server/migrations/2023_05_03_025307_create_gateways_table.php +1 -1
  40. package/server/migrations/2023_05_03_025307_create_network_stores_table.php +1 -1
  41. package/server/migrations/2023_05_03_025307_create_networks_table.php +1 -1
  42. package/server/migrations/2023_05_03_025307_create_notification_channels_table.php +1 -1
  43. package/server/migrations/2023_05_03_025307_create_payment_methods_table.php +1 -1
  44. package/server/migrations/2023_05_03_025307_create_product_addon_categories_table.php +1 -1
  45. package/server/migrations/2023_05_03_025307_create_product_addons_table.php +1 -1
  46. package/server/migrations/2023_05_03_025307_create_product_hours_table.php +1 -1
  47. package/server/migrations/2023_05_03_025307_create_product_store_locations_table.php +1 -1
  48. package/server/migrations/2023_05_03_025307_create_product_variant_options_table.php +1 -1
  49. package/server/migrations/2023_05_03_025307_create_product_variants_table.php +1 -1
  50. package/server/migrations/2023_05_03_025307_create_products_table.php +1 -1
  51. package/server/migrations/2023_05_03_025307_create_reviews_table.php +1 -1
  52. package/server/migrations/2023_05_03_025307_create_store_hours_table.php +1 -1
  53. package/server/migrations/2023_05_03_025307_create_store_locations_table.php +1 -1
  54. package/server/migrations/2023_05_03_025307_create_stores_table.php +1 -1
  55. package/server/migrations/2023_05_03_025307_create_votes_table.php +1 -1
  56. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_carts_table.php +1 -1
  57. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_checkouts_table.php +1 -1
  58. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_gateways_table.php +1 -1
  59. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_network_stores_table.php +1 -1
  60. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_networks_table.php +1 -1
  61. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_notification_channels_table.php +1 -1
  62. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_payment_methods_table.php +1 -1
  63. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_product_addon_categories_table.php +1 -1
  64. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_product_addons_table.php +1 -1
  65. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_product_hours_table.php +1 -1
  66. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_product_store_locations_table.php +1 -1
  67. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_product_variant_options_table.php +1 -1
  68. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_product_variants_table.php +1 -1
  69. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_products_table.php +1 -1
  70. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_reviews_table.php +1 -1
  71. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_store_hours_table.php +1 -1
  72. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_store_locations_table.php +1 -1
  73. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_stores_table.php +1 -1
  74. package/server/migrations/2023_05_03_025310_add_foreign_keys_to_votes_table.php +1 -1
  75. package/server/src/Http/Controllers/ActionController.php +2 -1
  76. package/server/src/Http/Controllers/OrderController.php +15 -2
  77. package/server/src/Http/Controllers/ProductController.php +2 -0
  78. package/server/src/Http/Controllers/v1/CheckoutController.php +337 -40
  79. package/server/src/Http/Controllers/v1/CustomerController.php +88 -3
  80. package/server/src/Http/Controllers/v1/ServiceQuoteController.php +5 -5
  81. package/server/src/Http/Controllers/v1/StoreController.php +35 -5
  82. package/server/src/Http/Requests/CreateCustomerRequest.php +4 -0
  83. package/server/src/Http/Requests/CreateStripeSetupIntentRequest.php +31 -0
  84. package/server/src/Http/Requests/CustomerRequest.php +31 -0
  85. package/server/src/Http/Requests/InitializeCheckoutRequest.php +2 -1
  86. package/server/src/Http/Resources/Cart.php +18 -1
  87. package/server/src/Http/Resources/Customer.php +19 -14
  88. package/server/src/Http/Resources/Store.php +5 -1
  89. package/server/src/Models/AddonCategory.php +14 -16
  90. package/server/src/Models/Cart.php +10 -5
  91. package/server/src/Models/Customer.php +2 -2
  92. package/server/src/Models/Gateway.php +9 -4
  93. package/server/src/Models/Product.php +9 -10
  94. package/server/src/Models/ProductAddonCategory.php +2 -0
  95. package/server/src/Models/Store.php +2 -2
  96. package/server/src/Observers/OrderObserver.php +7 -1
  97. package/server/src/Rules/IsValidLocation.php +2 -2
  98. package/server/src/Support/QPay.php +35 -1
  99. package/server/src/Support/Storefront.php +34 -0
  100. package/server/src/Support/StripeUtils.php +38 -0
  101. package/server/src/routes.php +19 -0
  102. package/translations/en-us.yaml +8 -2
@@ -14,6 +14,7 @@ use Fleetbase\Http\Controllers\Controller;
14
14
  use Fleetbase\Models\Transaction;
15
15
  use Fleetbase\Models\TransactionItem;
16
16
  use Fleetbase\Storefront\Http\Requests\CaptureOrderRequest;
17
+ use Fleetbase\Storefront\Http\Requests\CreateStripeSetupIntentRequest;
17
18
  use Fleetbase\Storefront\Http\Requests\InitializeCheckoutRequest;
18
19
  use Fleetbase\Storefront\Models\Cart;
19
20
  use Fleetbase\Storefront\Models\Checkout;
@@ -25,8 +26,11 @@ use Fleetbase\Storefront\Models\StoreLocation;
25
26
  use Fleetbase\Storefront\Notifications\StorefrontOrderPreparing;
26
27
  use Fleetbase\Storefront\Support\QPay;
27
28
  use Fleetbase\Storefront\Support\Storefront;
29
+ use Fleetbase\Storefront\Support\StripeUtils;
30
+ use Fleetbase\Support\SocketCluster\SocketClusterService;
28
31
  use Illuminate\Http\Request;
29
32
  use Illuminate\Support\Arr;
33
+ use Illuminate\Support\Facades\Log;
30
34
  use Illuminate\Support\Str;
31
35
  use Stripe\Exception\InvalidRequestException;
32
36
 
@@ -41,7 +45,7 @@ class CheckoutController extends Controller
41
45
  $isCashOnDelivery = $request->input('cash') || $gatewayCode === 'cash';
42
46
  $isPickup = $request->input('pickup', false);
43
47
  $tip = $request->input('tip', false);
44
- $deliveryTip = $request->or(['delivery_tip', 'deliveryTip'], false);
48
+ $deliveryTip = $request->or(['deliveryTip', 'delivery_tip'], false);
45
49
 
46
50
  // create checkout options
47
51
  $checkoutOptions = Utils::createObject([
@@ -63,7 +67,7 @@ class CheckoutController extends Controller
63
67
  }
64
68
 
65
69
  if (!$gateway) {
66
- return response()->error('No gateway configured!');
70
+ return response()->apiError('No gateway configured!');
67
71
  }
68
72
 
69
73
  // handle checkout initialization based on gateway
@@ -72,11 +76,11 @@ class CheckoutController extends Controller
72
76
  }
73
77
 
74
78
  // handle checkout initialization based on gateway
75
- if ($gateway->isQpayGateway) {
76
- return static::initializeQpayCheckout($customer, $gateway, $serviceQuote, $cart, $checkoutOptions);
79
+ if ($gateway->isQPayGateway) {
80
+ return static::initializeQPayCheckout($customer, $gateway, $serviceQuote, $cart, $checkoutOptions);
77
81
  }
78
82
 
79
- return response()->error('Unable to initialize checkout!');
83
+ return response()->apiError('Unable to initialize checkout!');
80
84
  }
81
85
 
82
86
  public static function initializeCashCheckout(Contact $customer, Gateway $gateway, ServiceQuote $serviceQuote, Cart $cart, $checkoutOptions)
@@ -86,7 +90,7 @@ class CheckoutController extends Controller
86
90
 
87
91
  // get amount/subtotal
88
92
  $amount = static::calculateCheckoutAmount($cart, $serviceQuote, $checkoutOptions);
89
- $currency = $cart->currency ?? session('storefront_currency');
93
+ $currency = $cart->getCurrency();
90
94
 
91
95
  // get store id if applicable
92
96
  $storeId = session('storefront_store');
@@ -128,18 +132,18 @@ class CheckoutController extends Controller
128
132
  ]);
129
133
  }
130
134
 
131
- public static function initializeStripeCheckout(Contact $customer, Gateway $gateway, ServiceQuote $serviceQuote, Cart $cart, $checkoutOptions)
135
+ public static function initializeStripeCheckout(Contact $customer, Gateway $gateway, ?ServiceQuote $serviceQuote, Cart $cart, $checkoutOptions)
132
136
  {
133
137
  // check if pickup order
134
138
  $isPickup = $checkoutOptions->is_pickup;
135
139
 
136
140
  // get amount/subtotal
137
141
  $amount = static::calculateCheckoutAmount($cart, $serviceQuote, $checkoutOptions);
138
- $currency = $cart->currency ?? session('storefront_currency');
142
+ $currency = $cart->getCurrency();
139
143
 
140
144
  // check for secret key first
141
145
  if (!isset($gateway->config->secret_key)) {
142
- return response()->error('Gateway not configured correctly!');
146
+ return response()->apiError('Gateway not configured correctly!');
143
147
  }
144
148
 
145
149
  // Set the stipre secret key from gateway
@@ -169,15 +173,27 @@ class CheckoutController extends Controller
169
173
  ['stripe_version' => '2020-08-27']
170
174
  );
171
175
  } else {
172
- return response()->error('Error from Stripe: ' . $errorMessage);
176
+ return response()->apiError('Error from Stripe: ' . $errorMessage);
173
177
  }
174
178
  }
175
179
 
176
- $paymentIntent = \Stripe\PaymentIntent::create([
177
- 'amount' => $amount,
180
+ // Prepare payment intent data
181
+ $paymentIntentData = [
182
+ 'amount' => Utils::formatAmountForStripe($amount, $currency),
178
183
  'currency' => $currency,
179
184
  'customer' => $customer->getMeta('stripe_id'),
180
- ]);
185
+ ];
186
+
187
+ // Check if customer has a saved default payment method
188
+ if (StripeUtils::isCustomerPaymentMethodValid($customer)) {
189
+ $paymentIntentData['payment_method'] = $$customer->getMeta('stripe_payment_method_id');
190
+ }
191
+
192
+ try {
193
+ $paymentIntent = \Stripe\PaymentIntent::create($paymentIntentData);
194
+ } catch (\Exception $e) {
195
+ return response()->apiError($e->getMessage());
196
+ }
181
197
 
182
198
  // create checkout token
183
199
  $checkout = Checkout::create([
@@ -186,7 +202,200 @@ class CheckoutController extends Controller
186
202
  'network_uuid' => session('storefront_network'),
187
203
  'cart_uuid' => $cart->uuid,
188
204
  'gateway_uuid' => $gateway->uuid,
189
- 'service_quote_uuid' => $serviceQuote->uuid,
205
+ 'service_quote_uuid' => $serviceQuote ? $serviceQuote->uuid : null,
206
+ 'owner_uuid' => $customer->uuid,
207
+ 'owner_type' => 'fleet-ops:contact',
208
+ 'amount' => $amount,
209
+ 'currency' => $currency,
210
+ 'is_pickup' => $isPickup,
211
+ 'options' => $checkoutOptions,
212
+ 'cart_state' => $cart->toArray(),
213
+ ]);
214
+
215
+ return response()->json([
216
+ 'paymentIntent' => $paymentIntent->id,
217
+ 'clientSecret' => $paymentIntent->client_secret,
218
+ 'ephemeralKey' => $ephemeralKey->secret,
219
+ 'customerId' => $customer->getMeta('stripe_id'),
220
+ 'token' => $checkout->token,
221
+ ]);
222
+ }
223
+
224
+ public function createStripeSetupIntentForCustomer(CreateStripeSetupIntentRequest $request)
225
+ {
226
+ $customerId = $request->input('customer');
227
+ $gateway = Storefront::findGateway('stripe');
228
+
229
+ if (!$gateway) {
230
+ return response()->apiError('Stripe not setup.');
231
+ }
232
+
233
+ $customer = Customer::findFromCustomerId($customerId);
234
+
235
+ \Stripe\Stripe::setApiKey($gateway->config->secret_key);
236
+
237
+ // Ensure customer has a stripe_id
238
+ if ($customer->missingMeta('stripe_id')) {
239
+ Storefront::createStripeCustomerForContact($customer);
240
+ }
241
+
242
+ // Prepare payment intent data
243
+ $paymentIntentData = [
244
+ 'customer' => $customer->getMeta('stripe_id'),
245
+ ];
246
+
247
+ // Check if customer has a saved default payment method
248
+ if (StripeUtils::isCustomerPaymentMethodValid($customer)) {
249
+ $paymentIntentData['payment_method'] = $$customer->getMeta('stripe_payment_method_id');
250
+ }
251
+
252
+ try {
253
+ // Create SetupIntent
254
+ $setupIntent = \Stripe\SetupIntent::create($paymentIntentData);
255
+
256
+ $defaultPaymentMethod = null;
257
+ $savedPaymentMethodId = $customer->getMeta('stripe_payment_method_id');
258
+
259
+ if ($savedPaymentMethodId) {
260
+ // Attempt to retrieve the stored payment method from Stripe
261
+ try {
262
+ $pm = \Stripe\PaymentMethod::retrieve($savedPaymentMethodId);
263
+ if ($pm && $pm->customer === $customer->getMeta('stripe_id')) {
264
+ $defaultPaymentMethod = [
265
+ 'paymentMethodId' => $pm->id,
266
+ 'id' => $pm->id,
267
+ 'brand' => Str::title($pm->card->brand),
268
+ 'last4' => $pm->card->last4,
269
+ 'label' => $pm->card->last4,
270
+ 'exp_month' => $pm->card->exp_month,
271
+ 'exp_year' => $pm->card->exp_year,
272
+ 'country' => $pm->card->country,
273
+ 'funding' => $pm->card->funding,
274
+ ];
275
+ }
276
+ } catch (\Exception $e) {
277
+ // If retrieval fails, we just won't have a defaultPaymentMethod
278
+ Log::warning('Failed to retrieve saved payment method from Stripe: ' . $e->getMessage());
279
+ }
280
+ }
281
+
282
+ return response()->json([
283
+ 'setupIntent' => $setupIntent->id,
284
+ 'clientSecret' => $setupIntent->client_secret,
285
+ 'defaultPaymentMethod' => $defaultPaymentMethod,
286
+ ]);
287
+ } catch (\Exception $e) {
288
+ return response()->apiError($e->getMessage());
289
+ }
290
+ }
291
+
292
+ public function updateStripePaymentIntent(Request $request)
293
+ {
294
+ // Extract necessary parameters from request
295
+ $customerId = $request->input('customer');
296
+ $cartId = $request->input('cart');
297
+ $serviceQuoteId = $request->or(['serviceQuote', 'service_quote']);
298
+ $paymentIntentId = $request->or(['paymentIntent', 'paymentIntentId', 'payment_intent_id']);
299
+ $isPickup = $request->input('pickup', false);
300
+ $tip = $request->input('tip', false);
301
+ $deliveryTip = $request->or(['deliveryTip', 'delivery_tip'], false);
302
+
303
+ // Create checkout options from request
304
+ $checkoutOptions = Utils::createObject([
305
+ 'is_pickup' => $isPickup,
306
+ 'tip' => $tip,
307
+ 'delivery_tip' => $deliveryTip,
308
+ ]);
309
+
310
+ // Retrieve the gateway (stripe)
311
+ $gateway = Storefront::findGateway('stripe');
312
+ if (!$gateway) {
313
+ return response()->apiError('No stripe gateway configured!');
314
+ }
315
+
316
+ // Retrieve and validate necessary models
317
+ $cart = Cart::retrieve($cartId);
318
+ if (!$cart) {
319
+ return response()->apiError('Invalid cart ID provided');
320
+ }
321
+
322
+ $customer = Customer::findFromCustomerId($customerId);
323
+ if (!$customer) {
324
+ return response()->apiError('Invalid customer ID provided');
325
+ }
326
+
327
+ $serviceQuote = ServiceQuote::select(['amount', 'meta', 'uuid', 'public_id'])
328
+ ->where('public_id', $serviceQuoteId)
329
+ ->first();
330
+
331
+ // Recalculate amount based on cart, serviceQuote, and checkoutOptions
332
+ $amount = static::calculateCheckoutAmount($cart, $serviceQuote, $checkoutOptions);
333
+ $currency = $cart->getCurrency();
334
+
335
+ // Check for Stripe secret key
336
+ if (!isset($gateway->config->secret_key)) {
337
+ return response()->apiError('Gateway not configured correctly!');
338
+ }
339
+
340
+ // Set Stripe API key
341
+ \Stripe\Stripe::setApiKey($gateway->config->secret_key);
342
+
343
+ // Ensure customer has a stripe_id
344
+ if ($customer->missingMeta('stripe_id')) {
345
+ Storefront::createStripeCustomerForContact($customer);
346
+ }
347
+
348
+ // Retrieve the existing PaymentIntent
349
+ try {
350
+ $paymentIntent = \Stripe\PaymentIntent::retrieve($paymentIntentId);
351
+ } catch (\Exception $e) {
352
+ return response()->apiError('Failed to retrieve PaymentIntent: ' . $e->getMessage());
353
+ }
354
+
355
+ // Check if PaymentIntent is in a modifiable state
356
+ $modifiableStatuses = ['requires_payment_method', 'requires_confirmation', 'requires_action', 'processing'];
357
+ if (!in_array($paymentIntent->status, $modifiableStatuses)) {
358
+ return response()->apiError('PaymentIntent cannot be updated at this stage.');
359
+ }
360
+
361
+ // Prepare the updated data
362
+ $updateData = [
363
+ 'amount' => Utils::formatAmountForStripe($amount, $currency),
364
+ 'currency' => $currency,
365
+ ];
366
+
367
+ // Update the PaymentIntent
368
+ try {
369
+ $paymentIntent = \Stripe\PaymentIntent::update($paymentIntentId, $updateData);
370
+ } catch (\Exception $e) {
371
+ return response()->apiError('Failed to update PaymentIntent: ' . $e->getMessage());
372
+ }
373
+
374
+ // If payment intent has a payment method set already update for the customer
375
+ $paymentIntentPaymentMethodId = $paymentIntent->payment_method;
376
+ $customerPaymentMethodId = $customer->getMeta('stripe_payment_method_id');
377
+ if ($paymentIntentPaymentMethodId !== $customerPaymentMethodId) {
378
+ $customer->updateMeta('stripe_payment_method_id', $paymentIntentPaymentMethodId);
379
+ }
380
+
381
+ // Create a new EphemeralKey if needed for the frontend
382
+ try {
383
+ $ephemeralKey = \Stripe\EphemeralKey::create(
384
+ ['customer' => $customer->getMeta('stripe_id')],
385
+ ['stripe_version' => '2020-08-27']
386
+ );
387
+ } catch (\Exception $e) {
388
+ return response()->apiError('Failed to create ephemeral key: ' . $e->getMessage());
389
+ }
390
+
391
+ // Create a new checkout token
392
+ $checkout = Checkout::create([
393
+ 'company_uuid' => session('company'),
394
+ 'store_uuid' => session('storefront_store'),
395
+ 'network_uuid' => session('storefront_network'),
396
+ 'cart_uuid' => $cart->uuid,
397
+ 'gateway_uuid' => $gateway->uuid,
398
+ 'service_quote_uuid' => $serviceQuote ? $serviceQuote->uuid : null,
190
399
  'owner_uuid' => $customer->uuid,
191
400
  'owner_type' => 'fleet-ops:contact',
192
401
  'amount' => $amount,
@@ -196,15 +405,17 @@ class CheckoutController extends Controller
196
405
  'cart_state' => $cart->toArray(),
197
406
  ]);
198
407
 
408
+ // Return JSON response with updated PaymentIntent and ephemeral key
199
409
  return response()->json([
200
- 'paymentIntent' => $paymentIntent->client_secret,
410
+ 'paymentIntent' => $paymentIntent->id,
411
+ 'clientSecret' => $paymentIntent->client_secret,
201
412
  'ephemeralKey' => $ephemeralKey->secret,
202
413
  'customerId' => $customer->getMeta('stripe_id'),
203
414
  'token' => $checkout->token,
204
415
  ]);
205
416
  }
206
417
 
207
- public static function initializeQpayCheckout(Contact $customer, Gateway $gateway, ServiceQuote $serviceQuote, Cart $cart, $checkoutOptions)
418
+ public static function initializeQPayCheckout(Contact $customer, Gateway $gateway, ?ServiceQuote $serviceQuote, Cart $cart, $checkoutOptions)
208
419
  {
209
420
  // Get store info
210
421
  $about = Storefront::about();
@@ -214,11 +425,11 @@ class CheckoutController extends Controller
214
425
 
215
426
  // get amount/subtotal
216
427
  $amount = static::calculateCheckoutAmount($cart, $serviceQuote, $checkoutOptions);
217
- $currency = $cart->currency ?? session('storefront_currency');
428
+ $currency = $cart->getCurrency();
218
429
 
219
430
  // check for secret key first
220
431
  if (!isset($gateway->config->username)) {
221
- return response()->error('Gateway not configured correctly!');
432
+ return response()->apiError('Gateway not configured correctly!');
222
433
  }
223
434
 
224
435
  // Create qpay instance
@@ -231,15 +442,8 @@ class CheckoutController extends Controller
231
442
  // Set auth token
232
443
  $qpay = $qpay->setAuthToken();
233
444
 
234
- // Create invoice description
235
- $invoiceAmount = round($amount / 100);
236
- $invoiceCode = $gateway->sandbox ? 'TEST_INVOICE' : $gateway->config->invoice_id;
237
- $invoiceDescription = $about->name . ' cart checkout';
238
- $invoiceReceiverCode = $gateway->public_id;
239
- $senderInvoiceNo = $cart->id;
240
-
241
- // Create qpay invoice
242
- $invoice = $qpay->createSimpleInvoice($invoiceAmount, $invoiceCode, $invoiceDescription, $invoiceReceiverCode, $senderInvoiceNo);
445
+ // Test payment
446
+ $testPayment = is_string(data_get($checkoutOptions, 'testPayment')) && $gateway->sandbox;
243
447
 
244
448
  // Create checkout token
245
449
  $checkout = Checkout::create([
@@ -248,7 +452,7 @@ class CheckoutController extends Controller
248
452
  'network_uuid' => session('storefront_network'),
249
453
  'cart_uuid' => $cart->uuid,
250
454
  'gateway_uuid' => $gateway->uuid,
251
- 'service_quote_uuid' => $serviceQuote->uuid,
455
+ 'service_quote_uuid' => $serviceQuote ? $serviceQuote->uuid : null,
252
456
  'owner_uuid' => $customer->uuid,
253
457
  'owner_type' => 'fleet-ops:contact',
254
458
  'amount' => $amount,
@@ -258,12 +462,103 @@ class CheckoutController extends Controller
258
462
  'cart_state' => $cart->toArray(),
259
463
  ]);
260
464
 
465
+ // Set QPay Callback
466
+ $callbackParams = ['checkout' => $checkout->public_id];
467
+ if ($testPayment) {
468
+ $callbackParams['test'] = data_get($checkoutOptions, 'testPayment');
469
+ }
470
+ $callbackUrl = Utils::apiUrl('storefront/v1/checkouts/capture-qpay', $callbackParams);
471
+
472
+ // Create invoice description
473
+ // $invoiceAmount = round($amount / 100);
474
+ $invoiceAmount = $amount;
475
+ $invoiceCode = $gateway->sandbox ? 'TEST_INVOICE' : $gateway->config->invoice_id;
476
+ $invoiceDescription = $about->name . ' cart checkout';
477
+ $invoiceReceiverCode = $gateway->public_id;
478
+ $senderInvoiceNo = $cart->id;
479
+
480
+ // Create qpay invoice
481
+ $invoice = $qpay->createSimpleInvoice($invoiceAmount, $invoiceCode, $invoiceDescription, $invoiceReceiverCode, $senderInvoiceNo, $callbackUrl);
482
+
483
+ // Update checkout with invoice id
484
+ $checkout->updateOption('qpay_invoice_id', data_get($invoice, 'invoice_id'));
485
+
261
486
  return response()->json([
262
- 'invoice' => $invoice,
263
- 'token' => $checkout->token,
487
+ 'invoice' => $invoice,
488
+ 'checkout' => $checkout->public_id,
489
+ 'token' => $checkout->token,
264
490
  ]);
265
491
  }
266
492
 
493
+ public function captureQPayCallback(Request $request)
494
+ {
495
+ $checkoutId = $request->input('checkout');
496
+ $respond = $request->boolean('respond');
497
+ $test = $request->input('test'); // success, error
498
+
499
+ if ($checkoutId) {
500
+ // Get the checkout instance
501
+ $checkout = Checkout::where('public_id', $checkoutId)->first();
502
+ if ($checkout) {
503
+ // Restore the payment gateway instance
504
+ $gateway = Gateway::where('uuid', $checkout->gateway_uuid)->first();
505
+ if ($gateway) {
506
+ try {
507
+ // If testing send back test data
508
+ // Handle test below
509
+ if ($gateway->sandbox && is_string($test) && in_array($test, ['success', 'error'])) {
510
+ $data = ['error' => null, 'checkout' => $checkout->public_id, 'payment' => null];
511
+ if ($test === 'success') {
512
+ $data['payment'] = QPay::createTestPaymentDataFromCheckout($checkout);
513
+ } else {
514
+ $data['error'] = ['error' => 'PAYMENT_NOT_PAID', 'message' => 'Payment has not paid!'];
515
+ }
516
+
517
+ SocketClusterService::publish('checkout.' . $checkout->public_id, $data);
518
+ if ($respond) {
519
+ return response()->json($data);
520
+ }
521
+
522
+ return;
523
+ }
524
+
525
+ // Create qpay instance
526
+ $qpay = QPay::instance($gateway->config->username, $gateway->config->password, $gateway->callback_url);
527
+ if ($gateway->sandbox) {
528
+ $qpay = $qpay->useSandbox();
529
+ }
530
+
531
+ // Set auth token
532
+ $qpay = $qpay->setAuthToken();
533
+
534
+ // Check payment status from qpay
535
+ $payment = $qpay->paymentGet($checkoutId);
536
+ if ($payment) {
537
+ $data = [];
538
+ if ($payment->error) {
539
+ $data = ['error' => (array) $payment, 'checkout' => $checkout->public_id, 'payment' => null];
540
+ } else {
541
+ $data = ['payment' =>(array) $payment, 'checkout' => $checkout->public_id, 'error' => null];
542
+ }
543
+
544
+ SocketClusterService::publish('checkout.' . $checkout->public_id, $data);
545
+ if ($respond) {
546
+ return response()->json($data);
547
+ }
548
+ }
549
+ } catch (\Exception $e) {
550
+ Log::error('[QPAY CHECKOUT ERROR: ' . $e->getMessage() . ']', $checkout->toArray());
551
+ if ($respond) {
552
+ return response()->apiError($e->getMessage());
553
+ }
554
+ }
555
+ }
556
+ }
557
+ }
558
+
559
+ return response()->json();
560
+ }
561
+
267
562
  /**
268
563
  * Process a cart item and create/save an entity.
269
564
  *
@@ -314,8 +609,8 @@ class CheckoutController extends Controller
314
609
  $customer = $checkout->owner;
315
610
  $serviceQuote = $checkout->serviceQuote;
316
611
  $gateway = $checkout->is_cod ? Gateway::cash() : $checkout->gateway;
317
- $origin = $serviceQuote->getMeta('origin', []);
318
- $destination = $serviceQuote->getMeta('destination');
612
+ $origin = $serviceQuote ? $serviceQuote->getMeta('origin', []) : null;
613
+ $destination = $serviceQuote ? $serviceQuote->getMeta('destination') : null;
319
614
  $cart = $checkout->cart;
320
615
 
321
616
  // if cart is null then cart has either been deleted or expired
@@ -327,7 +622,7 @@ class CheckoutController extends Controller
327
622
 
328
623
  // $amount = $checkout->amount ?? ($checkout->is_pickup ? $cart->subtotal : $cart->subtotal + $serviceQuote->amount);
329
624
  $amount = static::calculateCheckoutAmount($cart, $serviceQuote, $checkout->options);
330
- $currency = $checkout->currency ?? ($cart->currency ?? session('storefront_currency'));
625
+ $currency = $checkout->currency ?? $cart->getCurrency();
331
626
  $store = $about;
332
627
 
333
628
  // check if order is via network for a single store
@@ -348,7 +643,7 @@ class CheckoutController extends Controller
348
643
 
349
644
  // super rare condition
350
645
  if (!$store) {
351
- return response()->error('No storefront in request to capture order!');
646
+ return response()->apiError('No storefront in request to capture order!');
352
647
  }
353
648
 
354
649
  // prepare for integrated vendor order if applicable
@@ -360,7 +655,7 @@ class CheckoutController extends Controller
360
655
  try {
361
656
  $integratedVendorOrder = $serviceQuote->integratedVendor->api()->createOrderFromServiceQuote($serviceQuote, $request);
362
657
  } catch (\Exception $e) {
363
- return response()->error($e->getMessage());
658
+ return response()->apiError($e->getMessage());
364
659
  }
365
660
  }
366
661
 
@@ -550,7 +845,9 @@ class CheckoutController extends Controller
550
845
  Storefront::alertNewOrder($order);
551
846
 
552
847
  // purchase service quote
553
- $order->purchaseQuote($serviceQuote->uuid, $transactionDetails);
848
+ if ($serviceQuote) {
849
+ $order->purchaseQuote($serviceQuote->uuid, $transactionDetails);
850
+ }
554
851
 
555
852
  // if order is auto accepted update status
556
853
  if ($about->isOption('auto_accept_orders')) {
@@ -601,10 +898,10 @@ class CheckoutController extends Controller
601
898
  $cart = $checkout->cart;
602
899
  // $amount = $checkout->amount ?? ($checkout->is_pickup ? $cart->subtotal : $cart->subtotal + $serviceQuote->amount);
603
900
  $amount = static::calculateCheckoutAmount($cart, $serviceQuote, $checkout->options);
604
- $currency = $checkout->currency ?? ($cart->currency ?? session('storefront_currency'));
901
+ $currency = $checkout->currency ?? $cart->getCurrency();
605
902
 
606
903
  if (!$about) {
607
- return response()->error('No network in request to capture order!');
904
+ return response()->apiError('No network in request to capture order!');
608
905
  }
609
906
 
610
907
  // prepare for integrated vendor order if applicable
@@ -616,7 +913,7 @@ class CheckoutController extends Controller
616
913
  try {
617
914
  $integratedVendorOrder = $serviceQuote->integratedVendor->api()->createOrderFromServiceQuote($serviceQuote, $request);
618
915
  } catch (\Exception $e) {
619
- return response()->error($e->getMessage());
916
+ return response()->apiError($e->getMessage());
620
917
  }
621
918
  }
622
919
 
@@ -901,7 +1198,7 @@ class CheckoutController extends Controller
901
1198
  *
902
1199
  * @param stdClass $checkoutOptions
903
1200
  */
904
- private static function calculateCheckoutAmount(Cart $cart, ServiceQuote $serviceQuote, $checkoutOptions): int
1201
+ private static function calculateCheckoutAmount(Cart $cart, ?ServiceQuote $serviceQuote, $checkoutOptions): int
905
1202
  {
906
1203
  // cast checkout options to object always
907
1204
  $checkoutOptions = (object) $checkoutOptions;