feat(checkout): enhance transfer payment flow with DNI input and validation
This commit is contained in:
@@ -81,9 +81,13 @@ export class CheckoutService {
|
|||||||
return purchase;
|
return purchase;
|
||||||
}
|
}
|
||||||
|
|
||||||
async generatePaymentIntent(tenantCode: string, purchaseId: number, method: 'qr' | 'transfer' | 'telepagos'): Promise<any> {
|
async generatePaymentIntent(tenantCode: string, purchaseId: number, method: 'qr' | 'transfer' | 'telepagos', payerDni?: string): Promise<any> {
|
||||||
|
const payload: any = { method };
|
||||||
|
if (payerDni) {
|
||||||
|
payload.payer_dni = payerDni;
|
||||||
|
}
|
||||||
const response = await firstValueFrom(
|
const response = await firstValueFrom(
|
||||||
this.http.post<any>(`${environment.url}tenants/${tenantCode}/compras/${purchaseId}/payment-intent`, { method })
|
this.http.post<any>(`${environment.url}tenants/${tenantCode}/compras/${purchaseId}/payment-intent`, payload)
|
||||||
);
|
);
|
||||||
if (!response) {
|
if (!response) {
|
||||||
throw new Error('Error al generar la intención de pago.');
|
throw new Error('Error al generar la intención de pago.');
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
(paymentMethodChange)="selectPaymentMethod($event)"
|
(paymentMethodChange)="selectPaymentMethod($event)"
|
||||||
(copyTransferValue)="copyTransferValue($event.field, $event.value)"
|
(copyTransferValue)="copyTransferValue($event.field, $event.value)"
|
||||||
(cancelStep)="onCancel()"
|
(cancelStep)="onCancel()"
|
||||||
|
(generateTransferIntent)="generateTransferIntent($event)"
|
||||||
(complete)="onComplete()"
|
(complete)="onComplete()"
|
||||||
/>
|
/>
|
||||||
</app-step>
|
</app-step>
|
||||||
|
|||||||
@@ -86,12 +86,7 @@ export class CheckoutPageComponent implements OnInit {
|
|||||||
];
|
];
|
||||||
protected readonly selectedPaymentMethod = signal<PaymentMethod>('qr');
|
protected readonly selectedPaymentMethod = signal<PaymentMethod>('qr');
|
||||||
protected readonly copiedTransferField = signal<TransferField | null>(null);
|
protected readonly copiedTransferField = signal<TransferField | null>(null);
|
||||||
protected readonly transferAccount = signal<TransferAccount>({
|
protected readonly transferAccount = signal<TransferAccount | null>(null);
|
||||||
titular: 'Nombre y Apellido',
|
|
||||||
entidad: 'TelePagos',
|
|
||||||
cvu: '0000000000000000000000',
|
|
||||||
alias: 'telepagos.ar',
|
|
||||||
});
|
|
||||||
|
|
||||||
protected readonly isCreatingPurchase = signal(false);
|
protected readonly isCreatingPurchase = signal(false);
|
||||||
protected readonly createdPurchaseId = signal<number | null>(null);
|
protected readonly createdPurchaseId = signal<number | null>(null);
|
||||||
@@ -219,6 +214,11 @@ export class CheckoutPageComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (method === 'transfer') {
|
||||||
|
this.transferAccount.set(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.isGeneratingIntent.set(true);
|
this.isGeneratingIntent.set(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -230,7 +230,32 @@ export class CheckoutPageComponent implements OnInit {
|
|||||||
|
|
||||||
if (method === 'qr' && response.qr_data?.qr_code) {
|
if (method === 'qr' && response.qr_data?.qr_code) {
|
||||||
this.qrData.set(response.qr_data.qr_code);
|
this.qrData.set(response.qr_data.qr_code);
|
||||||
} else if (method === 'transfer' && response.transfer_data) {
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate payment intent:', error);
|
||||||
|
} finally {
|
||||||
|
this.isGeneratingIntent.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async generateTransferIntent(dni: string): Promise<void> {
|
||||||
|
const purchaseId = this.createdPurchaseId();
|
||||||
|
const tenant = this.tenantService.tenant();
|
||||||
|
|
||||||
|
if (!purchaseId || !tenant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isGeneratingIntent.set(true);
|
||||||
|
try {
|
||||||
|
const response = await this.checkoutService.generatePaymentIntent(
|
||||||
|
tenant.codigo,
|
||||||
|
purchaseId,
|
||||||
|
'transfer',
|
||||||
|
dni
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.transfer_data) {
|
||||||
this.transferAccount.set({
|
this.transferAccount.set({
|
||||||
titular: response.transfer_data.titular,
|
titular: response.transfer_data.titular,
|
||||||
entidad: response.transfer_data.entidad,
|
entidad: response.transfer_data.entidad,
|
||||||
@@ -239,7 +264,7 @@ export class CheckoutPageComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to generate payment intent:', error);
|
console.error('Failed to generate transfer payment intent:', error);
|
||||||
} finally {
|
} finally {
|
||||||
this.isGeneratingIntent.set(false);
|
this.isGeneratingIntent.set(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
[transferAccount]="transferAccount()"
|
[transferAccount]="transferAccount()"
|
||||||
[copiedTransferField]="copiedTransferField()"
|
[copiedTransferField]="copiedTransferField()"
|
||||||
(copyTransferValue)="requestCopy($event.field, $event.value)"
|
(copyTransferValue)="requestCopy($event.field, $event.value)"
|
||||||
|
(submitDni)="generateTransferIntent.emit($event)"
|
||||||
/>
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
<app-checkout-payment-telepagos />
|
<app-checkout-payment-telepagos />
|
||||||
@@ -68,7 +69,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
hostClass="checkout-payment__action-btn"
|
hostClass="checkout-payment__action-btn"
|
||||||
buttonClass="w-100"
|
buttonClass="w-100"
|
||||||
[disabled]="isGeneratingIntent()"
|
[disabled]="isGeneratingIntent() || (selectedPaymentMethod() === 'transfer' && !transferAccount())"
|
||||||
(click)="complete.emit()"
|
(click)="complete.emit()"
|
||||||
>
|
>
|
||||||
Finalizar compra
|
Finalizar compra
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
&__card {
|
&__card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 290px;
|
max-width: 320px;
|
||||||
min-height: 269px;
|
min-height: 269px;
|
||||||
padding: 1.5rem 1.75rem;
|
padding: 1.5rem 1.75rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export class CheckoutPaymentStepComponent {
|
|||||||
readonly paymentMethods = input.required<ReadonlyArray<PaymentMethodOption>>();
|
readonly paymentMethods = input.required<ReadonlyArray<PaymentMethodOption>>();
|
||||||
readonly selectedPaymentMethod = input.required<PaymentMethod>();
|
readonly selectedPaymentMethod = input.required<PaymentMethod>();
|
||||||
readonly copiedTransferField = input<TransferField | null>(null);
|
readonly copiedTransferField = input<TransferField | null>(null);
|
||||||
readonly transferAccount = input.required<TransferAccount>();
|
readonly transferAccount = input<TransferAccount | null>(null);
|
||||||
readonly isGeneratingIntent = input<boolean>(false);
|
readonly isGeneratingIntent = input<boolean>(false);
|
||||||
readonly qrData = input<string | null>(null);
|
readonly qrData = input<string | null>(null);
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ export class CheckoutPaymentStepComponent {
|
|||||||
readonly copyTransferValue = output<{ field: TransferField; value: string }>();
|
readonly copyTransferValue = output<{ field: TransferField; value: string }>();
|
||||||
readonly cancelStep = output<void>();
|
readonly cancelStep = output<void>();
|
||||||
readonly complete = output<void>();
|
readonly complete = output<void>();
|
||||||
|
readonly generateTransferIntent = output<string>();
|
||||||
|
|
||||||
protected selectPaymentMethod(method: PaymentMethod): void {
|
protected selectPaymentMethod(method: PaymentMethod): void {
|
||||||
this.paymentMethodChange.emit(method);
|
this.paymentMethodChange.emit(method);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.payment-panel__card {
|
.payment-panel__card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 290px;
|
max-width: 320px;
|
||||||
min-height: 269px;
|
min-height: 269px;
|
||||||
padding: 1.5rem 1.75rem;
|
padding: 1.5rem 1.75rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.payment-panel__card {
|
.payment-panel__card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 290px;
|
max-width: 320px;
|
||||||
min-height: 269px;
|
min-height: 269px;
|
||||||
padding: 1.5rem 1.75rem;
|
padding: 1.5rem 1.75rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|||||||
@@ -1,46 +1,74 @@
|
|||||||
<div class="payment-panel__card">
|
<div class="payment-panel__card">
|
||||||
<h3 class="payment-panel__title">Transferi a la siguiente cuenta desde cualquier billetera</h3>
|
@if (transferAccount()) {
|
||||||
<div class="payment-panel__divider"></div>
|
<h3 class="payment-panel__title">Transferi a la siguiente cuenta desde cualquier billetera</h3>
|
||||||
|
<div class="payment-panel__divider"></div>
|
||||||
|
|
||||||
<div class="payment-panel__account">
|
<div class="payment-panel__account">
|
||||||
<p class="payment-panel__eyebrow">DATOS DE CUENTA:</p>
|
<p class="payment-panel__eyebrow">DATOS DE CUENTA:</p>
|
||||||
|
|
||||||
<div class="payment-detail">
|
<div class="payment-detail">
|
||||||
<span class="payment-detail__label">Titular:</span>
|
<span class="payment-detail__label">Titular:</span>
|
||||||
<strong class="payment-detail__value">{{ transferAccount().titular }}</strong>
|
<strong class="payment-detail__value">{{ transferAccount()!.titular }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="payment-detail">
|
||||||
|
<span class="payment-detail__label">Entidad:</span>
|
||||||
|
<strong class="payment-detail__value">{{ transferAccount()!.entidad }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="payment-detail payment-detail--copy">
|
||||||
|
<span class="payment-detail__label">CVU:</span>
|
||||||
|
<strong class="payment-detail__value">{{ transferAccount()!.cvu }}</strong>
|
||||||
|
<app-icon-button
|
||||||
|
variant="copy"
|
||||||
|
ariaLabel="Copiar CVU"
|
||||||
|
(click)="requestCopy('cvu', transferAccount()!.cvu)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (copiedTransferField() === 'cvu') {
|
||||||
|
<small class="payment-detail__feedback">CVU copiado</small>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="payment-detail payment-detail--copy">
|
||||||
|
<span class="payment-detail__label">Alias:</span>
|
||||||
|
<strong class="payment-detail__value">{{ transferAccount()!.alias }}</strong>
|
||||||
|
<app-icon-button
|
||||||
|
variant="copy"
|
||||||
|
ariaLabel="Copiar alias"
|
||||||
|
(click)="requestCopy('alias', transferAccount()!.alias)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (copiedTransferField() === 'alias') {
|
||||||
|
<small class="payment-detail__feedback">Alias copiado</small>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
} @else {
|
||||||
<div class="payment-detail">
|
<div class="dni-form-container">
|
||||||
<span class="payment-detail__label">Entidad:</span>
|
<h3 class="dni-form-title">Ingresá DNI de quién va a transferir</h3>
|
||||||
<strong class="payment-detail__value">{{ transferAccount().entidad }}</strong>
|
<form class="dni-form row gx-2" (ngSubmit)="onSubmitDni()">
|
||||||
|
<div class="col-8">
|
||||||
|
<app-input
|
||||||
|
type="text"
|
||||||
|
placeholder="99.999.999"
|
||||||
|
[value]="dniControl.value"
|
||||||
|
(valueChange)="dniControl.setValue($event.toString()); dniControl.markAsDirty()"
|
||||||
|
[invalid]="dniControl.invalid && (dniControl.dirty || dniControl.touched)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<app-button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
hostClass="w-100 h-100"
|
||||||
|
buttonClass="w-100 h-100 px-0"
|
||||||
|
[disabled]="dniControl.invalid"
|
||||||
|
>
|
||||||
|
Continuar
|
||||||
|
</app-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div class="payment-detail payment-detail--copy">
|
|
||||||
<span class="payment-detail__label">CVU:</span>
|
|
||||||
<strong class="payment-detail__value">{{ transferAccount().cvu }}</strong>
|
|
||||||
<app-icon-button
|
|
||||||
variant="copy"
|
|
||||||
ariaLabel="Copiar CVU"
|
|
||||||
(click)="requestCopy('cvu', transferAccount().cvu)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (copiedTransferField() === 'cvu') {
|
|
||||||
<small class="payment-detail__feedback">CVU copiado</small>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="payment-detail payment-detail--copy">
|
|
||||||
<span class="payment-detail__label">Alias:</span>
|
|
||||||
<strong class="payment-detail__value">{{ transferAccount().alias }}</strong>
|
|
||||||
<app-icon-button
|
|
||||||
variant="copy"
|
|
||||||
ariaLabel="Copiar alias"
|
|
||||||
(click)="requestCopy('alias', transferAccount().alias)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (copiedTransferField() === 'alias') {
|
|
||||||
<small class="payment-detail__feedback">Alias copiado</small>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
.payment-panel__card {
|
.payment-panel__card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 290px;
|
max-width: 320px;
|
||||||
min-height: 269px;
|
min-height: 269px;
|
||||||
padding: 1.5rem 1.75rem;
|
padding: 1.5rem 1.75rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: #ffffff;
|
background: #bbb6b6;
|
||||||
color: #666666;
|
color: #666666;
|
||||||
box-shadow: 0 0 0 1px #f1f1f1;
|
box-shadow: 0 0 0 1px #f1f1f1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -76,3 +76,42 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dni-form-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dni-form-title {
|
||||||
|
color: #8a8a8a;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 1.25rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dni-form {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
app-input {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
::ng-deep .input-control,
|
||||||
|
::ng-deep input.form-control {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .btn {
|
||||||
|
min-width: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +1,37 @@
|
|||||||
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
|
||||||
|
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { IconButtonComponent } from '../../../../../../shared/components/icon-button/icon-button.component';
|
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 } from '../../checkout-page.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-checkout-payment-transfer',
|
selector: 'app-checkout-payment-transfer',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [IconButtonComponent],
|
imports: [IconButtonComponent, ButtonComponent, InputComponent, ReactiveFormsModule],
|
||||||
templateUrl: './checkout-payment-transfer.component.html',
|
templateUrl: './checkout-payment-transfer.component.html',
|
||||||
styleUrl: './checkout-payment-transfer.component.scss',
|
styleUrl: './checkout-payment-transfer.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class CheckoutPaymentTransferComponent {
|
export class CheckoutPaymentTransferComponent {
|
||||||
readonly transferAccount = input.required<TransferAccount>();
|
readonly transferAccount = input<TransferAccount | null>(null);
|
||||||
readonly copiedTransferField = input<TransferField | null>(null);
|
readonly copiedTransferField = input<TransferField | null>(null);
|
||||||
|
|
||||||
readonly copyTransferValue = output<{ field: TransferField; value: string }>();
|
readonly copyTransferValue = output<{ field: TransferField; value: string }>();
|
||||||
|
readonly submitDni = output<string>();
|
||||||
|
|
||||||
|
protected readonly dniControl = new FormControl('', {
|
||||||
|
nonNullable: true,
|
||||||
|
validators: [Validators.required, Validators.pattern(/^\d{7,8}$/)]
|
||||||
|
});
|
||||||
|
|
||||||
protected requestCopy(field: TransferField, value: string): void {
|
protected requestCopy(field: TransferField, value: string): void {
|
||||||
this.copyTransferValue.emit({ field, value });
|
this.copyTransferValue.emit({ field, value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected onSubmitDni(): void {
|
||||||
|
if (this.dniControl.valid) {
|
||||||
|
this.submitDni.emit(this.dniControl.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user