feat(checkout): map editing policy to cart controls

This commit is contained in:
2026-08-20 10:56:33 -03:00
parent 00a01e1a8e
commit e1d9590905
7 changed files with 238 additions and 16 deletions

View File

@@ -59,18 +59,22 @@
[discount]="cartDiscount()"
[total]="cartTotal()"
[readonly]="!canModifyCart()"
[allowModify]="canModifyCart()"
[allowModify]="true"
[showModifyWhenReadonly]="true"
[allowUpdateQuantity]="canUpdateCartQuantity()"
[requireEditingMode]="
createdPurchase()?.status === 'created' || createdPurchase()?.status === 'pending_payment'
"
[allowDelete]="false"
[allowUpdateVariant]="canUpdateCartVariant()"
[requireEditingMode]="true"
[allowDelete]="canDeleteCartItems()"
[persistQuantityChanges]="false"
[persistVariantChanges]="false"
[persistDeleteChanges]="false"
[editing]="isEditingItems()"
[editingDisabled]="isUpdatingItem() || isPreparingItemEdit()"
backgroundColor="transparent"
(editingChange)="onEditingItemsChange($event)"
(itemQuantityChange)="onPurchaseItemQuantityChange($event)"
(itemVariantChange)="onPurchaseItemVariantChange($event)"
(itemRemove)="onPurchaseItemRemove($event)"
/>
</div>
</div>

View File

