diff --git a/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.spec.ts b/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.spec.ts index c249803..4e8373e 100644 --- a/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.spec.ts +++ b/src/app/features/store/components/product-attribute-selector/product-attribute-selector.component.spec.ts @@ -2,6 +2,7 @@ import { TestBed } from '@angular/core/testing'; import { beforeEach, describe, expect, it } from 'vitest'; import { ProductAttribute } from '../../../../core/services/catalog/catalog.interface'; +import { createCatalogAvailability } from '../../../../core/services/catalog/catalog-availability'; import { ProductAttributeSelectorComponent } from './product-attribute-selector.component'; describe('ProductAttributeSelectorComponent', () => { @@ -31,7 +32,7 @@ describe('ProductAttributeSelectorComponent', () => { fixture.componentRef.setInput('variants', [ { id: 1, - maximum_addable_quantity: null, + availability: createCatalogAvailability(null), values: { size: 'S' }, }, ]); @@ -51,12 +52,12 @@ describe('ProductAttributeSelectorComponent', () => { fixture.componentRef.setInput('variants', [ { id: 1, - maximum_addable_quantity: 0, + availability: createCatalogAvailability(0), values: { size: 'S' }, }, { id: 2, - maximum_addable_quantity: 2, + availability: createCatalogAvailability(2), values: { size: 'M' }, }, ]); @@ -92,9 +93,9 @@ describe('ProductAttributeSelectorComponent', () => { ]); fixture.componentRef.setInput('inventoryPolicy', 'unlimited'); fixture.componentRef.setInput('variants', [ - { id: 1, maximum_addable_quantity: null, values: { event_date: '1' } }, - { id: 2, maximum_addable_quantity: null, values: { event_date: '2' } }, - { id: 3, maximum_addable_quantity: null, values: { event_date: ['1', '2'] } }, + { id: 1, availability: createCatalogAvailability(null), values: { event_date: '1' } }, + { id: 2, availability: createCatalogAvailability(null), values: { event_date: '2' } }, + { id: 3, availability: createCatalogAvailability(null), values: { event_date: ['1', '2'] } }, ]); fixture.detectChanges(); @@ -135,8 +136,16 @@ describe('ProductAttributeSelectorComponent', () => { ]); fixture.componentRef.setInput('inventoryPolicy', 'unlimited'); fixture.componentRef.setInput('variants', [ - { id: 1, maximum_addable_quantity: null, values: { size: 'S', internal_type: 'adult' } }, - { id: 2, maximum_addable_quantity: null, values: { size: 'M', internal_type: 'child' } }, + { + id: 1, + availability: createCatalogAvailability(null), + values: { size: 'S', internal_type: 'adult' }, + }, + { + id: 2, + availability: createCatalogAvailability(null), + values: { size: 'M', internal_type: 'child' }, + }, ]); fixture.detectChanges(); diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts b/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts index 5e020c8..47fe911 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts @@ -10,6 +10,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { CatalogItemDetail } from '../../../../core/services/catalog/catalog.interface'; import { CatalogService } from '../../../../core/services/catalog/catalog.service'; import { CatalogAvailabilityService } from '../../../../core/services/catalog/catalog-availability.service'; +import { createCatalogAvailability } from '../../../../core/services/catalog/catalog-availability'; import { CartService } from '../../../../core/services/cart/cart.service'; import { ToastService } from '../../../../core/services/toast.service'; import { ProductDetailPageComponent } from './product-detail-page.component'; @@ -38,7 +39,7 @@ describe('ProductDetailPageComponent', () => { has_tickets: false, minimum_use_date: null, maximum_use_date: null, - maximum_addable_quantity: 10, + availability: createCatalogAvailability(10), attributes: [], variants: [], }; @@ -179,7 +180,7 @@ describe('ProductDetailPageComponent', () => { it('reloads product availability when the cart changes', async () => { catalogServiceStub.getCatalogItem.mockReturnValue( - of({ ...mockProduct, maximum_addable_quantity: 4 }), + of({ ...mockProduct, availability: createCatalogAvailability(4) }), ); await configureTestingModule(); const fixture = TestBed.createComponent(ProductDetailPageComponent); @@ -207,10 +208,10 @@ describe('ProductDetailPageComponent', () => { const detailProduct: CatalogItemDetail = { ...mockProduct, images: ['https://example.com/product.png'], - variants: [{ id: 123, maximum_addable_quantity: 10, values: {} }], + variants: [{ id: 123, availability: createCatalogAvailability(10), values: {} }], selected_variant: { id: 123, - maximum_addable_quantity: 10, + availability: createCatalogAvailability(10), images: ['https://example.com/variant1.png', 'https://example.com/variant2.png'], values: {}, }, @@ -312,11 +313,15 @@ describe('ProductDetailPageComponent', () => { }, ], variants: [ - { id: 123, maximum_addable_quantity: 10, values: { color: 'beige', material: 'Cuero' } }, + { + id: 123, + availability: createCatalogAvailability(10), + values: { color: 'beige', material: 'Cuero' }, + }, ], selected_variant: { id: 123, - maximum_addable_quantity: 10, + availability: createCatalogAvailability(10), images: ['https://example.com/variant1.png'], values: { color: 'beige', @@ -353,8 +358,18 @@ describe('ProductDetailPageComponent', () => { purpose: 'entry', has_tickets: true, variants: [ - { id: 101, event_date_id: 20, maximum_addable_quantity: 10, values: { event_date: '20' } }, - { id: 102, event_date_id: 21, maximum_addable_quantity: 10, values: { event_date: '21' } }, + { + id: 101, + event_date_id: 20, + availability: createCatalogAvailability(10), + values: { event_date: '20' }, + }, + { + id: 102, + event_date_id: 21, + availability: createCatalogAvailability(10), + values: { event_date: '21' }, + }, ], attributes: [ { @@ -385,7 +400,7 @@ describe('ProductDetailPageComponent', () => { selected_variant: { id: 101, event_date_id: 20, - maximum_addable_quantity: 10, + availability: createCatalogAvailability(10), images: [], values: {}, }, @@ -407,6 +422,8 @@ describe('ProductDetailPageComponent', () => { options[1].click(); fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); expect(catalogServiceStub.getCatalogItem).toHaveBeenCalledWith(1, 102); }); @@ -429,7 +446,7 @@ describe('ProductDetailPageComponent', () => { fixture.componentInstance['selectedVariant'].set({ id: 1, - maximum_addable_quantity: 10, + availability: createCatalogAvailability(10), values: {}, }); fixture.detectChanges(); @@ -519,7 +536,7 @@ describe('ProductDetailPageComponent', () => { error: { code: 'purchase.limit_exceeded', message: 'Podés agregar hasta 2 unidades más de “Auriculares Bluetooth”.', - maximum_addable_quantity: 2, + availability: createCatalogAvailability(2), }, }), ); @@ -544,13 +561,13 @@ describe('ProductDetailPageComponent', () => { variants: [ { id: 123, - maximum_addable_quantity: 5, + availability: createCatalogAvailability(5), values: {}, }, ], selected_variant: { id: 123, - maximum_addable_quantity: 5, + availability: createCatalogAvailability(5), images: [], values: {}, }, @@ -583,7 +600,7 @@ describe('ProductDetailPageComponent', () => { resolveProduct({ ...mockProduct, variants: [], - maximum_addable_quantity: 4, + availability: createCatalogAvailability(4), }); await configureTestingModule(); @@ -609,13 +626,13 @@ describe('ProductDetailPageComponent', () => { variants: [ { id: 123, - maximum_addable_quantity: 5, + availability: createCatalogAvailability(5), values: {}, }, ], selected_variant: { id: 123, - maximum_addable_quantity: 5, + availability: createCatalogAvailability(5), images: [], values: {}, }, @@ -647,7 +664,7 @@ describe('ProductDetailPageComponent', () => { it('allows unlimited variants to increase quantity without a maximum', async () => { const unlimitedVariant = { id: 321, - maximum_addable_quantity: null, + availability: createCatalogAvailability(null), values: {}, }; resolveProduct({ @@ -655,7 +672,7 @@ describe('ProductDetailPageComponent', () => { inventory_policy: 'unlimited', selected_variant: { id: 321, - maximum_addable_quantity: null, + availability: createCatalogAvailability(null), images: [], values: {}, }, @@ -679,7 +696,7 @@ describe('ProductDetailPageComponent', () => { it('caps an unlimited variant at the per-user purchase limit', async () => { const unlimitedVariant = { id: 322, - maximum_addable_quantity: 2, + availability: createCatalogAvailability(2), values: {}, }; resolveProduct({ @@ -688,7 +705,7 @@ describe('ProductDetailPageComponent', () => { max_units_per_user: 2, selected_variant: { id: 322, - maximum_addable_quantity: 2, + availability: createCatalogAvailability(2), images: [], values: { event_date: '20' }, }, @@ -712,14 +729,14 @@ describe('ProductDetailPageComponent', () => { it('disables purchase actions for tracked variants without stock', async () => { const trackedVariant = { id: 654, - maximum_addable_quantity: 0, + availability: createCatalogAvailability(0), values: {}, }; resolveProduct({ ...mockProduct, selected_variant: { id: 654, - maximum_addable_quantity: 0, + availability: createCatalogAvailability(0), images: [], values: {}, }, diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.resolver.spec.ts b/src/app/features/store/pages/product-detail-page/product-detail-page.resolver.spec.ts index 06656d9..7adbf03 100644 --- a/src/app/features/store/pages/product-detail-page/product-detail-page.resolver.spec.ts +++ b/src/app/features/store/pages/product-detail-page/product-detail-page.resolver.spec.ts @@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { CatalogItemDetail } from '../../../../core/services/catalog/catalog.interface'; import { CatalogService } from '../../../../core/services/catalog/catalog.service'; +import { createCatalogAvailability } from '../../../../core/services/catalog/catalog-availability'; import { PRODUCT_DETAIL_ERROR_MESSAGE, PRODUCT_DETAIL_INVALID_ID_MESSAGE, @@ -29,7 +30,7 @@ describe('productDetailResolver', () => { has_tickets: false, minimum_use_date: null, maximum_use_date: null, - maximum_addable_quantity: 0, + availability: createCatalogAvailability(0), attributes: [], variants: [], }; diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts b/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts index 60367d9..6d7d4a0 100644 --- a/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts @@ -13,6 +13,7 @@ import { } from '../../../../core/services/catalog/catalog.interface'; import { CatalogService } from '../../../../core/services/catalog/catalog.service'; import { CatalogAvailabilityService } from '../../../../core/services/catalog/catalog-availability.service'; +import { createCatalogAvailability } from '../../../../core/services/catalog/catalog-availability'; import { Tenant } from '../../../../core/services/tenant.interface'; import { TenantService } from '../../../../core/services/tenant.service'; import { ToastService } from '../../../../core/services/toast.service'; @@ -83,6 +84,7 @@ const pageOneItems: CatalogFeaturedItem[] = [ nombre: 'Auriculares Bluetooth', precio: '24999.00', image: '/catalog/auriculares.jpg', + availability: createCatalogAvailability(null), }, { id: 2, @@ -90,6 +92,7 @@ const pageOneItems: CatalogFeaturedItem[] = [ nombre: 'Teclado Mecanico', precio: '18999.00', image: null, + availability: createCatalogAvailability(null), }, ]; @@ -406,7 +409,14 @@ describe('StoreHomePageComponent', () => { it('requests another page for the selected featured group', async () => { const pageTwoItems: CatalogFeaturedItem[] = [ - { id: 3, type: 'product', nombre: 'Mouse Gamer', precio: '15999.00', image: null }, + { + id: 3, + type: 'product', + nombre: 'Mouse Gamer', + precio: '15999.00', + image: null, + availability: createCatalogAvailability(null), + }, ]; const catalogServiceStub = { getCatalog: vi.fn(), 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 32ecc20..35b12db 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 @@ -6,6 +6,7 @@ import { of } from 'rxjs'; import { ApiPaginatedResponse } from '../../../core/services/api-paginated-response.interface'; import { CartService } from '../../../core/services/cart/cart.service'; import { CatalogService } from '../../../core/services/catalog/catalog.service'; +import { createCatalogAvailability } from '../../../core/services/catalog/catalog-availability'; import { CatalogFeaturedItems, CatalogGroupLayout, @@ -23,6 +24,7 @@ describe('ProductListComponent', () => { descripcion: 'Primera descripcion', precio: '100.00', image: '/images/one.png', + availability: createCatalogAvailability(null), variants: [], }, { @@ -32,6 +34,7 @@ describe('ProductListComponent', () => { descripcion: 'Segunda descripcion', precio: 200, image: null, + availability: createCatalogAvailability(null), variants: [], }, ]; @@ -193,8 +196,8 @@ describe('ProductListComponent', () => { const itemWithVariants: ProductListItem = { ...items[0], variants: [ - { id: 91, maximum_addable_quantity: 3, values: { fecha: '10 de octubre' } }, - { id: 92, maximum_addable_quantity: 4, values: { fecha: '11 de octubre' } }, + { id: 91, availability: createCatalogAvailability(3), values: { fecha: '10 de octubre' } }, + { id: 92, availability: createCatalogAvailability(4), values: { fecha: '11 de octubre' } }, ], }; const fixture = await render('column_with_cart', [itemWithVariants]); @@ -220,7 +223,7 @@ describe('ProductListComponent', () => { variants: [ { id: 91, - maximum_addable_quantity: 2, + availability: createCatalogAvailability(2), values: { fecha: '10 de octubre' }, }, ], @@ -248,7 +251,7 @@ describe('ProductListComponent', () => { variants: [ { id: 91, - maximum_addable_quantity: 0, + availability: createCatalogAvailability(0), values: { fecha: '10 de octubre' }, }, ], @@ -304,7 +307,7 @@ describe('ProductListComponent', () => { { id: 401, precio: '10000.00', - maximum_addable_quantity: 1, + availability: createCatalogAvailability(1), values: { tipo: { value: 'vip', label: 'VIP' }, sector: { value: 'a', label: 'Sector A' }, @@ -315,7 +318,7 @@ describe('ProductListComponent', () => { { id: 402, precio: '12000.00', - maximum_addable_quantity: 1, + availability: createCatalogAvailability(1), values: { tipo: { value: 'vip', label: 'VIP' }, sector: { value: 'a', label: 'Sector A' }, diff --git a/src/app/shared/components/product-vertical-with-cart-card/product-vertical-with-cart-card.component.spec.ts b/src/app/shared/components/product-vertical-with-cart-card/product-vertical-with-cart-card.component.spec.ts index 4d9d902..204db94 100644 --- a/src/app/shared/components/product-vertical-with-cart-card/product-vertical-with-cart-card.component.spec.ts +++ b/src/app/shared/components/product-vertical-with-cart-card/product-vertical-with-cart-card.component.spec.ts @@ -58,11 +58,22 @@ describe('ProductVerticalWithCartCardComponent', () => { it('shows the backend availability message with the reusable tooltip', async () => { const fixture = await createComponent(); - fixture.componentRef.setInput('maximumAddableQuantity', 0); - fixture.componentRef.setInput( - 'unavailableMessage', - 'Alcanzaste el cupo máximo permitido para este producto.', - ); + fixture.componentRef.setInput('availability', { + maximum_quantity: 0, + restrictions: [ + { + code: 'user_quota_reached', + message: 'Alcanzaste el cupo máximo permitido para este producto.', + scope: 'product', + }, + ], + capabilities: { + select_variant: false, + change_quantity: false, + add_to_cart: false, + buy_now: false, + }, + }); fixture.detectChanges(); const tooltip = (fixture.nativeElement as HTMLElement).querySelector('app-tooltip'); diff --git a/src/app/shared/components/variant-selector/variant-selector.component.spec.ts b/src/app/shared/components/variant-selector/variant-selector.component.spec.ts index 47763a5..c72276d 100644 --- a/src/app/shared/components/variant-selector/variant-selector.component.spec.ts +++ b/src/app/shared/components/variant-selector/variant-selector.component.spec.ts @@ -62,6 +62,8 @@ describe('VariantSelectorComponent', () => { ]); fixture.componentRef.setInput('selectedVariant', 2); fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); const selects = Array.from( fixture.nativeElement.querySelectorAll('select'), @@ -80,6 +82,28 @@ describe('VariantSelectorComponent', () => { expect(fixture.componentInstance.selectedVariant()).toBe(1); }); + it('does not offer variants whose availability forbids selection', async () => { + await TestBed.configureTestingModule({ + imports: [VariantSelectorComponent], + }).compileComponents(); + const fixture = TestBed.createComponent(VariantSelectorComponent); + fixture.componentRef.setInput('variants', [ + { id: 1, values: { talle: 'S' } }, + { + id: 2, + values: { talle: 'M' }, + availability: { capabilities: { select_variant: false } }, + }, + ]); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toContain('S'); + expect(fixture.nativeElement.textContent).not.toContain('M'); + expect(fixture.componentInstance.selectedVariant()).toBe(1); + }); + it('requires manual selections when autoSelectFirst is disabled', async () => { await TestBed.configureTestingModule({ imports: [VariantSelectorComponent],