Compare commits

...

3 Commits

11 changed files with 208 additions and 85 deletions

View File

@@ -81,9 +81,13 @@ export class CheckoutService {
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(
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) {
throw new Error('Error al generar la intención de pago.');

View File

@@ -21,6 +21,7 @@
(paymentMethodChange)="selectPaymentMethod($event)"
(copyTransferValue)="copyTransferValue($event.field, $event.value)"
(cancelStep)="onCancel()"
(generateTransferIntent)="generateTransferIntent($event)"
(complete)="onComplete()"
/>
</app-step>

View File

@@ -86,12 +86,7 @@ export class CheckoutPageComponent implements OnInit {
];
protected readonly selectedPaymentMethod = signal<PaymentMethod>('qr');
protected readonly copiedTransferField = signal<TransferField | null>(null);
protected readonly transferAccount = signal<TransferAccount>({
titular: 'Nombre y Apellido',
entidad: 'TelePagos',
cvu: '0000000000000000000000',
alias: 'telepagos.ar',
});
protected readonly transferAccount = signal<TransferAccount | null>(null);
protected readonly isCreatingPurchase = signal(false);
protected readonly createdPurchaseId = signal<number | null>(null);
@@ -219,6 +214,11 @@ export class CheckoutPageComponent implements OnInit {
return;
}
if (method === 'transfer') {
this.transferAccount.set(null);
return;
}
this.isGeneratingIntent.set(true);
try {
@@ -230,7 +230,32 @@ export class CheckoutPageComponent implements OnInit {
if (method === 'qr' && 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({
titular: response.transfer_data.titular,
entidad: response.transfer_data.entidad,
@@ -239,7 +264,7 @@ export class CheckoutPageComponent implements OnInit {
});
}
} catch (error) {
console.error('Failed to generate payment intent:', error);
console.error('Failed to generate transfer payment intent:', error);
} finally {
this.isGeneratingIntent.set(false);
}

View File

@@ -46,33 +46,13 @@
[transferAccount]="transferAccount()"
[copiedTransferField]="copiedTransferField()"
(copyTransferValue)="requestCopy($event.field, $event.value)"
(submitDni)="generateTransferIntent.emit($event)"
(completePurchase)="complete.emit()"
/>
} @else {
<app-checkout-payment-telepagos />
}
</section>
</div>
<div class="checkout-payment__actions">
<app-button
type="button"
variant="cancel"
hostClass="checkout-payment__action-btn"
buttonClass="w-100"
[disabled]="true"
>
Cancelar
</app-button>
<app-button
type="button"
hostClass="checkout-payment__action-btn"
buttonClass="w-100"
[disabled]="isGeneratingIntent()"
(click)="complete.emit()"
>
Finalizar compra
</app-button>
</div>
</div>

View File

@@ -23,8 +23,8 @@
&__action-btn {
width: 100%;
flex: 1 1 290px;
max-width: 290px;
flex: 1 1 320px;
max-width: 320px;
display: block;
}
}
@@ -112,7 +112,7 @@
min-width: 0;
&__card {
width: 100%;
max-width: 290px;
max-width: 320px;
min-height: 269px;
padding: 1.5rem 1.75rem;
border-radius: 5px;

View File

@@ -18,7 +18,7 @@ export class CheckoutPaymentStepComponent {
readonly paymentMethods = input.required<ReadonlyArray<PaymentMethodOption>>();
readonly selectedPaymentMethod = input.required<PaymentMethod>();
readonly copiedTransferField = input<TransferField | null>(null);
readonly transferAccount = input.required<TransferAccount>();
readonly transferAccount = input<TransferAccount | null>(null);
readonly isGeneratingIntent = input<boolean>(false);
readonly qrData = input<string | null>(null);
@@ -26,6 +26,7 @@ export class CheckoutPaymentStepComponent {
readonly copyTransferValue = output<{ field: TransferField; value: string }>();
readonly cancelStep = output<void>();
readonly complete = output<void>();
readonly generateTransferIntent = output<string>();
protected selectPaymentMethod(method: PaymentMethod): void {
this.paymentMethodChange.emit(method);

View File

@@ -1,6 +1,6 @@
.payment-panel__card {
width: 100%;
max-width: 290px;
max-width: 320px;
min-height: 269px;
padding: 1.5rem 1.75rem;
border-radius: 5px;

View File

@@ -1,6 +1,6 @@
.payment-panel__card {
width: 100%;
max-width: 290px;
max-width: 320px;
min-height: 269px;
padding: 1.5rem 1.75rem;
border-radius: 5px;

View File

@@ -1,46 +1,95 @@
<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>
<div class="payment-panel__account">
<p class="payment-panel__eyebrow">DATOS DE CUENTA:</p>
<div class="payment-detail">
<span class="payment-detail__label">Titular:</span>
<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 class="dni-form-container">
<h3 class="dni-form-title">Ingresá DNI de quién va a transferir</h3>
<form class="dni-form row gx-2" (submit)="$event.preventDefault(); onSubmitDni()">
<div class="col-8">
<app-input
type="text"
placeholder="99.999.999"
[value]="dniControl.value"
(valueChange)="dniControl.setValue($event.toString()); dniControl.markAsDirty()"
[disabled]="isDniDisabled()"
[invalid]="dniControl.invalid && (dniControl.dirty || dniControl.touched)"
/>
</div>
<div class="col-4">
@if (isDniDisabled()) {
<app-button
type="button"
variant="secondary"
hostClass="w-100 h-100"
buttonClass="w-100 h-100 px-0"
(click)="enableEditing()"
>
Modificar
</app-button>
} @else {
<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>
@if (transferAccount()) {
<div class="payment-panel__divider"></div>
<div class="payment-panel__account">
<p class="payment-panel__eyebrow">DATOS DE CUENTA:</p>
<div class="payment-detail">
<span class="payment-detail__label">Titular:</span>
<span class="payment-detail__value">{{ transferAccount()?.titular }}</span>
</div>
<div class="payment-detail">
<span class="payment-detail__label">Entidad:</span>
<span class="payment-detail__value">{{ transferAccount()?.entidad }}</span>
</div>
<div class="payment-detail payment-detail--copy">
<span class="payment-detail__label">CVU:</span>
<span class="payment-detail__value">{{ transferAccount()?.cvu }}</span>
<app-icon-button
variant="copy"
ariaLabel="Copiar CVU"
(click)="requestCopy('cvu', transferAccount()!.cvu)"
/>
@if (copiedTransferField() === 'cvu') {
<span class="payment-detail__feedback" style="grid-column: 1 / -1; text-align: center;">¡Copiado!</span>
}
</div>
<div class="payment-detail payment-detail--copy">
<span class="payment-detail__label">Alias:</span>
<span class="payment-detail__value">{{ transferAccount()?.alias }}</span>
<app-icon-button
variant="copy"
ariaLabel="Copiar Alias"
(click)="requestCopy('alias', transferAccount()!.alias)"
/>
@if (copiedTransferField() === 'alias') {
<span class="payment-detail__feedback" style="grid-column: 1 / -1; text-align: center;">¡Copiado!</span>
}
</div>
</div>
<div class="mt-4">
<app-button
type="button"
variant="primary"
hostClass="w-100"
buttonClass="w-100"
(click)="completePurchase.emit()"
>
Ya transferí
</app-button>
</div>
}
</div>

View File

@@ -1,6 +1,6 @@
.payment-panel__card {
width: 100%;
max-width: 290px;
max-width: 320px;
min-height: 269px;
padding: 1.5rem 1.75rem;
border-radius: 5px;
@@ -76,3 +76,42 @@
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%;
}
}

View File

@@ -1,22 +1,46 @@
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, input, output, signal } 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 } from '../../checkout-page.models';
@Component({
selector: 'app-checkout-payment-transfer',
standalone: true,
imports: [IconButtonComponent],
imports: [IconButtonComponent, ButtonComponent, InputComponent, ReactiveFormsModule],
templateUrl: './checkout-payment-transfer.component.html',
styleUrl: './checkout-payment-transfer.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CheckoutPaymentTransferComponent {
readonly transferAccount = input.required<TransferAccount>();
readonly transferAccount = input<TransferAccount | null>(null);
readonly copiedTransferField = input<TransferField | null>(null);
readonly copyTransferValue = output<{ field: TransferField; value: string }>();
readonly submitDni = output<string>();
readonly completePurchase = output<void>();
protected readonly dniControl = new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.pattern(/^\d{7,8}$/)]
});
protected readonly isEditing = signal<boolean>(false);
protected readonly isDniDisabled = computed(() => !!this.transferAccount() && !this.isEditing());
protected requestCopy(field: TransferField, value: string): void {
this.copyTransferValue.emit({ field, value });
}
protected onSubmitDni(): void {
if (this.dniControl.valid) {
this.isEditing.set(false);
this.submitDni.emit(this.dniControl.value);
}
}
protected enableEditing(): void {
this.isEditing.set(true);
}
}