diff --git a/src/app/shared/components/cart/cart.component.spec.ts b/src/app/shared/components/cart/cart.component.spec.ts index 7aaf644..ebb46da 100644 --- a/src/app/shared/components/cart/cart.component.spec.ts +++ b/src/app/shared/components/cart/cart.component.spec.ts @@ -175,6 +175,63 @@ describe('CartComponent', () => { expect(removeItem).not.toHaveBeenCalled(); }); + it('notifies the user and refreshes the cart when a mutation reports expiration', async () => { + const expirationMessage = 'La reserva de stock venció. Usá el carrito activo para continuar.'; + const removeItem = vi.fn().mockReturnValue( + throwError(() => ({ + error: { + code: 'stock_reservation.expired', + message: expirationMessage, + }, + })), + ); + const loadCart = vi.fn().mockReturnValue(of({})); + const danger = vi.fn(); + + await TestBed.configureTestingModule({ + imports: [CartComponent], + providers: [ + { + provide: CartService, + useValue: { + cart: signal(null).asReadonly(), + loadCart, + updateItemQuantity: vi.fn(), + removeItem, + }, + }, + { + provide: ModalService, + useValue: { openConfirmDelete: vi.fn().mockReturnValue(of(true)) }, + }, + { + provide: ToastService, + useValue: { success: vi.fn(), info: vi.fn(), danger }, + }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(CartComponent); + fixture.componentRef.setInput('items', [ + { + cartItemId: 10, + imageUrl: null, + product: 'Producto vencido', + originalPrice: null, + discountedPrice: 1000, + discountPercentage: null, + attributes: [], + quantity: 1, + }, + ]); + fixture.detectChanges(); + + fixture.debugElement.query(By.css('app-cart-item')).triggerEventHandler('remove'); + + expect(danger).toHaveBeenCalledWith(expirationMessage); + expect(loadCart).toHaveBeenCalledOnce(); + }); + it('optimistically updates quantity and rolls back on error', async () => { vi.useFakeTimers(); const updateItemQuantity = vi.fn().mockReturnValue(throwError(() => new Error('Error'))); diff --git a/src/app/shared/components/cart/cart.component.ts b/src/app/shared/components/cart/cart.component.ts index bcd1bb1..2f57de5 100644 --- a/src/app/shared/components/cart/cart.component.ts +++ b/src/app/shared/components/cart/cart.component.ts @@ -98,11 +98,12 @@ export class CartComponent { this.clearOverride(update.cartItemId); }, error: (err: HttpErrorResponse) => { - console.error('Error updating cart quantity', err); - const msg = - err.error?.message || 'Error al actualizar la cantidad del producto.'; - this.toastService.danger(msg); this.clearOverride(update.cartItemId); + this.handleMutationError( + err, + 'Error al actualizar la cantidad del producto.', + 'Error updating cart quantity', + ); }, }), catchError(() => EMPTY), @@ -205,9 +206,12 @@ export class CartComponent { this.toastService.success(response.message || 'Variante actualizada.'); }, error: (error: HttpErrorResponse) => { - console.error('Error updating cart item variant', error); this.clearVariantOverride(cartItemId); - this.toastService.danger(error.error?.message || 'No se pudo actualizar la variante.'); + this.handleMutationError( + error, + 'No se pudo actualizar la variante.', + 'Error updating cart item variant', + ); }, }); } @@ -305,10 +309,29 @@ export class CartComponent { this.toastService.info(msg); }, error: (err: HttpErrorResponse) => { - console.error('Error removing item from cart', err); - const msg = err.error?.message || 'Error al eliminar el producto del carrito.'; - this.toastService.danger(msg); + this.handleMutationError( + err, + 'Error al eliminar el producto del carrito.', + 'Error removing item from cart', + ); }, }); } + + private handleMutationError( + error: HttpErrorResponse, + fallbackMessage: string, + logMessage: string, + ): void { + console.error(logMessage, error); + this.toastService.danger(error.error?.message || fallbackMessage); + + if (error.error?.code !== 'stock_reservation.expired') { + return; + } + + this.cartService.loadCart().subscribe({ + error: (refreshError) => console.error('Error refreshing expired cart', refreshError), + }); + } }