fix(checkout): lock reviewed payments and refresh cart on exit
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
<app-checkout-payment-step
|
||||
[paymentMethods]="paymentMethods"
|
||||
[selectedPaymentMethod]="selectedPaymentMethod()"
|
||||
[paymentMethodDisabled]="hasSubmittedTransfer()"
|
||||
[copiedTransferField]="copiedTransferField()"
|
||||
[transferAccount]="transferAccount()"
|
||||
[transferDni]="transferDni()"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { signal } from '@angular/core';
|
||||
import { getTestBed, TestBed } from '@angular/core/testing';
|
||||
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
|
||||
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router';
|
||||
import { of } from 'rxjs';
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||
@@ -11,6 +12,7 @@ import { CatalogService } from '../../../../core/services/catalog/catalog.servic
|
||||
import { GlobalLoadingService } from '../../../../core/services/global-loading/global-loading.service';
|
||||
import { TenantService } from '../../../../core/services/tenant.service';
|
||||
import { ToastService } from '../../../../core/services/toast.service';
|
||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||
import { CartEditingPolicy } from '../../../../core/services/tenant.interface';
|
||||
import { CheckoutPageComponent } from './checkout-page.component';
|
||||
|
||||
@@ -30,6 +32,7 @@ describe('CheckoutPageComponent payment validation', () => {
|
||||
start: ReturnType<typeof vi.fn>;
|
||||
stop: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let cartServiceStub: { loadCart: ReturnType<typeof vi.fn> };
|
||||
let routeQueryParamMap: ReturnType<typeof convertToParamMap>;
|
||||
let authUserState: ReturnType<typeof signal>;
|
||||
let tenantState: ReturnType<
|
||||
@@ -62,7 +65,7 @@ describe('CheckoutPageComponent payment validation', () => {
|
||||
generatePaymentIntent: vi.fn().mockResolvedValue({
|
||||
qr_data: { qr_code: 'qr-value' },
|
||||
}),
|
||||
submitPurchaseForReview: vi.fn().mockResolvedValue({ status: 'pending_payment' }),
|
||||
submitPurchaseForReview: vi.fn().mockResolvedValue({ status: 'in_review' }),
|
||||
getPurchase: vi.fn().mockResolvedValue({ status: 'pending_payment' }),
|
||||
withCustomLoading: vi.fn(),
|
||||
};
|
||||
@@ -70,6 +73,9 @@ describe('CheckoutPageComponent payment validation', () => {
|
||||
routerStub = { navigate: vi.fn() };
|
||||
toastServiceStub = { danger: vi.fn() };
|
||||
globalLoadingServiceStub = { start: vi.fn(), stop: vi.fn() };
|
||||
cartServiceStub = {
|
||||
loadCart: vi.fn().mockReturnValue(of({ id: 30, items: [], subtotal: '0.00' })),
|
||||
};
|
||||
routeQueryParamMap = convertToParamMap({});
|
||||
authUserState = signal(null);
|
||||
tenantState = signal({
|
||||
@@ -92,6 +98,7 @@ describe('CheckoutPageComponent payment validation', () => {
|
||||
{ provide: AuthService, useValue: { user: authUserState } },
|
||||
{ provide: GlobalLoadingService, useValue: globalLoadingServiceStub },
|
||||
{ provide: ToastService, useValue: toastServiceStub },
|
||||
{ provide: CartService, useValue: cartServiceStub },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
@@ -220,8 +227,11 @@ describe('CheckoutPageComponent payment validation', () => {
|
||||
expect(component.hasSubmittedTransfer()).toBe(true);
|
||||
expect(component.isPurchaseModificationDisabled()).toBe(true);
|
||||
|
||||
await component.selectPaymentMethod('qr');
|
||||
await component.onModifyPurchase();
|
||||
|
||||
expect(component.selectedPaymentMethod()).toBe('transfer');
|
||||
expect(checkoutServiceStub.generatePaymentIntent).not.toHaveBeenCalled();
|
||||
expect(checkoutServiceStub.cancelPurchase).not.toHaveBeenCalled();
|
||||
expect(globalLoadingServiceStub.start).not.toHaveBeenCalled();
|
||||
expect(routerStub.navigate).not.toHaveBeenCalled();
|
||||
@@ -267,19 +277,11 @@ describe('CheckoutPageComponent payment validation', () => {
|
||||
expect(component.transferValidationStatus()).toBe('error');
|
||||
});
|
||||
|
||||
it('cancels transfer polling when the payment method changes or the component is destroyed', async () => {
|
||||
const first = createComponent();
|
||||
first.component.selectedPaymentMethod.set('transfer');
|
||||
first.component.onComplete();
|
||||
checkoutServiceStub.generatePaymentIntent.mockResolvedValueOnce({});
|
||||
await first.component.selectPaymentMethod('qr');
|
||||
await vi.advanceTimersByTimeAsync(12_000);
|
||||
expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled();
|
||||
|
||||
const second = createComponent();
|
||||
second.component.selectedPaymentMethod.set('transfer');
|
||||
second.component.onComplete();
|
||||
second.fixture.destroy();
|
||||
it('cancels transfer polling when the component is destroyed', async () => {
|
||||
const checkout = createComponent();
|
||||
checkout.component.selectedPaymentMethod.set('transfer');
|
||||
await checkout.component.onComplete();
|
||||
checkout.fixture.destroy();
|
||||
await vi.advanceTimersByTimeAsync(12_000);
|
||||
expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -510,6 +512,7 @@ describe('CheckoutPageComponent payment validation', () => {
|
||||
await Promise.resolve();
|
||||
|
||||
expect(checkoutServiceStub.cancelPurchase).toHaveBeenCalledWith('tenant-test', 25);
|
||||
expect(cartServiceStub.loadCart).toHaveBeenCalledOnce();
|
||||
expect(globalLoadingServiceStub.start).toHaveBeenCalledOnce();
|
||||
expect(globalLoadingServiceStub.stop).not.toHaveBeenCalled();
|
||||
expect(component.createdPurchaseId()).toBeNull();
|
||||
@@ -566,6 +569,7 @@ describe('CheckoutPageComponent payment validation', () => {
|
||||
await expect(component.canDeactivate()).resolves.toBe(true);
|
||||
|
||||
expect(checkoutServiceStub.cancelPurchase).toHaveBeenCalledWith('tenant-test', 25);
|
||||
expect(cartServiceStub.loadCart).toHaveBeenCalledOnce();
|
||||
expect(component.createdPurchaseId()).toBeNull();
|
||||
expect(globalLoadingServiceStub.start).toHaveBeenCalledOnce();
|
||||
expect(globalLoadingServiceStub.stop).toHaveBeenCalledOnce();
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { FormBuilder, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { startWith } from 'rxjs';
|
||||
import { firstValueFrom, startWith } from 'rxjs';
|
||||
|
||||
import { TenantService } from '../../../../core/services/tenant.service';
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||
import { GlobalLoadingService } from '../../../../core/services/global-loading/global-loading.service';
|
||||
import { ToastService } from '../../../../core/services/toast.service';
|
||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||
import { CartComponent, CartItemMock } from '../../../../shared/components/cart/cart.component';
|
||||
import { StepComponent } from '../../../../shared/components/stepper/step.component';
|
||||
import { StepperComponent } from '../../../../shared/components/stepper/stepper.component';
|
||||
@@ -66,6 +67,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
||||
private readonly authService = inject(AuthService);
|
||||
private readonly globalLoadingService = inject(GlobalLoadingService);
|
||||
private readonly toastService = inject(ToastService);
|
||||
private readonly cartService = inject(CartService);
|
||||
private readonly qrPollingIntervalMs = 5_000;
|
||||
private readonly qrPollingMaxAttempts = 9;
|
||||
private readonly transferPollingIntervalMs = 3_000;
|
||||
@@ -286,6 +288,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
try {
|
||||
await this.checkoutService.cancelPurchase(tenant.codigo, purchaseId);
|
||||
await firstValueFrom(this.cartService.loadCart());
|
||||
this.createdPurchaseId.set(null);
|
||||
this.createdPurchase.set(null);
|
||||
|
||||
@@ -301,7 +304,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
protected async selectPaymentMethod(method: PaymentMethod): Promise<void> {
|
||||
if (this.navigationStarted) {
|
||||
if (this.navigationStarted || this.hasSubmittedTransfer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -353,6 +356,10 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
protected async generateTransferIntent(dni: string): Promise<void> {
|
||||
if (this.hasSubmittedTransfer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const purchaseId = this.createdPurchaseId();
|
||||
const tenant = this.tenantService.tenant();
|
||||
|
||||
@@ -642,6 +649,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (
|
||||
(purchase.status === 'pending_payment' && purchase.expires_at === null) ||
|
||||
purchase.status === 'in_review' ||
|
||||
purchase.status === 'paid' ||
|
||||
purchase.status === 'cancelled' ||
|
||||
purchase.status === 'rejected' ||
|
||||
|
||||
@@ -7,13 +7,18 @@
|
||||
|
||||
<div class="payment-methods__list" role="radiogroup" aria-label="Metodo de pago">
|
||||
@for (method of paymentMethods(); track method.id) {
|
||||
<label class="payment-method" [class.is-selected]="selectedPaymentMethod() === method.id">
|
||||
<label
|
||||
class="payment-method"
|
||||
[class.is-selected]="selectedPaymentMethod() === method.id"
|
||||
[class.is-disabled]="paymentMethodDisabled()"
|
||||
>
|
||||
<input
|
||||
class="payment-method__radio"
|
||||
type="radio"
|
||||
name="payment-method"
|
||||
[value]="method.id"
|
||||
[checked]="selectedPaymentMethod() === method.id"
|
||||
[disabled]="paymentMethodDisabled()"
|
||||
(change)="selectPaymentMethod(method.id)"
|
||||
/>
|
||||
|
||||
|
||||
@@ -63,10 +63,14 @@
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
&:not(.is-disabled):hover {
|
||||
color: #4f4f4f;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
color: var(--tenant-primary, #6376f3);
|
||||
}
|
||||
@@ -79,6 +83,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.is-disabled &__radio {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&__label {
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
@@ -91,11 +99,13 @@
|
||||
font-size: 0.95rem;
|
||||
opacity: 0;
|
||||
transform: translateX(-4px);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
&.is-selected &__chevron,
|
||||
&:hover &__chevron {
|
||||
&:not(.is-disabled):hover &__chevron {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
@@ -140,6 +150,4 @@
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,12 @@ import {
|
||||
imports: [CheckoutPaymentQrComponent, CheckoutPaymentTransferComponent],
|
||||
templateUrl: './checkout-payment-step.component.html',
|
||||
styleUrl: './checkout-payment-step.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CheckoutPaymentStepComponent {
|
||||
readonly paymentMethods = input.required<ReadonlyArray<PaymentMethodOption>>();
|
||||
readonly selectedPaymentMethod = input.required<PaymentMethod>();
|
||||
readonly paymentMethodDisabled = input<boolean>(false);
|
||||
readonly copiedTransferField = input<TransferField | null>(null);
|
||||
readonly transferAccount = input<TransferAccount | null>(null);
|
||||
readonly transferDni = input<string>('');
|
||||
@@ -40,6 +41,10 @@ export class CheckoutPaymentStepComponent {
|
||||
readonly generateTransferIntent = output<string>();
|
||||
|
||||
protected selectPaymentMethod(method: PaymentMethod): void {
|
||||
if (this.paymentMethodDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.paymentMethodChange.emit(method);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user