@@ -18,6 +18,8 @@ describe('CheckoutPageComponent payment validation', () => {
startCheckout: ReturnType<typeof vi.fn>;
updateCustomerData: ReturnType<typeof vi.fn>;
updateItemQuantity: ReturnType<typeof vi.fn>;
updateItemVariant: ReturnType<typeof vi.fn>;
removeItem: ReturnType<typeof vi.fn>;
prepareItemEditing: ReturnType<typeof vi.fn>;
cancelPurchase: ReturnType<typeof vi.fn>;
generatePaymentIntent: ReturnType<typeof vi.fn>;
@@ -36,7 +38,7 @@ describe('CheckoutPageComponent payment validation', () => {
let tenantState: ReturnType<
typeof signal<{
codigo: string;
cart_editing_policy?: CartEditingPolicy;
checkout_editing_policy?: CartEditingPolicy;
}>
>;
@@ -60,6 +62,8 @@ describe('CheckoutPageComponent payment validation', () => {
}),
updateCustomerData: vi.fn(),
updateItemQuantity: vi.fn(),
updateItemVariant: vi.fn(),
removeItem: vi.fn(),
prepareItemEditing: vi.fn(),
cancelPurchase: vi.fn().mockResolvedValue({ status: 'cancelled' }),
generatePaymentIntent: vi.fn().mockResolvedValue({
@@ -95,7 +99,7 @@ describe('CheckoutPageComponent payment validation', () => {
authUserState = signal(null);
tenantState = signal({
codigo: 'tenant-test',
cart_editing_policy: {
checkout_editing_policy: {
code: 'full',
allow_modify: true,
allow_delete: true,
@@ -448,7 +452,14 @@ describe('CheckoutPageComponent payment validation', () => {
quantity: 3,
});
expect(checkoutServiceStub.updateItemQuantity).toHaveBeenCalledWith('tenant-test', 25, 91, 3);
expect(checkoutServiceStub.updateItemQuantity).toHaveBeenCalledWith(
'tenant-test',
25,
91,
3,
null,
'purchase',
);
expect(component.createdPurchase()).toBe(updatedPurchase);
expect(component.isUpdatingItem()).toBe(false);
});
@@ -481,10 +492,10 @@ describe('CheckoutPageComponent payment validation', () => {
expect(component.isEditingItems()).toBe(false);
});
it('does not allow item editing when the tenant disables cart editing', async () => {
it('shows Modificar and returns to the home cart when checkout editing is disabled', async () => {
tenantState.set({
codigo: 'tenant-test',
cart_editing_policy: {
checkout_editing_policy: {
code: 'disabled',
allow_modify: false,
allow_delete: false,
@@ -492,7 +503,37 @@ describe('CheckoutPageComponent payment validation', () => {
allow_update_variant: false,
},
});
const { component } = createComponent();
const { fixture, component } = createComponent();
component.createdPurchase.set({
id: 25,
status: 'created',
items: [
{
id: 91,
quantity: 2,
unit_price: '100.00',
line_total: '200.00',
source_catalog_item_id: 8,
source_variant_id: null,
item_details: {
nombre: 'Remera',
descripcion: null,
slug: 'remera',
imagen: null,
attributes: [],
},
},
],
subtotal: '200.00',
total: '200.00',
});
fixture.detectChanges();
const modifyButton = (fixture.nativeElement as HTMLElement).querySelector('.cart-edit-btn');
expect(modifyButton?.textContent?.trim()).toBe('Modificar');
expect(
(fixture.nativeElement as HTMLElement).querySelector('app-quantity-selector'),
).toBeNull();
await component.onEditingItemsChange(true);
@@ -500,6 +541,9 @@ describe('CheckoutPageComponent payment validation', () => {
expect(component.canModifyCart()).toBe(false);
expect(component.isEditingItems()).toBe(false);
expect(checkoutServiceStub.prepareItemEditing).not.toHaveBeenCalled();
expect(routerStub.navigate).toHaveBeenCalledWith(['/'], {
queryParams: { openCart: true },
});
});
it('updates customer data on the existing purchase before payment', async () => {

View File

@@ -80,10 +80,16 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
protected readonly isLoadingPurchase = signal(true);
protected readonly checkoutStepIndex = signal(0);
protected readonly canUpdateCartQuantity = computed(
() => this.tenantService.tenant()?.cart_editing_policy?.allow_update_quantity ?? false,
() => this.tenantService.tenant()?.checkout_editing_policy?.allow_update_quantity ?? false,
);
protected readonly canUpdateCartVariant = computed(
() => this.tenantService.tenant()?.checkout_editing_policy?.allow_update_variant ?? false,
);
protected readonly canDeleteCartItems = computed(
() => this.tenantService.tenant()?.checkout_editing_policy?.allow_delete ?? false,
);
protected readonly canModifyCart = computed(
() => this.tenantService.tenant()?.cart_editing_policy?.allow_modify ?? false,
() => this.tenantService.tenant()?.checkout_editing_policy?.allow_modify ?? false,
);
protected readonly cartSubtotal = computed(() => {
@@ -176,6 +182,8 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
value: attribute.value === null ? '' : String(attribute.value),
})),
quantity: item.quantity,
variantId: item.source_variant_id,
variants: item.variants,
};
}
@@ -185,6 +193,9 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
}
if (editing && !this.canModifyCart()) {
await this.router.navigate(['/'], {
queryParams: { openCart: true },
});
return;
}
@@ -251,6 +262,8 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
purchaseId,
itemId,
event.quantity,
this.createdPurchase()?.cart_id ?? null,
this.createdPurchase()?.items_source ?? 'purchase',
);
this.createdPurchase.set(purchase);
} catch (error) {
@@ -260,6 +273,79 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
}
}
protected async onPurchaseItemVariantChange(event: {
item: CartItemMock;
variantId: number;
}): Promise<void> {
const tenant = this.tenantService.tenant();
const purchase = this.createdPurchase();
const itemId = event.item.cartItemId;
if (
!this.canUpdateCartVariant() ||
!tenant ||
!purchase ||
!itemId ||
this.isUpdatingItem() ||
!this.isEditingItems()
) {
return;
}
this.isUpdatingItem.set(true);
try {
this.createdPurchase.set(
await this.checkoutService.updateItemVariant(
tenant.codigo,
purchase.id,
itemId,
event.variantId,
event.item.quantity,
purchase.cart_id,
purchase.items_source,
),
);
} catch (error) {
console.error('Failed to update purchase item variant:', error);
} finally {
this.isUpdatingItem.set(false);
}
}
protected async onPurchaseItemRemove(event: { item: CartItemMock }): Promise<void> {
const tenant = this.tenantService.tenant();
const purchase = this.createdPurchase();
const itemId = event.item.cartItemId;
if (
!this.canDeleteCartItems() ||
!tenant ||
!purchase ||
!itemId ||
this.isUpdatingItem() ||
!this.isEditingItems()
) {
return;
}
this.isUpdatingItem.set(true);
try {
this.createdPurchase.set(
await this.checkoutService.removeItem(
tenant.codigo,
purchase.id,
itemId,
purchase.cart_id,
purchase.items_source,
),
);
} catch (error) {
console.error('Failed to remove purchase item:', error);
} finally {
this.isUpdatingItem.set(false);
}
}
protected async onStep1Continue(): Promise<void> {
if (this.form.invalid || this.isUpdatingPurchase() || this.isEditingItems()) return;