@fleetbase/storefront-engine 0.3.25 → 0.3.27

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/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/storefront-api",
3
- "version": "0.3.25",
3
+ "version": "0.3.27",
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.3.25",
3
+ "version": "0.3.27",
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.3.25",
3
+ "version": "0.3.27",
4
4
  "description": "Headless Commerce & Marketplace Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "storefront",
@@ -19,6 +19,7 @@ use Fleetbase\Storefront\Http\Requests\InitializeCheckoutRequest;
19
19
  use Fleetbase\Storefront\Models\Cart;
20
20
  use Fleetbase\Storefront\Models\Checkout;
21
21
  use Fleetbase\Storefront\Models\Customer;
22
+ use Fleetbase\Storefront\Models\FoodTruck;
22
23
  use Fleetbase\Storefront\Models\Gateway;
23
24
  use Fleetbase\Storefront\Models\Product;
24
25
  use Fleetbase\Storefront\Models\Store;
@@ -475,8 +476,8 @@ class CheckoutController extends Controller
475
476
  $invoiceAmount = $amount;
476
477
  $invoiceCode = $gateway->sandbox ? 'TEST_INVOICE' : $gateway->config->invoice_id;
477
478
  $invoiceDescription = $about->name . ' cart checkout';
478
- $invoiceReceiverCode = $gateway->public_id;
479
- $senderInvoiceNo = $cart->id;
479
+ $invoiceReceiverCode = $checkout->public_id;
480
+ $senderInvoiceNo = $checkout->public_id;
480
481
 
481
482
  // Create qpay invoice
482
483
  $invoice = $qpay->createSimpleInvoice($invoiceAmount, $invoiceCode, $invoiceDescription, $invoiceReceiverCode, $senderInvoiceNo, $callbackUrl);
@@ -491,69 +492,137 @@ class CheckoutController extends Controller
491
492
  ]);
492
493
  }
493
494
 
495
+ /**
496
+ * Capture and process QPay callback.
497
+ *
498
+ * This controller method handles QPay callback requests by verifying and processing
499
+ * payment information for a specified checkout. It performs the following steps:
500
+ *
501
+ * - Retrieves the checkout identifier from the request.
502
+ * - Looks up the associated Checkout and Gateway records.
503
+ * - If in sandbox mode and a test scenario is provided, it simulates a test payment
504
+ * response for either success or error scenarios.
505
+ * - Initializes a QPay instance with the gateway configuration and sets the authentication token.
506
+ * - Retrieves the invoice ID from the checkout options and performs a payment check using QPay's API.
507
+ * - Publishes the payment data or error response to the SocketCluster channel.
508
+ *
509
+ * Depending on the 'respond' flag from the request, the method returns a JSON response
510
+ * or completes the processing without returning data.
511
+ *
512
+ * @param Request $request The HTTP request containing:
513
+ * - `checkout` (string): The public checkout identifier.
514
+ * - `respond` (boolean): Whether to return a JSON response.
515
+ * - `test` (string|null): A test scenario indicator ('success' or 'error') for sandbox mode.
516
+ *
517
+ * @return \Illuminate\Http\JsonResponse a JSON response with payment data or error details
518
+ *
519
+ * @throws \Exception if an error occurs during payment processing, an API error is returned
520
+ */
494
521
  public function captureQPayCallback(Request $request)
