| 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' => (float) Required. The item price. |
| 69 |
* 'compare_price' => (float) Required. The compare price. |
| 70 |
* 'manage_cost' => (string) Optional. Whether to manage costs. |
| 71 |
* 'item_cost' => (float) Required if manage cost is yes. The item cost. |
| 72 |
* 'manage_stock' => (string) Required. Whether to manage stock. |
| 73 |
* 'stock_status' => (string) Required. The stock status. |
| 74 |
* 'stock' => (int) Required. The stock quantity. |
| 75 |
* 'media' => (array) Optional. Info of media files for each variant. |
| 76 |
* 'id' => (string) Required if upload any media. The media ID. |
| 77 |
* 'url' => (string) Required if upload any media. The media URL. |
| 78 |
* 'title' => (string) Required if upload any media. The media title. |
| 79 |
* 'other_info' => (array) Optional. Other information for the variant. |
| 80 |
* 'payment_type' => (string) Required. The payment type. |
| 81 |
* 'times' => (string) Required. The number of times. |
| 82 |
* 'repeat_interval' => (string) Required. The repeat interval unit. |
| 83 |
* 'signup_fee' => (string) Required. The signup fee. |
| 84 |
* 'downloadable_files' => (array) Required if downloadable is true. |
| 85 |
* 'download_limit' => (string) Required. The download limit. |
| 86 |
* 'download_expiry' => (string) Required. The download expiry. |
| 87 |
* 'downloadable' => (bool) Optional. Whether the product is downloadable. |
| 88 |
* 'files' => (array) Optional. Info of downloadable files for each variant. |
| 89 |
* 'title' => (string) Required if files. The file title. |
| 90 |
* 'type' => (string) Required if files. The file type. |
| 91 |
* 'file_name' => (string) Required if files. The file name. |
| 92 |
* 'file_path' => (string) Required if files. The file path. |
| 93 |
* 'file_url' => (string) Required if files. The file URL. |
| 94 |
* 'serial' => (string) Required if files. The file serial |
| 95 |
* ]; |
| 96 |
*/ |
| 97 |
public static function create($variant, $params = []) |
| 98 |
{ |
| 99 |
$otherInfo = Arr::get($variant, 'other_info'); |
| 100 |
if (Arr::get($otherInfo, 'payment_type') == 'onetime') { |
| 101 |
$otherInfo = Arr::only($otherInfo, [ |
| 102 |
'payment_type', |
| 103 |
'description', |
| 104 |
'package_slug', |
| 105 |
'weight', |
| 106 |
'weight_unit', |
| 107 |
'length', |
| 108 |
'width', |
| 109 |
'height', |
| 110 |
]); |
| 111 |
} |
| 112 |
if (Arr::get($otherInfo, 'payment_type') == 'subscription') { |
| 113 |
if (Arr::get($otherInfo, 'manage_setup_fee') == 'no') { |
| 114 |
unset($otherInfo['signup_fee_name']); |
| 115 |
unset($otherInfo['signup_fee']); |
| 116 |
unset($otherInfo['setup_fee_per_item']); |
| 117 |
} |
| 118 |
if (Arr::get($otherInfo, 'manage_setup_fee') == 'yes') { |
| 119 |
$signupFee = Helper::toCent(floatval(Arr::get($otherInfo, 'signup_fee', 0))); |
| 120 |
Arr::set($otherInfo, 'signup_fee', $signupFee); |
| 121 |
} |
| 122 |
} |
| 123 |
|
| 124 |
// Cast physical attributes in other_info to float |
| 125 |
foreach (['weight', 'length', 'width', 'height'] as $attr) { |
| 126 |
if (isset($otherInfo[$attr])) { |
| 127 |
$otherInfo[$attr] = floatval($otherInfo[$attr]); |
| 128 |
} |
| 129 |
} |
| 130 |
|
| 131 |
$hasSubscription = Arr::get($variant, 'other_info.payment_type') === 'subscription'; |
| 132 |
$isDownloadable = Arr::get($variant, 'downloadable', true); |
| 133 |
$itemPrice = Arr::get($variant, 'item_price', 1); |
| 134 |
$comparePrice = Arr::get($variant, 'compare_price'); |
| 135 |
$available = Arr::get($variant, 'available', 0); |
| 136 |
$stockStatus = Arr::get($variant, 'stock_status', Helper::IN_STOCK); |
| 137 |
if (Arr::get($variant, 'manage_stock') == 1) { |
| 138 |
$stockStatus = ($available > 0) ? Helper::IN_STOCK : Helper::OUT_OF_STOCK; |
| 139 |
} else if (Arr::get($variant, 'manage_stock') == 0) { |
| 140 |
$stockStatus = Helper::IN_STOCK; |
| 141 |
} |
| 142 |
$variantData = [ |
| 143 |
'post_id' => Arr::get($variant, 'post_id'), |
| 144 |
'serial_index' => Arr::get($variant, 'serial_index'), |
| 145 |
'manage_stock' => Arr::get($variant, 'manage_stock', 0), |
| 146 |
'total_stock' => Arr::get($variant, 'total_stock'), |
| 147 |
'available' => Arr::get($variant, 'available'), |
| 148 |
'committed' => Arr::get($variant, 'committed'), |
| 149 |
'on_hold' => Arr::get($variant, 'on_hold'), |
| 150 |
'stock_status' => $stockStatus, |
| 151 |
'item_price' => Helper::toCent($itemPrice), |
| 152 |
//'compare_price' => ($comparePrice !== '' && $comparePrice >= $itemPrice) ? Helper::toCent($comparePrice) : Helper::toCent($itemPrice), |
| 153 |
'compare_price' => ($comparePrice !== '' && $comparePrice >= $itemPrice) ? Helper::toCent($comparePrice) : 0, |
| 154 |
'item_cost' => Helper::toCent(Arr::get($variant, 'item_cost', 0)), |
| 155 |
'manage_cost' => Arr::get($variant, 'manage_cost', 'false'), |
| 156 |
'fulfillment_type' => Arr::get($variant, 'fulfillment_type', 'physical'), |
| 157 |
'shipping_class' => Arr::get($variant, 'shipping_class') ?: null, |
| 158 |
'variation_title' => Arr::get($variant, 'variation_title', ''), |
| 159 |
'other_info' => $otherInfo, |
| 160 |
'downloadable' => $isDownloadable, |
| 161 |
'payment_type' => $hasSubscription ? 'subscription' : 'onetime', |
| 162 |
]; |
| 163 |
|
| 164 |
$sku = Arr::get($variant, 'sku'); |
| 165 |
if (!empty($sku)) { |
| 166 |
$variantData['sku'] = $sku; |
| 167 |
} |
| 168 |
|
| 169 |
$isCreated = static::getQuery()->create($variantData); |
| 170 |
if ($isCreated) { |
| 171 |
ProductDetailResource::update( |
| 172 |
[], |
| 173 |
Arr::get($variant, 'detail_id'), |
| 174 |
['action' => 'variant_modified'] |
| 175 |
); |
| 176 |
$media = Arr::get($variant, 'media', []); |
| 177 |
if (!empty($media)) { |
| 178 |
static::setImage($media, $isCreated->id); |
| 179 |
} |
| 180 |
return static::makeSuccessResponse( |
| 181 |
$isCreated, |
| 182 |
__('Pricing has been created', 'fluent-cart') |
| 183 |
); |
| 184 |
} |
| 185 |
return static::makeErrorResponse([ |
| 186 |
['code' => 400, 'message' => __('Pricing creation failed!', 'fluent-cart')] |
| 187 |
]); |
| 188 |
} |
| 189 |
|
| 190 |
/** |
| 191 |
* Update a variant with the given data. |
| 192 |
* |
| 193 |
* @param array $data Array containing the necessary parameters. |
| 194 |
* |
| 195 |
* $variant = [ |
| 196 |
* 'id' => (int) Required. The variant ID. |
| 197 |
* 'post_id' => (int) Required. The product ID. |
| 198 |
* 'variant_title' => (string) Required. The variant title. |
| 199 |
* 'item_price' => (float) Required. The item price. |
| 200 |
* 'compare_price' => (float) Required. The compare price. |
| 201 |
* 'manage_cost' => (string) Optional. Whether to manage costs. |
| 202 |
* 'item_cost' => (float) Required if manage cost is yes. The item cost. |
| 203 |
* 'manage_stock' => (string) Required. Whether to manage stock. |
| 204 |
* 'stock_status' => (string) Required. The stock status. |
| 205 |
* 'stock' => (int) Required. The stock quantity. |
| 206 |
* 'media' => (array) Optional. Info of media files for each variant. |
| 207 |
* 'id' => (string) Required if upload any media. The media ID. |
| 208 |
* 'url' => (string) Required if upload any media. The media URL. |
| 209 |
* 'title' => (string) Required if upload any media. The media title. |
| 210 |
* 'other_info' => (array) Optional. Other information for the variant. |
| 211 |
* 'payment_type' => (string) Required. The payment type. |
| 212 |
* 'times' => (string) Required. The number of times. |
| 213 |
* 'repeat_interval' => (string) Required. The repeat interval unit. |
| 214 |
* 'signup_fee' => (string) Required. The signup fee. |
| 215 |
* 'downloadable_files' => (array) Required if downloadable is true. |
| 216 |
* 'download_limit' => (string) Required. The download limit. |
| 217 |
* 'download_expiry' => (string) Required. The download expiry. |
| 218 |
* 'downloadable' => (bool) Optional. Whether the product is downloadable. |
| 219 |
* 'files' => (array) Optional. Info of downloadable files for each variant. |
| 220 |
* 'title' => (string) Required if files. The file title. |
| 221 |
* 'type' => (string) Required if files. The file type. |
| 222 |
* 'file_name' => (string) Required if files. The file name. |
| 223 |
* 'file_path' => (string) Required if files. The file path. |
| 224 |
* 'file_url' => (string) Required if files. The file URL. |
| 225 |
* 'serial' => (string) Required if files. The file serial |
| 226 |
* 'product_terms' => (array) Optional. Terms of the product. |
| 227 |
* 'product-categories' => (array) Required if categories. Product categories. |
| 228 |
* [0] => (int) Optional. The category ID. |
| 229 |
* 'product-tags' => (array) Required if tags. Product tags. |
| 230 |
* [0] => (int) Optional. The tag ID. |
| 231 |
* 'product-types' => (array) Required if types. Product types. |
| 232 |
* [0] => (int) Optional. The type ID. |
| 233 |
* ]; |
| 234 |
*/ |
| 235 |
public static function update($variant, $variantId, $params = []) |
| 236 |
{ |
| 237 |
$variant ??= []; |
| 238 |
$variantId = Arr::get($variant, 'id'); |
| 239 |
$otherInfo = Arr::get($variant, 'other_info'); |
| 240 |
|
| 241 |
|
| 242 |
// Get existing variation to preserve other_info values |
| 243 |
$existingVariation = static::getQuery()->find($variantId); |
| 244 |
$existingOtherInfo = $existingVariation->other_info ?? []; |
| 245 |
|
| 246 |
if (Arr::get($otherInfo, 'payment_type') == 'onetime') { |
| 247 |
$otherInfo = Arr::only($otherInfo, [ |
| 248 |
'payment_type', |
| 249 |
'description', |
| 250 |
'bundle_child_ids', |
| 251 |
'package_slug', |
| 252 |
'weight', |
| 253 |
'weight_unit', |
| 254 |
'length', |
| 255 |
'width', |
| 256 |
'height', |
| 257 |
]); |
| 258 |
} |
| 259 |
if (Arr::get($otherInfo, 'payment_type') == 'subscription') { |
| 260 |
if (Arr::get($otherInfo, 'manage_setup_fee') == 'no') { |
| 261 |
unset($otherInfo['signup_fee_name']); |
| 262 |
unset($otherInfo['signup_fee']); |
| 263 |
unset($otherInfo['setup_fee_per_item']); |
| 264 |
} |
| 265 |
if (Arr::get($otherInfo, 'manage_setup_fee') == 'yes') { |
| 266 |
$signupFee = Helper::toCent(floatval(Arr::get($otherInfo, 'signup_fee', 0))); |
| 267 |
Arr::set($otherInfo, 'signup_fee', $signupFee); |
| 268 |
} |
| 269 |
} |
| 270 |
|
| 271 |
$otherInfo['is_bundle_product'] = Arr::get($existingOtherInfo, 'is_bundle_product', 'no'); |
| 272 |
$otherInfo['bundle_child_ids'] = Arr::get($existingOtherInfo, 'bundle_child_ids', []); |
| 273 |
|
| 274 |
// Preserve physical attributes — use new value from other_info if present, else keep existing |
| 275 |
foreach (['weight', 'length', 'width', 'height'] as $attr) { |
| 276 |
if (isset($otherInfo[$attr])) { |
| 277 |
$otherInfo[$attr] = floatval($otherInfo[$attr]); |
| 278 |
} elseif (isset($existingOtherInfo[$attr])) { |
| 279 |
$otherInfo[$attr] = $existingOtherInfo[$attr]; |
| 280 |
} |
| 281 |
} |
| 282 |
|
| 283 |
$isDownloadable = Arr::get($variant, 'downloadable', true); |
| 284 |
$itemPrice = Arr::get($variant, 'item_price', 1); |
| 285 |
$comparePrice = Arr::get($variant, 'compare_price'); |
| 286 |
$available = Arr::get($variant, 'available', 0); |
| 287 |
$stockStatus = Arr::get($variant, 'stock_status', Helper::IN_STOCK); |
| 288 |
if (Arr::get($variant, 'manage_stock') == 1) { |
| 289 |
$stockStatus = ($available > 0) ? Helper::IN_STOCK : Helper::OUT_OF_STOCK; |
| 290 |
} else if (Arr::get($variant, 'manage_stock') == 0) { |
| 291 |
$stockStatus = Helper::IN_STOCK; |
| 292 |
} |
| 293 |
|
| 294 |
$hasSubscription = Arr::get($variant, 'other_info.payment_type') === 'subscription'; |
| 295 |
$variantData = [ |
| 296 |
'post_id' => Arr::get($variant, 'post_id'), |
| 297 |
'serial_index' => Arr::get($variant, 'serial_index'), |
| 298 |
'manage_stock' => Arr::get($variant, 'manage_stock', 0), |
| 299 |
'total_stock' => Arr::get($variant, 'total_stock'), |
| 300 |
'available' => Arr::get($variant, 'available'), |
| 301 |
'committed' => Arr::get($variant, 'committed'), |
| 302 |
'on_hold' => Arr::get($variant, 'on_hold'), |
| 303 |
'shipping_class' => Arr::get($variant, 'shipping_class') ?: null, |
| 304 |
'stock_status' => $stockStatus, |
| 305 |
'item_price' => Helper::toCent($itemPrice), |
| 306 |
//'compare_price' => ($comparePrice !== '' && $comparePrice >= $itemPrice) ? Helper::toCent($comparePrice) : Helper::toCent($itemPrice), |
| 307 |
'compare_price' => ($comparePrice !== '' && $comparePrice >= $itemPrice) ? Helper::toCent($comparePrice) : 0, |
| 308 |
'item_cost' => Helper::toCent(Arr::get($variant, 'item_cost', 0)), |
| 309 |
'manage_cost' => Arr::get($variant, 'manage_cost', 'false'), |
| 310 |
'fulfillment_type' => Arr::get($variant, 'fulfillment_type', 'physical'), |
| 311 |
'variation_title' => Arr::get($variant, 'variation_title', ''), |
| 312 |
'sku' => Arr::get($variant, 'sku') ?: null, |
| 313 |
'other_info' => $otherInfo, |
| 314 |
'downloadable' => $isDownloadable, |
| 315 |
'payment_type' => $hasSubscription ? 'subscription' : 'onetime', |
| 316 |
]; |
| 317 |
|
| 318 |
// $result = ProductVariation::query()->find($variantId)->fill($variantData)->save(); |
| 319 |
$isUpdated = static::getQuery()->find($variantId); |
| 320 |
$isUpdated->update($variantData); |
| 321 |
if ($isUpdated) { |
| 322 |
ProductDetailResource::update( |
| 323 |
[], |
| 324 |
Arr::get($params, 'detail_id'), |
| 325 |
['action' => 'variant_modified'] |
| 326 |
); |
| 327 |
$media = Arr::get($variant, 'media', []); |
| 328 |
if (!empty($media)) { |
| 329 |
static::setImage($media, $variantId); |
| 330 |
} else { |
| 331 |
ProductMetaResource::delete($variantId); |
| 332 |
} |
| 333 |
|
| 334 |
|
| 335 |
return static::makeSuccessResponse( |
| 336 |
$isUpdated, |
| 337 |
__('Pricing has been updated', 'fluent-cart') |
| 338 |
); |
| 339 |
} |
| 340 |
return static::makeErrorResponse([ |
| 341 |
['code' => 400, 'message' => __('Pricing creation failed!', 'fluent-cart')] |
| 342 |
]); |
| 343 |
} |
| 344 |
|
| 345 |
|
| 346 |
/** |
| 347 |
* Delete a variant and its associated data. |
| 348 |
* |
| 349 |
* @param int $variantId The id of the variant to be deleted. |
| 350 |
* @param array $params Additional parameters for the deletion process. |
| 351 |
* |
| 352 |
*/ |
| 353 |
public static function delete($variantId, $params = []) |
| 354 |
{ |
| 355 |
$variant = static::getQuery() |
| 356 |
->with('order_items', function ($query) use ($variantId) { |
| 357 |
return $query->whereHas('order', function ($query) { |
| 358 |
return $query->search(["status" => ["column" => "status", "operator" => "in", "value" => [Status::ORDER_PROCESSING, Status::ORDER_ON_HOLD]]]); |
| 359 |
}); |
| 360 |
}) |
| 361 |
->find($variantId); |
| 362 |
$variantTitle = $variant->variation_title; |
| 363 |
|
| 364 |
if (!empty($variant)) { |
| 365 |
if (count($variant->order_items) > 0) { |
| 366 |
return static::makeErrorResponse([ |
| 367 |
['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')] |
| 368 |
]); |
| 369 |
} |
| 370 |
$variant->media()->delete(); |
| 371 |
// $variant->downloadable_files()->delete(); |
| 372 |
$deletedVariant = $variant->delete(); |
| 373 |
if ($deletedVariant) { |
| 374 |
fluent_cart_success_log( |
| 375 |
__('Pricing deleted', 'fluent-cart'), |
| 376 |
sprintf( |
| 377 |
/* translators: %s is the pricing title */ |
| 378 |
__('Pricing %s is deleted', 'fluent-cart'), $variantTitle), |
| 379 |
[ |
| 380 |
'module_name' => 'Product', |
| 381 |
'module_id' => 0, |
| 382 |
'module_type' => ProductVariation::class, |
| 383 |
] |
| 384 |
); |
| 385 |
return static::makeSuccessResponse( |
| 386 |
'', |
| 387 |
__('Selected pricing and associated data has been deleted', 'fluent-cart') |
| 388 |
); |
| 389 |
} |
| 390 |
|
| 391 |
|
| 392 |
return static::makeErrorResponse([ |
| 393 |
['code' => 400, 'message' => __('Pricing deletion failed!', 'fluent-cart')] |
| 394 |
]); |
| 395 |
} |
| 396 |
|
| 397 |
return static::makeErrorResponse([ |
| 398 |
['code' => 404, 'message' => __('Pricing not found in database.', 'fluent-cart')] |
| 399 |
]); |
| 400 |
|
| 401 |
} |
| 402 |
|
| 403 |
public static function setImage($media, $variantId, $params = []) |
| 404 |
{ |
| 405 |
|
| 406 |
$media ??= []; |
| 407 |
$exist = ProductMetaResource::find($variantId); |
| 408 |
if ($exist) { |
| 409 |
return ProductMetaResource::update($media, $variantId); |
| 410 |
|
| 411 |
} else { |
| 412 |
return ProductMetaResource::create($media, ['product_id' => $variantId]); |
| 413 |
} |
| 414 |
} |
| 415 |
|
| 416 |
/** |
| 417 |
* Update a variant pricing table info with the given data. |
| 418 |
* @param int $variantId The id of the variant. |
| 419 |
* @param array $data Array containing the necessary parameters. |
| 420 |
* |
| 421 |
* $variant = [ |
| 422 |
* 'description' => (string) Required. The variant description. |
| 423 |
* ]; |
| 424 |
*/ |
| 425 |
public static function updatePricingTable($variant, $variantId, $params = []) |
| 426 |
{ |
| 427 |
|
| 428 |
$variant ??= []; |
| 429 |
$description = Arr::get($variant, 'description'); |
| 430 |
$isUpdated = static::getQuery()->find($variantId); |
| 431 |
$otherInfo = $isUpdated->other_info; |
| 432 |
$otherInfo['description'] = $description; |
| 433 |
$isUpdated->update([ |
| 434 |
'other_info' => $otherInfo, |
| 435 |
]); |
| 436 |
|
| 437 |
if ($isUpdated) { |
| 438 |
return static::makeSuccessResponse( |
| 439 |
$isUpdated, |
| 440 |
__('Pricing table has been updated', 'fluent-cart') |
| 441 |
); |
| 442 |
} |
| 443 |
return static::makeErrorResponse([ |
| 444 |
['code' => 400, 'message' => __('Failed to update pricing table!', 'fluent-cart')] |
| 445 |
]); |
| 446 |
} |
| 447 |
} |
| 448 |
|