diff --git a/src/app/core/services/checkout.service.ts b/src/app/core/services/checkout.service.ts index 7899eda..80e537a 100644 --- a/src/app/core/services/checkout.service.ts +++ b/src/app/core/services/checkout.service.ts @@ -17,6 +17,32 @@ export interface DirectCheckoutItem { cantidad: number; } +export interface UnavailableCheckoutItem { + index: number; + catalog_item_id: number; + variant_id: number | null; + requested_quantity: number; + available_quantity: number; + message: string; +} + +export interface InsufficientStockResponse { + code: 'purchase.insufficient_stock'; + message: string; + errors: Record; + unavailable_items: UnavailableCheckoutItem[]; +} + +export function isInsufficientStockResponse(value: unknown): value is InsufficientStockResponse { + if (typeof value !== 'object' || value === null) return false; + + const response = value as Partial; + + return ( + response.code === 'purchase.insufficient_stock' && Array.isArray(response.unavailable_items) + ); +} + export type StartCheckoutPayload = | { cart_id: number; diff --git a/src/app/shared/components/product-list/product-list.component.html b/src/app/shared/components/product-list/product-list.component.html index 8f10cba..f53c79d 100644 --- a/src/app/shared/components/product-list/product-list.component.html +++ b/src/app/shared/components/product-list/product-list.component.html @@ -34,6 +34,7 @@ [price]="price(item)" [imageUrl]="loadImages() ? (item.image ?? null) : null" [variants]="item.variants ?? []" + [unavailableVariantIds]="unavailableVariantIds()" [disabled]="loading()" (buy)="emitTicketBuy(item, $event)" /> diff --git a/src/app/shared/components/product-list/product-list.component.spec.ts b/src/app/shared/components/product-list/product-list.component.spec.ts index eb3d164..02db057 100644 --- a/src/app/shared/components/product-list/product-list.component.spec.ts +++ b/src/app/shared/components/product-list/product-list.component.spec.ts @@ -276,6 +276,25 @@ describe('ProductListComponent', () => { }); }); + it('removes unavailable variants from the ticket selector', async () => { + const ticket: ProductListItem = { + ...items[0], + variants: [ + { id: 401, stock_tecnico: 1, values: { asiento: { value: '1', label: '1' } } }, + { id: 402, stock_tecnico: 1, values: { asiento: { value: '2', label: '2' } } }, + ], + }; + const fixture = await render('ticket_selector', [ticket], 'single'); + fixture.componentRef.setInput('unavailableVariantIds', new Set([401])); + await fixture.whenStable(); + fixture.detectChanges(); + + const selector = fixture.debugElement.query(By.directive(ProductTicketSelectorComponent)) + .componentInstance as ProductTicketSelectorComponent; + + expect(selector['selectableVariants']().map((variant) => variant.id)).toEqual([402]); + }); + it('renders carousel groups with the reusable carousel', async () => { const fixture = await render('column_with_image', items, 'carousel'); const element = fixture.nativeElement as HTMLElement; diff --git a/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.ts b/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.ts index db9fc8c..4cb66d1 100644 --- a/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.ts +++ b/src/app/shared/components/product-ticket-selector/product-ticket-selector.component.ts @@ -1,4 +1,12 @@ -import { ChangeDetectionStrategy, Component, computed, input, output, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + input, + output, + signal, +} from '@angular/core'; import { ButtonComponent } from '../button/button.component'; import { IconButtonComponent } from '../icon-button/icon-button.component'; @@ -30,6 +38,7 @@ export class ProductTicketSelectorComponent { readonly price = input(0); readonly imageUrl = input(null); readonly variants = input([]); + readonly unavailableVariantIds = input>(new Set()); readonly disabled = input(false); readonly buy = output(); @@ -38,7 +47,11 @@ export class ProductTicketSelectorComponent { private nextRowId = 2; protected readonly selectableVariants = computed(() => - this.variants().filter((variant) => variant.stock_tecnico == null || variant.stock_tecnico > 0), + this.variants().filter( + (variant) => + !this.unavailableVariantIds().has(variant.id as number) && + (variant.stock_tecnico == null || variant.stock_tecnico > 0), + ), ); protected readonly hasSelection = computed( () => this.rows().length > 0 && this.rows().every((row) => row.variantId !== null), @@ -47,8 +60,7 @@ export class ProductTicketSelectorComponent { () => this.rows().length < this.selectableVariants().length, ); protected readonly priceRange = computed(() => { - const prices = this.variants() - .filter((variant) => variant.stock_tecnico == null || variant.stock_tecnico > 0) + const prices = this.selectableVariants() .map((variant) => Number(variant.precio ?? this.price())) .filter(Number.isFinite); @@ -66,6 +78,24 @@ export class ProductTicketSelectorComponent { }; }); + constructor() { + effect(() => { + const unavailable = this.unavailableVariantIds(); + + if (!this.rows().some((row) => row.variantId !== null && unavailable.has(row.variantId))) { + return; + } + + this.rows.update((rows) => + rows.map((row) => + row.variantId !== null && unavailable.has(row.variantId) + ? { ...row, variantId: null } + : row, + ), + ); + }); + } + protected onBuy(): void { const variantIds = this.rows().flatMap((row) => row.variantId === null ? [] : [row.variantId],