From d636b2b82bfe0c8d51b76462c4288aa7c8c97b76 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Thu, 20 Aug 2026 16:28:58 -0300 Subject: [PATCH 1/3] feat(checkout): restore cart before modifying items --- .../checkout-page/checkout-page.component.ts | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/app/features/store/pages/checkout-page/checkout-page.component.ts b/src/app/features/store/pages/checkout-page/checkout-page.component.ts index 6e67b73..1acce85 100644 --- a/src/app/features/store/pages/checkout-page/checkout-page.component.ts +++ b/src/app/features/store/pages/checkout-page/checkout-page.component.ts @@ -199,9 +199,28 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { } if (editing && !this.canModifyCart()) { - await this.router.navigate(['/'], { - queryParams: { openCart: true }, - }); + const tenant = this.tenantService.tenant(); + const purchaseId = this.createdPurchaseId(); + + if (!tenant || !purchaseId) { + return; + } + + this.isPreparingItemEdit.set(true); + try { + await this.checkoutService.cancelPurchase(tenant.codigo, purchaseId); + this.createdPurchaseId.set(null); + this.createdPurchase.set(null); + await firstValueFrom(this.cartService.loadCart()); + await this.router.navigate(['/'], { + queryParams: { openCart: true }, + }); + } catch (error) { + console.error('Failed to restore cart for editing:', error); + } finally { + this.isPreparingItemEdit.set(false); + } + return; } From b14f34d3e8d8819f2a7a655c238649c264a6af07 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Thu, 20 Aug 2026 16:30:09 -0300 Subject: [PATCH 2/3] test(checkout): cover cart restoration before editing --- .../pages/checkout-page/checkout-page.component.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts b/src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts index 11218aa..aed8a07 100644 --- a/src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts +++ b/src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts @@ -537,7 +537,7 @@ describe('CheckoutPageComponent payment validation', () => { expect(component.isEditingItems()).toBe(false); }); - it('shows Modificar and returns to the home cart when checkout editing is disabled', async () => { + it('shows Modificar, restores the previous cart and opens it when checkout editing is disabled', async () => { tenantState.set({ codigo: 'tenant-test', checkout_editing_policy: { @@ -586,6 +586,10 @@ describe('CheckoutPageComponent payment validation', () => { expect(component.canModifyCart()).toBe(false); expect(component.isEditingItems()).toBe(false); expect(checkoutServiceStub.prepareItemEditing).not.toHaveBeenCalled(); + expect(checkoutServiceStub.cancelPurchase).toHaveBeenCalledWith('tenant-test', 25); + expect(cartServiceStub.loadCart).toHaveBeenCalled(); + expect(component.createdPurchaseId()).toBeNull(); + expect(component.createdPurchase()).toBeNull(); expect(routerStub.navigate).toHaveBeenCalledWith(['/'], { queryParams: { openCart: true }, }); From 9b69a1d3871e64e2301749a5c99712bb9d5069ba Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 21 Aug 2026 10:10:20 -0300 Subject: [PATCH 3/3] feat(checkout): add error handling for expired purchases in checkout process --- .../checkout-page.component.html | 31 +-- .../checkout-page.component.scss | 24 +- .../checkout-page.component.spec.ts | 135 ++++------ .../checkout-page/checkout-page.component.ts | 230 ++++-------------- .../components/cart/cart.component.html | 6 +- .../components/cart/cart.component.spec.ts | 51 ++++ .../shared/components/cart/cart.component.ts | 7 + 7 files changed, 155 insertions(+), 329 deletions(-) diff --git a/src/app/features/store/pages/checkout-page/checkout-page.component.html b/src/app/features/store/pages/checkout-page/checkout-page.component.html index 8fb9344..50990be 100644 --- a/src/app/features/store/pages/checkout-page/checkout-page.component.html +++ b/src/app/features/store/pages/checkout-page/checkout-page.component.html @@ -5,12 +5,7 @@ } @else {
-
+
- @if (isEditingItems()) { -
-

Terminá de modificar las cantidades para continuar con el pago.

-
- } -
diff --git a/src/app/features/store/pages/checkout-page/checkout-page.component.scss b/src/app/features/store/pages/checkout-page/checkout-page.component.scss index 04b30ae..33c677c 100644 --- a/src/app/features/store/pages/checkout-page/checkout-page.component.scss +++ b/src/app/features/store/pages/checkout-page/checkout-page.component.scss @@ -11,8 +11,7 @@ order: 1; } - .checkout-page__stepper-col, - .checkout-page__editing-notice { + .checkout-page__stepper-col { order: 2; } } @@ -21,27 +20,6 @@ min-width: 0; border-radius: 4px; min-height: 420px; - - &--editing { - display: none; - } - } - - &__editing-notice { - display: grid; - min-height: 420px; - place-items: center; - padding: 2rem; - border-radius: 4px; - background: #f5f5f5; - color: #666666; - text-align: center; - - p { - max-width: 360px; - margin: 0; - font-size: 14px; - } } &__cart-col { diff --git a/src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts b/src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts index aed8a07..2490c8d 100644 --- a/src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts +++ b/src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts @@ -9,8 +9,10 @@ import { AuthService } from '../../../../core/services/auth/auth.service'; import { CartService } from '../../../../core/services/cart/cart.service'; import { CheckoutService } from '../../../../core/services/checkout.service'; import { CatalogService } from '../../../../core/services/catalog/catalog.service'; +import { GlobalLoadingService } from '../../../../core/services/global-loading/global-loading.service'; import { TenantService } from '../../../../core/services/tenant.service'; import { CartEditingPolicy } from '../../../../core/services/tenant.interface'; +import { ToastService } from '../../../../core/services/toast.service'; import { CheckoutPageComponent } from './checkout-page.component'; describe('CheckoutPageComponent payment validation', () => { @@ -20,7 +22,6 @@ describe('CheckoutPageComponent payment validation', () => { updateItemQuantity: ReturnType; updateItemVariant: ReturnType; removeItem: ReturnType; - prepareItemEditing: ReturnType; cancelPurchase: ReturnType; generatePaymentIntent: ReturnType; submitPurchaseForReview: ReturnType; @@ -34,6 +35,11 @@ describe('CheckoutPageComponent payment validation', () => { clearCart: ReturnType; }; let routerStub: { navigate: ReturnType }; + let toastServiceStub: { danger: ReturnType }; + let globalLoadingServiceStub: { + start: ReturnType; + stop: ReturnType; + }; let routeQueryParamMap: ReturnType; let authUserState: ReturnType; let tenantState: ReturnType< @@ -65,7 +71,6 @@ describe('CheckoutPageComponent payment validation', () => { updateItemQuantity: vi.fn(), updateItemVariant: vi.fn(), removeItem: vi.fn(), - prepareItemEditing: vi.fn(), cancelPurchase: vi.fn().mockResolvedValue({ status: 'cancelled' }), generatePaymentIntent: vi.fn().mockResolvedValue({ qr_data: { qr_code: 'qr-value' }, @@ -97,6 +102,8 @@ describe('CheckoutPageComponent payment validation', () => { }); }); routerStub = { navigate: vi.fn() }; + toastServiceStub = { danger: vi.fn() }; + globalLoadingServiceStub = { start: vi.fn(), stop: vi.fn() }; routeQueryParamMap = convertToParamMap({}); authUserState = signal(null); tenantState = signal({ @@ -118,6 +125,8 @@ describe('CheckoutPageComponent payment validation', () => { { provide: CartService, useValue: cartServiceStub }, { provide: TenantService, useValue: { tenant: tenantState } }, { provide: AuthService, useValue: { user: authUserState } }, + { provide: GlobalLoadingService, useValue: globalLoadingServiceStub }, + { provide: ToastService, useValue: toastServiceStub }, { provide: ActivatedRoute, useValue: { @@ -472,83 +481,30 @@ describe('CheckoutPageComponent payment validation', () => { expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25]); }); - it('updates a purchase item while editing and refreshes checkout totals', async () => { - const updatedPurchase = { - id: 25, - items: [], - subtotal: '300.00', - total: '300.00', - }; - checkoutServiceStub.updateItemQuantity.mockResolvedValue(updatedPurchase); - const { component } = createComponent(); - component.isEditingItems.set(true); - - await component.onPurchaseItemQuantityChange({ - item: { - cartItemId: 91, - imageUrl: null, - product: 'Remera', - originalPrice: null, - discountedPrice: 100, - discountPercentage: null, - attributes: [], - quantity: 2, - }, - quantity: 3, + it('shows the API error in a toast when restoring the cart fails', async () => { + const message = 'La compra venció. Iniciá una nueva compra.'; + checkoutServiceStub.cancelPurchase.mockRejectedValue({ + error: { code: 'purchase.expired', message }, }); + const { component } = createComponent(); - expect(checkoutServiceStub.updateItemQuantity).toHaveBeenCalledWith( - 'tenant-test', - 25, - 91, - 3, - null, - 'purchase', + await component.onModifyPurchase(); + + expect(toastServiceStub.danger).toHaveBeenCalledWith(message); + expect(routerStub.navigate).not.toHaveBeenCalled(); + expect(globalLoadingServiceStub.start).toHaveBeenCalledOnce(); + expect(globalLoadingServiceStub.stop).toHaveBeenCalledOnce(); + expect(component.isRestoringCart()).toBe(false); + }); + + it('restores the previous cart and opens it when Modificar is clicked', async () => { + let finishNavigation!: (navigated: boolean) => void; + routerStub.navigate.mockReturnValue( + new Promise((resolve) => { + finishNavigation = resolve; + }), ); - expect(component.createdPurchase()).toBe(updatedPurchase); - expect(component.isUpdatingItem()).toBe(false); - }); - - it('keeps the payment step selected while editing and regenerates payment afterward', async () => { - const editablePurchase = { - id: 25, - status: 'created', - items: [], - subtotal: '100.00', - total: '100.00', - }; - checkoutServiceStub.prepareItemEditing.mockResolvedValue(editablePurchase); const { component } = createComponent(); - component.stepper = { currentStepIndex: signal(1) }; - const selectPaymentMethod = vi - .spyOn(component, 'selectPaymentMethod') - .mockResolvedValue(undefined); - - await component.onEditingItemsChange(true); - - expect(checkoutServiceStub.prepareItemEditing).toHaveBeenCalledWith('tenant-test', 25); - expect(component.isEditingItems()).toBe(true); - expect(component.createdPurchase()).toBe(editablePurchase); - - await component.onEditingItemsChange(false); - - expect(component.stepper.currentStepIndex()).toBe(1); - expect(selectPaymentMethod).toHaveBeenCalledWith('qr'); - expect(component.isEditingItems()).toBe(false); - }); - - it('shows Modificar, restores the previous cart and opens it when checkout editing is disabled', async () => { - tenantState.set({ - codigo: 'tenant-test', - checkout_editing_policy: { - code: 'disabled', - allow_modify: false, - allow_delete: false, - allow_update_quantity: false, - allow_update_variant: false, - }, - }); - const { fixture, component } = createComponent(); component.createdPurchase.set({ id: 25, status: 'created', @@ -572,27 +528,28 @@ describe('CheckoutPageComponent payment validation', () => { subtotal: '200.00', total: '200.00', }); - fixture.detectChanges(); + const modification = component.onModifyPurchase(); + await Promise.resolve(); + await Promise.resolve(); - 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); - - expect(component.canUpdateCartQuantity()).toBe(false); - expect(component.canModifyCart()).toBe(false); - expect(component.isEditingItems()).toBe(false); - expect(checkoutServiceStub.prepareItemEditing).not.toHaveBeenCalled(); expect(checkoutServiceStub.cancelPurchase).toHaveBeenCalledWith('tenant-test', 25); - expect(cartServiceStub.loadCart).toHaveBeenCalled(); + expect(globalLoadingServiceStub.start).toHaveBeenCalledOnce(); + expect(globalLoadingServiceStub.stop).not.toHaveBeenCalled(); + expect(cartServiceStub.loadCart).toHaveBeenCalledOnce(); expect(component.createdPurchaseId()).toBeNull(); expect(component.createdPurchase()).toBeNull(); expect(routerStub.navigate).toHaveBeenCalledWith(['/'], { queryParams: { openCart: true }, }); + + finishNavigation(true); + await modification; + + expect(globalLoadingServiceStub.stop).toHaveBeenCalledOnce(); + + await component.canDeactivate(); + + expect(cartServiceStub.loadCart).toHaveBeenCalledOnce(); }); it('updates customer data on the existing purchase before payment', async () => { diff --git a/src/app/features/store/pages/checkout-page/checkout-page.component.ts b/src/app/features/store/pages/checkout-page/checkout-page.component.ts index 1acce85..c43dc86 100644 --- a/src/app/features/store/pages/checkout-page/checkout-page.component.ts +++ b/src/app/features/store/pages/checkout-page/checkout-page.component.ts @@ -20,6 +20,8 @@ import { } from '../../../../core/services/checkout.service'; import { AuthService } from '../../../../core/services/auth/auth.service'; import { CartService } from '../../../../core/services/cart/cart.service'; +import { GlobalLoadingService } from '../../../../core/services/global-loading/global-loading.service'; +import { ToastService } from '../../../../core/services/toast.service'; import { BankAccount } from '../../../../core/services/tenant.interface'; import { CartComponent, CartItemMock } from '../../../../shared/components/cart/cart.component'; import { StepComponent } from '../../../../shared/components/stepper/step.component'; @@ -58,6 +60,8 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { private readonly checkoutService = inject(CheckoutService); private readonly authService = inject(AuthService); private readonly cartService = inject(CartService); + private readonly globalLoadingService = inject(GlobalLoadingService); + private readonly toastService = inject(ToastService); private readonly qrPollingIntervalMs = 5_000; private readonly qrPollingMaxAttempts = 9; private readonly transferPollingIntervalMs = 3_000; @@ -84,19 +88,6 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { protected readonly createdPurchase = signal(null); protected readonly isLoadingPurchase = signal(true); protected readonly checkoutStepIndex = signal(0); - protected readonly canUpdateCartQuantity = computed( - () => 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()?.checkout_editing_policy?.allow_modify ?? false, - ); - protected readonly cartSubtotal = computed(() => { const purchase = this.createdPurchase(); return purchase ? parseFloat(purchase.subtotal) : 0; @@ -125,9 +116,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { protected readonly transferDni = signal(''); protected readonly isUpdatingPurchase = signal(false); - protected readonly isEditingItems = signal(false); - protected readonly isUpdatingItem = signal(false); - protected readonly isPreparingItemEdit = signal(false); + protected readonly isRestoringCart = signal(false); protected readonly createdPurchaseId = signal(null); protected readonly isGeneratingIntent = signal(false); protected readonly isPaymentLoading = computed(() => this.isGeneratingIntent()); @@ -193,187 +182,39 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { }; } - protected async onEditingItemsChange(editing: boolean): Promise { - if (this.isUpdatingItem() || this.isPreparingItemEdit()) { + protected async onModifyPurchase(): Promise { + if (this.isRestoringCart()) { return; } - if (editing && !this.canModifyCart()) { - const tenant = this.tenantService.tenant(); - const purchaseId = this.createdPurchaseId(); - - if (!tenant || !purchaseId) { - return; - } - - this.isPreparingItemEdit.set(true); - try { - await this.checkoutService.cancelPurchase(tenant.codigo, purchaseId); - this.createdPurchaseId.set(null); - this.createdPurchase.set(null); - await firstValueFrom(this.cartService.loadCart()); - await this.router.navigate(['/'], { - queryParams: { openCart: true }, - }); - } catch (error) { - console.error('Failed to restore cart for editing:', error); - } finally { - this.isPreparingItemEdit.set(false); - } - - return; - } - - if (!editing) { - this.isEditingItems.set(false); - - if (this.stepper?.currentStepIndex() === 1) { - void this.selectPaymentMethod(this.selectedPaymentMethod()); - } - - return; - } - - this.isEditingItems.set(true); - this.stopQrPolling(); - this.stopTransferPolling(); - this.qrData.set(null); - this.qrPaymentStatus.set('idle'); - this.transferAccount.set(null); - this.transferValidationStatus.set('idle'); - const tenant = this.tenantService.tenant(); const purchaseId = this.createdPurchaseId(); - if (!tenant || !purchaseId) { - this.isEditingItems.set(false); return; } - this.isPreparingItemEdit.set(true); + this.isRestoringCart.set(true); + this.globalLoadingService.start(); try { - const purchase = await this.checkoutService.prepareItemEditing(tenant.codigo, purchaseId); - this.createdPurchase.set(purchase); + await this.checkoutService.cancelPurchase(tenant.codigo, purchaseId); + this.createdPurchaseId.set(null); + this.createdPurchase.set(null); + await firstValueFrom(this.cartService.loadCart()); + this.navigationStarted = true; + await this.router.navigate(['/'], { + queryParams: { openCart: true }, + }); } catch (error) { - console.error('Failed to prepare purchase item editing:', error); - this.isEditingItems.set(false); + console.error('Failed to restore cart for editing:', error); + this.showRequestError(error, 'No se pudo recuperar el carrito para modificar la compra.'); } finally { - this.isPreparingItemEdit.set(false); - } - } - - protected async onPurchaseItemQuantityChange(event: { - item: CartItemMock; - quantity: number; - }): Promise { - const tenant = this.tenantService.tenant(); - const purchaseId = this.createdPurchaseId(); - const itemId = event.item.cartItemId; - - if ( - !this.canUpdateCartQuantity() || - !tenant || - !purchaseId || - !itemId || - this.isUpdatingItem() || - !this.isEditingItems() - ) { - return; - } - - this.isUpdatingItem.set(true); - try { - const purchase = await this.checkoutService.updateItemQuantity( - tenant.codigo, - purchaseId, - itemId, - event.quantity, - this.createdPurchase()?.cart_id ?? null, - this.createdPurchase()?.items_source ?? 'purchase', - ); - this.createdPurchase.set(purchase); - } catch (error) { - console.error('Failed to update purchase item quantity:', error); - } finally { - this.isUpdatingItem.set(false); - } - } - - protected async onPurchaseItemVariantChange(event: { - item: CartItemMock; - variantId: number; - }): Promise { - 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 { - 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); + this.globalLoadingService.stop(); + this.isRestoringCart.set(false); } } protected async onStep1Continue(): Promise { - if (this.form.invalid || this.isUpdatingPurchase() || this.isEditingItems()) return; + if (this.form.invalid || this.isUpdatingPurchase()) return; const tenant = this.tenantService.tenant(); const purchaseId = this.createdPurchaseId(); @@ -397,7 +238,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { void this.selectPaymentMethod(this.selectedPaymentMethod()); } catch (error) { console.error('Failed to create purchase:', error); - // Here we could show an alert or toast + this.showRequestError(error, 'No se pudieron actualizar los datos de la compra.'); } finally { this.isUpdatingPurchase.set(false); } @@ -423,13 +264,14 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { await firstValueFrom(this.cartService.loadCart()); } catch (error) { console.error('Failed to load the active cart after leaving checkout:', error); + this.showRequestError(error, 'No se pudo cargar el carrito.'); } return true; } protected async selectPaymentMethod(method: PaymentMethod): Promise { - if (this.navigationStarted || this.isEditingItems()) { + if (this.navigationStarted) { return; } @@ -474,16 +316,13 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { } } catch (error) { console.error('Failed to generate payment intent:', error); + this.showRequestError(error, 'No se pudo generar el pago.'); } finally { this.isGeneratingIntent.set(false); } } protected async generateTransferIntent(dni: string): Promise { - if (this.isEditingItems()) { - return; - } - const purchaseId = this.createdPurchaseId(); const tenant = this.tenantService.tenant(); @@ -511,6 +350,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { } } catch (error) { console.error('Failed to generate transfer payment intent:', error); + this.showRequestError(error, 'No se pudo generar el pago por transferencia.'); } finally { this.isGeneratingIntent.set(false); } @@ -541,7 +381,6 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { !purchaseId || !tenant || this.navigationStarted || - this.isEditingItems() || this.transferValidationStatus() === 'checking' ) { return; @@ -570,6 +409,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { this.scheduleTransferPoll(runId); } catch (error) { console.error('Failed to submit transfer payment for review:', error); + this.showRequestError(error, 'No se pudo enviar el pago para su validación.'); if (runId === this.transferPollingRunId) { this.transferValidationStatus.set('error'); @@ -800,7 +640,21 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { } } catch (error) { console.error('Failed to load purchase:', error); + this.showRequestError(error, 'No se pudo cargar la compra.'); void this.router.navigate(['/']); } } + + private showRequestError(error: unknown, fallbackMessage: string): void { + const payload = + typeof error === 'object' && error !== null && 'error' in error + ? (error as { error?: { message?: unknown } }).error + : undefined; + const message = + typeof payload?.message === 'string' && payload.message.trim() + ? payload.message + : fallbackMessage; + + this.toastService.danger(message); + } } diff --git a/src/app/shared/components/cart/cart.component.html b/src/app/shared/components/cart/cart.component.html index 715f1de..66dab93 100644 --- a/src/app/shared/components/cart/cart.component.html +++ b/src/app/shared/components/cart/cart.component.html @@ -9,17 +9,17 @@ @if ( allowModify() && (!readonly() || showModifyWhenReadonly()) && - requireEditingMode() && + (requireEditingMode() || modifyAsAction()) && items().length > 0 ) { } diff --git a/src/app/shared/components/cart/cart.component.spec.ts b/src/app/shared/components/cart/cart.component.spec.ts index 29dc1ad..7aaf644 100644 --- a/src/app/shared/components/cart/cart.component.spec.ts +++ b/src/app/shared/components/cart/cart.component.spec.ts @@ -367,6 +367,57 @@ describe('CartComponent', () => { expect(editingChange).toHaveBeenLastCalledWith(false); }); + it('emits Modificar as an action without toggling to Listo', async () => { + await TestBed.configureTestingModule({ + imports: [CartComponent], + providers: [ + { + provide: CartService, + useValue: { + cart: signal(null).asReadonly(), + updateItemQuantity: vi.fn(), + removeItem: vi.fn(), + }, + }, + { provide: ModalService, useValue: {} }, + { + provide: ToastService, + useValue: { success: vi.fn(), info: vi.fn(), danger: vi.fn() }, + }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(CartComponent); + fixture.componentRef.setInput('items', [ + { + cartItemId: 10, + imageUrl: null, + product: 'Producto de prueba', + originalPrice: null, + discountedPrice: 1000, + discountPercentage: null, + attributes: [], + quantity: 1, + }, + ]); + fixture.componentRef.setInput('readonly', true); + fixture.componentRef.setInput('showModifyWhenReadonly', true); + fixture.componentRef.setInput('modifyAsAction', true); + const modify = vi.fn(); + fixture.componentInstance.modify.subscribe(modify); + fixture.detectChanges(); + + const modifyButton = fixture.debugElement.query(By.css('.cart-edit-btn')); + expect(modifyButton.nativeElement.textContent.trim()).toBe('Modificar'); + + modifyButton.nativeElement.click(); + fixture.detectChanges(); + + expect(modify).toHaveBeenCalledOnce(); + expect(modifyButton.nativeElement.textContent.trim()).toBe('Modificar'); + expect(fixture.componentInstance.editing()).toBe(false); + }); + it('allows editing directly when the optional Modificar toggle is disabled', async () => { await TestBed.configureTestingModule({ imports: [CartComponent], diff --git a/src/app/shared/components/cart/cart.component.ts b/src/app/shared/components/cart/cart.component.ts index 6b907ff..bcd1bb1 100644 --- a/src/app/shared/components/cart/cart.component.ts +++ b/src/app/shared/components/cart/cart.component.ts @@ -56,6 +56,7 @@ export class CartComponent { readonly allowUpdateQuantity = input(true); readonly allowModify = input(true); readonly showModifyWhenReadonly = input(false); + readonly modifyAsAction = input(false); readonly requireEditingMode = input(false); readonly allowUpdateVariant = input(true); readonly allowDelete = input(true); @@ -66,6 +67,7 @@ export class CartComponent { readonly editing = model(false); readonly closed = output(); + readonly modify = output(); readonly itemQuantityChange = output<{ item: CartItemMock; index: number; @@ -257,6 +259,11 @@ export class CartComponent { return; } + if (this.modifyAsAction()) { + this.modify.emit(); + return; + } + const editing = !this.editing(); this.editing.set(editing); }