495
522
  {
496
- $checkoutId = $request->input('checkout');
497
- $respond = $request->boolean('respond');
498
- $test = $request->input('test'); // success, error
499
-
500
- if ($checkoutId) {
501
- // Get the checkout instance
502
- $checkout = Checkout::where('public_id', $checkoutId)->first();
503
- if ($checkout) {
504
- // Restore the payment gateway instance
505
- $gateway = Gateway::where('uuid', $checkout->gateway_uuid)->first();
506
- if ($gateway) {
507
- try {
508
- // If testing send back test data
509
- // Handle test below
510
- if ($gateway->sandbox && is_string($test) && in_array($test, ['success', 'error'])) {
511
- $data = ['error' => null, 'checkout' => $checkout->public_id, 'payment' => null];
512
- if ($test === 'success') {
513
- $data['payment'] = QPay::createTestPaymentDataFromCheckout($checkout);
514
- } else {
515
- $data['error'] = ['error' => 'PAYMENT_NOT_PAID', 'message' => 'Payment has not paid!'];
516
- }
517
-
518
- SocketClusterService::publish('checkout.' . $checkout->public_id, $data);
519
- if ($respond) {
520
- return response()->json($data);
521
- }
522
-
523
- return;
524
- }
525
-
526
- // Create qpay instance
527
- $qpay = QPay::instance($gateway->config->username, $gateway->config->password, $gateway->callback_url);
528
- if ($gateway->sandbox) {
529
- $qpay = $qpay->useSandbox();
530
- }
531
-
532
- // Set auth token
533
- $qpay = $qpay->setAuthToken();
534
-
535
- // Check payment status from qpay
536
- $payment = $qpay->paymentGet($checkoutId);
537
- if ($payment) {
538
- $data = [];
539
- if ($payment->error) {
540
- $data = ['error' => (array) $payment, 'checkout' => $checkout->public_id, 'payment' => null];
541
- } else {
542
- $data = ['payment' =>(array) $payment, 'checkout' => $checkout->public_id, 'error' => null];
543
- }
544
-
545
- SocketClusterService::publish('checkout.' . $checkout->public_id, $data);
546
- if ($respond) {
547
- return response()->json($data);
548
- }
549
- }
550
- } catch (\Exception $e) {
551
- Log::error('[QPAY CHECKOUT ERROR: ' . $e->getMessage() . ']', $checkout->toArray());
552
- if ($respond) {
553
- return response()->apiError($e->getMessage());
554
- }
555
- }
523
+ $checkoutId = $request->input('checkout');
524
+ $shouldRespond = $request->boolean('respond');
525
+ $testScenario = $request->input('test'); // Expected: 'success' or 'error'
526
+
527
+ if (!$checkoutId) {
528
+ return response()->json([
529
+ 'error' => 'CHECKOUT_ID_MISSING',
530
+ 'checkout' => null,
531
+ 'payment' => null,
532
+ ]);
533
+ }
534
+
535
+ $checkout = Checkout::where('public_id', $checkoutId)->first();
536
+ if (!$checkout) {
537
+ return response()->json([
538
+ 'error' => 'CHECKOUT_SESSION_NOT_FOUND',
539
+ 'checkout' => null,
540
+ 'payment' => null,
541
+ ]);
542
+ }
543
+
544
+ $gateway = Gateway::where('uuid', $checkout->gateway_uuid)->first();
545
+ if (!$gateway) {
546
+ return response()->json([
547
+ 'error' => 'GATEWAY_NOT_CONFIGURED',
548
+ 'checkout' => $checkout->public_id,
549
+ 'payment' => null,
550
+ ]);
551
+ }
552
+
553
+ try {
554
+ // Handle test scenarios if in sandbox mode.
555
+ if ($gateway->sandbox && in_array($testScenario, ['success', 'error'], true)) {
556
+ $data = [
557
+ 'checkout' => $checkout->public_id,
558
+ 'payment' => null,
559
+ 'error' => null,
560
+ ];
561
+
562
+ if ($testScenario === 'success') {
563
+ $data['payment'] = QPay::createTestPaymentDataFromCheckout($checkout);
564
+ } else {
565
+ $data['error'] = [
566
+ 'error' => 'PAYMENT_NOT_PAID',
567
+ 'message' => 'Payment has not been paid!',
568
+ ];
556
569
  }
570
+
571
+ SocketClusterService::publish('checkout.' . $checkout->public_id, $data);
572
+
573
+ return $shouldRespond ? response()->json($data) : response()->json();
574
+ }
575
+
576
+ // Create the QPay instance.
577
+ $qpay = QPay::instance(
578
+ $gateway->config->username,
579
+ $gateway->config->password,
580
+ $gateway->callback_url
581
+ );
582
+
583
+ if ($gateway->sandbox) {
584
+ $qpay->useSandbox();
585
+ }
586
+
587
+ $qpay->setAuthToken();
588
+
589
+ $invoiceId = $checkout->getOption('qpay_invoice_id');
590
+ if (!$invoiceId) {
591
+ Log::error("Missing QPay invoice ID for checkout: {$checkout->public_id}");
592
+
593
+ return response()->json([
594
+ 'error' => 'MISSING_INVOICE_ID',
595
+ 'checkout' => $checkout->public_id,
596
+ 'payment' => null,
597
+ ]);
598
+ }
599
+
600
+ $paymentCheck = $qpay->paymentCheck($invoiceId);
601
+
602
+ if (!$paymentCheck || empty($paymentCheck->count) || $paymentCheck->count < 1) {
603
+ return response()->json([
604
+ 'error' => 'PAYMENT_NOTFOUND',
605
+ 'checkout' => $checkout->public_id,
606
+ 'payment' => null,
607
+ ]);
608
+ }
609
+
610
+ $payment = data_get($paymentCheck, 'rows.0');
611
+ if ($payment) {
612
+ $data = [
613
+ 'checkout' => $checkout->public_id,
614
+ 'payment' => (array) $payment,
615
+ 'error' => null,
616
+ ];
617
+
618
+ SocketClusterService::publish('checkout.' . $checkout->public_id, $data);
619
+
620
+ return $shouldRespond ? response()->json($data) : response()->json();
621
+ }
622
+ } catch (\Exception $e) {
623
+ Log::error('[QPAY CHECKOUT ERROR]: ' . $e->getMessage(), ['checkout' => $checkout->toArray()]);
624
+ if ($shouldRespond) {
625
+ return response()->apiError($e->getMessage());
557
626
  }
558
627
  }
559
628
 
@@ -598,6 +667,7 @@ class CheckoutController extends Controller
598
667
  {
599
668
  $token = $request->input('token');
600
669
  $transactionDetails = $request->input('transactionDetails', []); // optional details to be supplied about transaction
670
+ $notes = $request->input('notes');
601
671
 
602
672
  // validate transaction details
603
673
  if (!is_array($transactionDetails)) {
@@ -737,6 +807,18 @@ class CheckoutController extends Controller
737
807
  $origin = Arr::first($origin);
738
808
  }
739
809
 
810
+ // Check if the order origin is from a food truck via cart property
811
+ $foodTruck = collect($cart->items)
812
+ ->map(function ($cartItem) {
813
+ return data_get($cartItem, 'food_truck_id');
814
+ })
815
+ ->unique()
816
+ ->filter()
817
+ ->map(function ($foodTruckId) {
818
+ return FoodTruck::where('public_id', $foodTruckId)->first();
819
+ })
820
+ ->first();
821
+
740
822
  // if there is no origin attempt to get from cart
741
823
  if (!$origin) {
742
824
  $storeLocation = collect($cart->items)->map(function ($cartItem) {
@@ -817,6 +899,11 @@ class CheckoutController extends Controller
817
899
  ...$transactionDetails,
818
900
  ]);
819
901
 
902
+ // if there is a food truck include it in the order meta
903
+ if ($foodTruck) {
904
+ $orderMeta['food_truck_id'] = $foodTruck->public_id;
905
+ }
906
+
820
907
  // initialize order creation input
821
908
  $orderInput = [
822
909
  'company_uuid' => $store->company_uuid ?? session('company'),
@@ -828,6 +915,7 @@ class CheckoutController extends Controller
828
915
  'type' => 'storefront',
829
916
  'status' => 'created',
830
917
  'meta' => $orderMeta,
918
+ 'notes' => $notes,
831
919
  ];
832
920
 
833
921
  // if it's integrated vendor order apply to meta
@@ -879,6 +967,7 @@ class CheckoutController extends Controller
879
967
  {
880
968
  $token = $request->input('token');
881
969
  $transactionDetails = $request->input('transactionDetails', []); // optional details to be supplied about transaction
970
+ $notes = $request->input('notes');
882
971
 
883
972
  // validate transaction details
884
973
  if (!is_array($transactionDetails)) {
@@ -1055,6 +1144,7 @@ class CheckoutController extends Controller
1055
1144
  'adhoc' => $about->isOption('auto_dispatch'),
1056
1145
  'type' => 'storefront',
1057
1146
  'status' => 'created',
1147
+ 'notes' => $notes,
1058
1148
  ];
1059
1149
 
1060
1150
  // if it's integrated vendor order apply to meta
@@ -270,7 +270,11 @@ class CustomerController extends Controller
270
270
  }
271
271
 
272
272
  // update the contact
273
- $contact->update($input);
273
+ try {
274
+ $contact->update($input);
275
+ } catch (\Exception $e) {
276
+ return response()->apiError($e->getMessage());
277
+ }
274
278
 
275
279
  // response the contact resource
276
280
  return new Customer($contact);
@@ -5,6 +5,7 @@ namespace Fleetbase\Storefront\Http\Controllers\v1;
5
5
  use Fleetbase\Http\Controllers\Controller;
6
6
  use Fleetbase\Storefront\Http\Resources\FoodTruck as FoodTruckResource;
7
7
  use Fleetbase\Storefront\Models\FoodTruck;
8
+ use Illuminate\Database\Eloquent\ModelNotFoundException;
8
9
  use Illuminate\Http\Request;
9
10
 
10
11
  class FoodTruckController extends Controller
@@ -36,4 +37,22 @@ class FoodTruckController extends Controller
36
37
 
37
38
  return FoodTruckResource::collection($results);
38
39
  }
40
+
41
+ /**
42
+ * Finds a single Storefront FoodTruck resources.
43
+ *
44
+ * @return \Fleetbase\Http\Resources\EntityCollection
45
+ */
46
+ public function find($id)
47
+ {
48
+ // find for the food truck
49
+ try {
50
+ $foodTruck = FoodTruck::findRecordOrFail($id);
51
+ } catch (ModelNotFoundException $exception) {
52
+ return response()->error('Food Truck resource not found.');
53
+ }
54
+
55
+ // response the product resource
56
+ return new FoodTruckResource($foodTruck);
57
+ }
39
58
  }
@@ -3,6 +3,7 @@
3
3
  namespace Fleetbase\Storefront\Http\Controllers\v1;
4
4
 
5
5
  use Fleetbase\Http\Controllers\Controller;
6
+ use Fleetbase\Models\Category;
6
7
  use Fleetbase\Storefront\Http\Resources\Gateway as GatewayResource;
7
8
  use Fleetbase\Storefront\Http\Resources\Network as NetworkResource;
8
9
  use Fleetbase\Storefront\Http\Resources\Product as ProductResource;
@@ -188,6 +189,15 @@ class StoreController extends Controller
188
189
  ->whereStatus('published')
189
190
  ->get();
190
191
 
192
+ // Search categories as well
193
+ $categories = Category::where(['company_uuid' => session('company'), 'for' => 'storefront_product'])->search($searchQuery)->get();
194
+ if ($categories) {
195
+ foreach ($categories as $category) {
196
+ $categoryProducts = Product::where('category_uuid', $category->uuid)->get();
197
+ $results = $results->merge($categoryProducts)->unique('uuid');
198
+ }
199
+ }
200
+
191
201
  return ProductResource::collection($results);
192
202
  }
193
203
 
@@ -20,23 +20,24 @@ class FoodTruck extends FleetbaseResource
20
20
  public function toArray($request)
21
21
  {
22
22
  return [
23
- 'id' => $this->when(Http::isInternalRequest(), $this->id, $this->public_id),
24
- 'uuid' => $this->when(Http::isInternalRequest(), $this->uuid),
25
- 'public_id' => $this->when(Http::isInternalRequest(), $this->public_id),
26
- 'company_uuid' => $this->when(Http::isInternalRequest(), $this->company_uuid),
27
- 'created_by_uuid' => $this->when(Http::isInternalRequest(), $this->created_by_uuid),
28
- 'store_uuid' => $this->when(Http::isInternalRequest(), $this->store_uuid),
29
- 'service_area_uuid' => $this->when(Http::isInternalRequest(), $this->service_area_uuid),
30
- 'zone_uuid' => $this->when(Http::isInternalRequest(), $this->zone_uuid),
31
- 'vehicle_uuid' => $this->when(Http::isInternalRequest(), $this->vehicle_uuid),
32
- 'vehicle' => $this->vehicle ? new VehicleResource($this->vehicle) : null,
33
- 'service_area' => $this->serviceArea ? new ServiceAreaResource($this->serviceArea) : null,
34
- 'zone' => $this->zone ? new ZoneResource($this->zone) : null,
35
- 'catalogs' => Catalog::collection($this->catalogs ?? []),
36
- 'online' => $this->vehicle ? $this->vehicle->online : false,
37
- 'status' => $this->status,
38
- 'created_at' => $this->created_at,
39
- 'updated_at' => $this->updated_at,
23
+ 'id' => $this->when(Http::isInternalRequest(), $this->id, $this->public_id),
24
+ 'uuid' => $this->when(Http::isInternalRequest(), $this->uuid),
25
+ 'public_id' => $this->when(Http::isInternalRequest(), $this->public_id),
26
+ 'company_uuid' => $this->when(Http::isInternalRequest(), $this->company_uuid),
27
+ 'created_by_uuid' => $this->when(Http::isInternalRequest(), $this->created_by_uuid),
28
+ 'store_uuid' => $this->when(Http::isInternalRequest(), $this->store_uuid),
29
+ 'service_area_uuid' => $this->when(Http::isInternalRequest(), $this->service_area_uuid),
30
+ 'zone_uuid' => $this->when(Http::isInternalRequest(), $this->zone_uuid),
31
+ 'vehicle_uuid' => $this->when(Http::isInternalRequest(), $this->vehicle_uuid),
32
+ 'vehicle' => $this->vehicle ? new VehicleResource($this->vehicle) : null,
33
+ 'service_area' => $this->serviceArea ? new ServiceAreaResource($this->serviceArea) : null,
34
+ 'zone' => $this->zone ? new ZoneResource($this->zone) : null,
35
+ 'catalogs' => Catalog::collection($this->catalogs ?? []),
36
+ 'location' => $this->vehicle ? $this->vehicle->location : null,
37
+ 'online' => $this->vehicle ? $this->vehicle->online : false,
38
+ 'status' => $this->status,
39
+ 'created_at' => $this->created_at,
40
+ 'updated_at' => $this->updated_at,
40
41
  ];
41
42
  }
42
43
  }
@@ -25,6 +25,8 @@ class ReviewCustomer extends FleetbaseResource
25
25
  'email' => $this->email,
26
26
  'phone' => $this->phone,
27
27
  'photo_url' => $this->photo_url,
28
+ 'reviews_count' => $this->resource->reviews()->count(),
29
+ 'uploads_count' => $this->resource->reviewUploads()->count(),
28
30
  'slug' => $this->slug,
29
31
  'created_at' => $this->created_at,
30
32
  'updated_at' => $this->updated_at,
@@ -48,12 +48,19 @@ class Product extends StorefrontModel
48
48
  */
49
49
  protected $table = 'products';
50
50
 
51
+ /**
52
+ * The default database connection to use.
53
+ *
54
+ * @var string
55
+ */
56
+ protected $connection = 'storefront';
57
+
51
58
  /**
52
59
  * These attributes that can be queried.
53
60
  *
54
61
  * @var array
55
62
  */
56
- protected $searchableColumns = ['name', 'description'];
63
+ protected $searchableColumns = ['name', 'description', 'tags'];
57
64
 
58
65
  /**
59
66
  * The attributes that are mass assignable.
@@ -383,6 +390,11 @@ class Product extends StorefrontModel
383
390
  $option['additional_cost'] = Utils::numbersOnly($option['additional_cost']);
384
391
  }
385
392
 
393
+ // additional cost can never be null
394
+ if ($option['additional_cost'] === null) {
395
+ $option['additional_cost'] = 0;
396
+ }
397
+
386
398
  $productVariantOptionInput = Arr::except($option, ['uuid', 'created_at', 'updated_at']);
387
399
  ProductVariantOption::where('uuid', $option['uuid'])->update($productVariantOptionInput);
388
400
  continue;
@@ -2,8 +2,8 @@
2
2
 
3
3
  namespace Fleetbase\Storefront\Models;
4
4
 
5
- use Fleetbase\FleetOps\Models\Contact;
6
5
  use Fleetbase\Models\File;
6
+ use Fleetbase\Models\User;
7
7
  use Fleetbase\Traits\HasApiModelBehavior;
8
8
  use Fleetbase\Traits\HasPublicid;
9
9
  use Fleetbase\Traits\HasUuid;
@@ -84,7 +84,7 @@ class Review extends StorefrontModel
84
84
  */
85
85
  public function customer()
86
86
  {
87
- return $this->setConnection(config('fleetbase.connection.db'))->belongsTo(Contact::class);
87
+ return $this->setConnection(config('fleetbase.connection.db'))->belongsTo(Customer::class);
88
88
  }
89
89
 
90
90
  /**
@@ -4,6 +4,7 @@ namespace Fleetbase\Storefront\Observers;
4
4
 
5
5
  use Fleetbase\Models\File;
6
6
  use Fleetbase\Storefront\Models\Product;
7
+ use Illuminate\Support\Facades\Log;
7
8
  use Illuminate\Support\Facades\Request;
8
9
 
9
10
  class ProductObserver
@@ -15,20 +16,25 @@ class ProductObserver
15
16
  */
16
17
  public function saved(Product $product): void
17
18
  {
18
- $addonCategories = Request::input('product.addon_categories', []);
19
- $variants = Request::input('product.variants', []);
20
- $files = Request::input('product.files', []);
19
+ try {
20
+ $addonCategories = Request::input('product.addon_categories', []);
21
+ $variants = Request::input('product.variants', []);
22
+ $files = Request::input('product.files', []);
21
23
 
22
- // save addon categories
23
- $product->setAddonCategories($addonCategories);
24
+ // save addon categories
25
+ $product->setAddonCategories($addonCategories);
24
26
 
25
- // save product variants
26
- $product->setProductVariants($variants);
27
+ // save product variants
28
+ $product->setProductVariants($variants);
27
29
 
28
- // set keys on files
29
- foreach ($files as $file) {
30
- $fileRecord = File::where('uuid', $file['uuid'])->first();
31
- $fileRecord->setKey($product);
30
+ // set keys on files
31
+ foreach ($files as $file) {
32
+ $fileRecord = File::where('uuid', $file['uuid'])->first();
33
+ $fileRecord->setKey($product);
34
+ }
35
+ } catch (\Exception $e) {
36
+ Log::error($e->getMessage());
37
+ throw $e;
32
38
  }
33
39
  }
34
40
  }