fix(checkout): restore cart on every exit
This commit is contained in:
@@ -587,14 +587,29 @@ describe('CheckoutPageComponent payment validation', () => {
|
|||||||
expect(component.stepper.next).toHaveBeenCalledOnce();
|
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();
|
const { component } = createComponent();
|
||||||
|
|
||||||
await expect(component.canDeactivate()).resolves.toBe(true);
|
await expect(component.canDeactivate()).resolves.toBe(true);
|
||||||
|
|
||||||
expect(checkoutServiceStub.cancelPurchase).not.toHaveBeenCalled();
|
expect(checkoutServiceStub.cancelPurchase).toHaveBeenCalledWith('tenant-test', 25);
|
||||||
expect(cartServiceStub.loadCart).toHaveBeenCalled();
|
expect(cartServiceStub.loadCart).toHaveBeenCalledOnce();
|
||||||
expect(cartServiceStub.clearCart).not.toHaveBeenCalled();
|
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);
|
expect(component.createdPurchaseId()).toBe(25);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
private transferPollingRunId = 0;
|
private transferPollingRunId = 0;
|
||||||
private paymentMethodRequestId = 0;
|
private paymentMethodRequestId = 0;
|
||||||
private navigationStarted = false;
|
private navigationStarted = false;
|
||||||
|
private restoreCartPromise: Promise<boolean> | null = null;
|
||||||
|
|
||||||
@ViewChild(StepperComponent) stepper!: StepperComponent;
|
@ViewChild(StepperComponent) stepper!: StepperComponent;
|
||||||
|
|
||||||
@@ -184,7 +185,6 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
})),
|
})),
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
variantId: item.source_variant_id,
|
variantId: item.source_variant_id,
|
||||||
variants: item.variants,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,29 +193,16 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tenant = this.tenantService.tenant();
|
|
||||||
const purchaseId = this.createdPurchaseId();
|
|
||||||
if (!tenant || !purchaseId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isRestoringCart.set(true);
|
|
||||||
this.globalLoadingService.start();
|
this.globalLoadingService.start();
|
||||||
try {
|
try {
|
||||||
await this.checkoutService.cancelPurchase(tenant.codigo, purchaseId);
|
const restored = await this.restoreCheckoutCart();
|
||||||
this.createdPurchaseId.set(null);
|
if (!restored) return;
|
||||||
this.createdPurchase.set(null);
|
|
||||||
await firstValueFrom(this.cartService.loadCart());
|
|
||||||
this.navigationStarted = true;
|
|
||||||
await this.router.navigate(['/'], {
|
await this.router.navigate(['/'], {
|
||||||
queryParams: { openCart: true },
|
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 {
|
} finally {
|
||||||
this.globalLoadingService.stop();
|
this.globalLoadingService.stop();
|
||||||
this.isRestoringCart.set(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,29 +238,71 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async onCancel(): Promise<void> {
|
protected async onCancel(): Promise<void> {
|
||||||
if (await this.canDeactivate()) {
|
void this.router.navigate(['/']);
|
||||||
void this.router.navigate(['/']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async canDeactivate(): Promise<boolean> {
|
public async canDeactivate(): Promise<boolean> {
|
||||||
this.stopQrPolling();
|
this.stopQrPolling();
|
||||||
this.stopTransferPolling();
|
this.stopTransferPolling();
|
||||||
|
|
||||||
|
if (this.restoreCartPromise) {
|
||||||
|
return this.restoreCartPromise;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.navigationStarted) {
|
if (this.navigationStarted) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The checkout cart remains attached to its purchase. Once the user adds a
|
this.globalLoadingService.start();
|
||||||
// new item, the cart API creates a separate active cart automatically.
|
|
||||||
try {
|
try {
|
||||||
await firstValueFrom(this.cartService.loadCart());
|
return await this.restoreCheckoutCart();
|
||||||
} catch (error) {
|
} finally {
|
||||||
console.error('Failed to load the active cart after leaving checkout:', error);
|
this.globalLoadingService.stop();
|
||||||
this.showRequestError(error, 'No se pudo cargar el carrito.');
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private restoreCheckoutCart(): Promise<boolean> {
|
||||||
|
if (this.restoreCartPromise) {
|
||||||
|
return this.restoreCartPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
this.restoreCartPromise = this.performCartRestore().finally(() => {
|
||||||
|
this.restoreCartPromise = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.restoreCartPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async performCartRestore(): Promise<boolean> {
|
||||||
|
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<void> {
|
protected async selectPaymentMethod(method: PaymentMethod): Promise<void> {
|
||||||
@@ -608,6 +637,8 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.createdPurchaseId.set(purchaseId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const purchase = await this.checkoutService
|
const purchase = await this.checkoutService
|
||||||
.withCustomLoading()
|
.withCustomLoading()
|
||||||
@@ -675,7 +706,6 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
const response = error.error as ApiErrorResponse | null;
|
const response = error.error as ApiErrorResponse | null;
|
||||||
|
|
||||||
if (response?.code === 'purchase.expired' || this.hasExpiredPurchase()) {
|
if (response?.code === 'purchase.expired' || this.hasExpiredPurchase()) {
|
||||||
this.navigationStarted = true;
|
|
||||||
this.stopQrPolling();
|
this.stopQrPolling();
|
||||||
this.stopTransferPolling();
|
this.stopTransferPolling();
|
||||||
this.toastService.danger(
|
this.toastService.danger(
|
||||||
|
|||||||
Reference in New Issue
Block a user