feat(checkout): show event purchase summary
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
[displaySeachBar]="tenant()?.display_seach_bar ?? true"
|
||||
[displayCart]="displayCart()"
|
||||
[cartDisabled]="isCheckoutRoute()"
|
||||
[checkoutRemainingSeconds]="checkoutRemainingSeconds()"
|
||||
[checkoutRemainingSeconds]="headerCheckoutRemainingSeconds()"
|
||||
(cartClick)="onCartClick()"
|
||||
(ticketsClick)="onTicketsClick()"
|
||||
(loginClick)="onLoginClick()"
|
||||
|
||||
@@ -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', () => {
|
||||
tenantState.set({
|
||||
...tenant,
|
||||
|
||||
@@ -74,6 +74,11 @@ export class StoreLayoutComponent implements OnInit {
|
||||
private readonly isMultiEventStorefront = computed(
|
||||
() => 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(() =>
|
||||
this.isMultiEventStorefront()
|
||||
? (this.tenant()?.event_categories ?? [])
|
||||
|
||||
@@ -121,11 +121,17 @@ export interface PurchaseDetailItemResponse {
|
||||
nombre: string;
|
||||
descripcion: string | null;
|
||||
slug: string;
|
||||
imagen: string | null;
|
||||
imagen?: string | null;
|
||||
attributes: PurchaseDetailAttributeResponse[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface PurchaseCheckoutEventResponse {
|
||||
id: number;
|
||||
title: string;
|
||||
image: string | null;
|
||||
}
|
||||
|
||||
export interface PurchaseDetailResponse extends PurchaseStatusResponse {
|
||||
id: number;
|
||||
cart_id: number | null;
|
||||
@@ -140,6 +146,7 @@ export interface PurchaseDetailResponse extends PurchaseStatusResponse {
|
||||
email: string | null;
|
||||
items_source: 'purchase';
|
||||
items: PurchaseDetailItemResponse[];
|
||||
event?: PurchaseCheckoutEventResponse | null;
|
||||
tickets_count?: number;
|
||||
has_generated_tickets?: boolean;
|
||||
subtotal: string;
|
||||
|
||||
@@ -134,6 +134,7 @@ export interface Tenant {
|
||||
footer_bg_image?: string | null;
|
||||
storefront_website_type_code?: string | null;
|
||||
header_type?: 'standard' | 'immersive';
|
||||
checkout_summary_type?: 'default' | 'event';
|
||||
extras?: WebsiteExtras;
|
||||
event?: TenantEvent | null;
|
||||
selected_bank_account_id?: number | null;
|
||||
|
||||
@@ -103,7 +103,7 @@ export class PurchaseDetailPage implements OnInit {
|
||||
id: item.id,
|
||||
title: item.item_details.nombre || 'Producto sin nombre',
|
||||
description: item.item_details.descripcion,
|
||||
imageUrl: item.item_details.imagen,
|
||||
imageUrl: item.item_details.imagen ?? null,
|
||||
quantity: item.quantity,
|
||||
attributes: item.item_details.attributes.map((attribute) => ({
|
||||
label: attribute.name,
|
||||
|
||||
@@ -47,7 +47,26 @@
|
||||
</div>
|
||||
|
||||
<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
|
||||
class="checkout-page__mobile-countdown d-flex d-md-none align-items-baseline justify-content-center gap-3"
|
||||
role="timer"
|
||||
|
||||
@@ -46,6 +46,38 @@
|
||||
line-height: 1;
|
||||
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 {
|
||||
|
||||
@@ -39,6 +39,8 @@ describe('CheckoutPageComponent payment validation', () => {
|
||||
let tenantState: ReturnType<
|
||||
typeof signal<{
|
||||
codigo: string;
|
||||
storefront_website_type_code?: string | null;
|
||||
checkout_summary_type?: 'default' | 'event';
|
||||
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 () => {
|
||||
let resolvePurchase!: (purchase: any) => void;
|
||||
routeParamMap = convertToParamMap({ id: 25 });
|
||||
|
||||
@@ -138,6 +138,12 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
||||
const purchase = this.createdPurchase();
|
||||
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 paymentMethods: ReadonlyArray<PaymentMethodOption> = [
|
||||
@@ -235,7 +241,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
||||
private mapPurchaseItemToMock(item: PurchaseDetailItemResponse): CartItemMock {
|
||||
return {
|
||||
cartItemId: item.id,
|
||||
imageUrl: item.item_details.imagen,
|
||||
imageUrl: this.usesEventCheckoutSummary() ? null : (item.item_details.imagen ?? null),
|
||||
product: item.item_details.nombre,
|
||||
originalPrice: null,
|
||||
discountedPrice: parseFloat(item.unit_price),
|
||||
|
||||
Reference in New Issue
Block a user