diff --git a/src/app/core/services/checkout.service.ts b/src/app/core/services/checkout.service.ts index ab22b4e..fdb9d08 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; } @@ -199,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/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 { 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..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({ @@ -160,19 +162,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 () => { @@ -189,25 +191,19 @@ describe('CheckoutPageComponent payment validation', () => { expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled(); }); - it('checks a transfer once and redirects to purchase status while 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).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' }); + 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'); @@ -360,16 +356,12 @@ 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'); }); - 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 502a303..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 @@ -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 @@ -432,22 +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); - return; - } - - if (purchase.status === 'pending_payment') { - this.transferValidationStatus.set('pending'); + if (purchase.status === 'in_review' || purchase.status === 'paid') { this.navigateToPurchaseStatus(purchaseId); 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'); } } @@ -575,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' || 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..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/checkout-payment-step.component.ts b/src/app/features/store/pages/checkout-page/checkout-payment-step.component.ts index 75e08c4..60b9630 100644 --- a/src/app/features/store/pages/checkout-page/checkout-payment-step.component.ts +++ b/src/app/features/store/pages/checkout-page/checkout-payment-step.component.ts @@ -30,6 +30,8 @@ export class CheckoutPaymentStepComponent { readonly qrData = input(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..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,34 +1,34 @@
-

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..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; 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..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 @@ -27,4 +27,25 @@ 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 = 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).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 4c98eab..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,19 +1,20 @@ -import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input } 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'; +import { PaymentVerificationErrorComponent } from '../payment-verification-error/payment-verification-error.component'; @Component({ selector: 'app-checkout-payment-qr', standalone: true, - imports: [QRCodeComponent, ButtonComponent], + 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); readonly paymentStatus = input('idle'); readonly isCheckingPayment = input(false); - readonly retryPolling = output(); + readonly paymentAmount = input(0); + readonly whatsappUrl = input(null); } 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'); + } + } +} 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..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') {
@@ -41,21 +57,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,30 +85,22 @@
-

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

+

Si ya pagaste, escribinos para que podamos revisarlo.

-
- - Reintentar - - - - - WhatsApp - -
+ @if (whatsappUrl()) { +
+ + + WhatsApp + +
+ }
} @else {
@@ -120,30 +114,22 @@
-

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 - - - - - 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 90b3923..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, OnDestroy, 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'; @@ -15,20 +16,31 @@ type PurchaseStatusView = 'approved' | 'pending' | 'rejected' | 'expired' | 'err styleUrl: './purchase-status-page.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) -export class PurchaseStatusPageComponent implements OnInit, OnDestroy { +export class PurchaseStatusPageComponent implements OnInit { 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 isRefreshing = signal(false); 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'); @@ -42,45 +54,23 @@ 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)); + this.hasGeneratedTickets.set(purchase.has_generated_tickets === true); } catch (error) { console.error('Failed to fetch purchase status:', error); this.status.set('error'); } finally { this.isLoading.set(false); - this.isRefreshing.set(false); } } @@ -100,33 +90,19 @@ export class PurchaseStatusPageComponent implements OnInit, OnDestroy { return 'pending'; } - private scheduleNextPoll(): void { - if (this.pollTimeoutId !== null) { - return; + protected goToTickets(): void { + const route = this.ticketsRoute(); + + if (route) { + void this.router.navigateByUrl(route); } - - 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.'); - return `https://wa.me/${phone}?text=${message}`; } protected openWhatsApp(): void { - window.open(this.getWhatsAppLink(), '_blank', 'noopener,noreferrer'); + const url = this.whatsappUrl(); + + if (url) { + window.open(url, '_blank', 'noopener,noreferrer'); + } } }