From 58e9dc06b1878d2b8ccae7bfdc0a5bbf57f45eb7 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 28 Jul 2026 16:03:17 -0300 Subject: [PATCH 1/7] refactor: simplify purchase status page by removing polling logic and updating messages --- .../purchase-status-page.component.html | 40 +------------ .../purchase-status-page.component.ts | 56 ++----------------- 2 files changed, 8 insertions(+), 88 deletions(-) diff --git a/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.html b/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.html index cb32afd..7953c02 100644 --- a/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.html +++ b/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.html @@ -41,21 +41,7 @@
-

Esta pantalla se actualiza automáticamente cuando el pago impacta.

- @if (isRefreshing()) { -

Actualizando estado...

- } - - - Actualizar estado - +

Te avisaremos cuando el pago sea confirmado.

} @else if (status() === 'expired') {
@@ -83,19 +69,9 @@
-

Si ya pagaste, podés reintentar la consulta o escribirnos para revisarlo.

+

Si ya pagaste, escribinos para que podamos revisarlo.

- - Reintentar - -
-

Reintentá en unos segundos. Si el problema sigue, comunicate con nosotros.

+

Volvé a ingresar más tarde. Si el problema sigue, comunicate con nosotros.

- - Reintentar - - | null = null; protected readonly isLoading = signal(true); - protected readonly isRefreshing = signal(false); protected readonly status = signal('pending'); ngOnInit(): void { @@ -42,45 +39,22 @@ export class PurchaseStatusPageComponent implements OnInit, OnDestroy { this.purchaseId = purchaseId; this.tenantCode = tenant.codigo; - void this.refreshStatus(true); + void this.loadStatus(); } - ngOnDestroy(): void { - this.clearScheduledPoll(); - } - - protected retryStatusCheck(): void { - void this.refreshStatus(false); - } - - private async refreshStatus(showLoader: boolean): Promise { + private async loadStatus(): Promise { if (!this.purchaseId || !this.tenantCode) { return; } - this.clearScheduledPoll(); - - if (showLoader) { - this.isLoading.set(true); - } else { - this.isRefreshing.set(true); - } - try { const purchase = await this.checkoutService.getPurchase(this.tenantCode, this.purchaseId); - const resolvedStatus = this.resolveStatus(purchase); - - this.status.set(resolvedStatus); - - if (resolvedStatus === 'pending') { - this.scheduleNextPoll(); - } + this.status.set(this.resolveStatus(purchase)); } catch (error) { console.error('Failed to fetch purchase status:', error); this.status.set('error'); } finally { this.isLoading.set(false); - this.isRefreshing.set(false); } } @@ -100,26 +74,6 @@ export class PurchaseStatusPageComponent implements OnInit, OnDestroy { return 'pending'; } - private scheduleNextPoll(): void { - if (this.pollTimeoutId !== null) { - return; - } - - this.pollTimeoutId = setTimeout(() => { - this.pollTimeoutId = null; - void this.refreshStatus(false); - }, this.pollIntervalMs); - } - - private clearScheduledPoll(): void { - if (this.pollTimeoutId === null) { - return; - } - - clearTimeout(this.pollTimeoutId); - this.pollTimeoutId = null; - } - protected getWhatsAppLink(): string { const phone = '5493416658247'; const message = encodeURIComponent('Hola! Mi compra fue realizada con \u00E9xito.'); From 15a74bb56ec8d11acb5daa875cdd3f200c7de18b Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 28 Jul 2026 16:26:45 -0300 Subject: [PATCH 2/7] feat: enhance purchase status page with ticket handling and WhatsApp integration --- src/app/core/services/checkout.service.ts | 2 + .../purchase-status-page.component.html | 90 +++++++----- .../purchase-status-page.component.scss | 2 +- .../purchase-status-page.component.spec.ts | 130 ++++++++++++++++++ .../purchase-status-page.component.ts | 34 ++++- 5 files changed, 216 insertions(+), 42 deletions(-) create mode 100644 src/app/features/store/pages/purchase-status-page/purchase-status-page.component.spec.ts diff --git a/src/app/core/services/checkout.service.ts b/src/app/core/services/checkout.service.ts index ab22b4e..1b1accd 100644 --- a/src/app/core/services/checkout.service.ts +++ b/src/app/core/services/checkout.service.ts @@ -68,6 +68,8 @@ export interface PurchaseDetailResponse extends PurchaseStatusResponse { email: string | null; items_source: 'purchase'; items: PurchaseDetailItemResponse[]; + tickets_count?: number; + has_generated_tickets?: boolean; subtotal: string; total: string; } diff --git a/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.html b/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.html index 7953c02..9018c1f 100644 --- a/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.html +++ b/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.html @@ -16,18 +16,34 @@
-

Comunicate con nosotros para coordinar el envío.

+ @if (ticketsRoute()) { +

A continuación, vas a poder ver los tickets que debés presentar en el evento.

- - - WhatsApp - + + Mis tickets + + } @else if (whatsappUrl()) { +

Comunicate con nosotros para coordinar el envío.

+ + + + WhatsApp + + } @else { +

Tu compra quedó registrada correctamente.

+ }
} @else if (status() === 'pending') {
@@ -71,18 +87,20 @@

Si ya pagaste, escribinos para que podamos revisarlo.

-
- - - WhatsApp - -
+ @if (whatsappUrl()) { +
+ + + WhatsApp + +
+ }
} @else {
@@ -98,18 +116,20 @@

Volvé a ingresar más tarde. Si el problema sigue, comunicate con nosotros.

-
- - - WhatsApp - -
+ @if (whatsappUrl()) { +
+ + + WhatsApp + +
+ }
}
diff --git a/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.scss b/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.scss index b77c4c2..ebf9bed 100644 --- a/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.scss +++ b/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.scss @@ -98,7 +98,7 @@ font-size: 13px; font-weight: 325; line-height: 1.45; - color: var(--color-text); + color: #666666; } &__hint { diff --git a/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.spec.ts b/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.spec.ts new file mode 100644 index 0000000..420a0e4 --- /dev/null +++ b/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.spec.ts @@ -0,0 +1,130 @@ +import { TestBed } from '@angular/core/testing'; +import { ActivatedRoute, convertToParamMap, Router } from '@angular/router'; + +import { CheckoutService, PurchaseDetailResponse } from '../../../../core/services/checkout.service'; +import { Tenant } from '../../../../core/services/tenant.interface'; +import { TenantService } from '../../../../core/services/tenant.service'; +import { PurchaseStatusPageComponent } from './purchase-status-page.component'; + +const tenant: Tenant = { + id: 1, + codigo: 'tickets-tenant', + nombre: 'Tickets tenant', + dominio: 'tickets.local', + primary_color: '#000000', + secondary_color: '#000000', + danger_color: '#000000', + success_color: '#000000', + header_bg_color: '#000000', + footer_bg_color: '#000000', + header_logo: '', + footer_logo: '', + categories: [], + social_media: [ + { + code: 'whatsapp', + icon: 'whatsapp', + name: 'WhatsApp', + url: 'https://wa.me/543411234567', + }, + ], + menues: [ + { + id: 1, + code: 'account', + label: 'Mi cuenta', + parent_menu_code: null, + content_type: 'dynamic', + route: '/mi-cuenta', + submenues: [ + { + id: 2, + code: 'account.tickets', + label: 'Mis tickets', + parent_menu_code: 'account', + content_type: 'dynamic', + route: '/mi-cuenta/tickets', + submenues: [], + }, + ], + }, + ], +}; + +const purchase = (hasGeneratedTickets: boolean): PurchaseDetailResponse => + ({ + status: 'paid', + has_generated_tickets: hasGeneratedTickets, + tickets_count: hasGeneratedTickets ? 1 : 0, + }) as PurchaseDetailResponse; + +describe('PurchaseStatusPageComponent', () => { + beforeEach(() => { + TestBed.resetTestingModule(); + }); + + async function render(hasGeneratedTickets: boolean) { + const checkoutService = { + getPurchase: vi.fn().mockResolvedValue(purchase(hasGeneratedTickets)), + }; + const router = { + navigate: vi.fn().mockResolvedValue(true), + navigateByUrl: vi.fn().mockResolvedValue(true), + }; + + await TestBed.configureTestingModule({ + imports: [PurchaseStatusPageComponent], + providers: [ + { provide: CheckoutService, useValue: checkoutService }, + { provide: TenantService, useValue: { tenant: () => tenant } }, + { + provide: ActivatedRoute, + useValue: { snapshot: { paramMap: convertToParamMap({ id: '42' }) } }, + }, + { provide: Router, useValue: router }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(PurchaseStatusPageComponent); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + return { + fixture, + element: fixture.nativeElement as HTMLElement, + checkoutService, + router, + }; + } + + it('shows the tickets action when this purchase generated tickets', async () => { + const { element, checkoutService, router } = await render(true); + + expect(checkoutService.getPurchase).toHaveBeenCalledOnce(); + expect(element.textContent).toContain('Ver mis tickets'); + expect(element.textContent).not.toContain('WhatsApp'); + + element.querySelector('app-button button')?.click(); + + expect(router.navigateByUrl).toHaveBeenCalledWith('/mi-cuenta/tickets'); + }); + + it('shows the tenant WhatsApp action when this purchase did not generate tickets', async () => { + const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null); + const { element } = await render(false); + + expect(element.textContent).toContain('WhatsApp'); + expect(element.textContent).not.toContain('Ver mis tickets'); + + element.querySelector('app-button button')?.click(); + + expect(openSpy).toHaveBeenCalledWith( + 'https://wa.me/543411234567', + '_blank', + 'noopener,noreferrer', + ); + + openSpy.mockRestore(); + }); +}); diff --git a/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.ts b/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.ts index a36db90..f3ad371 100644 --- a/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.ts +++ b/src/app/features/store/pages/purchase-status-page/purchase-status-page.component.ts @@ -1,7 +1,8 @@ -import { ChangeDetectionStrategy, Component, OnInit, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit, computed, inject, signal } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { CheckoutService, PurchaseStatusResponse } from '../../../../core/services/checkout.service'; +import { findMenu } from '../../../../core/services/menu.utils'; import { TenantService } from '../../../../core/services/tenant.service'; import { ButtonComponent } from '../../../../shared/components/button/button.component'; @@ -26,6 +27,20 @@ export class PurchaseStatusPageComponent implements OnInit { protected readonly isLoading = signal(true); protected readonly status = signal('pending'); + protected readonly hasGeneratedTickets = signal(false); + protected readonly ticketsRoute = computed(() => { + if (!this.hasGeneratedTickets()) { + return null; + } + + return findMenu(this.tenantService.tenant()?.menues ?? [], 'account.tickets')?.route ?? null; + }); + protected readonly whatsappUrl = computed( + () => + this.tenantService + .tenant() + ?.social_media?.find((socialMedia) => socialMedia.code === 'whatsapp')?.url ?? null, + ); ngOnInit(): void { const purchaseId = this.route.snapshot.paramMap.get('id'); @@ -50,6 +65,7 @@ export class PurchaseStatusPageComponent implements OnInit { try { const purchase = await this.checkoutService.getPurchase(this.tenantCode, this.purchaseId); this.status.set(this.resolveStatus(purchase)); + this.hasGeneratedTickets.set(purchase.has_generated_tickets === true); } catch (error) { console.error('Failed to fetch purchase status:', error); this.status.set('error'); @@ -74,13 +90,19 @@ export class PurchaseStatusPageComponent implements OnInit { return 'pending'; } - protected getWhatsAppLink(): string { - const phone = '5493416658247'; - const message = encodeURIComponent('Hola! Mi compra fue realizada con \u00E9xito.'); - return `https://wa.me/${phone}?text=${message}`; + protected goToTickets(): void { + const route = this.ticketsRoute(); + + if (route) { + void this.router.navigateByUrl(route); + } } protected openWhatsApp(): void { - window.open(this.getWhatsAppLink(), '_blank', 'noopener,noreferrer'); + const url = this.whatsappUrl(); + + if (url) { + window.open(url, '_blank', 'noopener,noreferrer'); + } } } From c233845ce07b1a5b4130a7fa02329cec6890250f Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 28 Jul 2026 16:57:00 -0300 Subject: [PATCH 3/7] feat: add WhatsApp support and improve payment timeout handling in checkout process --- .../checkout-page.component.html | 2 + .../checkout-page.component.spec.ts | 8 +- .../checkout-page/checkout-page.component.ts | 8 +- .../checkout-payment-step.component.html | 3 +- .../checkout-payment-step.component.ts | 3 +- .../checkout-payment-qr.component.html | 73 +++++++++++-------- .../checkout-payment-qr.component.scss | 37 ++++++++++ .../checkout-payment-qr.component.spec.ts | 19 +++++ .../checkout-payment-qr.component.ts | 24 +++++- 9 files changed, 136 insertions(+), 41 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 71b7365..f062cd9 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 @@ -31,6 +31,8 @@ [qrData]="qrData()" [qrPaymentStatus]="qrPaymentStatus()" [isCheckingQrPayment]="isCheckingQrPayment()" + [qrPaymentAmount]="cartTotal()" + [whatsappUrl]="whatsappUrl()" [transferValidationStatus]="transferValidationStatus()" (paymentMethodChange)="selectPaymentMethod($event)" (copyTransferValue)="copyTransferValue($event.field, $event.value)" 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 af9b7d0..3186cad 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 @@ -160,19 +160,19 @@ describe('CheckoutPageComponent payment validation', () => { fixture.destroy(); }); - it('stops QR polling after five minutes and can restart it', async () => { + it('stops QR polling after 45 seconds and can restart it', async () => { const { component } = createComponent(); await component.selectPaymentMethod('qr'); - await vi.advanceTimersByTimeAsync(300_000); + await vi.advanceTimersByTimeAsync(45_000); - expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(60); + expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(9); expect(component.qrPaymentStatus()).toBe('timed_out'); component.retryQrPolling(); expect(component.qrPaymentStatus()).toBe('waiting'); await vi.advanceTimersByTimeAsync(5_000); - expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(61); + expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(10); }); it('cancels QR polling when payment method changes or component is destroyed', 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 502a303..e5fa990 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 @@ -59,7 +59,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { private readonly authService = inject(AuthService); private readonly cartService = inject(CartService); private readonly qrPollingIntervalMs = 5_000; - private readonly qrPollingMaxAttempts = 60; + private readonly qrPollingMaxAttempts = 9; private qrPollingTimeoutId: ReturnType | null = null; private qrPollingAttempts = 0; @@ -119,6 +119,12 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { protected readonly qrPaymentStatus = signal('idle'); protected readonly isCheckingQrPayment = signal(false); protected readonly transferValidationStatus = signal('idle'); + protected readonly whatsappUrl = computed( + () => + this.tenantService + .tenant() + ?.social_media?.find((socialMedia) => socialMedia.code === 'whatsapp')?.url ?? null, + ); constructor() { this.form.statusChanges diff --git a/src/app/features/store/pages/checkout-page/checkout-payment-step.component.html b/src/app/features/store/pages/checkout-page/checkout-payment-step.component.html index 43605e3..2778b24 100644 --- a/src/app/features/store/pages/checkout-page/checkout-payment-step.component.html +++ b/src/app/features/store/pages/checkout-page/checkout-payment-step.component.html @@ -44,7 +44,8 @@ [qrData]="qrData()" [paymentStatus]="qrPaymentStatus()" [isCheckingPayment]="isCheckingQrPayment()" - (retryPolling)="retryQrPolling.emit()" + [paymentAmount]="qrPaymentAmount()" + [whatsappUrl]="whatsappUrl()" /> } @else if (selectedPaymentMethod() === 'transfer') { (null); readonly qrPaymentStatus = input('idle'); readonly isCheckingQrPayment = input(false); + readonly qrPaymentAmount = input(0); + readonly whatsappUrl = input(null); readonly transferValidationStatus = input('idle'); readonly paymentMethodChange = output(); @@ -37,7 +39,6 @@ export class CheckoutPaymentStepComponent { readonly cancelStep = output(); readonly complete = output(); readonly generateTransferIntent = output(); - readonly retryQrPolling = output(); protected selectPaymentMethod(method: PaymentMethod): void { this.paymentMethodChange.emit(method); diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.html b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.html index 75f74e2..0cfb35d 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.html +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.html @@ -1,34 +1,47 @@
-

Ingresa a tu billetera y escanea el siguiente QR

-
- -
- @if (qrData()) { - - } @else { - - - - } - - @if (isCheckingPayment()) { -
- - Verificando pago -
- } -
- @if (paymentStatus() === 'timed_out') { -

- Todavía no recibimos el pago. -

- - Volver a verificar - - } @else if (paymentStatus() === 'failed') { - + + } @else { +

Ingresa a tu billetera y escanea el siguiente QR

+
+ +
+ @if (qrData()) { + + } @else { + + + + } + + @if (isCheckingPayment()) { +
+ + Verificando pago +
+ } +
+ + @if (paymentStatus() === 'failed') { + + } }
diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.scss b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.scss index 5d74160..6ec30a9 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.scss +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.scss @@ -116,6 +116,43 @@ } } +.payment-timeout { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding-top: 0.85rem; + + &__icon { + display: grid; + width: 84px; + height: 84px; + place-items: center; + border: 1px solid var(--tenant-danger, #dc3545); + border-radius: 50%; + color: var(--tenant-danger, #dc3545); + font-size: 2.7rem; + } + + &__title { + max-width: 250px; + margin: 1.35rem 0 0; + color: var(--tenant-danger, #dc3545); + font-size: 1.05rem; + font-weight: 700; + line-height: 1.3; + } + + &__message { + max-width: 250px; + margin: 2rem 0 1.75rem; + color: #666666; + font-size: 1rem; + line-height: 1.25; + } + +} + .payment-verification { position: absolute; inset: 0; diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts index 3bb8378..f6f31dd 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts @@ -27,4 +27,23 @@ describe('CheckoutPaymentQrComponent', () => { expect(overlay.textContent).toContain('Verificando pago'); expect(overlay.querySelector('.payment-verification__spinner')).not.toBeNull(); }); + + it('shows the payment verification error and WhatsApp action after timeout', async () => { + await TestBed.configureTestingModule({ + imports: [CheckoutPaymentQrComponent], + }).compileComponents(); + + const fixture = TestBed.createComponent(CheckoutPaymentQrComponent); + fixture.componentRef.setInput('paymentStatus', 'timed_out'); + fixture.componentRef.setInput('paymentAmount', 300000); + fixture.componentRef.setInput('whatsappUrl', 'https://wa.me/543412602222'); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + const whatsapp = element.querySelector('.payment-timeout__whatsapp'); + expect(element.textContent).toMatch(/No pudimos verificar el pago de \$\s*300\.000\./); + expect(element.textContent).toContain('Por favor contactate con nosotros para resolverlo.'); + expect(whatsapp?.href).toBe('https://wa.me/543412602222'); + expect(element.textContent).not.toContain('Volver a verificar'); + }); }); diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.ts b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.ts index 4c98eab..93b19a1 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.ts +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.ts @@ -1,12 +1,12 @@ -import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { QRCodeComponent } from '../../../../../../shared/components/qrcode/qrcode.component'; +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; import { ButtonComponent } from '../../../../../../shared/components/button/button.component'; +import { QRCodeComponent } from '../../../../../../shared/components/qrcode/qrcode.component'; import { QrPaymentStatus } from '../../checkout-page.models'; @Component({ selector: 'app-checkout-payment-qr', standalone: true, - imports: [QRCodeComponent, ButtonComponent], + imports: [ButtonComponent, QRCodeComponent], templateUrl: './checkout-payment-qr.component.html', styleUrl: './checkout-payment-qr.component.scss', changeDetection: ChangeDetectionStrategy.OnPush @@ -15,5 +15,21 @@ export class CheckoutPaymentQrComponent { readonly qrData = input(null); readonly paymentStatus = input('idle'); readonly isCheckingPayment = input(false); - readonly retryPolling = output(); + readonly paymentAmount = input(0); + readonly whatsappUrl = input(null); + protected readonly formattedAmount = computed(() => + new Intl.NumberFormat('es-AR', { + style: 'currency', + currency: 'ARS', + maximumFractionDigits: 0, + }).format(this.paymentAmount()), + ); + + protected openWhatsApp(): void { + const url = this.whatsappUrl(); + + if (url) { + window.open(url, '_blank', 'noopener,noreferrer'); + } + } } From 4cb81ed70d7a45d76b5e29765b96b48f1b1ec45e Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 28 Jul 2026 17:02:12 -0300 Subject: [PATCH 4/7] feat: implement payment verification error component and update checkout flow --- .../checkout-page.component.spec.ts | 12 +- .../checkout-page/checkout-page.component.ts | 1 - .../checkout-payment-step.component.html | 17 +- .../checkout-payment-qr.component.html | 21 +- .../checkout-payment-qr.component.scss | 45 +--- .../checkout-payment-qr.component.spec.ts | 6 +- .../checkout-payment-qr.component.ts | 23 +- .../checkout-payment-transfer.component.html | 199 +++++++++--------- .../checkout-payment-transfer.component.scss | 14 -- .../checkout-payment-transfer.component.ts | 33 ++- .../payment-verification-error.component.html | 17 ++ .../payment-verification-error.component.scss | 35 +++ .../payment-verification-error.component.ts | 32 +++ 13 files changed, 243 insertions(+), 212 deletions(-) create mode 100644 src/app/features/store/pages/checkout-page/components/payment-verification-error/payment-verification-error.component.html create mode 100644 src/app/features/store/pages/checkout-page/components/payment-verification-error/payment-verification-error.component.scss create mode 100644 src/app/features/store/pages/checkout-page/components/payment-verification-error/payment-verification-error.component.ts 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 3186cad..4924c6f 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 @@ -189,7 +189,7 @@ describe('CheckoutPageComponent payment validation', () => { expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled(); }); - it('checks a transfer once and redirects to purchase status while pending', async () => { + it('shows the verification error without redirecting while a transfer is pending', async () => { const { component } = createComponent(); component.selectedPaymentMethod.set('transfer'); @@ -197,13 +197,13 @@ describe('CheckoutPageComponent payment validation', () => { expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1); expect(component.transferValidationStatus()).toBe('pending'); expect(cartServiceStub.clearCart).not.toHaveBeenCalled(); - expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25]); + expect(routerStub.navigate).not.toHaveBeenCalled(); await vi.advanceTimersByTimeAsync(30_000); expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1); await component.onComplete(); - expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1); + expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(2); }); it('navigates after a transfer is confirmed as paid', async () => { @@ -360,11 +360,7 @@ describe('CheckoutPageComponent payment validation', () => { expect(component.checkoutStepIndex()).toBe(1); expect(component.selectedPaymentMethod()).toBe('qr'); - expect(checkoutServiceStub.generatePaymentIntent).toHaveBeenCalledWith( - 'tenant-test', - 25, - 'qr', - ); + expect(checkoutServiceStub.generatePaymentIntent).toHaveBeenCalledWith('tenant-test', 25, 'qr'); expect(component.qrData()).toBe('qr-value'); expect(component.qrPaymentStatus()).toBe('waiting'); }); 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 e5fa990..35f0296 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 @@ -447,7 +447,6 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { if (purchase.status === 'pending_payment') { this.transferValidationStatus.set('pending'); - this.navigateToPurchaseStatus(purchaseId); return; } diff --git a/src/app/features/store/pages/checkout-page/checkout-payment-step.component.html b/src/app/features/store/pages/checkout-page/checkout-payment-step.component.html index 2778b24..5726668 100644 --- a/src/app/features/store/pages/checkout-page/checkout-payment-step.component.html +++ b/src/app/features/store/pages/checkout-page/checkout-payment-step.component.html @@ -1,14 +1,13 @@
-

Selecciona el metodo de pago

+

+ Selecciona el metodo de pago +

@for (method of paymentMethods(); track method.id) { -
- diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.html b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.html index 0cfb35d..820d125 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.html +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.html @@ -1,22 +1,9 @@
@if (paymentStatus() === 'timed_out') { - + } @else {

Ingresa a tu billetera y escanea el siguiente QR

diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.scss b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.scss index 6ec30a9..26e513f 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.scss +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.scss @@ -57,7 +57,9 @@ rgba(17, 17, 17, 0.95) 84% 100% ); background-size: 18px 18px; - background-position: 0 0, 9px 9px; + background-position: + 0 0, + 9px 9px; display: flex; justify-content: center; align-items: center; @@ -86,7 +88,9 @@ height: 38px; border: 5px solid #111111; background: #ffffff; - box-shadow: inset 0 0 0 8px #ffffff, inset 0 0 0 14px #111111; + box-shadow: + inset 0 0 0 8px #ffffff, + inset 0 0 0 14px #111111; &--tl { top: 10px; @@ -116,43 +120,6 @@ } } -.payment-timeout { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; - padding-top: 0.85rem; - - &__icon { - display: grid; - width: 84px; - height: 84px; - place-items: center; - border: 1px solid var(--tenant-danger, #dc3545); - border-radius: 50%; - color: var(--tenant-danger, #dc3545); - font-size: 2.7rem; - } - - &__title { - max-width: 250px; - margin: 1.35rem 0 0; - color: var(--tenant-danger, #dc3545); - font-size: 1.05rem; - font-weight: 700; - line-height: 1.3; - } - - &__message { - max-width: 250px; - margin: 2rem 0 1.75rem; - color: #666666; - font-size: 1rem; - line-height: 1.25; - } - -} - .payment-verification { position: absolute; inset: 0; diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts index f6f31dd..7eba580 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts @@ -40,10 +40,12 @@ describe('CheckoutPaymentQrComponent', () => { fixture.detectChanges(); const element = fixture.nativeElement as HTMLElement; - const whatsapp = element.querySelector('.payment-timeout__whatsapp'); + const whatsapp = Array.from(element.querySelectorAll('button')).find((button) => + button.textContent?.includes('WhatsApp'), + ); expect(element.textContent).toMatch(/No pudimos verificar el pago de \$\s*300\.000\./); expect(element.textContent).toContain('Por favor contactate con nosotros para resolverlo.'); - expect(whatsapp?.href).toBe('https://wa.me/543412602222'); + expect(whatsapp).toBeDefined(); expect(element.textContent).not.toContain('Volver a verificar'); }); }); diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.ts b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.ts index 93b19a1..221c01e 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.ts +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.ts @@ -1,15 +1,15 @@ -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { ButtonComponent } from '../../../../../../shared/components/button/button.component'; +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; import { QRCodeComponent } from '../../../../../../shared/components/qrcode/qrcode.component'; import { QrPaymentStatus } from '../../checkout-page.models'; +import { PaymentVerificationErrorComponent } from '../payment-verification-error/payment-verification-error.component'; @Component({ selector: 'app-checkout-payment-qr', standalone: true, - imports: [ButtonComponent, QRCodeComponent], + imports: [PaymentVerificationErrorComponent, QRCodeComponent], templateUrl: './checkout-payment-qr.component.html', styleUrl: './checkout-payment-qr.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CheckoutPaymentQrComponent { readonly qrData = input(null); @@ -17,19 +17,4 @@ export class CheckoutPaymentQrComponent { readonly isCheckingPayment = input(false); readonly paymentAmount = input(0); readonly whatsappUrl = input(null); - protected readonly formattedAmount = computed(() => - new Intl.NumberFormat('es-AR', { - style: 'currency', - currency: 'ARS', - maximumFractionDigits: 0, - }).format(this.paymentAmount()), - ); - - protected openWhatsApp(): void { - const url = this.whatsappUrl(); - - if (url) { - window.open(url, '_blank', 'noopener,noreferrer'); - } - } } diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-transfer/checkout-payment-transfer.component.html b/src/app/features/store/pages/checkout-page/components/checkout-payment-transfer/checkout-payment-transfer.component.html index 482eea6..1808163 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-transfer/checkout-payment-transfer.component.html +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-transfer/checkout-payment-transfer.component.html @@ -1,105 +1,106 @@
-
-

Ingresá DNI de quién va a transferir

-
-
- -
-
- @if (isDniDisabled()) { - - Modificar - - } @else { - - Continuar - - } -
-
-
- - @if (transferAccount() && !isEditing()) { -
- diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-transfer/checkout-payment-transfer.component.scss b/src/app/features/store/pages/checkout-page/components/checkout-payment-transfer/checkout-payment-transfer.component.scss index b65c086..def5cf0 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-transfer/checkout-payment-transfer.component.scss +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-transfer/checkout-payment-transfer.component.scss @@ -115,17 +115,3 @@ height: 100%; } } - -.payment-validation { - margin: 0.75rem 0 0; - font-size: 0.8rem; - line-height: 1.35; - - &--pending { - color: #8a681d; - } - - &--error { - color: #b42318; - } -} diff --git a/src/app/features/store/pages/checkout-page/components/checkout-payment-transfer/checkout-payment-transfer.component.ts b/src/app/features/store/pages/checkout-page/components/checkout-payment-transfer/checkout-payment-transfer.component.ts index f52e02a..29aed7b 100644 --- a/src/app/features/store/pages/checkout-page/components/checkout-payment-transfer/checkout-payment-transfer.component.ts +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-transfer/checkout-payment-transfer.component.ts @@ -1,23 +1,46 @@ -import { ChangeDetectionStrategy, Component, computed, input, output, signal, OnInit, effect, untracked } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + input, + output, + signal, + OnInit, + effect, + untracked, +} from '@angular/core'; import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; import { IconButtonComponent } from '../../../../../../shared/components/icon-button/icon-button.component'; import { ButtonComponent } from '../../../../../../shared/components/button/button.component'; import { InputComponent } from '../../../../../../shared/components/input/input.component'; -import { TransferAccount, TransferField, TransferValidationStatus } from '../../checkout-page.models'; +import { + TransferAccount, + TransferField, + TransferValidationStatus, +} from '../../checkout-page.models'; +import { PaymentVerificationErrorComponent } from '../payment-verification-error/payment-verification-error.component'; @Component({ selector: 'app-checkout-payment-transfer', standalone: true, - imports: [IconButtonComponent, ButtonComponent, InputComponent, ReactiveFormsModule], + imports: [ + IconButtonComponent, + ButtonComponent, + InputComponent, + ReactiveFormsModule, + PaymentVerificationErrorComponent, + ], templateUrl: './checkout-payment-transfer.component.html', styleUrl: './checkout-payment-transfer.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CheckoutPaymentTransferComponent implements OnInit { readonly transferAccount = input(null); readonly transferDni = input(''); readonly copiedTransferField = input(null); readonly validationStatus = input('idle'); + readonly paymentAmount = input(0); + readonly whatsappUrl = input(null); readonly copyTransferValue = output<{ field: TransferField; value: string }>(); readonly submitDni = output(); @@ -25,7 +48,7 @@ export class CheckoutPaymentTransferComponent implements OnInit { protected readonly dniControl = new FormControl('', { nonNullable: true, - validators: [Validators.required, Validators.pattern(/^\d{7,8}$/)] + validators: [Validators.required, Validators.pattern(/^\d{7,8}$/)], }); protected readonly isEditing = signal(false); diff --git a/src/app/features/store/pages/checkout-page/components/payment-verification-error/payment-verification-error.component.html b/src/app/features/store/pages/checkout-page/components/payment-verification-error/payment-verification-error.component.html new file mode 100644 index 0000000..64e0374 --- /dev/null +++ b/src/app/features/store/pages/checkout-page/components/payment-verification-error/payment-verification-error.component.html @@ -0,0 +1,17 @@ + diff --git a/src/app/features/store/pages/checkout-page/components/payment-verification-error/payment-verification-error.component.scss b/src/app/features/store/pages/checkout-page/components/payment-verification-error/payment-verification-error.component.scss new file mode 100644 index 0000000..a82da35 --- /dev/null +++ b/src/app/features/store/pages/checkout-page/components/payment-verification-error/payment-verification-error.component.scss @@ -0,0 +1,35 @@ +.payment-timeout { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding-top: 0.85rem; + + &__icon { + display: grid; + width: 84px; + height: 84px; + place-items: center; + border: 1px solid var(--tenant-danger, #dc3545); + border-radius: 50%; + color: var(--tenant-danger, #dc3545); + font-size: 2.7rem; + } + + &__title { + max-width: 250px; + margin: 1.35rem 0 0; + color: var(--tenant-danger, #dc3545); + font-size: 1.05rem; + font-weight: 700; + line-height: 1.3; + } + + &__message { + max-width: 250px; + margin: 2rem 0 1.75rem; + color: #666666; + font-size: 1rem; + line-height: 1.25; + } +} diff --git a/src/app/features/store/pages/checkout-page/components/payment-verification-error/payment-verification-error.component.ts b/src/app/features/store/pages/checkout-page/components/payment-verification-error/payment-verification-error.component.ts new file mode 100644 index 0000000..7ec1e4f --- /dev/null +++ b/src/app/features/store/pages/checkout-page/components/payment-verification-error/payment-verification-error.component.ts @@ -0,0 +1,32 @@ +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; + +import { ButtonComponent } from '../../../../../../shared/components/button/button.component'; + +@Component({ + selector: 'app-payment-verification-error', + standalone: true, + imports: [ButtonComponent], + templateUrl: './payment-verification-error.component.html', + styleUrl: './payment-verification-error.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PaymentVerificationErrorComponent { + readonly paymentAmount = input(0); + readonly whatsappUrl = input(null); + + protected readonly formattedAmount = computed(() => + new Intl.NumberFormat('es-AR', { + style: 'currency', + currency: 'ARS', + maximumFractionDigits: 0, + }).format(this.paymentAmount()), + ); + + protected openWhatsApp(): void { + const url = this.whatsappUrl(); + + if (url) { + window.open(url, '_blank', 'noopener,noreferrer'); + } + } +} From 76327d34ec7a8b9e4b583033f96abbe2f65f33ff Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 29 Jul 2026 08:33:54 -0300 Subject: [PATCH 5/7] refactor: update styles for category items page header and title --- .../category-items-page/category-items-page.component.scss | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/features/store/pages/category-items-page/category-items-page.component.scss b/src/app/features/store/pages/category-items-page/category-items-page.component.scss index f756dd7..e282747 100644 --- a/src/app/features/store/pages/category-items-page/category-items-page.component.scss +++ b/src/app/features/store/pages/category-items-page/category-items-page.component.scss @@ -3,15 +3,14 @@ align-items: center; justify-content: center; padding: 16px 28px; - background-color: #d9d9d9; } .category-items__title { - color: #313131; - font-size: 30px; - font-weight: 400; + font-size: 24px; + font-weight: bold; line-height: 1.2; text-align: center; + color:var(--bs-primary) } .category-items__alert-error { From 9bb3374ba0443492d78df44b3966a681933c9639 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 29 Jul 2026 09:20:17 -0300 Subject: [PATCH 6/7] feat: add submitPurchaseForReview method and update checkout flow to handle purchase review status --- src/app/core/services/checkout.service.ts | 19 ++++++++++++++++ .../checkout-page.component.spec.ts | 22 ++++++++----------- .../checkout-page/checkout-page.component.ts | 19 ++++++++-------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/app/core/services/checkout.service.ts b/src/app/core/services/checkout.service.ts index 1b1accd..fdb9d08 100644 --- a/src/app/core/services/checkout.service.ts +++ b/src/app/core/services/checkout.service.ts @@ -201,6 +201,25 @@ export class CheckoutService { }; } + async submitPurchaseForReview( + tenantCode: string, + purchaseId: number, + ): Promise { + const response = await firstValueFrom( + this.http.post<{ data?: PurchaseStatusResponse } | PurchaseStatusResponse>( + `${environment.url}tenants/${tenantCode}/compras/${purchaseId}/review`, + {}, + ), + ); + + const purchase = this.extractResponseData(response); + if (!purchase) { + throw new Error('Error al enviar la compra a revisi\u00f3n.'); + } + + return { status: purchase.status ?? null }; + } + async cancelPurchase(tenantCode: string, purchaseId: number): Promise { const response = await firstValueFrom( this.http.post<{ data?: PurchaseStatusResponse } | PurchaseStatusResponse>( 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 4924c6f..b9f4845 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 @@ -21,6 +21,7 @@ describe('CheckoutPageComponent payment validation', () => { cancelPurchase: ReturnType; generatePaymentIntent: ReturnType; getPurchase: ReturnType; + submitPurchaseForReview: ReturnType; }; let cartServiceStub: { cart: ReturnType; @@ -58,6 +59,7 @@ describe('CheckoutPageComponent payment validation', () => { qr_data: { qr_code: 'qr-value' }, }), getPurchase: vi.fn().mockResolvedValue({ status: 'pending_payment' }), + submitPurchaseForReview: vi.fn().mockResolvedValue({ status: 'in_review' }), }; cartServiceStub = { cart: signal({ @@ -189,25 +191,19 @@ describe('CheckoutPageComponent payment validation', () => { expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled(); }); - it('shows the verification error without redirecting while a transfer is pending', async () => { + it('submits a transfer for review and navigates to its status page', async () => { const { component } = createComponent(); component.selectedPaymentMethod.set('transfer'); await component.onComplete(); - expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1); - expect(component.transferValidationStatus()).toBe('pending'); + + expect(checkoutServiceStub.submitPurchaseForReview).toHaveBeenCalledWith('tenant-test', 25); expect(cartServiceStub.clearCart).not.toHaveBeenCalled(); - expect(routerStub.navigate).not.toHaveBeenCalled(); - - await vi.advanceTimersByTimeAsync(30_000); - expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1); - - await component.onComplete(); - expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(2); + expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25]); }); it('navigates after a transfer is confirmed as paid', async () => { - checkoutServiceStub.getPurchase.mockResolvedValue({ status: 'paid' }); + checkoutServiceStub.submitPurchaseForReview.mockResolvedValue({ status: 'paid' }); const { component } = createComponent(); component.selectedPaymentMethod.set('transfer'); @@ -219,7 +215,7 @@ describe('CheckoutPageComponent payment validation', () => { it('shows a retryable state when transfer validation fails', async () => { vi.spyOn(console, 'error').mockImplementation(() => undefined); - checkoutServiceStub.getPurchase.mockRejectedValue(new Error('network error')); + checkoutServiceStub.submitPurchaseForReview.mockRejectedValue(new Error('network error')); const { component } = createComponent(); component.selectedPaymentMethod.set('transfer'); @@ -365,7 +361,7 @@ describe('CheckoutPageComponent payment validation', () => { expect(component.qrPaymentStatus()).toBe('waiting'); }); - it.each(['paid', 'cancelled', 'rejected', 'expired'])( + it.each(['in_review', 'paid', 'cancelled', 'rejected', 'expired'])( 'redirects a %s purchase to its status page', async (status) => { routeQueryParamMap = convertToParamMap({ purchase: 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 35f0296..d802a94 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 @@ -438,21 +438,19 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { this.transferValidationStatus.set('checking'); try { - const purchase = await this.checkoutService.getPurchase(tenant.codigo, purchaseId); + const purchase = await this.checkoutService.submitPurchaseForReview( + tenant.codigo, + purchaseId, + ); - if (purchase.status === 'paid') { - this.handleConfirmedPayment(purchaseId); + if (purchase.status === 'in_review' || purchase.status === 'paid') { + this.navigateToPurchaseStatus(purchaseId); return; } - if (purchase.status === 'pending_payment') { - this.transferValidationStatus.set('pending'); - return; - } - - this.transferValidationStatus.set('pending'); + this.transferValidationStatus.set('error'); } catch (error) { - console.error('Failed to validate transfer payment:', error); + console.error('Failed to submit purchase for review:', error); this.transferValidationStatus.set('error'); } } @@ -580,6 +578,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { const purchase = await this.checkoutService.getPurchase(tenant.codigo, purchaseId); if ( + purchase.status === 'in_review' || purchase.status === 'paid' || purchase.status === 'cancelled' || purchase.status === 'rejected' || From 109a1cab5a86af49e3db2251cd8c54cd70abdea5 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 29 Jul 2026 09:36:22 -0300 Subject: [PATCH 7/7] refactor: update payment validation logic to handle transfer statuses more effectively --- .../checkout-page.component.spec.ts | 24 ++++++++++++------- .../checkout-page/checkout-page.component.ts | 2 +- 2 files changed, 16 insertions(+), 10 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 b9f4845..bdcdd71 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 @@ -191,24 +191,30 @@ describe('CheckoutPageComponent payment validation', () => { expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled(); }); - it('submits a transfer for review and navigates to its status page', async () => { - const { component } = createComponent(); - component.selectedPaymentMethod.set('transfer'); + it.each(['created', 'pending_payment', 'in_review'])( + 'shows the payment error when a transfer remains %s', + async (status) => { + checkoutServiceStub.submitPurchaseForReview.mockResolvedValue({ status }); + const { component } = createComponent(); + component.selectedPaymentMethod.set('transfer'); - await component.onComplete(); + await component.onComplete(); - expect(checkoutServiceStub.submitPurchaseForReview).toHaveBeenCalledWith('tenant-test', 25); - expect(cartServiceStub.clearCart).not.toHaveBeenCalled(); - expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25]); - }); + expect(checkoutServiceStub.submitPurchaseForReview).toHaveBeenCalledWith('tenant-test', 25); + expect(component.transferValidationStatus()).toBe('error'); + expect(cartServiceStub.clearCart).not.toHaveBeenCalled(); + expect(routerStub.navigate).not.toHaveBeenCalled(); + }, + ); - it('navigates after a transfer is confirmed as paid', async () => { + it('submits a paid transfer and navigates to its confirmation page', async () => { checkoutServiceStub.submitPurchaseForReview.mockResolvedValue({ status: 'paid' }); const { component } = createComponent(); component.selectedPaymentMethod.set('transfer'); await component.onComplete(); + expect(checkoutServiceStub.submitPurchaseForReview).toHaveBeenCalledWith('tenant-test', 25); expect(cartServiceStub.clearCart).not.toHaveBeenCalled(); expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 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 d802a94..21db68e 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 @@ -443,7 +443,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy { purchaseId, ); - if (purchase.status === 'in_review' || purchase.status === 'paid') { + if (purchase.status === 'paid') { this.navigateToPurchaseStatus(purchaseId); return; }