feat: implement cart editing policies and update related services
- Introduced CartEditingPolicy enum to manage cart editing rules. - Updated Tenant model to use cart_editing_policy instead of a boolean flag. - Refactored PurchaseItemSnapshotFactory to include variant details. - Added PurchaseResponseLoader service to streamline purchase loading. - Enhanced SourceCartService with methods to sync item quantities and selections. - Updated StartCheckoutService to utilize the new PurchaseResponseLoader. - Created new API routes for editing purchase items. - Added tests to validate cart editing policies and their effects on item updates. - Migrated existing data to the new cart editing policy structure. - Updated language files to reflect changes in cart editing messages.
This commit is contained in:
commit
cc22e33799
|
|
@ -1169,6 +1169,99 @@
|
||||||
},
|
},
|
||||||
"response": []
|
"response": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Prepare Item Editing Purchase",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Accept",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Ruta Laravel: `POST /api/tenants/{tenant:codigo}/compras/{compra}/edit-items`\n\nControlador: `App\\Domains\\Purchase\\Controllers\\PurchaseController@prepareItemEditing`\n\nRequiere autenticación Sanctum.",
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/tenants/{{tenant_code}}/compras/{{purchase_id}}/edit-items",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"tenants",
|
||||||
|
"{{tenant_code}}",
|
||||||
|
"compras",
|
||||||
|
"{{purchase_id}}",
|
||||||
|
"edit-items"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"type": "bearer",
|
||||||
|
"bearer": [
|
||||||
|
{
|
||||||
|
"key": "token",
|
||||||
|
"value": "{{token}}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Item Purchase",
|
||||||
|
"request": {
|
||||||
|
"method": "PATCH",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Accept",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Ruta Laravel: `PATCH /api/tenants/{tenant:codigo}/compras/{compra}/items/{item}`\n\nControlador: `App\\Domains\\Purchase\\Controllers\\PurchaseController@updateItem`\n\nRequiere autenticación Sanctum.",
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/tenants/{{tenant_code}}/compras/{{purchase_id}}/items/{{purchase_item_id}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"tenants",
|
||||||
|
"{{tenant_code}}",
|
||||||
|
"compras",
|
||||||
|
"{{purchase_id}}",
|
||||||
|
"items",
|
||||||
|
"{{purchase_item_id}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"quantity\": 2,\n \"variant_id\": {{variant_id}}\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"type": "bearer",
|
||||||
|
"bearer": [
|
||||||
|
{
|
||||||
|
"key": "token",
|
||||||
|
"value": "{{token}}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Create Payment Intent Purchase",
|
"name": "Create Payment Intent Purchase",
|
||||||
"request": {
|
"request": {
|
||||||
|
|
@ -2121,7 +2214,7 @@
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
"raw": "{\n \"client_id\": {{client_id}},\n \"codigo\": \"{{tenant_code}}\",\n \"nombre\": \"Tenant Demo\",\n \"dominio\": \"{{tenant_domain}}\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"secondary_color\": \"#2563EB\",\n \"danger_color\": \"#DC2626\",\n \"success_color\": \"#16A34A\",\n \"header_bg_color\": \"#FFFFFF\",\n \"footer_bg_color\": \"#111827\",\n \"header_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"footer_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"search_product_layout\": \"column_with_image\",\n \"search_group_layout\": \"paginated\",\n \"search_items_per_page\": 12,\n \"display_categories\": true,\n \"display_seach_bar\": true,\n \"display_cart\": true,\n \"cart_editing_enabled\": true,\n \"website_type_code\": \"shopit\"\n}",
|
"raw": "{\n \"client_id\": {{client_id}},\n \"codigo\": \"{{tenant_code}}\",\n \"nombre\": \"Tenant Demo\",\n \"dominio\": \"{{tenant_domain}}\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"secondary_color\": \"#2563EB\",\n \"danger_color\": \"#DC2626\",\n \"success_color\": \"#16A34A\",\n \"header_bg_color\": \"#FFFFFF\",\n \"footer_bg_color\": \"#111827\",\n \"header_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"footer_logo\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\",\n \"search_product_layout\": \"column_with_image\",\n \"search_group_layout\": \"paginated\",\n \"search_items_per_page\": 12,\n \"display_categories\": true,\n \"display_seach_bar\": true,\n \"display_cart\": true,\n \"cart_editing_policy\": \"full\",\n \"website_type_code\": \"shopit\"\n}",
|
||||||
"options": {
|
"options": {
|
||||||
"raw": {
|
"raw": {
|
||||||
"language": "json"
|
"language": "json"
|
||||||
|
|
@ -2187,7 +2280,7 @@
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
"raw": "{\n \"nombre\": \"Tenant Demo Actualizado\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"cart_editing_enabled\": true\n}",
|
"raw": "{\n \"nombre\": \"Tenant Demo Actualizado\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"cart_editing_policy\": \"full\"\n}",
|
||||||
"options": {
|
"options": {
|
||||||
"raw": {
|
"raw": {
|
||||||
"language": "json"
|
"language": "json"
|
||||||
|
|
@ -2227,7 +2320,7 @@
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
"raw": "{\n \"nombre\": \"Tenant Demo Actualizado\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"cart_editing_enabled\": true\n}",
|
"raw": "{\n \"nombre\": \"Tenant Demo Actualizado\",\n \"site_title\": \"ShopIt Demo\",\n \"primary_color\": \"#111827\",\n \"cart_editing_policy\": \"full\"\n}",
|
||||||
"options": {
|
"options": {
|
||||||
"raw": {
|
"raw": {
|
||||||
"language": "json"
|
"language": "json"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ namespace App\Domains\Cart\Resources;
|
||||||
|
|
||||||
use App\Domains\Cart\Models\CartItem;
|
use App\Domains\Cart\Models\CartItem;
|
||||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
|
use App\Domains\Catalog\Models\Variant;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
@ -22,6 +23,8 @@ class CartItemResource extends JsonResource
|
||||||
$imageUrl = null;
|
$imageUrl = null;
|
||||||
$tenant = $request->route('tenant');
|
$tenant = $request->route('tenant');
|
||||||
$displayImage = ! $tenant instanceof Tenant || $tenant->display_cart_item_images;
|
$displayImage = ! $tenant instanceof Tenant || $tenant->display_cart_item_images;
|
||||||
|
$includeVariants = $tenant instanceof Tenant
|
||||||
|
&& $tenant->cart_editing_policy->allowsVariantChanges();
|
||||||
|
|
||||||
if ($displayImage && $selectedItem?->relationLoaded('attachments')) {
|
if ($displayImage && $selectedItem?->relationLoaded('attachments')) {
|
||||||
$imageUrl = $selectedItem->attachments->first()?->getTemporaryUrl(1440);
|
$imageUrl = $selectedItem->attachments->first()?->getTemporaryUrl(1440);
|
||||||
|
|
@ -39,14 +42,27 @@ class CartItemResource extends JsonResource
|
||||||
'variant_id' => $this->variant_id,
|
'variant_id' => $this->variant_id,
|
||||||
'nombre' => $selectedItem?->getName(),
|
'nombre' => $selectedItem?->getName(),
|
||||||
'imagen' => $imageUrl,
|
'imagen' => $imageUrl,
|
||||||
'variant' => $this->variant === null ? null : [
|
'variant' => $this->variant === null ? null : $this->variantData($this->variant),
|
||||||
'id' => $this->variant->id,
|
'variants' => $this->when(
|
||||||
'precio' => $this->formatMoney($this->variant->getPrice()),
|
$includeVariants,
|
||||||
'stock_tecnico' => $this->catalogItem->inventory_policy === InventoryPolicy::Unlimited
|
fn () => $this->catalogItem
|
||||||
? null
|
->visibleVariants($this->variant_id)
|
||||||
: $this->variant->inventory->availableStock(),
|
->map(fn (Variant $variant): array => $this->variantData($variant))
|
||||||
'values' => $this->variant->selectorOptions($this->catalogItem->itemAttributes),
|
->values(),
|
||||||
],
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
protected function variantData(Variant $variant): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $variant->id,
|
||||||
|
'precio' => $this->formatMoney($variant->precio ?? $this->catalogItem->precio),
|
||||||
|
'stock_tecnico' => $this->catalogItem->inventory_policy === InventoryPolicy::Unlimited
|
||||||
|
? null
|
||||||
|
: $variant->inventory->availableStock(),
|
||||||
|
'values' => $variant->selectorOptions($this->catalogItem->itemAttributes),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ class CartService
|
||||||
return $this->makeEmptyCart($tenant);
|
return $this->makeEmptyCart($tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->loadCart($cart);
|
return $this->loadCart($cart, $tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,7 +55,7 @@ class CartService
|
||||||
$cart->addItem($catalogItemId, $variantId, $quantity);
|
$cart->addItem($catalogItemId, $variantId, $quantity);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'cart' => $this->loadCart($cart),
|
'cart' => $this->loadCart($cart, $tenant),
|
||||||
'guest_token' => $resolvedIdentity['generated_guest_token'],
|
'guest_token' => $resolvedIdentity['generated_guest_token'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -68,11 +68,23 @@ class CartService
|
||||||
?int $variantId,
|
?int $variantId,
|
||||||
bool $updateVariant,
|
bool $updateVariant,
|
||||||
): Cart {
|
): Cart {
|
||||||
|
if ($updateVariant && ! $tenant->cart_editing_policy->allowsVariantChanges()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'variant_id' => __('api.cart.variant_change_disabled'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $updateVariant && ! $tenant->cart_editing_policy->allowsQuantityChanges()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'cantidad' => __('api.cart.editing_disabled'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$identity = $this->requireIdentity($request);
|
$identity = $this->requireIdentity($request);
|
||||||
$cart = $this->findCartOrFail($tenant, $identity);
|
$cart = $this->findCartOrFail($tenant, $identity);
|
||||||
$cart->updateItem($cartItemId, $quantity, $variantId, $updateVariant);
|
$cart->updateItem($cartItemId, $quantity, $variantId, $updateVariant);
|
||||||
|
|
||||||
return $this->loadCart($cart);
|
return $this->loadCart($cart, $tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateCheckoutItem(
|
public function updateCheckoutItem(
|
||||||
|
|
@ -178,11 +190,17 @@ class CartService
|
||||||
|
|
||||||
public function removeItem(Tenant $tenant, Request $request, int $cartItemId): Cart
|
public function removeItem(Tenant $tenant, Request $request, int $cartItemId): Cart
|
||||||
{
|
{
|
||||||
|
if (! $tenant->cart_editing_policy->allowsRemoval()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'cart_item' => __('api.cart.editing_disabled'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$identity = $this->requireIdentity($request);
|
$identity = $this->requireIdentity($request);
|
||||||
$cart = $this->findCartOrFail($tenant, $identity);
|
$cart = $this->findCartOrFail($tenant, $identity);
|
||||||
$cart->removeItem($cartItemId);
|
$cart->removeItem($cartItemId);
|
||||||
|
|
||||||
return $this->loadCart($cart);
|
return $this->loadCart($cart, $tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function makeGuestTokenCookie(string $guestToken): Cookie
|
public function makeGuestTokenCookie(string $guestToken): Cookie
|
||||||
|
|
@ -212,9 +230,9 @@ class CartService
|
||||||
return $cart;
|
return $cart;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function loadCart(Cart $cart): Cart
|
protected function loadCart(Cart $cart, Tenant $tenant): Cart
|
||||||
{
|
{
|
||||||
return $cart->fresh()->load([
|
$relations = [
|
||||||
'items.catalogItem.attachments',
|
'items.catalogItem.attachments',
|
||||||
'items.catalogItem.inventory',
|
'items.catalogItem.inventory',
|
||||||
'items.catalogItem.itemAttributes.attribute',
|
'items.catalogItem.itemAttributes.attribute',
|
||||||
|
|
@ -223,7 +241,21 @@ class CartService
|
||||||
'items.variant.definitions.itemAttribute.attribute.options',
|
'items.variant.definitions.itemAttribute.attribute.options',
|
||||||
'items.variant.eventDates',
|
'items.variant.eventDates',
|
||||||
'items.variant.eventDate',
|
'items.variant.eventDate',
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
if ($tenant->cart_editing_policy->allowsVariantChanges()) {
|
||||||
|
$relations = [
|
||||||
|
...$relations,
|
||||||
|
'items.catalogItem.variants' => fn ($query) => $query->orderBy('id'),
|
||||||
|
'items.catalogItem.variants.inventory',
|
||||||
|
'items.catalogItem.variants.definitions' => fn ($query) => $query->orderBy('id'),
|
||||||
|
'items.catalogItem.variants.definitions.itemAttribute.attribute.options',
|
||||||
|
'items.catalogItem.variants.eventDates',
|
||||||
|
'items.catalogItem.variants.eventDate',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cart->fresh()->load($relations);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ use App\Domains\Purchase\Models\Purchase;
|
||||||
use App\Domains\Purchase\Requests\PaymentIntentRequest;
|
use App\Domains\Purchase\Requests\PaymentIntentRequest;
|
||||||
use App\Domains\Purchase\Requests\StartCheckoutRequest;
|
use App\Domains\Purchase\Requests\StartCheckoutRequest;
|
||||||
use App\Domains\Purchase\Requests\UpdatePurchaseCustomerRequest;
|
use App\Domains\Purchase\Requests\UpdatePurchaseCustomerRequest;
|
||||||
|
use App\Domains\Purchase\Requests\UpdatePurchaseItemRequest;
|
||||||
use App\Domains\Purchase\Resources\PurchaseResource;
|
use App\Domains\Purchase\Resources\PurchaseResource;
|
||||||
|
use App\Domains\Purchase\Services\Checkout\PurchaseResponseLoader;
|
||||||
use App\Domains\Purchase\Services\CheckoutService;
|
use App\Domains\Purchase\Services\CheckoutService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
|
@ -50,21 +52,16 @@ class PurchaseController extends Controller
|
||||||
return PurchaseResource::make($purchase)->response()->setStatusCode(201);
|
return PurchaseResource::make($purchase)->response()->setStatusCode(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show(Request $request, Tenant $tenant, Purchase $compra): PurchaseResource
|
public function show(
|
||||||
{
|
Request $request,
|
||||||
|
Tenant $tenant,
|
||||||
|
Purchase $compra,
|
||||||
|
PurchaseResponseLoader $responses,
|
||||||
|
): PurchaseResource {
|
||||||
$compra = $this->resolveScopedPurchase($tenant, $request->user()->id, $compra);
|
$compra = $this->resolveScopedPurchase($tenant, $request->user()->id, $compra);
|
||||||
|
|
||||||
$compra->loadMissing([
|
$compra->loadMissing('items')->loadCount('tickets');
|
||||||
'items',
|
$responses->load($compra);
|
||||||
'cart.items.catalogItem.inventory',
|
|
||||||
'cart.items.catalogItem.attachments',
|
|
||||||
'cart.items.variant.inventory',
|
|
||||||
'cart.items.variant.attachments',
|
|
||||||
'cart.items.variant.definitions.itemAttribute.attribute',
|
|
||||||
'cart.items.variant.eventDates',
|
|
||||||
'cart.items.variant.eventDate',
|
|
||||||
])->loadCount('tickets');
|
|
||||||
$compra->items->load('imageAttachment');
|
|
||||||
|
|
||||||
return PurchaseResource::make($compra);
|
return PurchaseResource::make($compra);
|
||||||
}
|
}
|
||||||
|
|
@ -82,8 +79,28 @@ class PurchaseController extends Controller
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function paymentIntent(
|
public function updateItem(
|
||||||
PaymentIntentRequest $request,
|
UpdatePurchaseItemRequest $request,
|
||||||
|
Tenant $tenant,
|
||||||
|
Purchase $compra,
|
||||||
|
PurchaseItem $item,
|
||||||
|
CheckoutService $checkoutService,
|
||||||
|
): PurchaseResource {
|
||||||
|
$compra = $this->resolveScopedPurchase($tenant, $request->user()->id, $compra);
|
||||||
|
|
||||||
|
return PurchaseResource::make(
|
||||||
|
$checkoutService->updateItem(
|
||||||
|
$compra,
|
||||||
|
$item,
|
||||||
|
$request->exists('quantity') ? (int) $request->validated('quantity') : null,
|
||||||
|
$request->exists('variant_id') ? (int) $request->validated('variant_id') : null,
|
||||||
|
$request->exists('variant_id'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepareItemEditing(
|
||||||
|
Request $request,
|
||||||
Tenant $tenant,
|
Tenant $tenant,
|
||||||
Purchase $compra,
|
Purchase $compra,
|
||||||
CheckoutService $checkoutService,
|
CheckoutService $checkoutService,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
namespace App\Domains\Purchase\Models;
|
namespace App\Domains\Purchase\Models;
|
||||||
|
|
||||||
use App\Domains\Attachable\Models\Attachment;
|
use App\Domains\Attachable\Models\Attachment;
|
||||||
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
|
use App\Domains\Catalog\Models\Variant;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
@ -59,4 +61,16 @@ class PurchaseItem extends Model
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Attachment::class, 'image_attachment_id');
|
return $this->belongsTo(Attachment::class, 'image_attachment_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<CatalogItem, $this> */
|
||||||
|
public function sourceCatalogItem(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(CatalogItem::class, 'source_catalog_item_id')->withTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Variant, $this> */
|
||||||
|
public function sourceVariant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Variant::class, 'source_variant_id')->withTrashed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Purchase\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdatePurchaseItemRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'quantity' => ['sometimes', 'required_without:variant_id', 'integer', 'min:1', 'max:100'],
|
||||||
|
'variant_id' => ['sometimes', 'required_without:quantity', 'integer', 'min:1'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Domains\Purchase\Resources;
|
namespace App\Domains\Purchase\Resources;
|
||||||
|
|
||||||
use App\Domains\Cart\Models\CartItem;
|
use App\Domains\Cart\Models\CartItem;
|
||||||
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Catalog\Models\CatalogItem;
|
use App\Domains\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
use App\Domains\Purchase\Models\PurchaseItem;
|
use App\Domains\Purchase\Models\PurchaseItem;
|
||||||
|
|
@ -25,6 +26,14 @@ class PurchaseItemResource extends JsonResource
|
||||||
: null;
|
: null;
|
||||||
$attributes = $this->variant_attributes ?? [];
|
$attributes = $this->variant_attributes ?? [];
|
||||||
|
|
||||||
|
$catalogItem = $this->relationLoaded('sourceCatalogItem')
|
||||||
|
? $this->sourceCatalogItem
|
||||||
|
: null;
|
||||||
|
$includeVariants = $tenant instanceof Tenant
|
||||||
|
&& $tenant->cart_editing_policy->allowsVariantChanges()
|
||||||
|
&& $catalogItem !== null
|
||||||
|
&& $catalogItem->relationLoaded('variants');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'quantity' => (int) $this->cantidad,
|
'quantity' => (int) $this->cantidad,
|
||||||
|
|
@ -39,6 +48,13 @@ class PurchaseItemResource extends JsonResource
|
||||||
'imagen' => $imageUrl,
|
'imagen' => $imageUrl,
|
||||||
'attributes' => $attributes,
|
'attributes' => $attributes,
|
||||||
],
|
],
|
||||||
|
'variants' => $this->when(
|
||||||
|
$includeVariants,
|
||||||
|
fn () => $catalogItem
|
||||||
|
->visibleVariants($this->source_variant_id)
|
||||||
|
->map(fn (Variant $variant): array => $this->variantData($catalogItem, $variant))
|
||||||
|
->values(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,4 +148,17 @@ class PurchaseItemResource extends JsonResource
|
||||||
{
|
{
|
||||||
return number_format((float) ($amount ?? 0), 2, '.', '');
|
return number_format((float) ($amount ?? 0), 2, '.', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
private function variantData(CatalogItem $catalogItem, Variant $variant): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $variant->id,
|
||||||
|
'precio' => $this->formatMoney($variant->precio ?? $catalogItem->precio),
|
||||||
|
'stock_tecnico' => $catalogItem->inventory_policy === InventoryPolicy::Unlimited
|
||||||
|
? null
|
||||||
|
: $variant->inventory->availableStock(),
|
||||||
|
'values' => $variant->selectorOptions($catalogItem->itemAttributes),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,15 @@ use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class EditCheckoutService
|
class EditCheckoutService
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly CatalogInventoryService $inventory,
|
||||||
|
private readonly UserPurchaseLimitService $purchaseLimits,
|
||||||
|
private readonly CatalogSelectionResolver $selections,
|
||||||
|
private readonly SourceCartService $sourceCart,
|
||||||
|
private readonly PurchaseItemSnapshotFactory $snapshots,
|
||||||
|
private readonly PurchaseResponseLoader $responses,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** @param array<string, string> $customerData */
|
/** @param array<string, string> $customerData */
|
||||||
public function updateCustomer(Purchase $purchase, array $customerData): Purchase
|
public function updateCustomer(Purchase $purchase, array $customerData): Purchase
|
||||||
{
|
{
|
||||||
|
|
@ -21,6 +30,229 @@ class EditCheckoutService
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updateItem(
|
||||||
|
Purchase $purchase,
|
||||||
|
PurchaseItem $purchaseItem,
|
||||||
|
?int $quantity,
|
||||||
|
?int $variantId,
|
||||||
|
bool $updateVariant,
|
||||||
|
): Purchase {
|
||||||
|
return DB::transaction(function () use (
|
||||||
|
$purchase,
|
||||||
|
$purchaseItem,
|
||||||
|
$quantity,
|
||||||
|
$variantId,
|
||||||
|
$updateVariant,
|
||||||
|
): Purchase {
|
||||||
|
$purchase = $this->lockPurchase($purchase);
|
||||||
|
|
||||||
|
if ($purchase->status !== Purchase::STATUS_CREATED || $this->hasExpired($purchase)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'purchase' => __('api.purchase.not_editable'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$purchaseItem = $this->lockPurchaseItem($purchase, $purchaseItem);
|
||||||
|
$tenant = $purchase->tenant()->firstOrFail();
|
||||||
|
$finalQuantity = $quantity ?? (int) $purchaseItem->cantidad;
|
||||||
|
|
||||||
|
if ($quantity !== null && ! $tenant->cart_editing_policy->allowsQuantityChanges()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'quantity' => __('api.cart.editing_disabled'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($updateVariant && ! $tenant->cart_editing_policy->allowsVariantChanges()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'variant_id' => __('api.purchase.variant_change_disabled'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($updateVariant && $variantId !== $purchaseItem->source_variant_id) {
|
||||||
|
$this->changeItemVariant(
|
||||||
|
$purchase,
|
||||||
|
$purchaseItem,
|
||||||
|
(int) $variantId,
|
||||||
|
$finalQuantity,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$difference = $finalQuantity - (int) $purchaseItem->cantidad;
|
||||||
|
|
||||||
|
if ($difference !== 0) {
|
||||||
|
$this->adjustReservation($purchase, $purchaseItem, $finalQuantity, $difference);
|
||||||
|
|
||||||
|
$purchaseItem->update([
|
||||||
|
'cantidad' => $finalQuantity,
|
||||||
|
'total' => (float) $purchaseItem->precio_unitario * $finalQuantity,
|
||||||
|
]);
|
||||||
|
$this->sourceCart->syncItemQuantity($purchase, $purchaseItem, $finalQuantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$purchase->update([
|
||||||
|
'total' => $purchase->calculateCurrentTotalAmount(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->loadPurchase($purchase);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function changeItemVariant(
|
||||||
|
Purchase $purchase,
|
||||||
|
PurchaseItem $sourceItem,
|
||||||
|
int $variantId,
|
||||||
|
int $quantity,
|
||||||
|
): void {
|
||||||
|
$tenant = $purchase->tenant()->firstOrFail();
|
||||||
|
$currentSelection = $this->selections->resolvePurchaseItem($tenant, $sourceItem);
|
||||||
|
$targetSelection = $this->selections->resolve(
|
||||||
|
$tenant,
|
||||||
|
(int) $sourceItem->source_catalog_item_id,
|
||||||
|
$variantId,
|
||||||
|
'item',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (! $targetSelection instanceof Variant) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'variant_id' => __('api.cart.variant_required'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var PurchaseItem|null $targetItem */
|
||||||
|
$targetItem = $purchase->items()
|
||||||
|
->where('source_catalog_item_id', $sourceItem->source_catalog_item_id)
|
||||||
|
->where('source_variant_id', $targetSelection->id)
|
||||||
|
->whereKeyNot($sourceItem->getKey())
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($targetItem !== null && $targetItem->reservation_status !== PurchaseItem::RESERVATION_ACTIVE) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'variant_id' => __('api.purchase.item_not_editable'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$otherItemQuantity = (int) $purchase->items()
|
||||||
|
->where('source_catalog_item_id', $sourceItem->source_catalog_item_id)
|
||||||
|
->whereKeyNot($sourceItem->getKey())
|
||||||
|
->sum('cantidad');
|
||||||
|
$this->purchaseLimits->assertCanPurchase(
|
||||||
|
$targetSelection->catalogItem,
|
||||||
|
(int) $purchase->user_id,
|
||||||
|
$otherItemQuantity + $quantity,
|
||||||
|
$purchase->getKey(),
|
||||||
|
'variant_id',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->inventory->release($currentSelection, (int) $sourceItem->cantidad);
|
||||||
|
$this->inventory->reserve($targetSelection, $quantity);
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'variant_id' => __('api.purchase.insufficient_stock'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$previousVariantId = $sourceItem->source_variant_id;
|
||||||
|
$finalQuantity = $quantity;
|
||||||
|
|
||||||
|
if ($targetItem !== null) {
|
||||||
|
$finalQuantity += (int) $targetItem->cantidad;
|
||||||
|
$targetItem->update($this->snapshots->fromVariant($targetSelection, $finalQuantity));
|
||||||
|
$sourceItem->delete();
|
||||||
|
} else {
|
||||||
|
$sourceItem->update($this->snapshots->fromVariant($targetSelection, $finalQuantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sourceCart->syncItemSelection(
|
||||||
|
$purchase,
|
||||||
|
(int) $sourceItem->source_catalog_item_id,
|
||||||
|
$previousVariantId,
|
||||||
|
$targetSelection->id,
|
||||||
|
$finalQuantity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepareItemEditing(Purchase $purchase): Purchase
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($purchase): Purchase {
|
||||||
|
$purchase = $this->lockPurchase($purchase);
|
||||||
|
$this->assertEditable($purchase);
|
||||||
|
|
||||||
|
if (! $purchase->tenant()->firstOrFail()->cart_editing_policy->allowsModification()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'purchase' => __('api.cart.editing_disabled'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$purchase->telepagosQr()->delete();
|
||||||
|
$purchase->update([
|
||||||
|
'status' => Purchase::STATUS_CREATED,
|
||||||
|
'payment_method' => null,
|
||||||
|
'transfer_payer_dni' => null,
|
||||||
|
'expires_at' => now()->addMinutes(
|
||||||
|
max(1, (int) config('purchase.checkout_expiration_minutes', 30)),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->loadPurchase($purchase);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function adjustReservation(
|
||||||
|
Purchase $purchase,
|
||||||
|
PurchaseItem $purchaseItem,
|
||||||
|
int $quantity,
|
||||||
|
int $difference,
|
||||||
|
): void {
|
||||||
|
$selection = $this->selections->resolvePurchaseItem($purchase->tenant, $purchaseItem);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($difference > 0) {
|
||||||
|
$otherItemQuantity = (int) $purchase->items()
|
||||||
|
->where('source_catalog_item_id', $purchaseItem->source_catalog_item_id)
|
||||||
|
->whereKeyNot($purchaseItem->getKey())
|
||||||
|
->sum('cantidad');
|
||||||
|
$catalogItem = $selection instanceof Variant ? $selection->catalogItem : $selection;
|
||||||
|
|
||||||
|
$this->purchaseLimits->assertCanPurchase(
|
||||||
|
$catalogItem,
|
||||||
|
(int) $purchase->user_id,
|
||||||
|
$otherItemQuantity + $quantity,
|
||||||
|
$purchase->getKey(),
|
||||||
|
);
|
||||||
|
$this->inventory->reserve($selection, $difference);
|
||||||
|
} else {
|
||||||
|
$this->inventory->release($selection, abs($difference));
|
||||||
|
}
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'quantity' => __('api.purchase.insufficient_stock'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function lockPurchaseItem(Purchase $purchase, PurchaseItem $item): PurchaseItem
|
||||||
|
{
|
||||||
|
/** @var PurchaseItem|null $lockedItem */
|
||||||
|
$lockedItem = $purchase->items()
|
||||||
|
->whereKey($item->getKey())
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($lockedItem === null) {
|
||||||
|
throw new NotFoundHttpException('Purchase item not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lockedItem->reservation_status !== PurchaseItem::RESERVATION_ACTIVE) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'item' => __('api.purchase.item_not_editable'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $lockedItem;
|
||||||
|
}
|
||||||
|
|
||||||
private function assertEditable(Purchase $purchase): void
|
private function assertEditable(Purchase $purchase): void
|
||||||
{
|
{
|
||||||
if (
|
if (
|
||||||
|
|
@ -49,15 +281,6 @@ class EditCheckoutService
|
||||||
|
|
||||||
private function loadPurchase(Purchase $purchase): Purchase
|
private function loadPurchase(Purchase $purchase): Purchase
|
||||||
{
|
{
|
||||||
return $purchase->load([
|
return $this->responses->load($purchase);
|
||||||
'items.imageAttachment',
|
|
||||||
'cart.items.catalogItem.inventory',
|
|
||||||
'cart.items.catalogItem.attachments',
|
|
||||||
'cart.items.variant.inventory',
|
|
||||||
'cart.items.variant.attachments',
|
|
||||||
'cart.items.variant.definitions.itemAttribute.attribute',
|
|
||||||
'cart.items.variant.eventDates',
|
|
||||||
'cart.items.variant.eventDate',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,35 @@ use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class PurchaseItemSnapshotFactory
|
class PurchaseItemSnapshotFactory
|
||||||
{
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function fromVariant(Variant $variant, int $quantity): array
|
||||||
|
{
|
||||||
|
$variant->loadMissing([
|
||||||
|
'attachments',
|
||||||
|
'catalogItem.attachments',
|
||||||
|
'definitions.itemAttribute.attribute.options',
|
||||||
|
'eventDates',
|
||||||
|
'eventDate',
|
||||||
|
]);
|
||||||
|
$unitPrice = $variant->getPrice();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'source_variant_id' => $variant->id,
|
||||||
|
'image_attachment_id' => $variant->attachments->first()?->id
|
||||||
|
?? $variant->catalogItem->attachments->first()?->id,
|
||||||
|
'nombre' => $variant->catalogItem->nombre,
|
||||||
|
'descripcion' => $variant->getDescription(),
|
||||||
|
'slug' => $variant->catalogItem->slug,
|
||||||
|
'item_nombre' => $variant->getName(),
|
||||||
|
'variant_attributes' => $this->snapshotAttributes($variant),
|
||||||
|
'cantidad' => $quantity,
|
||||||
|
'precio_unitario' => $unitPrice,
|
||||||
|
'discount_total' => null,
|
||||||
|
'tax_total' => null,
|
||||||
|
'total' => $unitPrice * $quantity,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Collection<int, CartItem> $cartItems
|
* @param Collection<int, CartItem> $cartItems
|
||||||
* @return array<int, array<string, mixed>>
|
* @return array<int, array<string, mixed>>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Purchase\Services\Checkout;
|
||||||
|
|
||||||
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
|
||||||
|
class PurchaseResponseLoader
|
||||||
|
{
|
||||||
|
public function load(Purchase $purchase): Purchase
|
||||||
|
{
|
||||||
|
$purchase->load(['tenant', 'items.imageAttachment']);
|
||||||
|
|
||||||
|
if (! $purchase->tenant->cart_editing_policy->allowsVariantChanges()) {
|
||||||
|
return $purchase;
|
||||||
|
}
|
||||||
|
|
||||||
|
$purchase->load([
|
||||||
|
'items.sourceCatalogItem.itemAttributes.attribute',
|
||||||
|
'items.sourceCatalogItem.variants' => fn ($query) => $query->orderBy('id'),
|
||||||
|
'items.sourceCatalogItem.variants.inventory',
|
||||||
|
'items.sourceCatalogItem.variants.definitions' => fn ($query) => $query->orderBy('id'),
|
||||||
|
'items.sourceCatalogItem.variants.definitions.itemAttribute.attribute.options',
|
||||||
|
'items.sourceCatalogItem.variants.eventDates',
|
||||||
|
'items.sourceCatalogItem.variants.eventDate',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $purchase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -62,6 +62,74 @@ class SourceCartService
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function syncItemQuantity(
|
||||||
|
Purchase $purchase,
|
||||||
|
PurchaseItem $purchaseItem,
|
||||||
|
int $quantity,
|
||||||
|
): void {
|
||||||
|
$sourceCart = $this->findSourceCart($purchase);
|
||||||
|
|
||||||
|
if ($sourceCart === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceCart->items()
|
||||||
|
->where('catalog_item_id', $purchaseItem->source_catalog_item_id)
|
||||||
|
->where('variant_id', $purchaseItem->source_variant_id)
|
||||||
|
->update(['cantidad' => $quantity]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncItemSelection(
|
||||||
|
Purchase $purchase,
|
||||||
|
int $catalogItemId,
|
||||||
|
?int $previousVariantId,
|
||||||
|
int $newVariantId,
|
||||||
|
int $finalQuantity,
|
||||||
|
): void {
|
||||||
|
$sourceCart = $this->findSourceCart($purchase);
|
||||||
|
|
||||||
|
if ($sourceCart === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var CartItem|null $previousItem */
|
||||||
|
$previousItem = $sourceCart->items()
|
||||||
|
->where('catalog_item_id', $catalogItemId)
|
||||||
|
->where('variant_id', $previousVariantId)
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
/** @var CartItem|null $targetItem */
|
||||||
|
$targetItem = $sourceCart->items()
|
||||||
|
->where('catalog_item_id', $catalogItemId)
|
||||||
|
->where('variant_id', $newVariantId)
|
||||||
|
->when($previousItem !== null, fn ($query) => $query->whereKeyNot($previousItem->getKey()))
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($targetItem !== null) {
|
||||||
|
$targetItem->update(['cantidad' => $finalQuantity]);
|
||||||
|
$previousItem?->delete();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($previousItem !== null) {
|
||||||
|
$previousItem->update([
|
||||||
|
'variant_id' => $newVariantId,
|
||||||
|
'cantidad' => $finalQuantity,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceCart->items()->create([
|
||||||
|
'catalog_item_id' => $catalogItemId,
|
||||||
|
'variant_id' => $newVariantId,
|
||||||
|
'cantidad' => $finalQuantity,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function finalize(Purchase $purchase): void
|
public function finalize(Purchase $purchase): void
|
||||||
{
|
{
|
||||||
$sourceCart = $this->findSourceCart($purchase);
|
$sourceCart = $this->findSourceCart($purchase);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ class StartCheckoutService
|
||||||
private readonly UserPurchaseLimitService $purchaseLimits,
|
private readonly UserPurchaseLimitService $purchaseLimits,
|
||||||
private readonly CatalogSelectionResolver $selections,
|
private readonly CatalogSelectionResolver $selections,
|
||||||
private readonly InsufficientStockMessageBuilder $stockMessages,
|
private readonly InsufficientStockMessageBuilder $stockMessages,
|
||||||
|
private readonly PurchaseResponseLoader $responses,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** @param array<string, mixed> $purchaseData */
|
/** @param array<string, mixed> $purchaseData */
|
||||||
|
|
@ -376,15 +377,6 @@ class StartCheckoutService
|
||||||
|
|
||||||
private function loadPurchase(Purchase $purchase): Purchase
|
private function loadPurchase(Purchase $purchase): Purchase
|
||||||
{
|
{
|
||||||
return $purchase->load([
|
return $this->responses->load($purchase);
|
||||||
'items.imageAttachment',
|
|
||||||
'cart.items.catalogItem.inventory',
|
|
||||||
'cart.items.catalogItem.attachments',
|
|
||||||
'cart.items.variant.inventory',
|
|
||||||
'cart.items.variant.attachments',
|
|
||||||
'cart.items.variant.definitions.itemAttribute.attribute',
|
|
||||||
'cart.items.variant.eventDates',
|
|
||||||
'cart.items.variant.eventDate',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,27 @@ class CheckoutService
|
||||||
return $this->editor->updateCustomer($purchase, $customerData);
|
return $this->editor->updateCustomer($purchase, $customerData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updateItem(
|
||||||
|
Purchase $purchase,
|
||||||
|
PurchaseItem $purchaseItem,
|
||||||
|
?int $quantity,
|
||||||
|
?int $variantId,
|
||||||
|
bool $updateVariant,
|
||||||
|
): Purchase {
|
||||||
|
return $this->editor->updateItem(
|
||||||
|
$purchase,
|
||||||
|
$purchaseItem,
|
||||||
|
$quantity,
|
||||||
|
$variantId,
|
||||||
|
$updateVariant,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepareItemEditing(Purchase $purchase): Purchase
|
||||||
|
{
|
||||||
|
return $this->editor->prepareItemEditing($purchase);
|
||||||
|
}
|
||||||
|
|
||||||
public function confirmPurchase(Purchase $purchase): void
|
public function confirmPurchase(Purchase $purchase): void
|
||||||
{
|
{
|
||||||
$this->completer->confirm($purchase);
|
$this->completer->confirm($purchase);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ Route::prefix('tenants/{tenant:codigo}')->middleware('auth:sanctum')->group(func
|
||||||
Route::get('compras', [PurchaseController::class, 'index']);
|
Route::get('compras', [PurchaseController::class, 'index']);
|
||||||
Route::post('compras/start-checkout', [PurchaseController::class, 'startCheckout']);
|
Route::post('compras/start-checkout', [PurchaseController::class, 'startCheckout']);
|
||||||
Route::get('compras/{compra}', [PurchaseController::class, 'show']);
|
Route::get('compras/{compra}', [PurchaseController::class, 'show']);
|
||||||
|
Route::post('compras/{compra}/edit-items', [PurchaseController::class, 'prepareItemEditing']);
|
||||||
|
Route::patch('compras/{compra}/items/{item}', [PurchaseController::class, 'updateItem']);
|
||||||
Route::patch('compras/{compra}/customer-data', [PurchaseController::class, 'updateCustomerData']);
|
Route::patch('compras/{compra}/customer-data', [PurchaseController::class, 'updateCustomerData']);
|
||||||
Route::post('compras/{compra}/payment-intent', [PurchaseController::class, 'paymentIntent']);
|
Route::post('compras/{compra}/payment-intent', [PurchaseController::class, 'paymentIntent']);
|
||||||
Route::post('compras/{compra}/complete', [PurchaseController::class, 'complete']);
|
Route::post('compras/{compra}/complete', [PurchaseController::class, 'complete']);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Tenant\Enums;
|
||||||
|
|
||||||
|
enum CartEditingPolicy: string
|
||||||
|
{
|
||||||
|
case Disabled = 'disabled';
|
||||||
|
case QuantityAndRemove = 'quantity_and_remove';
|
||||||
|
case Full = 'full';
|
||||||
|
|
||||||
|
public function allowsQuantityChanges(): bool
|
||||||
|
{
|
||||||
|
return $this !== self::Disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function allowsRemoval(): bool
|
||||||
|
{
|
||||||
|
return $this !== self::Disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function allowsVariantChanges(): bool
|
||||||
|
{
|
||||||
|
return $this === self::Full;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function allowsModification(): bool
|
||||||
|
{
|
||||||
|
return $this->allowsRemoval()
|
||||||
|
|| $this->allowsQuantityChanges()
|
||||||
|
|| $this->allowsVariantChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ use App\Domains\Client\Models\Client;
|
||||||
use App\Domains\Event\Models\EventDate;
|
use App\Domains\Event\Models\EventDate;
|
||||||
use App\Domains\Menu\Models\Menu;
|
use App\Domains\Menu\Models\Menu;
|
||||||
use App\Domains\Menu\Models\TenantMenu;
|
use App\Domains\Menu\Models\TenantMenu;
|
||||||
|
use App\Domains\Tenant\Enums\CartEditingPolicy;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
@ -44,7 +45,7 @@ use Illuminate\Support\Facades\Schema;
|
||||||
'display_categories',
|
'display_categories',
|
||||||
'display_seach_bar',
|
'display_seach_bar',
|
||||||
'display_cart',
|
'display_cart',
|
||||||
'cart_editing_enabled',
|
'cart_editing_policy',
|
||||||
'display_cart_item_images',
|
'display_cart_item_images',
|
||||||
'scanner_category_validation_enabled',
|
'scanner_category_validation_enabled',
|
||||||
'event_title',
|
'event_title',
|
||||||
|
|
@ -63,7 +64,7 @@ class Tenant extends Model
|
||||||
'display_categories' => true,
|
'display_categories' => true,
|
||||||
'display_seach_bar' => true,
|
'display_seach_bar' => true,
|
||||||
'display_cart' => true,
|
'display_cart' => true,
|
||||||
'cart_editing_enabled' => true,
|
'cart_editing_policy' => CartEditingPolicy::Full->value,
|
||||||
'display_cart_item_images' => true,
|
'display_cart_item_images' => true,
|
||||||
'scanner_category_validation_enabled' => true,
|
'scanner_category_validation_enabled' => true,
|
||||||
];
|
];
|
||||||
|
|
@ -114,7 +115,7 @@ class Tenant extends Model
|
||||||
'display_categories' => 'boolean',
|
'display_categories' => 'boolean',
|
||||||
'display_seach_bar' => 'boolean',
|
'display_seach_bar' => 'boolean',
|
||||||
'display_cart' => 'boolean',
|
'display_cart' => 'boolean',
|
||||||
'cart_editing_enabled' => 'boolean',
|
'cart_editing_policy' => CartEditingPolicy::class,
|
||||||
'display_cart_item_images' => 'boolean',
|
'display_cart_item_images' => 'boolean',
|
||||||
'scanner_category_validation_enabled' => 'boolean',
|
'scanner_category_validation_enabled' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ namespace App\Domains\Tenant\Requests;
|
||||||
use App\Domains\Catalog\Enums\GroupLayout;
|
use App\Domains\Catalog\Enums\GroupLayout;
|
||||||
use App\Domains\Catalog\Enums\ProductLayout;
|
use App\Domains\Catalog\Enums\ProductLayout;
|
||||||
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
||||||
|
use App\Domains\Tenant\Enums\CartEditingPolicy;
|
||||||
use App\Domains\Tenant\Services\WebsiteExtraService;
|
use App\Domains\Tenant\Services\WebsiteExtraService;
|
||||||
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
||||||
use Closure;
|
use Closure;
|
||||||
|
|
@ -107,7 +108,7 @@ class StoreTenantRequest extends FormRequest
|
||||||
'display_categories' => ['sometimes', 'boolean'],
|
'display_categories' => ['sometimes', 'boolean'],
|
||||||
'display_seach_bar' => ['sometimes', 'boolean'],
|
'display_seach_bar' => ['sometimes', 'boolean'],
|
||||||
'display_cart' => ['sometimes', 'boolean'],
|
'display_cart' => ['sometimes', 'boolean'],
|
||||||
'cart_editing_enabled' => ['sometimes', 'boolean'],
|
'cart_editing_policy' => ['sometimes', Rule::enum(CartEditingPolicy::class)],
|
||||||
'display_cart_item_images' => ['sometimes', 'boolean'],
|
'display_cart_item_images' => ['sometimes', 'boolean'],
|
||||||
'scanner_category_validation_enabled' => ['sometimes', 'boolean'],
|
'scanner_category_validation_enabled' => ['sometimes', 'boolean'],
|
||||||
'website_type_code' => [
|
'website_type_code' => [
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ namespace App\Domains\Tenant\Requests;
|
||||||
use App\Domains\Catalog\Enums\GroupLayout;
|
use App\Domains\Catalog\Enums\GroupLayout;
|
||||||
use App\Domains\Catalog\Enums\ProductLayout;
|
use App\Domains\Catalog\Enums\ProductLayout;
|
||||||
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
||||||
|
use App\Domains\Tenant\Enums\CartEditingPolicy;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
||||||
use Closure;
|
use Closure;
|
||||||
|
|
@ -128,7 +129,7 @@ class UpdateTenantRequest extends FormRequest
|
||||||
'display_categories' => ['sometimes', 'boolean'],
|
'display_categories' => ['sometimes', 'boolean'],
|
||||||
'display_seach_bar' => ['sometimes', 'boolean'],
|
'display_seach_bar' => ['sometimes', 'boolean'],
|
||||||
'display_cart' => ['sometimes', 'boolean'],
|
'display_cart' => ['sometimes', 'boolean'],
|
||||||
'cart_editing_enabled' => ['sometimes', 'boolean'],
|
'cart_editing_policy' => ['sometimes', Rule::enum(CartEditingPolicy::class)],
|
||||||
'display_cart_item_images' => ['sometimes', 'boolean'],
|
'display_cart_item_images' => ['sometimes', 'boolean'],
|
||||||
'scanner_category_validation_enabled' => ['sometimes', 'boolean'],
|
'scanner_category_validation_enabled' => ['sometimes', 'boolean'],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Tenant\Resources;
|
||||||
|
|
||||||
|
use App\Domains\Tenant\Enums\CartEditingPolicy;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin CartEditingPolicy */
|
||||||
|
class CartEditingPolicyResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, bool|string> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'code' => $this->resource->value,
|
||||||
|
'allow_modify' => $this->resource->allowsModification(),
|
||||||
|
'allow_delete' => $this->resource->allowsRemoval(),
|
||||||
|
'allow_update_quantity' => $this->resource->allowsQuantityChanges(),
|
||||||
|
'allow_update_variant' => $this->resource->allowsVariantChanges(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -75,7 +75,7 @@ class TenantResource extends JsonResource
|
||||||
'display_categories' => $this->display_categories,
|
'display_categories' => $this->display_categories,
|
||||||
'display_seach_bar' => $this->display_seach_bar,
|
'display_seach_bar' => $this->display_seach_bar,
|
||||||
'display_cart' => $this->display_cart,
|
'display_cart' => $this->display_cart,
|
||||||
'cart_editing_enabled' => $this->cart_editing_enabled,
|
'cart_editing_policy' => CartEditingPolicyResource::make($this->cart_editing_policy),
|
||||||
'display_cart_item_images' => $this->display_cart_item_images,
|
'display_cart_item_images' => $this->display_cart_item_images,
|
||||||
'scanner_category_validation_enabled' => $this->scanner_category_validation_enabled,
|
'scanner_category_validation_enabled' => $this->scanner_category_validation_enabled,
|
||||||
'social_media' => $this->whenLoaded(
|
'social_media' => $this->whenLoaded(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/** @var array<string, string> */
|
||||||
|
private const TENANT_POLICIES = [
|
||||||
|
'sonder' => 'quantity_and_remove',
|
||||||
|
'fiesta_futbol_infantil' => 'full',
|
||||||
|
'desfile_pura_tendencia' => 'disabled',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('tenants', function (Blueprint $table): void {
|
||||||
|
$table->string('cart_editing_policy')
|
||||||
|
->default('full')
|
||||||
|
->after('display_cart');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('tenants')
|
||||||
|
->where('cart_editing_enabled', false)
|
||||||
|
->update(['cart_editing_policy' => 'disabled']);
|
||||||
|
|
||||||
|
foreach (self::TENANT_POLICIES as $tenantCode => $policy) {
|
||||||
|
DB::table('tenants')
|
||||||
|
->where('codigo', $tenantCode)
|
||||||
|
->update(['cart_editing_policy' => $policy]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('tenants', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('cart_editing_enabled');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('tenants', function (Blueprint $table): void {
|
||||||
|
$table->boolean('cart_editing_enabled')
|
||||||
|
->default(true)
|
||||||
|
->after('display_cart');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('tenants')
|
||||||
|
->where('cart_editing_policy', 'disabled')
|
||||||
|
->update(['cart_editing_enabled' => false]);
|
||||||
|
|
||||||
|
Schema::table('tenants', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('cart_editing_policy');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -74,7 +74,7 @@ class DesfilePuraTendenciaSeeder extends Seeder
|
||||||
'display_categories' => false,
|
'display_categories' => false,
|
||||||
'display_seach_bar' => false,
|
'display_seach_bar' => false,
|
||||||
'display_cart' => true,
|
'display_cart' => true,
|
||||||
'cart_editing_enabled' => false,
|
'cart_editing_policy' => 'disabled',
|
||||||
'display_cart_item_images' => false,
|
'display_cart_item_images' => false,
|
||||||
'scanner_category_validation_enabled' => false,
|
'scanner_category_validation_enabled' => false,
|
||||||
'website_type_code' => 'onticket',
|
'website_type_code' => 'onticket',
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ class TenantSeeder extends Seeder
|
||||||
'display_categories' => true,
|
'display_categories' => true,
|
||||||
'display_seach_bar' => true,
|
'display_seach_bar' => true,
|
||||||
'display_cart' => true,
|
'display_cart' => true,
|
||||||
|
'cart_editing_policy' => 'quantity_and_remove',
|
||||||
'header_logo' => $this->uploadedImage('images/tennants/sonder/sonder_header.png', 'sonder_header.png'),
|
'header_logo' => $this->uploadedImage('images/tennants/sonder/sonder_header.png', 'sonder_header.png'),
|
||||||
'footer_logo' => $this->uploadedImage('images/tennants/sonder/sonder_footer.png', 'sonder_footer.png'),
|
'footer_logo' => $this->uploadedImage('images/tennants/sonder/sonder_footer.png', 'sonder_footer.png'),
|
||||||
'social_media' => self::SOCIAL_MEDIA,
|
'social_media' => self::SOCIAL_MEDIA,
|
||||||
|
|
@ -131,6 +132,7 @@ class TenantSeeder extends Seeder
|
||||||
'display_categories' => false,
|
'display_categories' => false,
|
||||||
'display_seach_bar' => false,
|
'display_seach_bar' => false,
|
||||||
'display_cart' => true,
|
'display_cart' => true,
|
||||||
|
'cart_editing_policy' => 'full',
|
||||||
'header_logo' => $this->uploadedImage(
|
'header_logo' => $this->uploadedImage(
|
||||||
'images/tennants/fiesta_futbol_infantil/futbol_infantil_header.png',
|
'images/tennants/fiesta_futbol_infantil/futbol_infantil_header.png',
|
||||||
'futbol_infantil_header.png',
|
'futbol_infantil_header.png',
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ return [
|
||||||
'google_email_unverified' => 'The Google account must have a verified email.',
|
'google_email_unverified' => 'The Google account must have a verified email.',
|
||||||
],
|
],
|
||||||
'cart' => [
|
'cart' => [
|
||||||
|
'editing_disabled' => 'Cart editing is disabled.',
|
||||||
|
'variant_change_disabled' => 'Variant changes are disabled for this cart.',
|
||||||
'item_added' => 'Product added to cart.',
|
'item_added' => 'Product added to cart.',
|
||||||
'quantity_updated' => 'Product quantity updated.',
|
'quantity_updated' => 'Product quantity updated.',
|
||||||
'item_updated' => 'Product updated.',
|
'item_updated' => 'Product updated.',
|
||||||
|
|
@ -34,6 +36,7 @@ return [
|
||||||
'variant_required' => 'You must select a variant for this item.',
|
'variant_required' => 'You must select a variant for this item.',
|
||||||
],
|
],
|
||||||
'purchase' => [
|
'purchase' => [
|
||||||
|
'variant_change_disabled' => 'Variant changes are disabled for this purchase.',
|
||||||
'source_required' => 'A cart or direct item is required.',
|
'source_required' => 'A cart or direct item is required.',
|
||||||
'payment_method_required' => 'The purchase payment method must be selected before finalizing.',
|
'payment_method_required' => 'The purchase payment method must be selected before finalizing.',
|
||||||
'not_editable' => 'The purchase is no longer editable.',
|
'not_editable' => 'The purchase is no longer editable.',
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ return [
|
||||||
'google_email_unverified' => 'La cuenta de Google debe tener el email verificado.',
|
'google_email_unverified' => 'La cuenta de Google debe tener el email verificado.',
|
||||||
],
|
],
|
||||||
'cart' => [
|
'cart' => [
|
||||||
|
'editing_disabled' => 'La edición del carrito está deshabilitada.',
|
||||||
|
'variant_change_disabled' => 'El cambio de variante está deshabilitado para este carrito.',
|
||||||
'item_added' => 'Producto agregado al carrito.',
|
'item_added' => 'Producto agregado al carrito.',
|
||||||
'quantity_updated' => 'Cantidad de producto actualizada.',
|
'quantity_updated' => 'Cantidad de producto actualizada.',
|
||||||
'item_updated' => 'Producto actualizado.',
|
'item_updated' => 'Producto actualizado.',
|
||||||
|
|
@ -34,6 +36,7 @@ return [
|
||||||
'variant_required' => 'Debe seleccionar una variante para este ítem.',
|
'variant_required' => 'Debe seleccionar una variante para este ítem.',
|
||||||
],
|
],
|
||||||
'purchase' => [
|
'purchase' => [
|
||||||
|
'variant_change_disabled' => 'El cambio de variante está deshabilitado para esta compra.',
|
||||||
'source_required' => 'Se requiere un carrito o un producto directo.',
|
'source_required' => 'Se requiere un carrito o un producto directo.',
|
||||||
'payment_method_required' => 'Debes seleccionar el método de pago antes de finalizar la compra.',
|
'payment_method_required' => 'Debes seleccionar el método de pago antes de finalizar la compra.',
|
||||||
'not_editable' => 'La compra ya no se puede modificar.',
|
'not_editable' => 'La compra ya no se puede modificar.',
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ function bodyFor(string $method, string $uri): ?array
|
||||||
'POST api/tenants/{tenant:codigo}/catalog-items/{catalogItem}/variant-options' => ['selected_values' => ['color' => 'azul'], 'cart_item_id' => '{{cart_item_id}}'],
|
'POST api/tenants/{tenant:codigo}/catalog-items/{catalogItem}/variant-options' => ['selected_values' => ['color' => 'azul'], 'cart_item_id' => '{{cart_item_id}}'],
|
||||||
'POST api/tenants/{tenant:codigo}/compras/start-checkout' => ['cart_id' => '{{cart_id}}'],
|
'POST api/tenants/{tenant:codigo}/compras/start-checkout' => ['cart_id' => '{{cart_id}}'],
|
||||||
'PATCH api/tenants/{tenant:codigo}/compras/{compra}/customer-data' => ['dni' => '30123456', 'telefono' => '+5491112345678', 'nombre_apellido' => 'Usuario Demo', 'email' => '{{user_email}}'],
|
'PATCH api/tenants/{tenant:codigo}/compras/{compra}/customer-data' => ['dni' => '30123456', 'telefono' => '+5491112345678', 'nombre_apellido' => 'Usuario Demo', 'email' => '{{user_email}}'],
|
||||||
'PATCH api/tenants/{tenant:codigo}/checkout-carts/{cart}/items/{cartItem}' => ['cantidad' => 2],
|
'PATCH api/tenants/{tenant:codigo}/compras/{compra}/items/{item}' => ['quantity' => 2, 'variant_id' => '{{variant_id}}'],
|
||||||
'POST api/tenants/{tenant:codigo}/compras/{compra}/payment-intent' => ['method' => 'transfer', 'transfer_payer_dni' => '30123456'],
|
'POST api/tenants/{tenant:codigo}/compras/{compra}/payment-intent' => ['method' => 'transfer', 'transfer_payer_dni' => '30123456'],
|
||||||
'POST api/tenants/{tenant:codigo}/tickets/pdf' => ['ticket_ids' => [1]],
|
'POST api/tenants/{tenant:codigo}/tickets/pdf' => ['ticket_ids' => [1]],
|
||||||
'POST api/v1/adminapp/login' => ['email' => '{{admin_email}}', 'password' => '{{admin_password}}'],
|
'POST api/v1/adminapp/login' => ['email' => '{{admin_email}}', 'password' => '{{admin_password}}'],
|
||||||
|
|
@ -136,13 +136,13 @@ function bodyFor(string $method, string $uri): ?array
|
||||||
'display_categories' => true,
|
'display_categories' => true,
|
||||||
'display_seach_bar' => true,
|
'display_seach_bar' => true,
|
||||||
'display_cart' => true,
|
'display_cart' => true,
|
||||||
'cart_editing_enabled' => true,
|
'cart_editing_policy' => 'full',
|
||||||
'website_type_code' => 'shopit',
|
'website_type_code' => 'shopit',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_array($key, ['PUT api/tenants/{tenant}', 'PATCH api/tenants/{tenant}'], true)) {
|
if (in_array($key, ['PUT api/tenants/{tenant}', 'PATCH api/tenants/{tenant}'], true)) {
|
||||||
return jsonBody(['nombre' => 'Tenant Demo Actualizado', 'site_title' => 'ShopIt Demo', 'primary_color' => '#111827', 'cart_editing_enabled' => true]);
|
return jsonBody(['nombre' => 'Tenant Demo Actualizado', 'site_title' => 'ShopIt Demo', 'primary_color' => '#111827', 'cart_editing_policy' => 'full']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($key === 'POST api/tenants/{tenant:codigo}/catalog-items') {
|
if ($key === 'POST api/tenants/{tenant:codigo}/catalog-items') {
|
||||||
|
|
@ -244,7 +244,7 @@ function requestName(string $method, string $action, bool $multiMethod): string
|
||||||
$verbs = [
|
$verbs = [
|
||||||
'index' => 'List', 'store' => 'Create', 'show' => 'Get', 'update' => 'Update',
|
'index' => 'List', 'store' => 'Create', 'show' => 'Get', 'update' => 'Update',
|
||||||
'destroy' => 'Delete', 'addItem' => 'Add Item', 'updateItemQuantity' => 'Update Item Quantity',
|
'destroy' => 'Delete', 'addItem' => 'Add Item', 'updateItemQuantity' => 'Update Item Quantity',
|
||||||
'updateCheckoutItem' => 'Update Checkout Cart Item',
|
'updateItem' => 'Update Item',
|
||||||
'removeItem' => 'Remove Item', 'search' => 'Search', 'category' => 'Get Category',
|
'removeItem' => 'Remove Item', 'search' => 'Search', 'category' => 'Get Category',
|
||||||
'featuredGroupItems' => 'List Featured Group Items', 'variantOptions' => 'Get Variant Options',
|
'featuredGroupItems' => 'List Featured Group Items', 'variantOptions' => 'Get Variant Options',
|
||||||
'startCheckout' => 'Start Checkout', 'updateCustomerData' => 'Update Customer Data',
|
'startCheckout' => 'Start Checkout', 'updateCustomerData' => 'Update Customer Data',
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,7 @@ class CartControllerTest extends TestCase
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('data.items.0.catalog_item_id', $item->id)
|
->assertJsonPath('data.items.0.catalog_item_id', $item->id)
|
||||||
->assertJsonPath('data.items.0.variant_id', $variant->id)
|
->assertJsonPath('data.items.0.variant_id', $variant->id)
|
||||||
|
->assertJsonPath('data.items.0.variants.0.id', $variant->id)
|
||||||
->assertJsonPath('data.items.0.cantidad', 5)
|
->assertJsonPath('data.items.0.cantidad', 5)
|
||||||
->assertJsonPath('data.subtotal', '125.00');
|
->assertJsonPath('data.subtotal', '125.00');
|
||||||
|
|
||||||
|
|
@ -476,6 +477,83 @@ class CartControllerTest extends TestCase
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_quantity_and_remove_policy_hides_variants_and_rejects_variant_changes(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant('acme');
|
||||||
|
$tenant->update(['cart_editing_policy' => 'quantity_and_remove']);
|
||||||
|
[$item, $firstVariant] = $this->createVariantItem($tenant, 10, '15.00');
|
||||||
|
$secondInventory = Inventory::query()->create(['real_stock' => 10]);
|
||||||
|
$secondVariant = $item->variants()->create(['inventory_id' => $secondInventory->id]);
|
||||||
|
$createResponse = $this->postJson('/api/tenants/acme/cart/items', [
|
||||||
|
'catalog_item_id' => $item->id,
|
||||||
|
'variant_id' => $firstVariant->id,
|
||||||
|
'cantidad' => 1,
|
||||||
|
])->assertJsonMissingPath('data.items.0.variants');
|
||||||
|
$guestToken = $createResponse->getCookie('guest_token', false)?->getValue();
|
||||||
|
$cartItemId = $createResponse->json('data.items.0.id');
|
||||||
|
|
||||||
|
$this->call(
|
||||||
|
'PATCH',
|
||||||
|
"/api/tenants/acme/cart/items/{$cartItemId}",
|
||||||
|
[],
|
||||||
|
['guest_token' => $guestToken],
|
||||||
|
[],
|
||||||
|
['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json'],
|
||||||
|
json_encode(['cantidad' => 2]),
|
||||||
|
)->assertOk()->assertJsonPath('data.items.0.cantidad', 2);
|
||||||
|
|
||||||
|
$this->call(
|
||||||
|
'PATCH',
|
||||||
|
"/api/tenants/acme/cart/items/{$cartItemId}",
|
||||||
|
[],
|
||||||
|
['guest_token' => $guestToken],
|
||||||
|
[],
|
||||||
|
['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json'],
|
||||||
|
json_encode(['cantidad' => 2, 'variant_id' => $secondVariant->id]),
|
||||||
|
)->assertUnprocessable()->assertJsonValidationErrors('variant_id');
|
||||||
|
|
||||||
|
$this->call(
|
||||||
|
'DELETE',
|
||||||
|
"/api/tenants/acme/cart/items/{$cartItemId}",
|
||||||
|
[],
|
||||||
|
['guest_token' => $guestToken],
|
||||||
|
[],
|
||||||
|
['HTTP_Accept' => 'application/json'],
|
||||||
|
)->assertOk()->assertJsonPath('data.items', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_disabled_policy_rejects_quantity_changes_and_removal(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant('acme');
|
||||||
|
$tenant->update(['cart_editing_policy' => 'disabled']);
|
||||||
|
$item = $this->createDirectItem($tenant, 10, '15.00');
|
||||||
|
$createResponse = $this->postJson('/api/tenants/acme/cart/items', [
|
||||||
|
'catalog_item_id' => $item->id,
|
||||||
|
'cantidad' => 1,
|
||||||
|
])->assertOk();
|
||||||
|
$guestToken = $createResponse->getCookie('guest_token', false)?->getValue();
|
||||||
|
$cartItemId = $createResponse->json('data.items.0.id');
|
||||||
|
|
||||||
|
$this->call(
|
||||||
|
'PATCH',
|
||||||
|
"/api/tenants/acme/cart/items/{$cartItemId}",
|
||||||
|
[],
|
||||||
|
['guest_token' => $guestToken],
|
||||||
|
[],
|
||||||
|
['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json'],
|
||||||
|
json_encode(['cantidad' => 2]),
|
||||||
|
)->assertUnprocessable()->assertJsonValidationErrors('cantidad');
|
||||||
|
|
||||||
|
$this->call(
|
||||||
|
'DELETE',
|
||||||
|
"/api/tenants/acme/cart/items/{$cartItemId}",
|
||||||
|
[],
|
||||||
|
['guest_token' => $guestToken],
|
||||||
|
[],
|
||||||
|
['HTTP_Accept' => 'application/json'],
|
||||||
|
)->assertUnprocessable()->assertJsonValidationErrors('cart_item');
|
||||||
|
}
|
||||||
|
|
||||||
public function test_it_requires_a_variant_when_the_item_has_variant_inventory(): void
|
public function test_it_requires_a_variant_when_the_item_has_variant_inventory(): void
|
||||||
{
|
{
|
||||||
$tenant = $this->createTenant('acme');
|
$tenant = $this->createTenant('acme');
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@ class AddCartEditingEnabledToTenantsTest extends TestCase
|
||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
public function test_it_disables_cart_editing_only_for_desfile(): void
|
public function test_it_migrates_the_boolean_to_the_equivalent_policy(): void
|
||||||
{
|
{
|
||||||
$migration = require database_path(
|
$migration = require database_path(
|
||||||
'migrations/2026_08_18_060000_add_cart_editing_enabled_to_tenants_table.php'
|
'migrations/2026_08_19_000000_replace_cart_editing_enabled_with_policy.php'
|
||||||
);
|
);
|
||||||
|
|
||||||
$migration->down();
|
$migration->down();
|
||||||
|
|
@ -21,7 +21,17 @@ class AddCartEditingEnabledToTenantsTest extends TestCase
|
||||||
|
|
||||||
$this->assertDatabaseHas('tenants', [
|
$this->assertDatabaseHas('tenants', [
|
||||||
'codigo' => 'desfile_pura_tendencia',
|
'codigo' => 'desfile_pura_tendencia',
|
||||||
'cart_editing_enabled' => false,
|
'cart_editing_policy' => 'disabled',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('tenants', [
|
||||||
|
'codigo' => 'sonder',
|
||||||
|
'cart_editing_policy' => 'quantity_and_remove',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('tenants', [
|
||||||
|
'codigo' => 'fiesta_futbol_infantil',
|
||||||
|
'cart_editing_policy' => 'full',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
DB::table('tenants')->insert([
|
DB::table('tenants')->insert([
|
||||||
|
|
@ -32,7 +42,7 @@ class AddCartEditingEnabledToTenantsTest extends TestCase
|
||||||
|
|
||||||
$this->assertDatabaseHas('tenants', [
|
$this->assertDatabaseHas('tenants', [
|
||||||
'codigo' => 'cart-editing-default',
|
'codigo' => 'cart-editing-default',
|
||||||
'cart_editing_enabled' => true,
|
'cart_editing_policy' => 'full',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -605,6 +605,182 @@ class StorePurchaseTest extends TestCase
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_it_changes_a_checkout_item_variant_and_moves_its_reservation(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
||||||
|
$tenant->update(['cart_editing_policy' => 'full']);
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$firstVariant = $this->createVariantForTenant('sonder', 10, '50.00');
|
||||||
|
$secondInventory = Inventory::query()->create(['real_stock' => 10]);
|
||||||
|
$secondVariant = $firstVariant->catalogItem->variants()->create([
|
||||||
|
'inventory_id' => $secondInventory->id,
|
||||||
|
'precio' => '70.00',
|
||||||
|
]);
|
||||||
|
$purchase = $this->createCheckoutPurchase($user, 'sonder', $firstVariant, 2);
|
||||||
|
$itemId = $purchase->items->firstOrFail()->id;
|
||||||
|
|
||||||
|
$this->actingAs($user, 'sanctum')
|
||||||
|
->patchJson("/api/tenants/sonder/compras/{$purchase->id}/items/{$itemId}", [
|
||||||
|
'variant_id' => $secondVariant->id,
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.items.0.source_variant_id', $secondVariant->id)
|
||||||
|
->assertJsonPath('data.items.0.quantity', 2)
|
||||||
|
->assertJsonPath('data.items.0.unit_price', '70.00')
|
||||||
|
->assertJsonPath('data.items.0.line_total', '140.00')
|
||||||
|
->assertJsonPath('data.total', '140.00')
|
||||||
|
->assertJsonPath('data.items.0.variants.0.id', $firstVariant->id)
|
||||||
|
->assertJsonPath('data.items.0.variants.1.id', $secondVariant->id);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('inventories', [
|
||||||
|
'id' => $firstVariant->inventory_id,
|
||||||
|
'reserved_stock' => 0,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('inventories', [
|
||||||
|
'id' => $secondInventory->id,
|
||||||
|
'reserved_stock' => 2,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('carrito_items', [
|
||||||
|
'cart_id' => $purchase->cart_id,
|
||||||
|
'catalog_item_id' => $firstVariant->catalog_item_id,
|
||||||
|
'variant_id' => $secondVariant->id,
|
||||||
|
'cantidad' => 2,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseMissing('carrito_items', [
|
||||||
|
'cart_id' => $purchase->cart_id,
|
||||||
|
'variant_id' => $firstVariant->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_rejects_checkout_variant_changes_when_the_policy_does_not_allow_them(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
||||||
|
$tenant->update(['cart_editing_policy' => 'quantity_and_remove']);
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$firstVariant = $this->createVariantForTenant('sonder', 10, '50.00');
|
||||||
|
$secondInventory = Inventory::query()->create(['real_stock' => 10]);
|
||||||
|
$secondVariant = $firstVariant->catalogItem->variants()->create([
|
||||||
|
'inventory_id' => $secondInventory->id,
|
||||||
|
]);
|
||||||
|
$purchase = $this->createCheckoutPurchase($user, 'sonder', $firstVariant, 2);
|
||||||
|
$itemId = $purchase->items->firstOrFail()->id;
|
||||||
|
|
||||||
|
$this->actingAs($user, 'sanctum')
|
||||||
|
->patchJson("/api/tenants/sonder/compras/{$purchase->id}/items/{$itemId}", [
|
||||||
|
'variant_id' => $secondVariant->id,
|
||||||
|
])
|
||||||
|
->assertUnprocessable()
|
||||||
|
->assertJsonValidationErrors('variant_id');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('compra_items', [
|
||||||
|
'id' => $itemId,
|
||||||
|
'source_variant_id' => $firstVariant->id,
|
||||||
|
'cantidad' => 2,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('inventories', [
|
||||||
|
'id' => $firstVariant->inventory_id,
|
||||||
|
'reserved_stock' => 2,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('inventories', [
|
||||||
|
'id' => $secondInventory->id,
|
||||||
|
'reserved_stock' => 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_checkout_variant_change_rolls_back_when_the_target_has_insufficient_stock(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
||||||
|
$tenant->update(['cart_editing_policy' => 'full']);
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$firstVariant = $this->createVariantForTenant('sonder', 10, '50.00');
|
||||||
|
$secondInventory = Inventory::query()->create(['real_stock' => 1]);
|
||||||
|
$secondVariant = $firstVariant->catalogItem->variants()->create([
|
||||||
|
'inventory_id' => $secondInventory->id,
|
||||||
|
'precio' => '70.00',
|
||||||
|
]);
|
||||||
|
$purchase = $this->createCheckoutPurchase($user, 'sonder', $firstVariant, 2);
|
||||||
|
$itemId = $purchase->items->firstOrFail()->id;
|
||||||
|
|
||||||
|
$this->actingAs($user, 'sanctum')
|
||||||
|
->patchJson("/api/tenants/sonder/compras/{$purchase->id}/items/{$itemId}", [
|
||||||
|
'variant_id' => $secondVariant->id,
|
||||||
|
])
|
||||||
|
->assertUnprocessable()
|
||||||
|
->assertJsonValidationErrors('variant_id');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('compra_items', [
|
||||||
|
'id' => $itemId,
|
||||||
|
'source_variant_id' => $firstVariant->id,
|
||||||
|
'cantidad' => 2,
|
||||||
|
'precio_unitario' => '50.00',
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('inventories', [
|
||||||
|
'id' => $firstVariant->inventory_id,
|
||||||
|
'reserved_stock' => 2,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('inventories', [
|
||||||
|
'id' => $secondInventory->id,
|
||||||
|
'reserved_stock' => 0,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('carrito_items', [
|
||||||
|
'cart_id' => $purchase->cart_id,
|
||||||
|
'variant_id' => $firstVariant->id,
|
||||||
|
'cantidad' => 2,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_changing_to_an_existing_checkout_variant_merges_purchase_and_cart_rows(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
||||||
|
$tenant->update(['cart_editing_policy' => 'full']);
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$firstVariant = $this->createVariantForTenant('sonder', 10, '50.00');
|
||||||
|
$secondInventory = Inventory::query()->create(['real_stock' => 10]);
|
||||||
|
$secondVariant = $firstVariant->catalogItem->variants()->create([
|
||||||
|
'inventory_id' => $secondInventory->id,
|
||||||
|
'precio' => '70.00',
|
||||||
|
]);
|
||||||
|
$cart = Cart::query()->create([
|
||||||
|
'tenant_codigo' => 'sonder',
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
$cart->addItem($firstVariant->catalog_item_id, $firstVariant->id, 2);
|
||||||
|
$cart->addItem($secondVariant->catalog_item_id, $secondVariant->id, 3);
|
||||||
|
$purchase = app(CheckoutService::class)->startCheckout($tenant, $user->id, [
|
||||||
|
'cart_id' => $cart->id,
|
||||||
|
]);
|
||||||
|
$sourceItem = $purchase->items->firstWhere('source_variant_id', $firstVariant->id);
|
||||||
|
$this->assertNotNull($sourceItem);
|
||||||
|
|
||||||
|
$this->actingAs($user, 'sanctum')
|
||||||
|
->patchJson("/api/tenants/sonder/compras/{$purchase->id}/items/{$sourceItem->id}", [
|
||||||
|
'variant_id' => $secondVariant->id,
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonCount(1, 'data.items')
|
||||||
|
->assertJsonPath('data.items.0.source_variant_id', $secondVariant->id)
|
||||||
|
->assertJsonPath('data.items.0.quantity', 5)
|
||||||
|
->assertJsonPath('data.items.0.line_total', '350.00')
|
||||||
|
->assertJsonPath('data.total', '350.00');
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('compra_items', 1);
|
||||||
|
$this->assertDatabaseHas('inventories', [
|
||||||
|
'id' => $firstVariant->inventory_id,
|
||||||
|
'reserved_stock' => 0,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('inventories', [
|
||||||
|
'id' => $secondInventory->id,
|
||||||
|
'reserved_stock' => 5,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseCount('carrito_items', 1);
|
||||||
|
$this->assertDatabaseHas('carrito_items', [
|
||||||
|
'cart_id' => $cart->id,
|
||||||
|
'variant_id' => $secondVariant->id,
|
||||||
|
'cantidad' => 5,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_it_rejects_a_quantity_update_above_the_user_purchase_limit(): void
|
public function test_it_rejects_a_quantity_update_above_the_user_purchase_limit(): void
|
||||||
{
|
{
|
||||||
$this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
$this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class DesfilePuraTendenciaSeederTest extends TestCase
|
||||||
'footer_bg_color' => '#D4441C',
|
'footer_bg_color' => '#D4441C',
|
||||||
'display_categories' => false,
|
'display_categories' => false,
|
||||||
'display_seach_bar' => false,
|
'display_seach_bar' => false,
|
||||||
'cart_editing_enabled' => false,
|
'cart_editing_policy' => 'disabled',
|
||||||
'display_cart_item_images' => false,
|
'display_cart_item_images' => false,
|
||||||
'site_title' => 'Desfile Pura Tendencia',
|
'site_title' => 'Desfile Pura Tendencia',
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ class TenantSeederTest extends TestCase
|
||||||
->sole();
|
->sole();
|
||||||
|
|
||||||
$this->assertSame('shopit', $sonder->website_type_code);
|
$this->assertSame('shopit', $sonder->website_type_code);
|
||||||
|
$this->assertSame('quantity_and_remove', $sonder->cart_editing_policy->value);
|
||||||
|
|
||||||
$carousel = $sonder->websiteExtras
|
$carousel = $sonder->websiteExtras
|
||||||
->firstWhere('websiteTypeExtra.codigo', 'carousel');
|
->firstWhere('websiteTypeExtra.codigo', 'carousel');
|
||||||
|
|
@ -81,6 +82,7 @@ class TenantSeederTest extends TestCase
|
||||||
->sole();
|
->sole();
|
||||||
|
|
||||||
$this->assertSame('onticket', $fiesta->website_type_code);
|
$this->assertSame('onticket', $fiesta->website_type_code);
|
||||||
|
$this->assertSame('full', $fiesta->cart_editing_policy->value);
|
||||||
|
|
||||||
$extras = $fiesta->websiteExtras->keyBy('websiteTypeExtra.codigo');
|
$extras = $fiesta->websiteExtras->keyBy('websiteTypeExtra.codigo');
|
||||||
$this->assertEqualsCanonicalizing(
|
$this->assertEqualsCanonicalizing(
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ class BootstrapTenantControllerTest extends TestCase
|
||||||
'display_categories' => false,
|
'display_categories' => false,
|
||||||
'display_seach_bar' => false,
|
'display_seach_bar' => false,
|
||||||
'display_cart' => false,
|
'display_cart' => false,
|
||||||
'cart_editing_enabled' => false,
|
'cart_editing_policy' => 'disabled',
|
||||||
'display_cart_item_images' => false,
|
'display_cart_item_images' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -91,7 +91,11 @@ class BootstrapTenantControllerTest extends TestCase
|
||||||
->assertJsonPath('data.display_categories', false)
|
->assertJsonPath('data.display_categories', false)
|
||||||
->assertJsonPath('data.display_seach_bar', false)
|
->assertJsonPath('data.display_seach_bar', false)
|
||||||
->assertJsonPath('data.display_cart', false)
|
->assertJsonPath('data.display_cart', false)
|
||||||
->assertJsonPath('data.cart_editing_enabled', false)
|
->assertJsonPath('data.cart_editing_policy.code', 'disabled')
|
||||||
|
->assertJsonPath('data.cart_editing_policy.allow_modify', false)
|
||||||
|
->assertJsonPath('data.cart_editing_policy.allow_delete', false)
|
||||||
|
->assertJsonPath('data.cart_editing_policy.allow_update_quantity', false)
|
||||||
|
->assertJsonPath('data.cart_editing_policy.allow_update_variant', false)
|
||||||
->assertJsonPath('data.display_cart_item_images', false)
|
->assertJsonPath('data.display_cart_item_images', false)
|
||||||
->assertJsonPath('data.header_bg_color', '#ffffff')->assertJsonPath('data.footer_bg_color', '#ffffff');
|
->assertJsonPath('data.header_bg_color', '#ffffff')->assertJsonPath('data.footer_bg_color', '#ffffff');
|
||||||
|
|
||||||
|
|
@ -758,7 +762,7 @@ class BootstrapTenantControllerTest extends TestCase
|
||||||
|
|
||||||
$response = $this->putJson("/api/tenants/{$tenant->codigo}", [
|
$response = $this->putJson("/api/tenants/{$tenant->codigo}", [
|
||||||
'primary_color' => '#000000',
|
'primary_color' => '#000000',
|
||||||
'cart_editing_enabled' => false,
|
'cart_editing_policy' => 'quantity_and_remove',
|
||||||
'display_cart_item_images' => false,
|
'display_cart_item_images' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -767,7 +771,11 @@ class BootstrapTenantControllerTest extends TestCase
|
||||||
->assertJsonPath('data.codigo', 'acme')
|
->assertJsonPath('data.codigo', 'acme')
|
||||||
->assertJsonPath('data.nombre', 'Acme')
|
->assertJsonPath('data.nombre', 'Acme')
|
||||||
->assertJsonPath('data.primary_color', '#000000')
|
->assertJsonPath('data.primary_color', '#000000')
|
||||||
->assertJsonPath('data.cart_editing_enabled', false)
|
->assertJsonPath('data.cart_editing_policy.code', 'quantity_and_remove')
|
||||||
|
->assertJsonPath('data.cart_editing_policy.allow_modify', true)
|
||||||
|
->assertJsonPath('data.cart_editing_policy.allow_delete', true)
|
||||||
|
->assertJsonPath('data.cart_editing_policy.allow_update_quantity', true)
|
||||||
|
->assertJsonPath('data.cart_editing_policy.allow_update_variant', false)
|
||||||
->assertJsonPath('data.display_cart_item_images', false);
|
->assertJsonPath('data.display_cart_item_images', false);
|
||||||
|
|
||||||
$this->assertDatabaseHas('tenants', [
|
$this->assertDatabaseHas('tenants', [
|
||||||
|
|
@ -775,7 +783,7 @@ class BootstrapTenantControllerTest extends TestCase
|
||||||
'codigo' => 'acme',
|
'codigo' => 'acme',
|
||||||
'nombre' => 'Acme',
|
'nombre' => 'Acme',
|
||||||
'primary_color' => '#000000',
|
'primary_color' => '#000000',
|
||||||
'cart_editing_enabled' => false,
|
'cart_editing_policy' => 'quantity_and_remove',
|
||||||
'display_cart_item_images' => false,
|
'display_cart_item_images' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue