From c54eb105cb627f37d82d71d3b5b579b102792c7a Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 7 Jul 2026 12:51:01 -0300 Subject: [PATCH] feat(purchase-status): enhance purchase status handling with polling and improved UI feedback --- src/app/core/services/checkout.service.ts | 57 +++++++-- .../checkout-page/checkout-page.component.ts | 16 +-- .../purchase-status-page.component.html | 108 ++++++++++++++++-- .../purchase-status-page.component.scss | 24 +++- .../purchase-status-page.component.ts | 95 ++++++++++++--- 5 files changed, 246 insertions(+), 54 deletions(-) diff --git a/src/app/core/services/checkout.service.ts b/src/app/core/services/checkout.service.ts index af5b4f3..40a3b09 100644 --- a/src/app/core/services/checkout.service.ts +++ b/src/app/core/services/checkout.service.ts @@ -12,6 +12,11 @@ export interface CreatePurchasePayload { email: string; } +export interface PurchaseStatusResponse { + payment_status: string | null; + status: string | null; +} + @Injectable({ providedIn: 'root' }) @@ -20,12 +25,16 @@ export class CheckoutService { async createPurchase(tenantCode: string, payload: CreatePurchasePayload): Promise<{ id: number }> { const response = await firstValueFrom( - this.http.post<{ data: { id: number } }>(`${environment.url}tenants/${tenantCode}/compras`, payload) + this.http.post<{ data?: { id: number }; id?: number }>(`${environment.url}tenants/${tenantCode}/compras`, payload) ); - if (!response?.data) { + + const purchase = this.extractResponseData<{ id: number }>(response); + + if (!purchase?.id) { throw new Error('Error al crear la compra.'); } - return response.data; + + return purchase; } async generatePaymentIntent(tenantCode: string, purchaseId: number, method: 'qr' | 'transfer' | 'telepagos'): Promise { @@ -38,23 +47,49 @@ export class CheckoutService { return response; } - async finalizePurchase(tenantCode: string, purchaseId: number): Promise<{ payment_status: string; status: string }> { + async finalizePurchase(tenantCode: string, purchaseId: number): Promise { const response = await firstValueFrom( - this.http.post<{ data: { payment_status: string; status: string } }>(`${environment.url}tenants/${tenantCode}/compras/${purchaseId}/finalize`, {}) + this.http.post<{ data?: PurchaseStatusResponse } | PurchaseStatusResponse>(`${environment.url}tenants/${tenantCode}/compras/${purchaseId}/finalize`, {}) ); - if (!response?.data) { + + const purchase = this.extractResponseData(response); + + if (!purchase) { throw new Error('Error al finalizar la compra.'); } - return response.data; + + return { + payment_status: purchase.payment_status ?? null, + status: purchase.status ?? null + }; } - async getPurchase(tenantCode: string, purchaseId: string | number): Promise<{ payment_status: string }> { + async getPurchase(tenantCode: string, purchaseId: string | number): Promise { const response = await firstValueFrom( - this.http.get<{ data: { payment_status: string } }>(`${environment.url}tenants/${tenantCode}/compras/${purchaseId}`) + this.http.get<{ data?: PurchaseStatusResponse } | PurchaseStatusResponse>(`${environment.url}tenants/${tenantCode}/compras/${purchaseId}`) ); - if (!response?.data) { + + const purchase = this.extractResponseData(response); + + if (!purchase) { throw new Error('Error al obtener la compra.'); } - return response.data; + + return { + payment_status: purchase.payment_status ?? null, + status: purchase.status ?? null + }; + } + + private extractResponseData(response: T | { data?: T } | null | undefined): T | null { + if (!response) { + return null; + } + + if (typeof response === 'object' && 'data' in response && response.data) { + return response.data; + } + + return response as T; } } 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 53d6303..e93177a 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 @@ -243,23 +243,11 @@ export class CheckoutPageComponent implements OnInit { protected async onFinalize(): Promise { const purchaseId = this.createdPurchaseId(); - const tenant = this.tenantService.tenant(); - if (!purchaseId || !tenant) { + if (!purchaseId) { return; } - try { - this.isGeneratingIntent.set(true); // Re-use this flag for loading state - await this.checkoutService.finalizePurchase(tenant.codigo, purchaseId); - void this.router.navigate(['/checkout/status', purchaseId]); - } catch (error) { - console.error('Failed to finalize purchase:', error); - // Even on failure, we probably want to navigate to the status page to show 'pending' or whatever state it is in. - // But maybe the endpoint returns error if not paid. For now, navigate anyway so the status page handles the state. - void this.router.navigate(['/checkout/status', purchaseId]); - } finally { - this.isGeneratingIntent.set(false); - } + void this.router.navigate(['/checkout/status', purchaseId]); } } 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 dfa4566..4ee4d66 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 @@ -30,20 +30,106 @@ } @else if (status() === 'pending') { -
- +
+
+ +
+

ESTAMOS VERIFICANDO TU PAGO

+

Tu compra ya fue registrada y estamos esperando la confirmación del pago.

-

PAGO PENDIENTE

-

Estamos esperando la confirmación de tu pago


-

Una vez que el pago sea procesado, actualizaremos el estado de tu compra.

- } @else { -
- +
+

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

+ @if (isRefreshing()) { +

Actualizando estado...

+ } + + + Actualizar estado + +
+ } @else if (status() === 'rejected') { +
+
+ +
+

NO PUDIMOS CONFIRMAR EL PAGO

+

Revisá el medio de pago o comunicate con nosotros para continuar.

+
+ +
+ +
+

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

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

NO PUDIMOS OBTENER EL ESTADO

+

Hubo un problema al consultar tu compra.

+
+ +
+ +
+

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

+ +
+ + Reintentar + + + + + WhatsApp + +
-

HUBO UN ERROR

-

No pudimos obtener el estado de tu compra

} -
\ No newline at end of file +
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 79aa713..8b8157b 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 @@ -79,6 +79,7 @@ font-weight: 700; line-height: 1.35; color: #8a8a8a; + max-width: 320px; } &__divider { @@ -92,7 +93,7 @@ } &__message { - max-width: 280px; + max-width: 320px; margin: 0; font-size: 13px; font-weight: 325; @@ -100,10 +101,29 @@ color: var(--color-text); } + &__hint { + margin: 0; + font-size: 12px; + line-height: 1.4; + color: #8a8a8a; + } + + &__actions { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + width: 100%; + } + &__button { width: auto; } + &__button-control { + min-width: 180px; + } + &__button-icon { display: inline-flex; align-items: center; @@ -113,4 +133,4 @@ font-size: 20px; line-height: 1; } -} \ No newline at end of file +} 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 2b1bdd1..1422339 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,10 +1,12 @@ -import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, inject, signal } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { CheckoutService, PurchaseStatusResponse } from '../../../../core/services/checkout.service'; import { TenantService } from '../../../../core/services/tenant.service'; -import { CheckoutService } from '../../../../core/services/checkout.service'; import { ButtonComponent } from '../../../../shared/components/button/button.component'; +type PurchaseStatusView = 'approved' | 'pending' | 'rejected' | 'error'; + @Component({ selector: 'app-purchase-status-page', standalone: true, @@ -13,14 +15,20 @@ import { ButtonComponent } from '../../../../shared/components/button/button.com styleUrl: './purchase-status-page.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) -export class PurchaseStatusPageComponent implements OnInit { +export class PurchaseStatusPageComponent implements OnInit, OnDestroy { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly checkoutService = inject(CheckoutService); private readonly tenantService = inject(TenantService); + private readonly pollIntervalMs = 5000; + private purchaseId: string | null = null; + private tenantCode: string | null = null; + private pollTimeoutId: ReturnType | null = null; + protected readonly isLoading = signal(true); - protected readonly status = signal<'approved' | 'pending' | 'error'>('pending'); + protected readonly isRefreshing = signal(false); + protected readonly status = signal('pending'); ngOnInit(): void { const purchaseId = this.route.snapshot.paramMap.get('id'); @@ -31,35 +39,90 @@ export class PurchaseStatusPageComponent implements OnInit { return; } - this.checkPurchaseStatus(tenant.codigo, purchaseId); + this.purchaseId = purchaseId; + this.tenantCode = tenant.codigo; + + void this.refreshStatus(true); } - private async checkPurchaseStatus(tenantCode: string, purchaseId: string): Promise { - try { - this.isLoading.set(true); - const purchase = await this.checkoutService.getPurchase(tenantCode, purchaseId); - const paymentStatus = purchase.payment_status; + ngOnDestroy(): void { + this.clearScheduledPoll(); + } - if (paymentStatus === 'approved') { - this.status.set('approved'); - } else { - this.status.set('pending'); + protected retryStatusCheck(): void { + void this.refreshStatus(false); + } + + private async refreshStatus(showLoader: boolean): 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(); } } catch (error) { console.error('Failed to fetch purchase status:', error); this.status.set('error'); } finally { this.isLoading.set(false); + this.isRefreshing.set(false); } } + private resolveStatus(purchase: PurchaseStatusResponse): PurchaseStatusView { + if (purchase.payment_status === 'approved' || purchase.status === 'paid') { + return 'approved'; + } + + if (purchase.payment_status === 'rejected' || purchase.status === 'cancelled') { + return 'rejected'; + } + + 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.'); + const message = encodeURIComponent('Hola! Mi compra fue realizada con \u00E9xito.'); return `https://wa.me/${phone}?text=${message}`; } protected openWhatsApp(): void { window.open(this.getWhatsAppLink(), '_blank', 'noopener,noreferrer'); } -} \ No newline at end of file +}