feat: enhance purchase status page with ticket handling and WhatsApp integration

This commit is contained in:
2026-07-28 16:26:45 -03:00
parent 58e9dc06b1
commit 15a74bb56e
5 changed files with 216 additions and 42 deletions

View File

@@ -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;
}

View File

@@ -16,18 +16,34 @@
<hr class="status-content__divider" />
<div class="status-content__section status-content__section--secondary">
<p class="status-content__message">Comunicate con nosotros para coordinar el env&iacute;o.</p>
@if (ticketsRoute()) {
<p class="status-content__message">A continuación, vas a poder ver los tickets que debés presentar en el evento.</p>
<app-button
type="button"
variant="secondary"
hostClass="status-content__button d-block"
buttonClass="status-content__button-control d-inline-flex align-items-center justify-content-center gap-2 py-2"
(click)="openWhatsApp()"
>
<i class="fa-brands fa-whatsapp status-content__button-icon"></i>
<span>WhatsApp</span>
</app-button>
<app-button
type="button"
variant="primary"
hostClass="status-content__button d-block"
buttonClass="status-content__button-control d-inline-flex align-items-center justify-content-center gap-2 py-2"
(click)="goToTickets()"
>
<span>Mis tickets</span>
</app-button>
} @else if (whatsappUrl()) {
<p class="status-content__message">Comunicate con nosotros para coordinar el env&iacute;o.</p>
<app-button
type="button"
variant="secondary"
hostClass="status-content__button d-block"
buttonClass="status-content__button-control d-inline-flex align-items-center justify-content-center gap-2 py-2"
(click)="openWhatsApp()"
>
<i class="fa-brands fa-whatsapp status-content__button-icon"></i>
<span>WhatsApp</span>
</app-button>
} @else {
<p class="status-content__message">Tu compra qued&oacute; registrada correctamente.</p>
}
</div>
} @else if (status() === 'pending') {
<div class="status-content__section status-content__section--primary">
@@ -71,18 +87,20 @@
<div class="status-content__section status-content__section--secondary">
<p class="status-content__message">Si ya pagaste, escribinos para que podamos revisarlo.</p>
<div class="status-content__actions">
<app-button
type="button"
variant="secondary"
hostClass="status-content__button d-block"
buttonClass="status-content__button-control d-inline-flex align-items-center justify-content-center gap-2 py-2"
(click)="openWhatsApp()"
>
<i class="fa-brands fa-whatsapp status-content__button-icon"></i>
<span>WhatsApp</span>
</app-button>
</div>
@if (whatsappUrl()) {
<div class="status-content__actions">
<app-button
type="button"
variant="secondary"
hostClass="status-content__button d-block"
buttonClass="status-content__button-control d-inline-flex align-items-center justify-content-center gap-2 py-2"
(click)="openWhatsApp()"
>
<i class="fa-brands fa-whatsapp status-content__button-icon"></i>
<span>WhatsApp</span>
</app-button>
</div>
}
</div>
} @else {
<div class="status-content__section status-content__section--primary">
@@ -98,18 +116,20 @@
<div class="status-content__section status-content__section--secondary">
<p class="status-content__message">Volv&eacute; a ingresar m&aacute;s tarde. Si el problema sigue, comunicate con nosotros.</p>
<div class="status-content__actions">
<app-button
type="button"
variant="secondary"
hostClass="status-content__button d-block"
buttonClass="status-content__button-control d-inline-flex align-items-center justify-content-center gap-2 py-2"
(click)="openWhatsApp()"
>
<i class="fa-brands fa-whatsapp status-content__button-icon"></i>
<span>WhatsApp</span>
</app-button>
</div>
@if (whatsappUrl()) {
<div class="status-content__actions">
<app-button
type="button"
variant="secondary"
hostClass="status-content__button d-block"
buttonClass="status-content__button-control d-inline-flex align-items-center justify-content-center gap-2 py-2"
(click)="openWhatsApp()"
>
<i class="fa-brands fa-whatsapp status-content__button-icon"></i>
<span>WhatsApp</span>
</app-button>
</div>
}
</div>
}
</div>

View File

@@ -98,7 +98,7 @@
font-size: 13px;
font-weight: 325;
line-height: 1.45;
color: var(--color-text);
color: #666666;
}
&__hint {

View File

@@ -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<HTMLButtonElement>('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<HTMLButtonElement>('app-button button')?.click();
expect(openSpy).toHaveBeenCalledWith(
'https://wa.me/543411234567',
'_blank',
'noopener,noreferrer',
);
openSpy.mockRestore();
});
});

View File

@@ -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<PurchaseStatusView>('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');
}
}
}