feat(checkout): implement purchase creation and payment intent generation with QR code support

This commit is contained in:
2026-07-06 11:26:39 -03:00
parent 1df6964859
commit 69fee4e20a
9 changed files with 436 additions and 45 deletions

View File

@@ -11,13 +11,23 @@ import { BankAccount } from './tenant.interface';
export class CheckoutService {
private readonly http = inject(HttpClient);
async getSelectedBankAccount(tenantCode: string): Promise<BankAccount> {
async createPurchase(tenantCode: string, payload: any): Promise<{ id: number }> {
const response = await firstValueFrom(
this.http.get<{ data: BankAccount }>(`${environment.url}tenants/${tenantCode}/bank-accounts/selected`)
this.http.post<{ data: { id: number } }>(`${environment.url}tenants/${tenantCode}/compras`, payload)
);
if (!response?.data) {
throw new Error('No se encontraron los datos de la cuenta bancaria seleccionada.');
throw new Error('Error al crear la compra.');
}
return response.data;
}
async generatePaymentIntent(tenantCode: string, purchaseId: number, method: 'qr' | 'transfer' | 'telepagos'): Promise<any> {
const response = await firstValueFrom(
this.http.post<any>(`${environment.url}tenants/${tenantCode}/compras/${purchaseId}/payment-intent`, { method })
);
if (!response) {
throw new Error('Error al generar la intención de pago.');
}
return response;
}
}

View File

@@ -16,6 +16,8 @@
[selectedPaymentMethod]="selectedPaymentMethod()"
[copiedTransferField]="copiedTransferField()"
[transferAccount]="transferAccount()"
[isGeneratingIntent]="isGeneratingIntent()"
[qrData]="qrData()"
(paymentMethodChange)="selectPaymentMethod($event)"
(copyTransferValue)="copyTransferValue($event.field, $event.value)"
(cancelStep)="onCancel()"

View File

@@ -63,7 +63,7 @@ export class CheckoutPageComponent {
protected readonly isStep1Valid = signal(this.form.valid);
protected readonly paymentMethods: ReadonlyArray<PaymentMethodOption> = [
{ id: 'qr', label: 'QR' },
{ id: 'transferencia', label: 'Transferencia' },
{ id: 'transfer', label: 'Transferencia' },
{ id: 'telepagos', label: 'TelePagos' }
];
protected readonly selectedPaymentMethod = signal<PaymentMethod>('qr');
@@ -75,29 +75,15 @@ export class CheckoutPageComponent {
alias: 'telepagos.ar'
});
protected readonly isCreatingPurchase = signal(false);
protected readonly createdPurchaseId = signal<number | null>(null);
protected readonly isGeneratingIntent = signal(false);
protected readonly qrData = signal<string | null>(null);
constructor() {
this.form.statusChanges
.pipe(startWith(this.form.status))
.subscribe(() => this.isStep1Valid.set(this.form.valid));
void this.loadSelectedBankAccount();
}
private async loadSelectedBankAccount(): Promise<void> {
const tenant = this.tenantService.tenant();
if (!tenant) return;
try {
const data = await this.checkoutService.getSelectedBankAccount(tenant.codigo);
this.transferAccount.set({
titular: data.titular,
entidad: data.entidad,
cvu: data.cvu,
alias: data.alias
});
} catch (error) {
console.error('Failed to load selected bank account:', error);
}
}
private mapCartItemToMock(item: CartItem): CartItemMock {
@@ -130,16 +116,76 @@ export class CheckoutPageComponent {
};
}
protected onStep1Continue(): void {
this.stepper.next();
protected async onStep1Continue(): Promise<void> {
if (this.form.invalid) return;
const tenant = this.tenantService.tenant();
if (!tenant) return;
this.isCreatingPurchase.set(true);
try {
const cartItems = this.cartService.cart()?.items || [];
const formValue = this.form.getRawValue();
const payload = {
...formValue,
nombre_apellido: formValue.nombre,
items: cartItems.map((item) => ({
producto_variante_id: item.product_variant_id,
cantidad: item.cantidad
}))
};
const response = await this.checkoutService.createPurchase(tenant.codigo, payload);
this.createdPurchaseId.set(response.id);
this.stepper.next();
// Auto trigger intent for default option
void this.selectPaymentMethod(this.selectedPaymentMethod());
} catch (error) {
console.error('Failed to create purchase:', error);
// Here we could show an alert or toast
} finally {
this.isCreatingPurchase.set(false);
}
}
protected onCancel(): void {
void this.router.navigate(['/']);
}
protected selectPaymentMethod(method: PaymentMethod): void {
protected async selectPaymentMethod(method: PaymentMethod): Promise<void> {
this.selectedPaymentMethod.set(method);
const purchaseId = this.createdPurchaseId();
const tenant = this.tenantService.tenant();
if (!purchaseId || !tenant || method === 'telepagos') {
return;
}
this.isGeneratingIntent.set(true);
try {
const response = await this.checkoutService.generatePaymentIntent(tenant.codigo, purchaseId, method);
if (method === 'qr' && response.qr_data?.qr_code) {
this.qrData.set(response.qr_data.qr_code);
} else if (method === 'transfer' && response.transfer_data) {
this.transferAccount.set({
titular: response.transfer_data.titular,
entidad: response.transfer_data.entidad,
cvu: response.transfer_data.cvu,
alias: response.transfer_data.alias
});
}
} catch (error) {
console.error('Failed to generate payment intent:', error);
} finally {
this.isGeneratingIntent.set(false);
}
}
protected async copyTransferValue(field: TransferField, value: string): Promise<void> {

View File

@@ -1,6 +1,6 @@
import { FormControl, FormGroup } from '@angular/forms';
export type PaymentMethod = 'qr' | 'transferencia' | 'telepagos';
export type PaymentMethod = 'qr' | 'transfer' | 'telepagos';
export type TransferField = 'cvu' | 'alias';
export interface PaymentMethodOption {

View File

@@ -35,18 +35,26 @@
</section>
<section class="payment-panel" aria-live="polite">
@if (selectedPaymentMethod() === 'qr') {
@if (isGeneratingIntent()) {
<div class="payment-panel__card" style="text-align: center; padding: 2rem;">
<h3 class="payment-panel__title">Cargando información de pago...</h3>
</div>
} @else if (selectedPaymentMethod() === 'qr') {
<div class="payment-panel__card">
<h3 class="payment-panel__title">Ingresa a tu billetera y escanea el siguiente QR</h3>
<div class="payment-panel__divider"></div>
<div class="qr-code" aria-label="QR de pago">
<span class="qr-code__finder qr-code__finder--tl"></span>
<span class="qr-code__finder qr-code__finder--tr"></span>
<span class="qr-code__finder qr-code__finder--bl"></span>
@if (qrData()) {
<qrcode [qrdata]="qrData()!" [width]="200" [errorCorrectionLevel]="'M'"></qrcode>
} @else {
<span class="qr-code__finder qr-code__finder--tl"></span>
<span class="qr-code__finder qr-code__finder--tr"></span>
<span class="qr-code__finder qr-code__finder--bl"></span>
}
</div>
</div>
} @else if (selectedPaymentMethod() === 'transferencia') {
} @else if (selectedPaymentMethod() === 'transfer') {
<div class="payment-panel__card">
<h3 class="payment-panel__title">Transferi a la siguiente cuenta desde cualquier billetera</h3>
<div class="payment-panel__divider"></div>
@@ -133,6 +141,7 @@
<app-button
type="button"
hostClass="checkout-payment__action-btn"
[disabled]="isGeneratingIntent()"
>
Finalizar compra
</app-button>

View File

@@ -219,6 +219,26 @@
);
background-size: 18px 18px;
background-position: 0 0, 9px 9px;
display: flex;
justify-content: center;
align-items: center;
qrcode {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
::ng-deep canvas,
::ng-deep img {
display: block !important;
width: 100% !important;
height: 100% !important;
max-width: 100%;
max-height: 100%;
}
}
}
.qr-code__finder {

View File

@@ -1,5 +1,7 @@
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
import { QRCodeComponent } from 'angularx-qrcode';
import { ButtonComponent } from '../../../../shared/components/button/button.component';
import { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component';
import { PaymentMethod, PaymentMethodOption, TransferAccount, TransferField } from './checkout-page.models';
@@ -7,7 +9,7 @@ import { PaymentMethod, PaymentMethodOption, TransferAccount, TransferField } fr
@Component({
selector: 'app-checkout-payment-step',
standalone: true,
imports: [ButtonComponent, IconButtonComponent],
imports: [ButtonComponent, IconButtonComponent, QRCodeComponent],
templateUrl: './checkout-payment-step.component.html',
styleUrl: './checkout-payment-step.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
@@ -17,6 +19,8 @@ export class CheckoutPaymentStepComponent {
readonly selectedPaymentMethod = input.required<PaymentMethod>();
readonly copiedTransferField = input<TransferField | null>(null);
readonly transferAccount = input.required<TransferAccount>();
readonly isGeneratingIntent = input<boolean>(false);
readonly qrData = input<string | null>(null);
readonly paymentMethodChange = output<PaymentMethod>();
readonly copyTransferValue = output<{ field: TransferField; value: string }>();