feat(checkout-page): implement countdown expiration check and purchase validation
This commit is contained in:
@@ -159,6 +159,64 @@ describe('CheckoutPageComponent payment validation', () => {
|
|||||||
expect(component.checkoutRemainingTime()).toBe('10:00');
|
expect(component.checkoutRemainingTime()).toBe('10:00');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('checks the purchase when the countdown expires and redirects to the expired status', async () => {
|
||||||
|
const countdown = TestBed.inject(CheckoutCountdownService);
|
||||||
|
checkoutServiceStub.getPurchase.mockResolvedValue({ status: 'expired' });
|
||||||
|
const { fixture, component } = createComponent();
|
||||||
|
component.createdPurchase.set({
|
||||||
|
id: 25,
|
||||||
|
status: 'created',
|
||||||
|
expires_at: new Date(Date.now() + 1_000).toISOString(),
|
||||||
|
expires_in_seconds: 1,
|
||||||
|
server_time: new Date().toISOString(),
|
||||||
|
items: [],
|
||||||
|
subtotal: '0.00',
|
||||||
|
total: '0.00',
|
||||||
|
});
|
||||||
|
countdown.synchronize(component.createdPurchase());
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(1_000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(checkoutServiceStub.getPurchase).toHaveBeenCalledOnce();
|
||||||
|
expect(checkoutServiceStub.getPurchase).toHaveBeenCalledWith('tenant-test', 25);
|
||||||
|
expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25], {
|
||||||
|
queryParams: { status: 'expired' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('checks the purchase only once when the expired countdown remains at zero', async () => {
|
||||||
|
const countdown = TestBed.inject(CheckoutCountdownService);
|
||||||
|
checkoutServiceStub.getPurchase.mockResolvedValue({
|
||||||
|
status: 'pending_payment',
|
||||||
|
expires_at: new Date(Date.now() - 1_000).toISOString(),
|
||||||
|
expires_in_seconds: 0,
|
||||||
|
server_time: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
const { fixture, component } = createComponent();
|
||||||
|
component.createdPurchase.set({
|
||||||
|
id: 25,
|
||||||
|
status: 'pending_payment',
|
||||||
|
expires_at: new Date(Date.now() - 1_000).toISOString(),
|
||||||
|
expires_in_seconds: 0,
|
||||||
|
server_time: new Date().toISOString(),
|
||||||
|
items: [],
|
||||||
|
subtotal: '0.00',
|
||||||
|
total: '0.00',
|
||||||
|
});
|
||||||
|
countdown.synchronize(component.createdPurchase());
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
await Promise.resolve();
|
||||||
|
fixture.detectChanges();
|
||||||
|
await vi.advanceTimersByTimeAsync(5_000);
|
||||||
|
|
||||||
|
expect(checkoutServiceStub.getPurchase).toHaveBeenCalledOnce();
|
||||||
|
expect(routerStub.navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('polls QR after five seconds and navigates only when payment is paid', async () => {
|
it('polls QR after five seconds and navigates only when payment is paid', async () => {
|
||||||
checkoutServiceStub.getPurchase
|
checkoutServiceStub.getPurchase
|
||||||
.mockResolvedValueOnce({ status: 'pending_payment' })
|
.mockResolvedValueOnce({ status: 'pending_payment' })
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
computed,
|
computed,
|
||||||
|
effect,
|
||||||
inject,
|
inject,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
@@ -86,6 +87,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
private paymentMethodRequestId = 0;
|
private paymentMethodRequestId = 0;
|
||||||
private navigationStarted = false;
|
private navigationStarted = false;
|
||||||
private cancelPurchasePromise: Promise<boolean> | null = null;
|
private cancelPurchasePromise: Promise<boolean> | null = null;
|
||||||
|
private expirationCheckPurchaseId: number | null = null;
|
||||||
|
|
||||||
@ViewChild(StepperComponent) stepper!: StepperComponent;
|
@ViewChild(StepperComponent) stepper!: StepperComponent;
|
||||||
|
|
||||||
@@ -176,6 +178,28 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
const remainingSeconds = this.checkoutCountdownService.remainingSeconds();
|
||||||
|
const purchaseId = this.createdPurchaseId() ?? this.createdPurchase()?.id ?? null;
|
||||||
|
|
||||||
|
if (remainingSeconds !== null && remainingSeconds > 0) {
|
||||||
|
this.expirationCheckPurchaseId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
remainingSeconds !== 0 ||
|
||||||
|
purchaseId === null ||
|
||||||
|
this.navigationStarted ||
|
||||||
|
this.expirationCheckPurchaseId === purchaseId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.expirationCheckPurchaseId = purchaseId;
|
||||||
|
void this.checkPurchaseAfterCountdownExpiration(purchaseId);
|
||||||
|
});
|
||||||
|
|
||||||
this.form.statusChanges
|
this.form.statusChanges
|
||||||
.pipe(startWith(this.form.status))
|
.pipe(startWith(this.form.status))
|
||||||
.subscribe(() => this.isStep1Valid.set(this.form.valid));
|
.subscribe(() => this.isStep1Valid.set(this.form.valid));
|
||||||
@@ -719,6 +743,35 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
void this.router.navigate(['/checkout/status', purchaseId]);
|
void this.router.navigate(['/checkout/status', purchaseId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkPurchaseAfterCountdownExpiration(purchaseId: number): Promise<void> {
|
||||||
|
const tenant = this.tenantService.tenant();
|
||||||
|
|
||||||
|
if (!tenant || this.navigationStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const purchase = await this.checkoutService
|
||||||
|
.withCustomLoading()
|
||||||
|
.getPurchase(tenant.codigo, purchaseId);
|
||||||
|
|
||||||
|
if (this.navigationStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (purchase.status === 'expired') {
|
||||||
|
this.navigateToExpiredPurchaseStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkoutCountdownService.synchronize(purchase);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.isPurchaseExpiredError(error)) {
|
||||||
|
this.navigateToExpiredPurchaseStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async loadPurchase(purchaseId: number): Promise<void> {
|
private async loadPurchase(purchaseId: number): Promise<void> {
|
||||||
const tenant = this.tenantService.tenant();
|
const tenant = this.tenantService.tenant();
|
||||||
if (!tenant) {
|
if (!tenant) {
|
||||||
|
|||||||
Reference in New Issue
Block a user