From 0bd6dc3b2251737657a27523275c37f6a803a4cc Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 21 Aug 2026 11:04:09 -0300 Subject: [PATCH] fix(checkout): restore cart on every exit --- .../checkout-page.component.spec.ts | 21 ++++- .../checkout-page/checkout-page.component.ts | 86 +++++++++++++------ 2 files changed, 76 insertions(+), 31 deletions(-) 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 ed184c7..beb9e6a 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 @@ -587,14 +587,29 @@ describe('CheckoutPageComponent payment validation', () => { expect(component.stepper.next).toHaveBeenCalledOnce(); }); - it('keeps the checkout purchase intact when navigating away', async () => { + it('restores the cart when navigating away from checkout', async () => { const { component } = createComponent(); await expect(component.canDeactivate()).resolves.toBe(true); - expect(checkoutServiceStub.cancelPurchase).not.toHaveBeenCalled(); - expect(cartServiceStub.loadCart).toHaveBeenCalled(); + expect(checkoutServiceStub.cancelPurchase).toHaveBeenCalledWith('tenant-test', 25); + expect(cartServiceStub.loadCart).toHaveBeenCalledOnce(); expect(cartServiceStub.clearCart).not.toHaveBeenCalled(); + expect(component.createdPurchaseId()).toBeNull(); + expect(globalLoadingServiceStub.start).toHaveBeenCalledOnce(); + expect(globalLoadingServiceStub.stop).toHaveBeenCalledOnce(); + }); + + it('prevents leaving checkout when the cart cannot be restored', async () => { + checkoutServiceStub.cancelPurchase.mockRejectedValue({ + error: { message: 'No se pudo cancelar la compra.' }, + }); + const { component } = createComponent(); + + await expect(component.canDeactivate()).resolves.toBe(false); + + expect(cartServiceStub.loadCart).not.toHaveBeenCalled(); + expect(toastServiceStub.danger).toHaveBeenCalledWith('No se pudo cancelar la compra.'); expect(component.createdPurchaseId()).toBe(25); }); 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 4ce7e59..e75473d 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 @@ -81,6 +81,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { private transferPollingRunId = 0; private paymentMethodRequestId = 0; private navigationStarted = false; + private restoreCartPromise: Promise | null = null; @ViewChild(StepperComponent) stepper!: StepperComponent; @@ -184,7 +185,6 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { })), quantity: item.quantity, variantId: item.source_variant_id, - variants: item.variants, }; } @@ -193,29 +193,16 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { return; } - const tenant = this.tenantService.tenant(); - const purchaseId = this.createdPurchaseId(); - if (!tenant || !purchaseId) { - return; - } - - this.isRestoringCart.set(true); this.globalLoadingService.start(); try { - await this.checkoutService.cancelPurchase(tenant.codigo, purchaseId); - this.createdPurchaseId.set(null); - this.createdPurchase.set(null); - await firstValueFrom(this.cartService.loadCart()); - this.navigationStarted = true; + const restored = await this.restoreCheckoutCart(); + if (!restored) return; + await this.router.navigate(['/'], { queryParams: { openCart: true }, }); - } catch (error) { - console.error('Failed to restore cart for editing:', error); - this.showRequestError(error, 'No se pudo recuperar el carrito para modificar la compra.'); } finally { this.globalLoadingService.stop(); - this.isRestoringCart.set(false); } } @@ -251,29 +238,71 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { } protected async onCancel(): Promise { - if (await this.canDeactivate()) { - void this.router.navigate(['/']); - } + void this.router.navigate(['/']); } public async canDeactivate(): Promise { this.stopQrPolling(); this.stopTransferPolling(); + if (this.restoreCartPromise) { + return this.restoreCartPromise; + } + if (this.navigationStarted) { return true; } - // The checkout cart remains attached to its purchase. Once the user adds a - // new item, the cart API creates a separate active cart automatically. + this.globalLoadingService.start(); try { - 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 await this.restoreCheckoutCart(); + } finally { + this.globalLoadingService.stop(); + } + } + + private restoreCheckoutCart(): Promise { + if (this.restoreCartPromise) { + return this.restoreCartPromise; } - return true; + this.restoreCartPromise = this.performCartRestore().finally(() => { + this.restoreCartPromise = null; + }); + + return this.restoreCartPromise; + } + + private async performCartRestore(): Promise { + const tenant = this.tenantService.tenant(); + const purchaseId = this.createdPurchaseId(); + if (!tenant || !purchaseId) { + return true; + } + + this.isRestoringCart.set(true); + + try { + await this.checkoutService.cancelPurchase(tenant.codigo, purchaseId); + this.createdPurchaseId.set(null); + this.createdPurchase.set(null); + + try { + await firstValueFrom(this.cartService.loadCart()); + } catch (error) { + console.error('Failed to load the restored cart:', error); + this.showRequestError(error, 'La compra se canceló, pero no se pudo cargar el carrito.'); + } + + this.navigationStarted = true; + return true; + } catch (error) { + console.error('Failed to restore the checkout cart:', error); + this.showRequestError(error, 'No se pudo recuperar el carrito.'); + return false; + } finally { + this.isRestoringCart.set(false); + } } protected async selectPaymentMethod(method: PaymentMethod): Promise { @@ -608,6 +637,8 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { return; } + this.createdPurchaseId.set(purchaseId); + try { const purchase = await this.checkoutService .withCustomLoading() @@ -675,7 +706,6 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { const response = error.error as ApiErrorResponse | null; if (response?.code === 'purchase.expired' || this.hasExpiredPurchase()) { - this.navigationStarted = true; this.stopQrPolling(); this.stopTransferPolling(); this.toastService.danger(