feat(checkout): show event purchase summary

This commit is contained in:
2026-09-21 10:59:51 -03:00
parent 607892173d
commit cd1c99f487
10 changed files with 140 additions and 5 deletions

View File

@@ -17,7 +17,7 @@
[displaySeachBar]="tenant()?.display_seach_bar ?? true" [displaySeachBar]="tenant()?.display_seach_bar ?? true"
[displayCart]="displayCart()" [displayCart]="displayCart()"
[cartDisabled]="isCheckoutRoute()" [cartDisabled]="isCheckoutRoute()"
[checkoutRemainingSeconds]="checkoutRemainingSeconds()" [checkoutRemainingSeconds]="headerCheckoutRemainingSeconds()"
(cartClick)="onCartClick()" (cartClick)="onCartClick()"
(ticketsClick)="onTicketsClick()" (ticketsClick)="onTicketsClick()"
(loginClick)="onLoginClick()" (loginClick)="onLoginClick()"

View File

@@ -428,6 +428,26 @@ describe('StoreLayoutComponent', () => {
); );
}); });
it('moves the countdown out of the header on multi-event checkout', () => {
tenantState.set({
...tenant,
storefront_website_type_code: 'onticket_multi_event',
checkout_summary_type: 'event',
});
checkoutRemainingSecondsState.set(587);
const router = TestBed.inject(Router);
vi.spyOn(router, 'url', 'get').mockReturnValue('/checkout/25');
const fixture = TestBed.createComponent(StoreLayoutComponent);
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
const header = fixture.debugElement.query(By.directive(StoreHeaderComponent));
expect(header.componentInstance.checkoutRemainingSeconds()).toBeNull();
expect(element.querySelector('.store-layout__checkout-timer')).toBeNull();
});
it('hides the configured header elements when the tenant disables them', () => { it('hides the configured header elements when the tenant disables them', () => {
tenantState.set({ tenantState.set({
...tenant, ...tenant,

View File

@@ -74,6 +74,11 @@ export class StoreLayoutComponent implements OnInit {
private readonly isMultiEventStorefront = computed( private readonly isMultiEventStorefront = computed(
() => this.tenant()?.storefront_website_type_code === 'onticket_multi_event', () => this.tenant()?.storefront_website_type_code === 'onticket_multi_event',
); );
protected readonly headerCheckoutRemainingSeconds = computed(() =>
this.isCheckoutRoute() && this.tenant()?.checkout_summary_type === 'event'
? null
: this.checkoutRemainingSeconds(),
);
protected readonly navigationCategories = computed(() => protected readonly navigationCategories = computed(() =>
this.isMultiEventStorefront() this.isMultiEventStorefront()
? (this.tenant()?.event_categories ?? []) ? (this.tenant()?.event_categories ?? [])

View File

@@ -121,11 +121,17 @@ export interface PurchaseDetailItemResponse {
nombre: string; nombre: string;
descripcion: string | null; descripcion: string | null;
slug: string; slug: string;
imagen: string | null; imagen?: string | null;
attributes: PurchaseDetailAttributeResponse[]; attributes: PurchaseDetailAttributeResponse[];
}; };
} }
export interface PurchaseCheckoutEventResponse {
id: number;
title: string;
image: string | null;
}
export interface PurchaseDetailResponse extends PurchaseStatusResponse { export interface PurchaseDetailResponse extends PurchaseStatusResponse {
id: number; id: number;
cart_id: number | null; cart_id: number | null;
@@ -140,6 +146,7 @@ export interface PurchaseDetailResponse extends PurchaseStatusResponse {
email: string | null; email: string | null;
items_source: 'purchase'; items_source: 'purchase';
items: PurchaseDetailItemResponse[]; items: PurchaseDetailItemResponse[];
event?: PurchaseCheckoutEventResponse | null;
tickets_count?: number; tickets_count?: number;
has_generated_tickets?: boolean; has_generated_tickets?: boolean;
subtotal: string; subtotal: string;

View File

@@ -134,6 +134,7 @@ export interface Tenant {
footer_bg_image?: string | null; footer_bg_image?: string | null;
storefront_website_type_code?: string | null; storefront_website_type_code?: string | null;
header_type?: 'standard' | 'immersive'; header_type?: 'standard' | 'immersive';
checkout_summary_type?: 'default' | 'event';
extras?: WebsiteExtras; extras?: WebsiteExtras;
event?: TenantEvent | null; event?: TenantEvent | null;
selected_bank_account_id?: number | null; selected_bank_account_id?: number | null;

View File

@@ -103,7 +103,7 @@ export class PurchaseDetailPage implements OnInit {
id: item.id, id: item.id,
title: item.item_details.nombre || 'Producto sin nombre', title: item.item_details.nombre || 'Producto sin nombre',
description: item.item_details.descripcion, description: item.item_details.descripcion,
imageUrl: item.item_details.imagen, imageUrl: item.item_details.imagen ?? null,
quantity: item.quantity, quantity: item.quantity,
attributes: item.item_details.attributes.map((attribute) => ({ attributes: item.item_details.attributes.map((attribute) => ({
label: attribute.name, label: attribute.name,

View File

@@ -47,7 +47,26 @@
</div> </div>
<div class="checkout-page__cart-col"> <div class="checkout-page__cart-col">
@if (checkoutRemainingTime(); as remainingTime) { @if (usesEventCheckoutSummary()) {
@if (checkoutRemainingTime(); as remainingTime) {
<div
class="checkout-page__summary-countdown d-flex align-items-baseline justify-content-center gap-3"
role="timer"
aria-label="Tiempo restante de compra"
>
<span class="checkout-page__summary-countdown-label">Tiempo restante de compra:</span>
<span class="checkout-page__summary-countdown-value">{{ remainingTime }}</span>
</div>
}
@if (checkoutEvent(); as event) {
@if (event.image) {
<div class="checkout-page__event-media">
<img [src]="event.image" [alt]="event.title" />
</div>
}
}
} @else if (checkoutRemainingTime(); as remainingTime) {
<div <div
class="checkout-page__mobile-countdown d-flex d-md-none align-items-baseline justify-content-center gap-3" class="checkout-page__mobile-countdown d-flex d-md-none align-items-baseline justify-content-center gap-3"
role="timer" role="timer"

View File

@@ -46,6 +46,38 @@
line-height: 1; line-height: 1;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
&__summary-countdown {
flex: 0 0 auto;
margin-bottom: 1.5rem;
color: var(--tenant-primary);
font-weight: 700;
white-space: nowrap;
}
&__summary-countdown-label {
font-size: 13px;
}
&__summary-countdown-value {
font-size: 20px;
line-height: 1;
font-variant-numeric: tabular-nums;
}
&__event-media {
flex: 0 0 auto;
width: 100%;
margin-bottom: 1rem;
overflow: hidden;
border-radius: 4px;
img {
display: block;
width: 100%;
height: auto;
}
}
} }
.checkout-page__loading { .checkout-page__loading {

View File

@@ -39,6 +39,8 @@ describe('CheckoutPageComponent payment validation', () => {
let tenantState: ReturnType< let tenantState: ReturnType<
typeof signal<{ typeof signal<{
codigo: string; codigo: string;
storefront_website_type_code?: string | null;
checkout_summary_type?: 'default' | 'event';
checkout_editing_policy?: CartEditingPolicy; checkout_editing_policy?: CartEditingPolicy;
}> }>
>; >;
@@ -468,6 +470,49 @@ describe('CheckoutPageComponent payment validation', () => {
}); });
}); });
it('uses the event image and removes item images for multi-event checkout', () => {
tenantState.set({
...tenantState(),
checkout_summary_type: 'event',
});
const { component } = createComponent();
component.createdPurchase.set({
id: 25,
status: 'created',
event: {
id: 12,
title: 'Festival de prueba',
image: 'https://example.com/event.jpg',
},
items: [
{
id: 91,
quantity: 1,
unit_price: '10000.00',
line_total: '10000.00',
source_catalog_item_id: 8,
source_variant_id: null,
item_details: {
nombre: 'Entrada general',
descripcion: null,
slug: 'entrada-general',
imagen: 'https://example.com/product.jpg',
attributes: [],
},
},
],
subtotal: '10000.00',
total: '10000.00',
});
expect(component.checkoutEvent()).toEqual({
id: 12,
title: 'Festival de prueba',
image: 'https://example.com/event.jpg',
});
expect(component.mappedCartItems()[0].imageUrl).toBeNull();
});
it('keeps the checkout hidden while the purchase is loading', async () => { it('keeps the checkout hidden while the purchase is loading', async () => {
let resolvePurchase!: (purchase: any) => void; let resolvePurchase!: (purchase: any) => void;
routeParamMap = convertToParamMap({ id: 25 }); routeParamMap = convertToParamMap({ id: 25 });

View File

@@ -138,6 +138,12 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
const purchase = this.createdPurchase(); const purchase = this.createdPurchase();
return purchase ? purchase.items.map((item) => this.mapPurchaseItemToMock(item)) : []; return purchase ? purchase.items.map((item) => this.mapPurchaseItemToMock(item)) : [];
}); });
protected readonly usesEventCheckoutSummary = computed(
() => this.tenantService.tenant()?.checkout_summary_type === 'event',
);
protected readonly checkoutEvent = computed(() =>
this.usesEventCheckoutSummary() ? (this.createdPurchase()?.event ?? null) : null,
);
protected readonly isStep1Valid = signal(this.form.valid); protected readonly isStep1Valid = signal(this.form.valid);
protected readonly paymentMethods: ReadonlyArray<PaymentMethodOption> = [ protected readonly paymentMethods: ReadonlyArray<PaymentMethodOption> = [
@@ -235,7 +241,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
private mapPurchaseItemToMock(item: PurchaseDetailItemResponse): CartItemMock { private mapPurchaseItemToMock(item: PurchaseDetailItemResponse): CartItemMock {
return { return {
cartItemId: item.id, cartItemId: item.id,
imageUrl: item.item_details.imagen, imageUrl: this.usesEventCheckoutSummary() ? null : (item.item_details.imagen ?? null),
product: item.item_details.nombre, product: item.item_details.nombre,
originalPrice: null, originalPrice: null,
discountedPrice: parseFloat(item.unit_price), discountedPrice: parseFloat(item.unit_price),