PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk 1.2.0 All 47 releases
fluent-cart / api / Resource / ProductVariationResource.php

ProductVariationResource.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at api/Resource/ProductVariationResource.php

465 lines 21.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\Api\Resource;
4
5 use FluentCart\App\Helpers\Helper;
6 use FluentCart\App\Helpers\Status;
7 use FluentCart\App\Models\ProductDetail;
8 use FluentCart\App\Models\ProductVariation;
9 use FluentCart\Framework\Database\Orm\Builder;
10 use FluentCart\Framework\Support\Arr;
11
12 class ProductVariationResource extends BaseResourceApi
13 {
14
15 public static function getQuery(): Builder
16 {
17 return ProductVariation::query();
18 }
19
20 public static function get(array $params = []): array
21 {
22 $variantIdsForFilter = Arr::get($params, 'variant_ids', []);
23
24 $query = static::getQuery()
25 ->select(Arr::get($params, 'select', '*'))
26 ->whereIn('id', $variantIdsForFilter);
27
28 if (Arr::get($params, 'with_detail')) {
29 $query->with('product_detail');
30 }
31
32 if (Arr::get($params, 'with_product')) {
33 $query->with('product');
34 }
35
36 $variants = $query->orderBy(
37 sanitize_sql_orderby(Arr::get($params, 'order_by', 'ID')) ?: 'ID',
38 sanitize_sql_orderby(Arr::get($params, 'order_type', 'ASC')) ?: 'ASC'
39 )
40 ->get();
41
42 return [
43 'variants' => $variants
44 ];
45 }
46
47 /**
48 * Find variant by its id.
49 *
50 * @param int $variantId The id of the variant.
51 * @param array $data Additional data for finding variant (optional).
52 *
53 */
54 public static function find($variantId, $data = [])
55 {
56 return static::getQuery()->find($variantId);
57 }
58
59 /**
60 * Create a new variant with the given data.
61 *
62 * @param array $data Array containing the necessary parameters.
63 *
64 * $data = [
65 * 'id' => (int) Required. The variant ID.
66 * 'post_id' => (int) Required. The product ID.
67 * 'variant_title' => (string) Required. The variant title.
68 * 'item_price' => (int) Required. The item price in CENTS — e.g. 129900 means
69 * $1,299.00. Same unit the API returns on read, and the same
70 * unit stored. See dev-docs/PRICING-AND-TAX.md §6.
71 * 'compare_price' => (int) Required. The compare price, in cents (see item_price).
72 * 'manage_cost' => (string) Optional. Whether to manage costs.
73 * 'item_cost' => (int) Required if manage cost is yes. The item cost, in cents.
74 * 'manage_stock' => (string) Required. Whether to manage stock.
75 * 'stock_status' => (string) Required. The stock status.
76 * 'stock' => (int) Required. The stock quantity.
77 * 'media' => (array) Optional. Info of media files for each variant.
78 * 'id' => (string) Required if upload any media. The media ID.
79 * 'url' => (string) Required if upload any media. The media URL.
80 * 'title' => (string) Required if upload any media. The media title.
81 * 'other_info' => (array) Optional. Other information for the variant.
82 * 'payment_type' => (string) Required. The payment type.
83 * 'times' => (string) Required. The number of times.
84 * 'repeat_interval' => (string) Required. The repeat interval unit.
85 * 'signup_fee' => (int) Required. The signup fee, in cents (see item_price).
86 * 'downloadable_files' => (array) Required if downloadable is true.
87 * 'download_limit' => (string) Required. The download limit.
88 * 'download_expiry' => (string) Required. The download expiry.
89 * 'downloadable' => (bool) Optional. Whether the product is downloadable.
90 * 'files' => (array) Optional. Info of downloadable files for each variant.
91 * 'title' => (string) Required if files. The file title.
92 * 'type' => (string) Required if files. The file type.
93 * 'file_name' => (string) Required if files. The file name.
94 * 'file_path' => (string) Required if files. The file path.
95 * 'file_url' => (string) Required if files. The file URL.
96 * 'serial' => (string) Required if files. The file serial
97 * ];
98 */
99 public static function create($variant, $params = [])
100 {
101 $otherInfo = Arr::wrap(Arr::get($variant, 'other_info'));
102 $otherInfo['tax_exempt'] = Arr::get($otherInfo, 'tax_exempt', 'no');
103 $otherInfo['tax_class'] = Arr::get($otherInfo, 'tax_class', 'standard');
104
105 if (Arr::get($otherInfo, 'payment_type') == 'onetime') {
106 $otherInfo = Arr::only($otherInfo, [
107 'payment_type',
108 'description',
109 'tax_class',
110 'tax_exempt',
111 'tax_inclusion',
112 'package_slug',
113 'weight',
114 'weight_unit',
115 'length',
116 'width',
117 'height',
118 ]);
119 }
120 if (Arr::get($otherInfo, 'payment_type') == 'subscription') {
121 if (Arr::get($otherInfo, 'manage_setup_fee') == 'no') {
122 unset($otherInfo['signup_fee_name']);
123 unset($otherInfo['signup_fee']);
124 unset($otherInfo['setup_fee_per_item']);
125 }
126 if (Arr::get($otherInfo, 'manage_setup_fee') == 'yes') {
127 // Already in cents — normalize only, do not scale.
128 $signupFee = Helper::roundCent(Arr::get($otherInfo, 'signup_fee', 0));
129 Arr::set($otherInfo, 'signup_fee', $signupFee);
130 }
131 }
132
133 // Cast physical attributes in other_info to float
134 foreach (['weight', 'length', 'width', 'height'] as $attr) {
135 if (isset($otherInfo[$attr])) {
136 $otherInfo[$attr] = floatval($otherInfo[$attr]);
137 }
138 }
139
140 $hasSubscription = Arr::get($variant, 'other_info.payment_type') === 'subscription';
141 $isDownloadable = Arr::get($variant, 'downloadable', true);
142 $itemPrice = Arr::get($variant, 'item_price', 1);
143 $comparePrice = Arr::get($variant, 'compare_price');
144 $available = Arr::get($variant, 'available', 0);
145 $stockStatus = Arr::get($variant, 'stock_status', Helper::IN_STOCK);
146 if (Arr::get($variant, 'manage_stock') == 1) {
147 $stockStatus = ($available > 0) ? Helper::IN_STOCK : Helper::OUT_OF_STOCK;
148 } else if (Arr::get($variant, 'manage_stock') == 0) {
149 $stockStatus = Helper::IN_STOCK;
150 }
151 $variantData = [
152 'post_id' => Arr::get($variant, 'post_id'),
153 'serial_index' => Arr::get($variant, 'serial_index'),
154 'manage_stock' => Arr::get($variant, 'manage_stock', 0),
155 'total_stock' => Arr::get($variant, 'total_stock'),
156 'available' => Arr::get($variant, 'available'),
157 'committed' => Arr::get($variant, 'committed'),
158 'on_hold' => Arr::get($variant, 'on_hold'),
159 'stock_status' => $stockStatus,
160 // Amounts arrive already in CENTS. roundCent() only normalizes float
161 // artifacts (a client-computed 19.99 * 100 arriving as
162 // 1998.9999999999998) to a whole-cent int — it does not scale.
163 'item_price' => Helper::roundCent($itemPrice),
164 'compare_price' => ($comparePrice !== '' && $comparePrice >= $itemPrice) ? Helper::roundCent($comparePrice) : 0,
165 'item_cost' => Helper::roundCent(Arr::get($variant, 'item_cost', 0)),
166 'manage_cost' => Arr::get($variant, 'manage_cost', 'false'),
167 'fulfillment_type' => Arr::get($variant, 'fulfillment_type', 'physical'),
168 'shipping_class' => Arr::get($variant, 'shipping_class') ?: null,
169 'variation_title' => Arr::get($variant, 'variation_title', ''),
170 'other_info' => $otherInfo,
171 'downloadable' => $isDownloadable,
172 'payment_type' => $hasSubscription ? 'subscription' : 'onetime',
173 ];
174
175 $sku = Arr::get($variant, 'sku');
176 if (!empty($sku)) {
177 $variantData['sku'] = $sku;
178 }
179
180 $isCreated = static::getQuery()->create($variantData);
181 if ($isCreated) {
182 ProductDetailResource::update(
183 [],
184 Arr::get($variant, 'detail_id'),
185 ['action' => 'variant_modified']
186 );
187 $media = Arr::get($variant, 'media', []);
188 if (!empty($media)) {
189 static::setImage($media, $isCreated->id);
190 }
191 return static::makeSuccessResponse(
192 $isCreated,
193 __('Pricing has been created', 'fluent-cart')
194 );
195 }
196 return static::makeErrorResponse([
197 ['code' => 400, 'message' => __('Pricing creation failed!', 'fluent-cart')]
198 ]);
199 }
200
201 /**
202 * Update a variant with the given data.
203 *
204 * @param array $data Array containing the necessary parameters.
205 *
206 * $variant = [
207 * 'id' => (int) Required. The variant ID.
208 * 'post_id' => (int) Required. The product ID.
209 * 'variant_title' => (string) Required. The variant title.
210 * 'item_price' => (int) Required. The item price in CENTS — e.g. 129900 means
211 * $1,299.00. Same unit the API returns on read, and the same
212 * unit stored. See dev-docs/PRICING-AND-TAX.md §6.
213 * 'compare_price' => (int) Required. The compare price, in cents (see item_price).
214 * 'manage_cost' => (string) Optional. Whether to manage costs.
215 * 'item_cost' => (int) Required if manage cost is yes. The item cost, in cents.
216 * 'manage_stock' => (string) Required. Whether to manage stock.
217 * 'stock_status' => (string) Required. The stock status.
218 * 'stock' => (int) Required. The stock quantity.
219 * 'media' => (array) Optional. Info of media files for each variant.
220 * 'id' => (string) Required if upload any media. The media ID.
221 * 'url' => (string) Required if upload any media. The media URL.
222 * 'title' => (string) Required if upload any media. The media title.
223 * 'other_info' => (array) Optional. Other information for the variant.
224 * 'payment_type' => (string) Required. The payment type.
225 * 'times' => (string) Required. The number of times.
226 * 'repeat_interval' => (string) Required. The repeat interval unit.
227 * 'signup_fee' => (int) Required. The signup fee, in cents (see item_price).
228 * 'downloadable_files' => (array) Required if downloadable is true.
229 * 'download_limit' => (string) Required. The download limit.
230 * 'download_expiry' => (string) Required. The download expiry.
231 * 'downloadable' => (bool) Optional. Whether the product is downloadable.
232 * 'files' => (array) Optional. Info of downloadable files for each variant.
233 * 'title' => (string) Required if files. The file title.
234 * 'type' => (string) Required if files. The file type.
235 * 'file_name' => (string) Required if files. The file name.
236 * 'file_path' => (string) Required if files. The file path.
237 * 'file_url' => (string) Required if files. The file URL.
238 * 'serial' => (string) Required if files. The file serial
239 * 'product_terms' => (array) Optional. Terms of the product.
240 * 'product-categories' => (array) Required if categories. Product categories.
241 * [0] => (int) Optional. The category ID.
242 * 'product-types' => (array) Required if types. Product types.
243 * [0] => (int) Optional. The type ID.
244 * ];
245 */
246 public static function update($variant, $variantId, $params = [])
247 {
248 $variant ??= [];
249 $variantId = Arr::get($variant, 'id');
250 $otherInfo = Arr::get($variant, 'other_info');
251
252
253 // Get existing variation to preserve other_info values
254 $existingVariation = static::getQuery()->find($variantId);
255 $existingOtherInfo = $existingVariation->other_info ?? [];
256
257 if (Arr::get($otherInfo, 'payment_type') == 'onetime') {
258 $otherInfo = Arr::only($otherInfo, [
259 'payment_type',
260 'description',
261 'tax_class',
262 'tax_exempt',
263 'tax_inclusion',
264 'bundle_child_ids',
265 'package_slug',
266 'weight',
267 'weight_unit',
268 'length',
269 'width',
270 'height',
271 ]);
272 }
273 if (Arr::get($otherInfo, 'payment_type') == 'subscription') {
274 if (Arr::get($otherInfo, 'manage_setup_fee') == 'no') {
275 unset($otherInfo['signup_fee_name']);
276 unset($otherInfo['signup_fee']);
277 unset($otherInfo['setup_fee_per_item']);
278 }
279 if (Arr::get($otherInfo, 'manage_setup_fee') == 'yes') {
280 // Already in cents — normalize only, do not scale.
281 $signupFee = Helper::roundCent(Arr::get($otherInfo, 'signup_fee', 0));
282 Arr::set($otherInfo, 'signup_fee', $signupFee);
283 }
284 }
285
286 $otherInfo['is_bundle_product'] = Arr::get($existingOtherInfo, 'is_bundle_product', 'no');
287 $otherInfo['bundle_child_ids'] = Arr::get($existingOtherInfo, 'bundle_child_ids', []);
288
289 // Preserve physical attributes — use new value from other_info if present, else keep existing
290 foreach (['weight', 'length', 'width', 'height'] as $attr) {
291 if (isset($otherInfo[$attr])) {
292 $otherInfo[$attr] = floatval($otherInfo[$attr]);
293 } elseif (isset($existingOtherInfo[$attr])) {
294 $otherInfo[$attr] = $existingOtherInfo[$attr];
295 }
296 }
297
298 $isDownloadable = Arr::get($variant, 'downloadable', true);
299 $itemPrice = Arr::get($variant, 'item_price', 1);
300 $comparePrice = Arr::get($variant, 'compare_price');
301 $available = Arr::get($variant, 'available', 0);
302 $stockStatus = Arr::get($variant, 'stock_status', Helper::IN_STOCK);
303 if (Arr::get($variant, 'manage_stock') == 1) {
304 $stockStatus = ($available > 0) ? Helper::IN_STOCK : Helper::OUT_OF_STOCK;
305 } else if (Arr::get($variant, 'manage_stock') == 0) {
306 $stockStatus = Helper::IN_STOCK;
307 }
308
309 $hasSubscription = Arr::get($variant, 'other_info.payment_type') === 'subscription';
310 $variantData = [
311 'post_id' => Arr::get($variant, 'post_id'),
312 'serial_index' => Arr::get($variant, 'serial_index'),
313 'manage_stock' => Arr::get($variant, 'manage_stock', 0),
314 'total_stock' => Arr::get($variant, 'total_stock'),
315 'available' => Arr::get($variant, 'available'),
316 'committed' => Arr::get($variant, 'committed'),
317 'on_hold' => Arr::get($variant, 'on_hold'),
318 'shipping_class' => Arr::get($variant, 'shipping_class') ?: null,
319 'stock_status' => $stockStatus,
320 // Amounts arrive already in CENTS. roundCent() only normalizes float
321 // artifacts (a client-computed 19.99 * 100 arriving as
322 // 1998.9999999999998) to a whole-cent int — it does not scale.
323 'item_price' => Helper::roundCent($itemPrice),
324 'compare_price' => ($comparePrice !== '' && $comparePrice >= $itemPrice) ? Helper::roundCent($comparePrice) : 0,
325 'item_cost' => Helper::roundCent(Arr::get($variant, 'item_cost', 0)),
326 'manage_cost' => Arr::get($variant, 'manage_cost', 'false'),
327 'fulfillment_type' => Arr::get($variant, 'fulfillment_type', 'physical'),
328 'variation_title' => Arr::get($variant, 'variation_title', ''),
329 'sku' => Arr::get($variant, 'sku') ?: null,
330 'other_info' => $otherInfo,
331 'downloadable' => $isDownloadable,
332 'payment_type' => $hasSubscription ? 'subscription' : 'onetime',
333 ];
334
335 // $result = ProductVariation::query()->find($variantId)->fill($variantData)->save();
336 $isUpdated = static::getQuery()->find($variantId);
337 $isUpdated->update($variantData);
338 if ($isUpdated) {
339 ProductDetailResource::update(
340 [],
341 Arr::get($params, 'detail_id'),
342 ['action' => 'variant_modified']
343 );
344 $media = Arr::get($variant, 'media', []);
345 if (!empty($media)) {
346 static::setImage($media, $variantId);
347 } else {
348 ProductMetaResource::delete($variantId);
349 }
350
351
352 return static::makeSuccessResponse(
353 $isUpdated,
354 __('Pricing has been updated', 'fluent-cart')
355 );
356 }
357 return static::makeErrorResponse([
358 ['code' => 400, 'message' => __('Pricing creation failed!', 'fluent-cart')]
359 ]);
360 }
361
362
363 /**
364 * Delete a variant and its associated data.
365 *
366 * @param int $variantId The id of the variant to be deleted.
367 * @param array $params Additional parameters for the deletion process.
368 *
369 */
370 public static function delete($variantId, $params = [])
371 {
372 $variant = static::getQuery()
373 ->with('order_items', function ($query) use ($variantId) {
374 return $query->whereHas('order', function ($query) {
375 return $query->search(["status" => ["column" => "status", "operator" => "in", "value" => [Status::ORDER_PROCESSING, Status::ORDER_ON_HOLD]]]);
376 });
377 })
378 ->find($variantId);
379 $variantTitle = $variant->variation_title;
380
381 if (!empty($variant)) {
382 if (count($variant->order_items) > 0) {
383 return static::makeErrorResponse([
384 ['code' => 400, 'message' => __('This pricing cannot be deleted at the moment. There are pending orders associated with it. Deleting the pricing will disrupt the order processing and might cause inconvenience to our customers.', 'fluent-cart')]
385 ]);
386 }
387 $variant->media()->delete();
388 // $variant->downloadable_files()->delete();
389 $deletedVariant = $variant->delete();
390 if ($deletedVariant) {
391 fluent_cart_success_log(
392 __('Pricing deleted', 'fluent-cart'),
393 sprintf(
394 /* translators: %s is the pricing title */
395 __('Pricing %s is deleted', 'fluent-cart'), $variantTitle),
396 [
397 'module_name' => 'Product',
398 'module_id' => 0,
399 'module_type' => ProductVariation::class,
400 ]
401 );
402 return static::makeSuccessResponse(
403 '',
404 __('Selected pricing and associated data has been deleted', 'fluent-cart')
405 );
406 }
407
408
409 return static::makeErrorResponse([
410 ['code' => 400, 'message' => __('Pricing deletion failed!', 'fluent-cart')]
411 ]);
412 }
413
414 return static::makeErrorResponse([
415 ['code' => 404, 'message' => __('Pricing not found in database.', 'fluent-cart')]
416 ]);
417
418 }
419
420 public static function setImage($media, $variantId, $params = [])
421 {
422
423 $media ??= [];
424 $exist = ProductMetaResource::find($variantId);
425 if ($exist) {
426 return ProductMetaResource::update($media, $variantId);
427
428 } else {
429 return ProductMetaResource::create($media, ['product_id' => $variantId]);
430 }
431 }
432
433 /**
434 * Update a variant pricing table info with the given data.
435 * @param int $variantId The id of the variant.
436 * @param array $data Array containing the necessary parameters.
437 *
438 * $variant = [
439 * 'description' => (string) Required. The variant description.
440 * ];
441 */
442 public static function updatePricingTable($variant, $variantId, $params = [])
443 {
444
445 $variant ??= [];
446 $description = Arr::get($variant, 'description');
447 $isUpdated = static::getQuery()->find($variantId);
448 $otherInfo = $isUpdated->other_info;
449 $otherInfo['description'] = $description;
450 $isUpdated->update([
451 'other_info' => $otherInfo,
452 ]);
453
454 if ($isUpdated) {
455 return static::makeSuccessResponse(
456 $isUpdated,
457 __('Pricing table has been updated', 'fluent-cart')
458 );
459 }
460 return static::makeErrorResponse([
461 ['code' => 400, 'message' => __('Failed to update pricing table!', 'fluent-cart')]
462 ]);
463 }
464 }
465