fix(checkout): refresh expired carts
This commit is contained in:
@@ -681,10 +681,13 @@ describe('StoreLayoutComponent', () => {
|
|||||||
expect((fixture.componentInstance as any).isCartOpen()).toBe(false);
|
expect((fixture.componentInstance as any).isCartOpen()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the backend message when starting checkout fails', async () => {
|
it('shows the backend message and refreshes the cart when its reservation expired', async () => {
|
||||||
const message = 'Alcanzaste el límite de compra para este producto.';
|
const message = 'La reserva de stock venció. Usá el carrito activo para continuar.';
|
||||||
checkoutServiceStub.startCheckout.mockRejectedValue(
|
checkoutServiceStub.startCheckout.mockRejectedValue(
|
||||||
new HttpErrorResponse({ status: 422, error: { message } }),
|
new HttpErrorResponse({
|
||||||
|
status: 422,
|
||||||
|
error: { code: 'stock_reservation.expired', message },
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
cartState.set({
|
cartState.set({
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -702,11 +705,14 @@ describe('StoreLayoutComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
||||||
|
const loadCart = vi.spyOn(TestBed.inject(CartService), 'loadCart');
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
loadCart.mockClear();
|
||||||
|
|
||||||
await (fixture.componentInstance as any).onCheckoutClick();
|
await (fixture.componentInstance as any).onCheckoutClick();
|
||||||
|
|
||||||
expect(TestBed.inject(ToastService).danger).toHaveBeenCalledWith(message);
|
expect(TestBed.inject(ToastService).danger).toHaveBeenCalledWith(message);
|
||||||
|
expect(loadCart).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows modifying quantities directly in the regular cart without a toggle', () => {
|
it('allows modifying quantities directly in the regular cart without a toggle', () => {
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ import { ButtonComponent } from '../../../shared/components/button/button.compon
|
|||||||
import { CartItem, CartItemVariantValue } from '../../services/cart/cart.interface';
|
import { CartItem, CartItemVariantValue } from '../../services/cart/cart.interface';
|
||||||
import { AuthService } from '../../services/auth/auth.service';
|
import { AuthService } from '../../services/auth/auth.service';
|
||||||
import { findMenu } from '../../services/menu.utils';
|
import { findMenu } from '../../services/menu.utils';
|
||||||
import { CheckoutService } from '../../services/checkout.service';
|
import {
|
||||||
|
CheckoutService,
|
||||||
|
isExpiredStockReservationResponse,
|
||||||
|
} from '../../services/checkout.service';
|
||||||
import { ToastService } from '../../services/toast.service';
|
import { ToastService } from '../../services/toast.service';
|
||||||
import { Category } from '../../services/tenant.interface';
|
import { Category } from '../../services/tenant.interface';
|
||||||
|
|
||||||
@@ -285,6 +288,11 @@ export class StoreLayoutComponent implements OnInit {
|
|||||||
: 'No se pudo iniciar la compra.';
|
: 'No se pudo iniciar la compra.';
|
||||||
this.toastService.danger(message);
|
this.toastService.danger(message);
|
||||||
|
|
||||||
|
if (error instanceof HttpErrorResponse && isExpiredStockReservationResponse(error.error)) {
|
||||||
|
this.cartService.loadCart().subscribe({
|
||||||
|
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
||||||
|
});
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.isCreatingPurchase.set(false);
|
this.isCreatingPurchase.set(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,21 @@ export function isInsufficientStockResponse(value: unknown): value is Insufficie
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExpiredStockReservationResponse {
|
||||||
|
code: 'stock_reservation.expired';
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isExpiredStockReservationResponse(
|
||||||
|
value: unknown,
|
||||||
|
): value is ExpiredStockReservationResponse {
|
||||||
|
return (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
(value as Partial<ExpiredStockReservationResponse>).code === 'stock_reservation.expired'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export type StartCheckoutPayload =
|
export type StartCheckoutPayload =
|
||||||
| {
|
| {
|
||||||
cart_id: number;
|
cart_id: number;
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ import {
|
|||||||
|
|
||||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||||
import { CheckoutService } from '../../../../core/services/checkout.service';
|
import {
|
||||||
|
CheckoutService,
|
||||||
|
isExpiredStockReservationResponse,
|
||||||
|
} from '../../../../core/services/checkout.service';
|
||||||
import {
|
import {
|
||||||
CatalogGroupLayout,
|
CatalogGroupLayout,
|
||||||
CategoryItemsResponse,
|
CategoryItemsResponse,
|
||||||
@@ -176,6 +179,11 @@ export class CategoryItemsPageComponent {
|
|||||||
: 'No se pudo iniciar la compra directa.';
|
: 'No se pudo iniciar la compra directa.';
|
||||||
this.toastService.danger(message);
|
this.toastService.danger(message);
|
||||||
|
|
||||||
|
if (error instanceof HttpErrorResponse && isExpiredStockReservationResponse(error.error)) {
|
||||||
|
this.cartService.loadCart().subscribe({
|
||||||
|
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
||||||
|
});
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.creatingDirectPurchase.set(false);
|
this.creatingDirectPurchase.set(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|||||||
|
|
||||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||||
import { CheckoutService } from '../../../../core/services/checkout.service';
|
import {
|
||||||
|
CheckoutService,
|
||||||
|
isExpiredStockReservationResponse,
|
||||||
|
} from '../../../../core/services/checkout.service';
|
||||||
import {
|
import {
|
||||||
CatalogFeaturedItem,
|
CatalogFeaturedItem,
|
||||||
CatalogFeaturedItems,
|
CatalogFeaturedItems,
|
||||||
@@ -208,6 +211,11 @@ export class SearchPageComponent {
|
|||||||
: 'No se pudo iniciar la compra directa.';
|
: 'No se pudo iniciar la compra directa.';
|
||||||
this.toastService.danger(message);
|
this.toastService.danger(message);
|
||||||
|
|
||||||
|
if (error instanceof HttpErrorResponse && isExpiredStockReservationResponse(error.error)) {
|
||||||
|
this.cartService.loadCart().subscribe({
|
||||||
|
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
||||||
|
});
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.creatingDirectPurchase.set(false);
|
this.creatingDirectPurchase.set(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { TenantService } from '../../../../core/services/tenant.service';
|
|||||||
import { ToastService } from '../../../../core/services/toast.service';
|
import { ToastService } from '../../../../core/services/toast.service';
|
||||||
import {
|
import {
|
||||||
CheckoutService,
|
CheckoutService,
|
||||||
|
isExpiredStockReservationResponse,
|
||||||
isInsufficientStockResponse,
|
isInsufficientStockResponse,
|
||||||
} from '../../../../core/services/checkout.service';
|
} from '../../../../core/services/checkout.service';
|
||||||
import {
|
import {
|
||||||
@@ -221,6 +222,14 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create direct purchase:', error);
|
console.error('Failed to create direct purchase:', error);
|
||||||
|
|
||||||
|
if (error instanceof HttpErrorResponse && isExpiredStockReservationResponse(error.error)) {
|
||||||
|
this.toastService.danger(error.error.message);
|
||||||
|
this.cartService.loadCart().subscribe({
|
||||||
|
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof HttpErrorResponse && isInsufficientStockResponse(error.error)) {
|
if (error instanceof HttpErrorResponse && isInsufficientStockResponse(error.error)) {
|
||||||
const unavailableIds = error.error.unavailable_items
|
const unavailableIds = error.error.unavailable_items
|
||||||
.map((item) => item.variant_id)
|
.map((item) => item.variant_id)
|
||||||
|
|||||||
Reference in New Issue
Block a user