@fleetbase/storefront-engine 0.3.30 → 0.3.31

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.30",
3
+ "version": "0.3.31",
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.30",
3
+ "version": "0.3.31",
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.30",
3
+ "version": "0.3.31",
4
4
  "description": "Headless Commerce & Marketplace Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "storefront",
@@ -7,7 +7,8 @@ $database = env('DB_DATABASE', 'fleetbase');
7
7
  $username = env('DB_USERNAME', 'fleetbase');
8
8
  $password = env('DB_PASSWORD', '');
9
9
 
10
- if ($databaseUrl = getenv('DATABASE_URL')) {
10
+ $databaseUrl = getenv('DATABASE_URL');
11
+ if (!empty($databaseUrl)) {
11
12
  $url = Utils::parseUrl($databaseUrl);
12
13
 
13
14
  $host = $url['host'];
@@ -5,17 +5,325 @@ namespace Fleetbase\Storefront\Http\Controllers\v1;
5
5
  use Fleetbase\FleetOps\Http\Resources\v1\DeletedResource;
6
6
  use Fleetbase\Http\Controllers\Controller;
7
7
  use Fleetbase\Models\Category;
8
+ use Fleetbase\Storefront\Http\Requests\CreateProductRequest;
9
+ use Fleetbase\Storefront\Http\Requests\UpdateProductRequest;
8
10
  use Fleetbase\Storefront\Http\Resources\Product as StorefrontProduct;
11
+ use Fleetbase\Storefront\Models\AddonCategory;
9
12
  use Fleetbase\Storefront\Models\Product;
13
+ use Fleetbase\Storefront\Models\ProductAddon;
14
+ use Fleetbase\Storefront\Models\ProductAddonCategory;
15
+ use Fleetbase\Storefront\Models\ProductVariant;
16
+ use Fleetbase\Storefront\Models\ProductVariantOption;
17
+ use Fleetbase\Storefront\Models\Store;
18
+ use Fleetbase\Support\Utils;
10
19
  use Illuminate\Database\Eloquent\ModelNotFoundException;
11
20
  use Illuminate\Http\Request;
12
21
 
13
22
  class ProductController extends Controller
14
23
  {
24
+ /**
25
+ * Create a new Storefront product.
26
+ *
27
+ * @return void
28
+ */
29
+ public function create(CreateProductRequest $request)
30
+ {
31
+ // Collect product details input
32
+ $input = $request->only([
33
+ 'name',
34
+ 'description',
35
+ 'tags',
36
+ 'meta',
37
+ 'sku',
38
+ 'price',
39
+ 'currency',
40
+ 'sale_price',
41
+ 'addons',
42
+ 'variants',
43
+ 'is_service',
44
+ 'is_bookable',
45
+ 'is_available',
46
+ 'is_on_sale',
47
+ 'is_recommended',
48
+ 'can_pickup',
49
+ 'youtube_urls',
50
+ 'status',
51
+ ]);
52
+
53
+ // Set product store
54
+ $input['store_uuid'] = session('storefront_store');
55
+
56
+ // Set product relations
57
+ $input['company_uuid'] = session('company');
58
+ $input['created_by_uuid'] = session('user');
59
+
60
+ // Prepare arrayable data
61
+ $input['tags'] = Utils::arrayFrom(data_get($input, 'tags', []));
62
+ $input['youtube_urls'] = Utils::arrayFrom(data_get($input, 'youtube_urls', []));
63
+
64
+ // Prepare money
65
+ $input['price'] = Utils::numbersOnly(data_get($input, 'price', 0));
66
+ $input['sale_price'] = Utils::numbersOnly(data_get($input, 'sale_price', 0));
67
+
68
+ // Set currency
69
+ $input['currency'] = data_get($input, 'currency', session('storefront_currency', 'USD'));
70
+
71
+ // Resolve category
72
+ if ($request->filled('category')) {
73
+ $categoryInput = $request->input('category');
74
+
75
+ if (Utils::isPublicId($categoryInput)) {
76
+ $category = Category::where([
77
+ 'company_uuid' => session('company'),
78
+ 'owner_uuid' => session('storefront_store'),
79
+ 'public_id' => $categoryInput,
80
+ 'for' => 'storefront_product',
81
+ ])->first();
82
+ }
83
+
84
+ // Create new product if data is array
85
+ if (is_array($categoryInput) && isset($categoryInput['name'])) {
86
+ $category = Category::create([
87
+ 'company_uuid' => session('company'),
88
+ 'owner_uuid' => session('storefront_store'),
89
+ 'owner_type' => Utils::getMutationType('storefront:store'),
90
+ 'name' => data_get($categoryInput, 'name'),
91
+ 'description' => data_get($categoryInput, 'description'),
92
+ 'tags' => Utils::arrayFrom(data_get($categoryInput, 'tags', [])),
93
+ 'for' => 'storefront_product',
94
+ ]);
95
+ }
96
+
97
+ // Set the cateogry for the product
98
+ if ($category instanceof Category) {
99
+ $input['category_uuid'] = $category->uuid;
100
+ }
101
+ }
102
+
103
+ // Create product
104
+ $product = Product::create($input);
105
+
106
+ // Resolve addon categories
107
+ if ($request->filled('addon_categories') && $request->isArray('addon_categories')) {
108
+ $request->collect('addon_categories')->each(function ($addonCategoryInput) use ($product) {
109
+ // Resolve existing addon category from ID
110
+ if (Utils::isPublicId($addonCategoryInput)) {
111
+ $addonCategory = AddonCategory::where('public_id', $addonCategoryInput)->first();
112
+ }
113
+
114
+ // Create new addon cateogry with addons
115
+ if (is_array($addonCategoryInput)) {
116
+ $addonCategory = AddonCategory::create([
117
+ 'company_uuid' => session('company'),
118
+ 'name' => data_get($addonCategoryInput, 'name'),
119
+ 'description' => data_get($addonCategoryInput, 'description'),
120
+ 'tags' => Utils::arrayFrom(data_get($addonCategoryInput, 'tags', [])),
121
+ ]);
122
+
123
+ if (isset($addonCategoryInput['addons']) && is_array($addonCategoryInput['addons'])) {
124
+ collect($addonCategoryInput['addons'])->each(function ($addonInput) use ($addonCategory) {
125
+ if (is_string($addonInput)) {
126
+ return ProductAddon::create([
127
+ 'category_uuid' => $addonCategory->uuid,
128
+ 'created_by_uuid' => session('user'),
129
+ 'name' => $addonInput,
130
+ ]);
131
+ }
132
+
133
+ if (is_array($addonInput)) {
134
+ return ProductAddon::create([
135
+ 'category_uuid' => $addonCategory->uuid,
136
+ 'created_by_uuid' => session('user'),
137
+ 'name' => data_get($addonInput, 'name'),
138
+ 'price' => Utils::numbersOnly(data_get($addonInput, 'price', 0)),
139
+ 'sale_price' => Utils::numbersOnly(data_get($addonInput, 'sale_price', 0)),
140
+ 'is_on_sale' => Utils::castBoolean(data_get($addonInput, 'is_on_sale', false)),
141
+ ]);
142
+ }
143
+ });
144
+ }
145
+ }
146
+
147
+ // Create product addon category
148
+ if ($addonCategory instanceof AddonCategory) {
149
+ ProductAddonCategory::create([
150
+ 'product_uuid' => $product->uuid,
151
+ 'category_uuid' => $addonCategory->uuid,
152
+ 'excluded_addons' => Utils::arrayFrom(data_get($addonCategoryInput, 'excluded_addons', [])),
153
+ 'max_selectable' => data_get($addonCategoryInput, 'max_selectable'),
154
+ 'is_required' => Utils::castBoolean(data_get($addonCategoryInput, 'is_required')),
155
+ ]);
156
+ }
157
+ });
158
+ }
159
+
160
+ // Resolve variants
161
+ if ($request->filled('variants') && $request->isArray('variants')) {
162
+ $request->collect('variants')->each(function ($variantInput) use ($product) {
163
+ // Create new variants for product
164
+ if (is_array($variantInput)) {
165
+ $productVariant = ProductVariant::create([
166
+ 'product_uuid' => $product->uuid,
167
+ 'name' => data_get($variantInput, 'name'),
168
+ 'description' => data_get($variantInput, 'description'),
169
+ 'meta' => data_get($variantInput, 'meta', []),
170
+ 'is_required' => Utils::castBoolean(data_get($variantInput, 'is_required')),
171
+ 'is_multiselect' => Utils::castBoolean(data_get($variantInput, 'is_multiselect')),
172
+ 'min' => data_get($variantInput, 'min', 0),
173
+ 'max' => data_get($variantInput, 'max', 1),
174
+ ]);
175
+
176
+ if (isset($variantInput['options']) && is_array($variantInput['options'])) {
177
+ collect($variantInput['options'])->each(function ($variantOptionInput) use ($productVariant) {
178
+ if (is_string($variantOptionInput)) {
179
+ ProductVariantOption::create([
180
+ 'product_variant_uuid' => $productVariant->uuid,
181
+ 'name' => $variantOptionInput,
182
+ ]);
183
+ }
184
+
185
+ if (is_array($variantOptionInput)) {
186
+ ProductVariantOption::create([
187
+ 'product_variant_uuid' => $productVariant->uuid,
188
+ 'name' => data_get($variantOptionInput, 'name'),
189
+ 'description' => data_get($variantOptionInput, 'description'),
190
+ 'additional_cost' => Utils::numbersOnly(data_get($variantOptionInput, 'additional_cost', 0)),
191
+ ]);
192
+ }
193
+ });
194
+ }
195
+ }
196
+ });
197
+ }
198
+
199
+ return new StorefrontProduct($product);
200
+ }
201
+
202
+ /**
203
+ * Updates a Storefront Product.
204
+ *
205
+ * @param string $id
206
+ *
207
+ * @return \Fleetbase\Storefront\Http\Resources\StorefrontProduct
208
+ */
209
+ public function update($id, UpdateProductRequest $request)
210
+ {
211
+ // Try to resolve the product by public_id or uuid (custom method)
212
+ try {
213
+ $product = Product::findRecordOrFail($id);
214
+ } catch (ModelNotFoundException $exception) {
215
+ return response()->json([
216
+ 'error' => 'Product not found.',
217
+ ], 404);
218
+ }
219
+
220
+ // Validate input
221
+ $input = $request->validated();
222
+
223
+ // Sanitize/transform
224
+ $input['tags'] = Utils::arrayFrom(data_get($input, 'tags', []));
225
+ $input['youtube_urls'] = Utils::arrayFrom(data_get($input, 'youtube_urls', []));
226
+ $input['price'] = Utils::numbersOnly(data_get($input, 'price', 0));
227
+ $input['sale_price'] = Utils::numbersOnly(data_get($input, 'sale_price', 0));
228
+
229
+ // Resolve or create category
230
+ if (isset($input['category'])) {
231
+ $categoryInput = $input['category'];
232
+ $category = null;
233
+
234
+ if (Utils::isPublicId($categoryInput)) {
235
+ $category = Category::where([
236
+ 'company_uuid' => session('company'),
237
+ 'owner_uuid' => session('storefront_store'),
238
+ 'public_id' => $categoryInput,
239
+ 'for' => 'storefront_product',
240
+ ])->first();
241
+ }
242
+
243
+ if (is_array($categoryInput) && isset($categoryInput['name'])) {
244
+ $category = Category::create([
245
+ 'company_uuid' => session('company'),
246
+ 'owner_uuid' => session('storefront_store'),
247
+ 'owner_type' => Utils::getMutationType('storefront:store'),
248
+ 'name' => data_get($categoryInput, 'name'),
249
+ 'description' => data_get($categoryInput, 'description'),
250
+ 'tags' => Utils::arrayFrom(data_get($categoryInput, 'tags', [])),
251
+ 'for' => 'storefront_product',
252
+ ]);
253
+ }
254
+
255
+ if ($category instanceof Category) {
256
+ $input['category_uuid'] = $category->uuid;
257
+ }
258
+ }
259
+
260
+ // Update the product
261
+ $product->update($input);
262
+
263
+ // Update sync addon categories
264
+ if ($request->filled('addon_categories') && $request->isArray('addon_categories')) {
265
+ $addonCategories = $request->collect('addon_categories')->map(function ($addonCategory) {
266
+ // Resolve category public_id to UUID
267
+ if (isset($addonCategory['category'])) {
268
+ $category = AddonCategory::where('public_id', $addonCategory['category'])->first();
269
+ if ($category) {
270
+ $addonCategory['category_uuid'] = $category->uuid;
271
+ }
272
+ }
273
+
274
+ // Resolve ProductAddonCategory public_id if present (e.g. for updates)
275
+ if (isset($addonCategory['id']) && Utils::isPublicId($addonCategory['id'])) {
276
+ $pac = ProductAddonCategory::where('public_id', $addonCategory['id'])->first();
277
+ if ($pac) {
278
+ $addonCategory['uuid'] = $pac->uuid;
279
+ }
280
+ }
281
+
282
+ return $addonCategory;
283
+ })->toArray();
284
+
285
+ $product->setAddonCategories($addonCategories);
286
+ }
287
+
288
+ // update sync product variants and options
289
+ if ($request->filled('variants') && $request->isArray('variants')) {
290
+ $variants = $request->collect('variants')->map(function ($variant) {
291
+ // Resolve variant public_id to UUID
292
+ if (isset($variant['id']) && Utils::isPublicId($variant['id'])) {
293
+ $variantModel = ProductVariant::where('public_id', $variant['id'])->first();
294
+ if ($variantModel) {
295
+ $variant['uuid'] = $variantModel->uuid;
296
+ }
297
+ }
298
+
299
+ // Resolve variant option IDs if using public_id
300
+ if (isset($variant['options']) && is_array($variant['options'])) {
301
+ $variant['options'] = collect($variant['options'])->map(function ($option) {
302
+ if (isset($option['id']) && Utils::isPublicId($option['id'])) {
303
+ $optionModel = ProductVariantOption::where('public_id', $option['id'])->first();
304
+ if ($optionModel) {
305
+ $option['uuid'] = $optionModel->uuid;
306
+ }
307
+ }
308
+
309
+ return $option;
310
+ })->toArray();
311
+ }
312
+
313
+ return $variant;
314
+ })->toArray();
315
+
316
+ $product->setProductVariants($variants);
317
+ }
318
+
319
+ // Return resource
320
+ return new StorefrontProduct($product);
321
+ }
322
+
15
323
  /**
16
324
  * Query for Storefront Product resources.
17
325
  *
18
- * @return \Fleetbase\Http\Resources\DriverCollection
326
+ * @return \Fleetbase\Http\Resources\ProductCollection
19
327
  */
20
328
  public function query(Request $request)
21
329
  {
@@ -0,0 +1,46 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\Storefront\Http\Requests;
4
+
5
+ use Fleetbase\Http\Requests\FleetbaseRequest;
6
+ use Illuminate\Validation\Rule;
7
+
8
+ class CreateProductRequest extends FleetbaseRequest
9
+ {
10
+ /**
11
+ * Determine if the user is authorized to make this request.
12
+ */
13
+ public function authorize(): bool
14
+ {
15
+ return session('storefront_key') || request()->session()->has('api_credential');
16
+ }
17
+
18
+ /**
19
+ * Get the validation rules that apply to the request.
20
+ */
21
+ public function rules(): array
22
+ {
23
+ return [
24
+ 'name' => 'required|string|max:255',
25
+ 'description' => 'nullable|string',
26
+ 'tags' => 'nullable|array',
27
+ 'meta' => 'nullable|array',
28
+ 'sku' => 'nullable|string|max:100',
29
+ 'price' => [Rule::requiredIf(fn () => $this->isMethod('POST')), 'numeric', 'min:0'],
30
+ 'sale_price' => 'nullable|numeric|min:0',
31
+ 'currency' => 'nullable|string|size:3',
32
+ 'addons' => 'nullable|array',
33
+ 'variants' => 'nullable|array',
34
+ 'is_service' => 'nullable|boolean',
35
+ 'is_bookable' => 'nullable|boolean',
36
+ 'is_available' => 'nullable|boolean',
37
+ 'is_on_sale' => 'nullable|boolean',
38
+ 'is_recommended' => 'nullable|boolean',
39
+ 'can_pickup' => 'nullable|boolean',
40
+ 'youtube_urls' => 'nullable|array',
41
+ 'status' => 'nullable|string|in:draft,active,archived',
42
+ 'category' => 'nullable',
43
+ 'addon_categories' => 'nullable|array',
44
+ ];
45
+ }
46
+ }
@@ -0,0 +1,7 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\Storefront\Http\Requests;
4
+
5
+ class UpdateProductRequest extends CreateProductRequest
6
+ {
7
+ }
@@ -4,6 +4,7 @@ namespace Fleetbase\Storefront\Http\Resources;
4
4
 
5
5
  use Fleetbase\Http\Resources\FleetbaseResource;
6
6
  use Fleetbase\Support\Http;
7
+ use Fleetbase\Support\Utils;
7
8
 
8
9
  class CatalogCategory extends FleetbaseResource
9
10
  {
@@ -26,8 +27,8 @@ class CatalogCategory extends FleetbaseResource
26
27
  'owner_uuid' => $this->when(Http::isInternalRequest(), $this->owner_uuid),
27
28
  'name' => $this->name,
28
29
  'description' => $this->description,
29
- 'tags' => $this->tags ?? [],
30
- 'meta' => $this->meta ?? [],
30
+ 'tags' => data_get($this, 'tags', []),
31
+ 'meta' => data_get($this, 'meta', Utils::createObject()),
31
32
  'products' => CatalogProduct::collection($this->products ?? []),
32
33
  'for' => $this->for,
33
34
  'order' => $this->order,
@@ -4,6 +4,7 @@ namespace Fleetbase\Storefront\Http\Resources;
4
4
 
5
5
  use Fleetbase\Http\Resources\FleetbaseResource;
6
6
  use Fleetbase\Support\Http;
7
+ use Fleetbase\Support\Utils;
7
8
 
8
9
  class Category extends FleetbaseResource
9
10
  {
@@ -28,7 +29,7 @@ class Category extends FleetbaseResource
28
29
  return $parentCategory->public_id;
29
30
  }
30
31
  ),
31
- 'tags' => $this->tags ?? [],
32
+ 'tags' => Utils::arrayFrom($this->tags),
32
33
  'translations' => $this->translations ?? [],
33
34
  'products' => $this->when($request->has('with_products') || $request->inArray('with', 'products'), $this->products ? Product::collection($this->products) : []),
34
35
  'subcategories' => $this->when(
@@ -40,6 +41,7 @@ class Category extends FleetbaseResource
40
41
  $this->subCategories->toArray()
41
42
  )
42
43
  ),
44
+ 'meta' => data_get($this, 'meta', Utils::createObject()),
43
45
  'order' => $this->order,
44
46
  'slug' => $this->slug,
45
47
  'created_at' => $this->created_at,
@@ -5,6 +5,7 @@ namespace Fleetbase\Storefront\Http\Resources;
5
5
  use Fleetbase\FleetOps\Http\Resources\v1\Place;
6
6
  use Fleetbase\Http\Resources\FleetbaseResource;
7
7
  use Fleetbase\Support\Http;
8
+ use Fleetbase\Support\Utils;
8
9
  use Illuminate\Support\Str;
9
10
 
10
11
  class Customer extends FleetbaseResource
@@ -33,7 +34,7 @@ class Customer extends FleetbaseResource
33
34
  'address' => data_get($this, 'place.address'),
34
35
  'addresses' => $this->whenLoaded('places', Place::collection($this->places)),
35
36
  'token' => $this->when($this->token, $this->token),
36
- 'meta' => $this->meta ?? [],
37
+ 'meta' => data_get($this, 'meta', Utils::createObject()),
37
38
  'slug' => $this->slug,
38
39
  'created_at' => $this->created_at,
39
40
  'updated_at' => $this->updated_at,
@@ -4,6 +4,7 @@ namespace Fleetbase\Storefront\Http\Resources;
4
4
 
5
5
  use Fleetbase\Http\Resources\FleetbaseResource;
6
6
  use Fleetbase\Support\Http;
7
+ use Fleetbase\Support\Utils;
7
8
  use Illuminate\Support\Arr;
8
9
  use Illuminate\Support\Collection;
9
10
  use Illuminate\Support\Str;
@@ -42,6 +43,7 @@ class Product extends FleetbaseResource
42
43
  'is_available' => $this->is_available,
43
44
  'tags' => $this->tags ?? [],
44
45
  'status' => $this->status,
46
+ 'meta' => data_get($this, 'meta', Utils::createObject()),
45
47
  'slug' => $this->slug,
46
48
  'translations' => $this->translations ?? [],
47
49
  'addon_categories' => $this->mapAddonCategories($this->addonCategories),
@@ -73,6 +73,8 @@ Route::prefix(config('storefront.api.routing.prefix', 'storefront'))->namespace(
73
73
  $router->group(['prefix' => 'products'], function () use ($router) {
74
74
  $router->get('/', 'ProductController@query');
75
75
  $router->get('{id}', 'ProductController@find');
76
+ $router->post('/', 'ProductController@create');
77
+ $router->put('{id}', 'ProductController@update');
76
78
  });
77
79
 
78
80
  // storefront/v1/food-trucks