From 938d15bf8301831edd9c421d21513f656f1c5c3c Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 21 Jul 2026 09:52:19 -0300 Subject: [PATCH] feat(checkout): implement QR payment polling and validation logic --- .../checkout-page.component.html | 4 + .../checkout-page.component.spec.ts | 200 ++++++++++++++++++ .../checkout-page/checkout-page.component.ts | 175 ++++++++++++++- .../checkout-page/checkout-page.models.ts | 2 + .../checkout-payment-step.component.html | 8 +- .../checkout-payment-step.component.ts | 16 +- .../checkout-payment-qr.component.html | 20 ++ .../checkout-payment-qr.component.scss | 45 ++++ .../checkout-payment-qr.component.spec.ts | 30 +++ .../checkout-payment-qr.component.ts | 9 +- .../checkout-payment-transfer.component.html | 13 +- .../checkout-payment-transfer.component.scss | 14 ++ .../checkout-payment-transfer.component.ts | 3 +- 13 files changed, 524 insertions(+), 15 deletions(-) create mode 100644 src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts create mode 100644 src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts 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 b7480a9..79ac560 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 @@ -19,11 +19,15 @@ [transferDni]="transferDni()" [isGeneratingIntent]="isPaymentLoading()" [qrData]="qrData()" + [qrPaymentStatus]="qrPaymentStatus()" + [isCheckingQrPayment]="isCheckingQrPayment()" + [transferValidationStatus]="transferValidationStatus()" (paymentMethodChange)="selectPaymentMethod($event)" (copyTransferValue)="copyTransferValue($event.field, $event.value)" (cancelStep)="onCancel()" (generateTransferIntent)="generateTransferIntent($event)" (complete)="onComplete()" + (retryQrPolling)="retryQrPolling()" /> 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 new file mode 100644 index 0000000..55be767 --- /dev/null +++ b/src/app/features/store/pages/checkout-page/checkout-page.component.spec.ts @@ -0,0 +1,200 @@ +import { signal } from '@angular/core'; +import { getTestBed, TestBed } from '@angular/core/testing'; +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; +import { 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'; +import { CartService } from '../../../../core/services/cart/cart.service'; +import { CheckoutService } from '../../../../core/services/checkout.service'; +import { TenantService } from '../../../../core/services/tenant.service'; +import { CheckoutPageComponent } from './checkout-page.component'; + +describe('CheckoutPageComponent payment validation', () => { + let checkoutServiceStub: { + generatePaymentIntent: ReturnType; + getPurchase: ReturnType; + }; + let cartServiceStub: { + cart: ReturnType; + isUpdating: ReturnType>; + loadCart: ReturnType; + clearCart: ReturnType; + }; + let routerStub: { navigate: ReturnType }; + + beforeAll(() => { + try { + getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); + } catch { + // Test environment may already be initialized by another setup entrypoint. + } + }); + + beforeEach(async () => { + vi.useFakeTimers(); + + checkoutServiceStub = { + generatePaymentIntent: vi.fn().mockResolvedValue({ + qr_data: { qr_code: 'qr-value' }, + }), + getPurchase: vi.fn().mockResolvedValue({ status: 'pending_payment' }), + }; + cartServiceStub = { + cart: signal({ + id: 10, + tenant_codigo: 'tenant-test', + status: 'active', + items: [], + subtotal: '0.00', + }), + isUpdating: signal(false), + loadCart: vi.fn().mockReturnValue(of({})), + clearCart: vi.fn(), + }; + cartServiceStub.clearCart.mockImplementation(() => { + cartServiceStub.cart.set({ + id: null, + tenant_codigo: 'tenant-test', + status: 'active', + items: [], + subtotal: '0.00', + }); + }); + routerStub = { navigate: vi.fn() }; + + await TestBed.configureTestingModule({ + imports: [CheckoutPageComponent], + providers: [ + { provide: CheckoutService, useValue: checkoutServiceStub }, + { provide: CartService, useValue: cartServiceStub }, + { provide: TenantService, useValue: { tenant: signal({ codigo: 'tenant-test' }) } }, + { provide: AuthService, useValue: { user: signal(null) } }, + { provide: Router, useValue: routerStub }, + ], + }) + .overrideComponent(CheckoutPageComponent, { set: { template: '' } }) + .compileComponents(); + }); + + afterEach(() => { + vi.useRealTimers(); + TestBed.resetTestingModule(); + }); + + function createComponent(): any { + const fixture = TestBed.createComponent(CheckoutPageComponent); + fixture.detectChanges(); + fixture.componentInstance['createdPurchaseId'].set(25); + return { fixture, component: fixture.componentInstance as any }; + } + + it('polls QR after five seconds and navigates only when payment is paid', async () => { + checkoutServiceStub.getPurchase + .mockResolvedValueOnce({ status: 'pending_payment' }) + .mockResolvedValueOnce({ status: 'paid' }); + const { component } = createComponent(); + + await component.selectPaymentMethod('qr'); + expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(5_000); + expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1); + expect(routerStub.navigate).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(5_000); + expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(2); + expect(cartServiceStub.clearCart).toHaveBeenCalledOnce(); + expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25]); + expect(checkoutServiceStub.generatePaymentIntent).toHaveBeenCalledTimes(1); + }); + + it('shows the QR verification state while each status request is in progress', async () => { + let resolvePurchase!: (purchase: { status: string }) => void; + checkoutServiceStub.getPurchase.mockReturnValue( + new Promise((resolve) => { + resolvePurchase = resolve; + }), + ); + const { fixture, component } = createComponent(); + + const statusRequest = component.checkQrPayment(component.qrPollingRunId); + expect(component.isCheckingQrPayment()).toBe(true); + + resolvePurchase({ status: 'pending_payment' }); + await statusRequest; + expect(component.isCheckingQrPayment()).toBe(false); + fixture.destroy(); + }); + + it('stops QR polling after five minutes and can restart it', async () => { + const { component } = createComponent(); + + await component.selectPaymentMethod('qr'); + await vi.advanceTimersByTimeAsync(300_000); + + expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(60); + expect(component.qrPaymentStatus()).toBe('timed_out'); + + component.retryQrPolling(); + expect(component.qrPaymentStatus()).toBe('waiting'); + await vi.advanceTimersByTimeAsync(5_000); + expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(61); + }); + + it('cancels QR polling when payment method changes or component is destroyed', async () => { + const first = createComponent(); + await first.component.selectPaymentMethod('qr'); + await first.component.selectPaymentMethod('transfer'); + await vi.advanceTimersByTimeAsync(10_000); + expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled(); + + const second = createComponent(); + await second.component.selectPaymentMethod('qr'); + second.fixture.destroy(); + await vi.advanceTimersByTimeAsync(10_000); + expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled(); + }); + + it('checks a transfer once and redirects to purchase status while pending', async () => { + const { component } = createComponent(); + component.selectedPaymentMethod.set('transfer'); + + await component.onComplete(); + expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1); + expect(component.transferValidationStatus()).toBe('pending'); + expect(cartServiceStub.clearCart).not.toHaveBeenCalled(); + expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25]); + + await vi.advanceTimersByTimeAsync(30_000); + expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1); + + await component.onComplete(); + expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1); + }); + + it('navigates after a transfer is confirmed as paid', async () => { + checkoutServiceStub.getPurchase.mockResolvedValue({ status: 'paid' }); + const { component } = createComponent(); + component.selectedPaymentMethod.set('transfer'); + + await component.onComplete(); + + expect(cartServiceStub.clearCart).toHaveBeenCalledOnce(); + expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25]); + }); + + it('shows a retryable state when transfer validation fails', async () => { + vi.spyOn(console, 'error').mockImplementation(() => undefined); + checkoutServiceStub.getPurchase.mockRejectedValue(new Error('network error')); + const { component } = createComponent(); + component.selectedPaymentMethod.set('transfer'); + + await component.onComplete(); + + expect(component.transferValidationStatus()).toBe('error'); + expect(cartServiceStub.clearCart).not.toHaveBeenCalled(); + expect(routerStub.navigate).not.toHaveBeenCalled(); + }); +}); 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 33115f7..19f4c69 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 @@ -4,6 +4,7 @@ import { computed, effect, inject, + OnDestroy, OnInit, signal, untracked, @@ -28,8 +29,10 @@ import { CheckoutForm, PaymentMethod, PaymentMethodOption, + QrPaymentStatus, TransferAccount, TransferField, + TransferValidationStatus, } from './checkout-page.models'; @Component({ @@ -46,13 +49,21 @@ import { styleUrl: './checkout-page.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CheckoutPageComponent implements OnInit { +export class CheckoutPageComponent implements OnInit, OnDestroy { private readonly formBuilder = inject(FormBuilder); private readonly cartService = inject(CartService); private readonly router = inject(Router); private readonly tenantService = inject(TenantService); private readonly checkoutService = inject(CheckoutService); private readonly authService = inject(AuthService); + private readonly qrPollingIntervalMs = 5_000; + private readonly qrPollingMaxAttempts = 60; + + private qrPollingTimeoutId: ReturnType | null = null; + private qrPollingAttempts = 0; + private qrPollingRunId = 0; + private paymentMethodRequestId = 0; + private navigationStarted = false; @ViewChild(StepperComponent) stepper!: StepperComponent; @@ -96,6 +107,9 @@ export class CheckoutPageComponent implements OnInit { () => this.isGeneratingIntent() || this.cartService.isUpdating(), ); protected readonly qrData = signal(null); + protected readonly qrPaymentStatus = signal('idle'); + protected readonly isCheckingQrPayment = signal(false); + protected readonly transferValidationStatus = signal('idle'); constructor() { effect(() => { @@ -105,7 +119,7 @@ export class CheckoutPageComponent implements OnInit { untracked(() => { const purchaseId = this.createdPurchaseId(); - if (cart && purchaseId) { + if (cart && purchaseId && !this.navigationStarted) { // Trigger payment intent generation when cart changes and we are on the payment step void this.selectPaymentMethod(this.selectedPaymentMethod()); } @@ -133,6 +147,10 @@ export class CheckoutPageComponent implements OnInit { this.cartService.loadCart().subscribe(); } + ngOnDestroy(): void { + this.stopQrPolling(); + } + private mapCartItemToMock(item: CartItem): CartItemMock { const fullName = item.product?.nombre ?? ''; let product = fullName; @@ -202,11 +220,24 @@ export class CheckoutPageComponent implements OnInit { } protected onCancel(): void { + this.stopQrPolling(); void this.router.navigate(['/']); } protected async selectPaymentMethod(method: PaymentMethod): Promise { + if (this.navigationStarted) { + return; + } + + this.stopQrPolling(); + this.qrPaymentStatus.set('idle'); + this.transferValidationStatus.set('idle'); this.selectedPaymentMethod.set(method); + const requestId = ++this.paymentMethodRequestId; + + if (method === 'qr') { + this.qrData.set(null); + } const purchaseId = this.createdPurchaseId(); const tenant = this.tenantService.tenant(); @@ -230,7 +261,12 @@ export class CheckoutPageComponent implements OnInit { ); if (method === 'qr' && response.qr_data?.qr_code) { + if (requestId !== this.paymentMethodRequestId || this.selectedPaymentMethod() !== 'qr') { + return; + } + this.qrData.set(response.qr_data.qr_code); + this.startQrPolling(); } } catch (error) { console.error('Failed to generate payment intent:', error); @@ -248,6 +284,7 @@ export class CheckoutPageComponent implements OnInit { } this.transferDni.set(dni); + this.transferValidationStatus.set('idle'); this.isGeneratingIntent.set(true); try { const response = await this.checkoutService.generatePaymentIntent( @@ -293,16 +330,140 @@ export class CheckoutPageComponent implements OnInit { const purchaseId = this.createdPurchaseId(); const tenant = this.tenantService.tenant(); - if (!purchaseId || !tenant) { + if ( + !purchaseId || + !tenant || + this.navigationStarted || + this.transferValidationStatus() === 'checking' + ) { return; } + this.transferValidationStatus.set('checking'); + try { - await this.checkoutService.completePurchase(tenant.codigo, purchaseId); - this.cartService.clearCart(); - void this.router.navigate(['/checkout/status', purchaseId]); + const purchase = await this.checkoutService.getPurchase(tenant.codigo, purchaseId); + + if (purchase.status === 'paid') { + this.handleConfirmedPayment(purchaseId); + return; + } + + if (purchase.status === 'pending_payment') { + this.transferValidationStatus.set('pending'); + this.navigateToPurchaseStatus(purchaseId); + return; + } + + this.transferValidationStatus.set('pending'); } catch (error) { - console.error('Failed to complete purchase:', error); + console.error('Failed to validate transfer payment:', error); + this.transferValidationStatus.set('error'); } } + + protected retryQrPolling(): void { + if (this.selectedPaymentMethod() === 'qr' && this.qrData()) { + this.startQrPolling(); + } + } + + private startQrPolling(): void { + this.stopQrPolling(); + this.qrPollingAttempts = 0; + this.qrPaymentStatus.set('waiting'); + + const runId = this.qrPollingRunId; + this.scheduleQrPoll(runId); + } + + private scheduleQrPoll(runId: number): void { + this.qrPollingTimeoutId = setTimeout(() => { + this.qrPollingTimeoutId = null; + void this.checkQrPayment(runId); + }, this.qrPollingIntervalMs); + } + + private async checkQrPayment(runId: number): Promise { + if (runId !== this.qrPollingRunId) { + return; + } + + const purchaseId = this.createdPurchaseId(); + const tenant = this.tenantService.tenant(); + + if (!purchaseId || !tenant || this.selectedPaymentMethod() !== 'qr') { + this.stopQrPolling(); + return; + } + + this.qrPollingAttempts += 1; + this.isCheckingQrPayment.set(true); + + try { + const purchase = await this.checkoutService.getPurchase(tenant.codigo, purchaseId); + + if (runId !== this.qrPollingRunId) { + return; + } + + if (purchase.status === 'paid') { + this.handleConfirmedPayment(purchaseId); + return; + } + + if (purchase.status === 'rejected' || purchase.status === 'cancelled') { + this.stopQrPolling(); + this.qrPaymentStatus.set('failed'); + return; + } + } catch (error) { + console.error('Failed to validate QR payment:', error); + } finally { + if (runId === this.qrPollingRunId) { + this.isCheckingQrPayment.set(false); + } + } + + if (runId !== this.qrPollingRunId) { + return; + } + + if (this.qrPollingAttempts >= this.qrPollingMaxAttempts) { + this.stopQrPolling(); + this.qrPaymentStatus.set('timed_out'); + return; + } + + this.scheduleQrPoll(runId); + } + + private stopQrPolling(): void { + this.qrPollingRunId += 1; + this.isCheckingQrPayment.set(false); + + if (this.qrPollingTimeoutId !== null) { + clearTimeout(this.qrPollingTimeoutId); + this.qrPollingTimeoutId = null; + } + } + + private handleConfirmedPayment(purchaseId: number): void { + this.navigateToPurchaseStatus(purchaseId, true); + } + + private navigateToPurchaseStatus(purchaseId: number, clearCart = false): void { + if (this.navigationStarted) { + return; + } + + this.navigationStarted = true; + this.stopQrPolling(); + + if (clearCart) { + this.cartService.clearCart(); + } + + void this.router.navigate(['/checkout/status', purchaseId]); + } } diff --git a/src/app/features/store/pages/checkout-page/checkout-page.models.ts b/src/app/features/store/pages/checkout-page/checkout-page.models.ts index 238be11..652a466 100644 --- a/src/app/features/store/pages/checkout-page/checkout-page.models.ts +++ b/src/app/features/store/pages/checkout-page/checkout-page.models.ts @@ -2,6 +2,8 @@ import { FormControl, FormGroup } from '@angular/forms'; export type PaymentMethod = 'qr' | 'transfer' | 'telepagos'; export type TransferField = 'cvu' | 'alias'; +export type QrPaymentStatus = 'idle' | 'waiting' | 'timed_out' | 'failed'; +export type TransferValidationStatus = 'idle' | 'checking' | 'pending' | 'error'; export interface PaymentMethodOption { id: PaymentMethod; 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 3772902..43605e3 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 @@ -40,12 +40,18 @@

Cargando información de pago...

} @else if (selectedPaymentMethod() === 'qr') { - + } @else if (selectedPaymentMethod() === 'transfer') { (''); readonly isGeneratingIntent = input(false); readonly qrData = input(null); + readonly qrPaymentStatus = input('idle'); + readonly isCheckingQrPayment = input(false); + readonly transferValidationStatus = input('idle'); readonly paymentMethodChange = output(); readonly copyTransferValue = output<{ field: TransferField; value: string }>(); 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 06e008d..75f74e2 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 @@ -10,5 +10,25 @@ } + + @if (isCheckingPayment()) { +
+ + Verificando pago +
+ } + + @if (paymentStatus() === 'timed_out') { +

+ Todavía no recibimos el pago. +

+ + Volver a verificar + + } @else 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 72a4918..5d74160 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 @@ -103,3 +103,48 @@ left: 10px; } } + +.payment-status { + margin: 1rem 0 0; + color: #6f6f6f; + font-size: 0.8rem; + font-weight: 500; + + &--warning { + margin-bottom: 0.75rem; + color: #9a6a12; + } +} + +.payment-verification { + position: absolute; + inset: 0; + z-index: 2; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.45rem; + background: rgba(255, 255, 255, 0.82); + + &__spinner { + width: 30px; + height: 30px; + border: 4px solid rgba(17, 17, 17, 0.2); + border-top-color: #111111; + border-radius: 50%; + animation: payment-verification-spin 0.75s linear infinite; + } + + &__message { + color: #111111; + font-size: 0.72rem; + font-weight: 700; + } +} + +@keyframes payment-verification-spin { + to { + transform: rotate(360deg); + } +} 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 new file mode 100644 index 0000000..3bb8378 --- /dev/null +++ b/src/app/features/store/pages/checkout-page/components/checkout-payment-qr/checkout-payment-qr.component.spec.ts @@ -0,0 +1,30 @@ +import { getTestBed, TestBed } from '@angular/core/testing'; +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; +import { beforeAll, describe, expect, it } from 'vitest'; + +import { CheckoutPaymentQrComponent } from './checkout-payment-qr.component'; + +describe('CheckoutPaymentQrComponent', () => { + beforeAll(() => { + try { + getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); + } catch { + // Test environment may already be initialized by another setup entrypoint. + } + }); + + it('renders the animated payment verification overlay over the QR', async () => { + await TestBed.configureTestingModule({ + imports: [CheckoutPaymentQrComponent], + }).compileComponents(); + + const fixture = TestBed.createComponent(CheckoutPaymentQrComponent); + fixture.componentRef.setInput('isCheckingPayment', true); + fixture.detectChanges(); + + const overlay = fixture.nativeElement.querySelector('.payment-verification') as HTMLElement; + expect(overlay).not.toBeNull(); + expect(overlay.textContent).toContain('Verificando pago'); + expect(overlay.querySelector('.payment-verification__spinner')).not.toBeNull(); + }); +}); 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 941eb0c..4c98eab 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,14 +1,19 @@ -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; import { QRCodeComponent } from '../../../../../../shared/components/qrcode/qrcode.component'; +import { ButtonComponent } from '../../../../../../shared/components/button/button.component'; +import { QrPaymentStatus } from '../../checkout-page.models'; @Component({ selector: 'app-checkout-payment-qr', standalone: true, - imports: [QRCodeComponent], + imports: [QRCodeComponent, ButtonComponent], templateUrl: './checkout-payment-qr.component.html', styleUrl: './checkout-payment-qr.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) export class CheckoutPaymentQrComponent { readonly qrData = input(null); + readonly paymentStatus = input('idle'); + readonly isCheckingPayment = input(false); + readonly retryPolling = output(); } 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 dd95c86..482eea6 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 @@ -85,10 +85,21 @@ variant="primary" hostClass="w-100" buttonClass="w-100" + [disabled]="validationStatus() === 'checking'" (click)="completePurchase.emit()" > - Ya transferí + {{ validationStatus() === 'checking' ? 'Validando pago...' : 'Ya transferí' }} + + @if (validationStatus() === 'pending') { +

+ Todavía no recibimos la transferencia. Podés volver a verificar. +

+ } @else if (validationStatus() === 'error') { + + } } 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 def5cf0..b65c086 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,3 +115,17 @@ 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 491ee08..f52e02a 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 @@ -3,7 +3,7 @@ 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 } from '../../checkout-page.models'; +import { TransferAccount, TransferField, TransferValidationStatus } from '../../checkout-page.models'; @Component({ selector: 'app-checkout-payment-transfer', @@ -17,6 +17,7 @@ export class CheckoutPaymentTransferComponent implements OnInit { readonly transferAccount = input(null); readonly transferDni = input(''); readonly copiedTransferField = input(null); + readonly validationStatus = input('idle'); readonly copyTransferValue = output<{ field: TransferField; value: string }>(); readonly submitDni = output();