Compare commits
6 Commits
feature/ti
...
53cee7ecdd
| Author | SHA1 | Date | |
|---|---|---|---|
| 53cee7ecdd | |||
| 757f362918 | |||
| a8e73af806 | |||
| 61789d9238 | |||
| dd2082f40e | |||
| c486131905 |
@@ -59,7 +59,13 @@
|
|||||||
"maximumError": "8kB"
|
"maximumError": "8kB"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all"
|
"outputHashing": "all",
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.production.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"optimization": true,
|
"optimization": true,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
[subtotal]="cartSubtotal()"
|
[subtotal]="cartSubtotal()"
|
||||||
[discount]="cartDiscount()"
|
[discount]="cartDiscount()"
|
||||||
[total]="cartTotal()"
|
[total]="cartTotal()"
|
||||||
|
[readonly]="!cartEditingEnabled()"
|
||||||
[backgroundColor]="'#ffffff'"
|
[backgroundColor]="'#ffffff'"
|
||||||
(closed)="isCartOpen.set(false)"
|
(closed)="isCartOpen.set(false)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -645,4 +645,35 @@ describe('StoreLayoutComponent', () => {
|
|||||||
expect(cartItem.componentInstance.quantityDisabled()).toBe(false);
|
expect(cartItem.componentInstance.quantityDisabled()).toBe(false);
|
||||||
expect(buyButton).toBeDefined();
|
expect(buyButton).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides quantity selectors when the tenant disables cart editing', () => {
|
||||||
|
tenantState.set({ ...tenant, cart_editing_enabled: false });
|
||||||
|
cartState.set({
|
||||||
|
id: 1,
|
||||||
|
tenant_codigo: tenant.codigo,
|
||||||
|
status: 'active',
|
||||||
|
subtotal: '100.00',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
cantidad: 1,
|
||||||
|
precio_unitario: '100.00',
|
||||||
|
catalog_item_id: 1,
|
||||||
|
variant_id: null,
|
||||||
|
nombre: 'Producto',
|
||||||
|
imagen: null,
|
||||||
|
variant: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
(fixture.componentInstance as any).isCartOpen.set(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const cartItem = fixture.debugElement.query(By.css('app-cart-item'));
|
||||||
|
expect(cartItem.componentInstance.readonly()).toBe(true);
|
||||||
|
expect(cartItem.nativeElement.querySelector('app-quantity-selector')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ export class StoreLayoutComponent implements OnInit {
|
|||||||
protected readonly isCartOpen = signal(false);
|
protected readonly isCartOpen = signal(false);
|
||||||
protected readonly isCreatingPurchase = signal(false);
|
protected readonly isCreatingPurchase = signal(false);
|
||||||
protected readonly displayCart = computed(() => this.tenant()?.display_cart ?? true);
|
protected readonly displayCart = computed(() => this.tenant()?.display_cart ?? true);
|
||||||
|
protected readonly cartEditingEnabled = computed(
|
||||||
|
() => this.tenant()?.cart_editing_enabled ?? true,
|
||||||
|
);
|
||||||
|
|
||||||
protected readonly cartSubtotal = computed(() => {
|
protected readonly cartSubtotal = computed(() => {
|
||||||
const cart = this.cartService.cart();
|
const cart = this.cartService.cart();
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export class CheckoutService extends BaseApiService {
|
|||||||
async generatePaymentIntent(
|
async generatePaymentIntent(
|
||||||
tenantCode: string,
|
tenantCode: string,
|
||||||
purchaseId: number,
|
purchaseId: number,
|
||||||
method: 'qr' | 'transfer' | 'telepagos',
|
method: 'qr' | 'transfer',
|
||||||
payerDni?: string,
|
payerDni?: string,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const payload: any = { method };
|
const payload: any = { method };
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ export interface Tenant {
|
|||||||
display_categories?: boolean;
|
display_categories?: boolean;
|
||||||
display_seach_bar?: boolean;
|
display_seach_bar?: boolean;
|
||||||
display_cart?: boolean;
|
display_cart?: boolean;
|
||||||
|
cart_editing_enabled?: boolean;
|
||||||
|
display_cart_item_images?: boolean;
|
||||||
social_media?: SocialMedia[];
|
social_media?: SocialMedia[];
|
||||||
menues?: Menu[];
|
menues?: Menu[];
|
||||||
categories: Category[];
|
categories: Category[];
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
[subtotal]="cartSubtotal()"
|
[subtotal]="cartSubtotal()"
|
||||||
[discount]="cartDiscount()"
|
[discount]="cartDiscount()"
|
||||||
[total]="cartTotal()"
|
[total]="cartTotal()"
|
||||||
|
[readonly]="!cartEditingEnabled()"
|
||||||
[allowEditing]="
|
[allowEditing]="
|
||||||
createdPurchase()?.status === 'created' || createdPurchase()?.status === 'pending_payment'
|
createdPurchase()?.status === 'created' || createdPurchase()?.status === 'pending_payment'
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ describe('CheckoutPageComponent payment validation', () => {
|
|||||||
let routerStub: { navigate: ReturnType<typeof vi.fn> };
|
let routerStub: { navigate: ReturnType<typeof vi.fn> };
|
||||||
let routeQueryParamMap: ReturnType<typeof convertToParamMap>;
|
let routeQueryParamMap: ReturnType<typeof convertToParamMap>;
|
||||||
let authUserState: ReturnType<typeof signal>;
|
let authUserState: ReturnType<typeof signal>;
|
||||||
|
let tenantState: ReturnType<typeof signal<{ codigo: string; cart_editing_enabled?: boolean }>>;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
try {
|
try {
|
||||||
@@ -88,6 +89,7 @@ describe('CheckoutPageComponent payment validation', () => {
|
|||||||
routerStub = { navigate: vi.fn() };
|
routerStub = { navigate: vi.fn() };
|
||||||
routeQueryParamMap = convertToParamMap({});
|
routeQueryParamMap = convertToParamMap({});
|
||||||
authUserState = signal(null);
|
authUserState = signal(null);
|
||||||
|
tenantState = signal({ codigo: 'tenant-test' });
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [CheckoutPageComponent],
|
imports: [CheckoutPageComponent],
|
||||||
@@ -95,7 +97,7 @@ describe('CheckoutPageComponent payment validation', () => {
|
|||||||
{ provide: CheckoutService, useValue: checkoutServiceStub },
|
{ provide: CheckoutService, useValue: checkoutServiceStub },
|
||||||
{ provide: CatalogService, useValue: { getCatalogItem: vi.fn() } },
|
{ provide: CatalogService, useValue: { getCatalogItem: vi.fn() } },
|
||||||
{ provide: CartService, useValue: cartServiceStub },
|
{ provide: CartService, useValue: cartServiceStub },
|
||||||
{ provide: TenantService, useValue: { tenant: signal({ codigo: 'tenant-test' }) } },
|
{ provide: TenantService, useValue: { tenant: tenantState } },
|
||||||
{ provide: AuthService, useValue: { user: authUserState } },
|
{ provide: AuthService, useValue: { user: authUserState } },
|
||||||
{
|
{
|
||||||
provide: ActivatedRoute,
|
provide: ActivatedRoute,
|
||||||
@@ -366,11 +368,7 @@ describe('CheckoutPageComponent payment validation', () => {
|
|||||||
|
|
||||||
expect(component.checkoutStepIndex()).toBe(1);
|
expect(component.checkoutStepIndex()).toBe(1);
|
||||||
expect(component.selectedPaymentMethod()).toBe('qr');
|
expect(component.selectedPaymentMethod()).toBe('qr');
|
||||||
expect(checkoutServiceStub.generatePaymentIntent).toHaveBeenCalledWith(
|
expect(checkoutServiceStub.generatePaymentIntent).toHaveBeenCalledWith('tenant-test', 25, 'qr');
|
||||||
'tenant-test',
|
|
||||||
25,
|
|
||||||
'qr',
|
|
||||||
);
|
|
||||||
expect(component.qrData()).toBe('qr-value');
|
expect(component.qrData()).toBe('qr-value');
|
||||||
expect(component.qrPaymentStatus()).toBe('waiting');
|
expect(component.qrPaymentStatus()).toBe('waiting');
|
||||||
});
|
});
|
||||||
@@ -472,6 +470,17 @@ describe('CheckoutPageComponent payment validation', () => {
|
|||||||
expect(component.isEditingItems()).toBe(false);
|
expect(component.isEditingItems()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not allow item editing when the tenant disables cart editing', async () => {
|
||||||
|
tenantState.set({ codigo: 'tenant-test', cart_editing_enabled: false });
|
||||||
|
const { component } = createComponent();
|
||||||
|
|
||||||
|
await component.onEditingItemsChange(true);
|
||||||
|
|
||||||
|
expect(component.cartEditingEnabled()).toBe(false);
|
||||||
|
expect(component.isEditingItems()).toBe(false);
|
||||||
|
expect(checkoutServiceStub.prepareItemEditing).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('updates customer data on the existing purchase before payment', async () => {
|
it('updates customer data on the existing purchase before payment', async () => {
|
||||||
const updatedPurchase = {
|
const updatedPurchase = {
|
||||||
id: 25,
|
id: 25,
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
protected readonly createdPurchase = signal<PurchaseDetailResponse | null>(null);
|
protected readonly createdPurchase = signal<PurchaseDetailResponse | null>(null);
|
||||||
protected readonly isLoadingPurchase = signal(true);
|
protected readonly isLoadingPurchase = signal(true);
|
||||||
protected readonly checkoutStepIndex = signal(0);
|
protected readonly checkoutStepIndex = signal(0);
|
||||||
|
protected readonly cartEditingEnabled = computed(
|
||||||
|
() => this.tenantService.tenant()?.cart_editing_enabled ?? true,
|
||||||
|
);
|
||||||
|
|
||||||
protected readonly cartSubtotal = computed(() => {
|
protected readonly cartSubtotal = computed(() => {
|
||||||
const purchase = this.createdPurchase();
|
const purchase = this.createdPurchase();
|
||||||
@@ -101,7 +104,6 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
protected readonly paymentMethods: ReadonlyArray<PaymentMethodOption> = [
|
protected readonly paymentMethods: ReadonlyArray<PaymentMethodOption> = [
|
||||||
{ id: 'qr', label: 'QR' },
|
{ id: 'qr', label: 'QR' },
|
||||||
{ id: 'transfer', label: 'Transferencia' },
|
{ id: 'transfer', label: 'Transferencia' },
|
||||||
{ id: 'telepagos', label: 'TelePagos' },
|
|
||||||
];
|
];
|
||||||
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);
|
||||||
@@ -179,6 +181,10 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (editing && !this.cartEditingEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!editing) {
|
if (!editing) {
|
||||||
this.isEditingItems.set(false);
|
this.isEditingItems.set(false);
|
||||||
|
|
||||||
@@ -224,7 +230,14 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
const purchaseId = this.createdPurchaseId();
|
const purchaseId = this.createdPurchaseId();
|
||||||
const itemId = event.item.cartItemId;
|
const itemId = event.item.cartItemId;
|
||||||
|
|
||||||
if (!tenant || !purchaseId || !itemId || this.isUpdatingItem() || !this.isEditingItems()) {
|
if (
|
||||||
|
!this.cartEditingEnabled() ||
|
||||||
|
!tenant ||
|
||||||
|
!purchaseId ||
|
||||||
|
!itemId ||
|
||||||
|
this.isUpdatingItem() ||
|
||||||
|
!this.isEditingItems()
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +344,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
const purchaseId = this.createdPurchaseId();
|
const purchaseId = this.createdPurchaseId();
|
||||||
const tenant = this.tenantService.tenant();
|
const tenant = this.tenantService.tenant();
|
||||||
|
|
||||||
if (!purchaseId || !tenant || method === 'telepagos') {
|
if (!purchaseId || !tenant) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
|
|
||||||
export type PaymentMethod = 'qr' | 'transfer' | 'telepagos';
|
export type PaymentMethod = 'qr' | 'transfer';
|
||||||
export type TransferField = 'cvu' | 'alias';
|
export type TransferField = 'cvu' | 'alias';
|
||||||
export type QrPaymentStatus = 'idle' | 'waiting' | 'timed_out' | 'failed';
|
export type QrPaymentStatus = 'idle' | 'waiting' | 'timed_out' | 'failed';
|
||||||
export type TransferValidationStatus = 'idle' | 'checking' | 'pending' | 'error';
|
export type TransferValidationStatus = 'idle' | 'checking' | 'pending' | 'error';
|
||||||
|
|||||||
@@ -18,14 +18,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span class="payment-method__label">
|
<span class="payment-method__label">
|
||||||
@if (method.id === 'telepagos') {
|
|
||||||
<span class="telepagos-logo" aria-label="TelePagos">
|
|
||||||
<span class="telepagos-logo__tele">tele</span
|
|
||||||
><span class="telepagos-logo__pagos">pagos</span>
|
|
||||||
</span>
|
|
||||||
} @else {
|
|
||||||
{{ method.label }}
|
{{ method.label }}
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<i class="fa-solid fa-angle-right payment-method__chevron" aria-hidden="true"></i>
|
<i class="fa-solid fa-angle-right payment-method__chevron" aria-hidden="true"></i>
|
||||||
@@ -59,8 +52,6 @@
|
|||||||
(submitDni)="generateTransferIntent.emit($event)"
|
(submitDni)="generateTransferIntent.emit($event)"
|
||||||
(completePurchase)="complete.emit()"
|
(completePurchase)="complete.emit()"
|
||||||
/>
|
/>
|
||||||
} @else {
|
|
||||||
<app-checkout-payment-telepagos />
|
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -101,26 +101,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.telepagos-logo {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 1.45rem;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
background: linear-gradient(90deg, #0a69d8 0 78%, #f6a11a 78% 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment-panel {
|
.payment-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
> app-checkout-payment-qr,
|
> app-checkout-payment-qr,
|
||||||
> app-checkout-payment-transfer,
|
> app-checkout-payment-transfer {
|
||||||
> app-checkout-payment-telepagos {
|
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
|
||||||
|
|
||||||
import { CheckoutPaymentQrComponent } from './components/checkout-payment-qr/checkout-payment-qr.component';
|
import { CheckoutPaymentQrComponent } from './components/checkout-payment-qr/checkout-payment-qr.component';
|
||||||
import { CheckoutPaymentTelepagosComponent } from './components/checkout-payment-telepagos/checkout-payment-telepagos.component';
|
|
||||||
import { CheckoutPaymentTransferComponent } from './components/checkout-payment-transfer/checkout-payment-transfer.component';
|
import { CheckoutPaymentTransferComponent } from './components/checkout-payment-transfer/checkout-payment-transfer.component';
|
||||||
import {
|
import {
|
||||||
PaymentMethod,
|
PaymentMethod,
|
||||||
@@ -15,7 +14,7 @@ import {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-checkout-payment-step',
|
selector: 'app-checkout-payment-step',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CheckoutPaymentQrComponent, CheckoutPaymentTelepagosComponent, CheckoutPaymentTransferComponent],
|
imports: [CheckoutPaymentQrComponent, CheckoutPaymentTransferComponent],
|
||||||
templateUrl: './checkout-payment-step.component.html',
|
templateUrl: './checkout-payment-step.component.html',
|
||||||
styleUrl: './checkout-payment-step.component.scss',
|
styleUrl: './checkout-payment-step.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<div class="payment-panel__card">
|
|
||||||
<h3 class="payment-panel__title">Descarga la app de TelePagos para finalizar la compra</h3>
|
|
||||||
<div class="payment-panel__divider"></div>
|
|
||||||
|
|
||||||
<div class="store-badges" aria-label="Tiendas disponibles">
|
|
||||||
<div class="store-badge">
|
|
||||||
<i class="fa-brands fa-google-play" aria-hidden="true"></i>
|
|
||||||
<span class="store-badge__text">
|
|
||||||
<small>Disponible en</small>
|
|
||||||
<strong>Google Play</strong>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="store-badge">
|
|
||||||
<i class="fa-brands fa-apple" aria-hidden="true"></i>
|
|
||||||
<span class="store-badge__text">
|
|
||||||
<small>Descargalo en</small>
|
|
||||||
<strong>App Store</strong>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
.payment-panel__card {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 320px;
|
|
||||||
min-height: 269px;
|
|
||||||
padding: 1.5rem 1.75rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #666666;
|
|
||||||
box-shadow: 0 0 0 1px #f1f1f1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment-panel__title {
|
|
||||||
max-width: 18rem;
|
|
||||||
margin: 0;
|
|
||||||
color: #8a8a8a;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment-panel__divider {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 220px;
|
|
||||||
height: 1px;
|
|
||||||
margin: 1.2rem 0 1.45rem;
|
|
||||||
background: #dddddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.store-badges {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 210px;
|
|
||||||
display: grid;
|
|
||||||
gap: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.store-badge {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.75rem 0.95rem;
|
|
||||||
border-radius: 0.85rem;
|
|
||||||
background: #111111;
|
|
||||||
color: #ffffff;
|
|
||||||
box-shadow: 0 10px 24px rgba(17, 17, 17, 0.16);
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 1.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__text {
|
|
||||||
display: grid;
|
|
||||||
text-align: left;
|
|
||||||
line-height: 1.1;
|
|
||||||
|
|
||||||
small {
|
|
||||||
font-size: 0.62rem;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-checkout-payment-telepagos',
|
|
||||||
standalone: true,
|
|
||||||
imports: [],
|
|
||||||
templateUrl: './checkout-payment-telepagos.component.html',
|
|
||||||
styleUrl: './checkout-payment-telepagos.component.scss',
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
|
||||||
export class CheckoutPaymentTelepagosComponent {}
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router';
|
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router';
|
||||||
|
|
||||||
import { CheckoutService, PurchaseDetailResponse } from '../../../../core/services/checkout.service';
|
import {
|
||||||
|
CheckoutService,
|
||||||
|
PurchaseDetailResponse,
|
||||||
|
} from '../../../../core/services/checkout.service';
|
||||||
import { Tenant } from '../../../../core/services/tenant.interface';
|
import { Tenant } from '../../../../core/services/tenant.interface';
|
||||||
import { TenantService } from '../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
import { PurchaseStatusPageComponent } from './purchase-status-page.component';
|
import { PurchaseStatusPageComponent } from './purchase-status-page.component';
|
||||||
@@ -130,4 +133,89 @@ describe('PurchaseStatusPageComponent', () => {
|
|||||||
|
|
||||||
openSpy.mockRestore();
|
openSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('polls every five seconds while the payment is pending and shows the confirmation', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const checkoutService = {
|
||||||
|
getPurchase: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ status: 'pending_payment' } as PurchaseDetailResponse)
|
||||||
|
.mockResolvedValueOnce(purchase(true)),
|
||||||
|
withCustomLoading() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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: { navigate: vi.fn(), navigateByUrl: vi.fn() } },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(PurchaseStatusPageComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
await Promise.resolve();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(checkoutService.getPurchase).toHaveBeenCalledOnce();
|
||||||
|
expect(fixture.nativeElement.textContent).toContain('ESTAMOS VERIFICANDO TU PAGO');
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(5_000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(checkoutService.getPurchase).toHaveBeenCalledTimes(2);
|
||||||
|
expect(fixture.nativeElement.textContent).toContain('COMPRA REALIZADA!');
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(10_000);
|
||||||
|
expect(checkoutService.getPurchase).toHaveBeenCalledTimes(2);
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stops polling when the page is destroyed', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const checkoutService = {
|
||||||
|
getPurchase: vi.fn().mockResolvedValue({ status: 'pending_payment' }),
|
||||||
|
withCustomLoading() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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: { navigate: vi.fn(), navigateByUrl: vi.fn() } },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(PurchaseStatusPageComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
await Promise.resolve();
|
||||||
|
fixture.destroy();
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(5_000);
|
||||||
|
|
||||||
|
expect(checkoutService.getPurchase).toHaveBeenCalledOnce();
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,29 +1,47 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit, computed, inject, signal } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
PLATFORM_ID,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
import { CheckoutService, PurchaseStatusResponse } from '../../../../core/services/checkout.service';
|
import {
|
||||||
|
CheckoutService,
|
||||||
|
PurchaseStatusResponse,
|
||||||
|
} from '../../../../core/services/checkout.service';
|
||||||
import { findMenu } from '../../../../core/services/menu.utils';
|
import { findMenu } from '../../../../core/services/menu.utils';
|
||||||
import { TenantService } from '../../../../core/services/tenant.service';
|
import { TenantService } from '../../../../core/services/tenant.service';
|
||||||
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
||||||
|
|
||||||
type PurchaseStatusView = 'approved' | 'pending' | 'rejected' | 'expired' | 'error';
|
type PurchaseStatusView = 'approved' | 'pending' | 'rejected' | 'expired' | 'error';
|
||||||
|
|
||||||
|
const PAYMENT_STATUS_POLL_INTERVAL_MS = 5_000;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-purchase-status-page',
|
selector: 'app-purchase-status-page',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ButtonComponent],
|
imports: [ButtonComponent],
|
||||||
templateUrl: './purchase-status-page.component.html',
|
templateUrl: './purchase-status-page.component.html',
|
||||||
styleUrl: './purchase-status-page.component.scss',
|
styleUrl: './purchase-status-page.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class PurchaseStatusPageComponent implements OnInit {
|
export class PurchaseStatusPageComponent implements OnInit, OnDestroy {
|
||||||
private readonly route = inject(ActivatedRoute);
|
private readonly route = inject(ActivatedRoute);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
private readonly checkoutService = inject(CheckoutService);
|
private readonly checkoutService = inject(CheckoutService);
|
||||||
private readonly tenantService = inject(TenantService);
|
private readonly tenantService = inject(TenantService);
|
||||||
|
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
||||||
|
|
||||||
private purchaseId: string | null = null;
|
private purchaseId: string | null = null;
|
||||||
private tenantCode: string | null = null;
|
private tenantCode: string | null = null;
|
||||||
|
private pollingTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private isDestroyed = false;
|
||||||
|
|
||||||
protected readonly isLoading = signal(true);
|
protected readonly isLoading = signal(true);
|
||||||
protected readonly status = signal<PurchaseStatusView>('pending');
|
protected readonly status = signal<PurchaseStatusView>('pending');
|
||||||
@@ -57,7 +75,12 @@ export class PurchaseStatusPageComponent implements OnInit {
|
|||||||
void this.loadStatus();
|
void this.loadStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadStatus(): Promise<void> {
|
ngOnDestroy(): void {
|
||||||
|
this.isDestroyed = true;
|
||||||
|
this.stopPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadStatus(isPolling = false): Promise<void> {
|
||||||
if (!this.purchaseId || !this.tenantCode) {
|
if (!this.purchaseId || !this.tenantCode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -66,15 +89,54 @@ export class PurchaseStatusPageComponent implements OnInit {
|
|||||||
const purchase = await this.checkoutService
|
const purchase = await this.checkoutService
|
||||||
.withCustomLoading()
|
.withCustomLoading()
|
||||||
.getPurchase(this.tenantCode, this.purchaseId);
|
.getPurchase(this.tenantCode, this.purchaseId);
|
||||||
this.status.set(this.resolveStatus(purchase));
|
|
||||||
|
if (this.isDestroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = this.resolveStatus(purchase);
|
||||||
|
this.status.set(status);
|
||||||
this.hasGeneratedTickets.set(purchase.has_generated_tickets === true);
|
this.hasGeneratedTickets.set(purchase.has_generated_tickets === true);
|
||||||
|
|
||||||
|
if (status === 'pending') {
|
||||||
|
this.schedulePolling();
|
||||||
|
} else {
|
||||||
|
this.stopPolling();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch purchase status:', error);
|
console.error('Failed to fetch purchase status:', error);
|
||||||
|
|
||||||
|
if (!this.isDestroyed) {
|
||||||
|
if (isPolling) {
|
||||||
|
this.schedulePolling();
|
||||||
|
} else {
|
||||||
this.status.set('error');
|
this.status.set('error');
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (!this.isDestroyed) {
|
||||||
this.isLoading.set(false);
|
this.isLoading.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private schedulePolling(): void {
|
||||||
|
if (!this.isBrowser || this.isDestroyed || this.pollingTimeout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pollingTimeout = setTimeout(() => {
|
||||||
|
this.pollingTimeout = null;
|
||||||
|
void this.loadStatus(true);
|
||||||
|
}, PAYMENT_STATUS_POLL_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopPolling(): void {
|
||||||
|
if (this.pollingTimeout) {
|
||||||
|
clearTimeout(this.pollingTimeout);
|
||||||
|
this.pollingTimeout = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private resolveStatus(purchase: PurchaseStatusResponse): PurchaseStatusView {
|
private resolveStatus(purchase: PurchaseStatusResponse): PurchaseStatusView {
|
||||||
if (purchase.status === 'paid') {
|
if (purchase.status === 'paid') {
|
||||||
|
|||||||
@@ -20,6 +20,17 @@
|
|||||||
<span class="ticket-selector__label">Seleccioná Entrada/s:</span>
|
<span class="ticket-selector__label">Seleccioná Entrada/s:</span>
|
||||||
|
|
||||||
<div class="ticket-selector__rows">
|
<div class="ticket-selector__rows">
|
||||||
|
@if (attributes().length > 0) {
|
||||||
|
<div class="ticket-selector__columns" aria-hidden="true">
|
||||||
|
<div class="ticket-selector__column-labels">
|
||||||
|
@for (attribute of attributes(); track attribute.key) {
|
||||||
|
<span class="ticket-selector__column-label">{{ attribute.label }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<span class="ticket-selector__action-spacer"></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@for (row of rows(); track row.id) {
|
@for (row of rows(); track row.id) {
|
||||||
<div class="ticket-selector__row">
|
<div class="ticket-selector__row">
|
||||||
<div class="ticket-selector__row-content">
|
<div class="ticket-selector__row-content">
|
||||||
|
|||||||
@@ -57,6 +57,30 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__column-labels {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(120px, 1fr));
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__column-label {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action-spacer {
|
||||||
|
width: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
&__row {
|
&__row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
@@ -160,6 +184,10 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__columns {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
&__map {
|
&__map {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,6 +209,23 @@ describe('ProductTicketSelectorComponent', () => {
|
|||||||
expect(removeButton?.classList.contains('icon-btn--bordered')).toBe(true);
|
expect(removeButton?.classList.contains('icon-btn--bordered')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows one header row with the label of every selector', async () => {
|
||||||
|
const { fixture } = await createComponent({
|
||||||
|
maps: [mapResponse([variant(401, 'general', 'A', '1', '1')])],
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerRows = fixture.nativeElement.querySelectorAll('.ticket-selector__columns');
|
||||||
|
const labels = fixture.nativeElement.querySelectorAll('.ticket-selector__column-label');
|
||||||
|
|
||||||
|
expect(headerRows).toHaveLength(1);
|
||||||
|
expect([...labels].map((label: Element) => label.textContent?.trim())).toEqual([
|
||||||
|
'Tipo',
|
||||||
|
'Sector',
|
||||||
|
'Fila',
|
||||||
|
'Asiento',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('clears only the seat after a stock conflict when the row still has alternatives', async () => {
|
it('clears only the seat after a stock conflict when the row still has alternatives', async () => {
|
||||||
const failed = variant(401, 'general', 'A', '1', '1');
|
const failed = variant(401, 'general', 'A', '1', '1');
|
||||||
const alternative = variant(402, 'general', 'A', '1', '2');
|
const alternative = variant(402, 'general', 'A', '1', '2');
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export class ProductTicketSelectorComponent {
|
|||||||
private readonly viewReady = signal(false);
|
private readonly viewReady = signal(false);
|
||||||
private readonly mapReady = signal(false);
|
private readonly mapReady = signal(false);
|
||||||
private readonly variants = signal<CatalogFeaturedItemVariant[]>([]);
|
private readonly variants = signal<CatalogFeaturedItemVariant[]>([]);
|
||||||
private readonly attributes = signal<VariantAttribute[]>([]);
|
protected readonly attributes = signal<VariantAttribute[]>([]);
|
||||||
private mapRequest: Subscription | null = null;
|
private mapRequest: Subscription | null = null;
|
||||||
|
|
||||||
readonly productId = input.required<number>();
|
readonly productId = input.required<number>();
|
||||||
|
|||||||
6
src/environments/environment.production.ts
Normal file
6
src/environments/environment.production.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const environment = {
|
||||||
|
production: true,
|
||||||
|
nombre: "Producción - activo",
|
||||||
|
url: "https://backend.shopit.com.ar/api/",
|
||||||
|
urlDescarga: "url/"
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user