Compare commits
3 Commits
91b8788c91
...
a825b3693b
| Author | SHA1 | Date | |
|---|---|---|---|
| a825b3693b | |||
| 679342ec8d | |||
| a5420e6a42 |
@@ -290,7 +290,7 @@ export class StoreLayoutComponent implements OnInit {
|
|||||||
this.toastService.danger(message);
|
this.toastService.danger(message);
|
||||||
|
|
||||||
if (error instanceof HttpErrorResponse && isExpiredStockReservationResponse(error.error)) {
|
if (error instanceof HttpErrorResponse && isExpiredStockReservationResponse(error.error)) {
|
||||||
this.cartService.loadCart().subscribe({
|
this.cartService.loadCart(true).subscribe({
|
||||||
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,20 @@ describe('CartService', () => {
|
|||||||
req.flush({ data: mockCart });
|
req.flush({ data: mockCart });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('refreshes catalog availability only after the expired cart has been reloaded', () => {
|
||||||
|
const availabilityChanged = vi.fn();
|
||||||
|
catalogAvailabilityService.availabilityChanged$.subscribe(availabilityChanged);
|
||||||
|
|
||||||
|
service.loadCart(true).subscribe();
|
||||||
|
|
||||||
|
expect(availabilityChanged).not.toHaveBeenCalled();
|
||||||
|
const req = httpMock.expectOne('http://api.test/tenants/acme/cart');
|
||||||
|
req.flush({ data: mockCart });
|
||||||
|
|
||||||
|
expect(service.cart()).toEqual(mockCart);
|
||||||
|
expect(availabilityChanged).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
it('propagates a custom loading mode to the request context', () => {
|
it('propagates a custom loading mode to the request context', () => {
|
||||||
service.withCustomLoading().loadCart().subscribe();
|
service.withCustomLoading().loadCart().subscribe();
|
||||||
|
|
||||||
|
|||||||
@@ -34,14 +34,19 @@ export class CartService extends BaseApiService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCart(): Observable<Cart> {
|
loadCart(refreshCatalogAvailability = false): Observable<Cart> {
|
||||||
return this.http
|
return this.http
|
||||||
.get<ApiResponse<Cart>>(`${this.tenantApiUrl}/cart`, {
|
.get<ApiResponse<Cart>>(`${this.tenantApiUrl}/cart`, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
map((response) => response.data),
|
map((response) => response.data),
|
||||||
tap((cart) => this.cartState.set(cart)),
|
tap((cart) => {
|
||||||
|
this.cartState.set(cart);
|
||||||
|
if (refreshCatalogAvailability) {
|
||||||
|
this.catalogAvailabilityService.notifyAvailabilityChanged();
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { provideHttpClient } from '@angular/common/http';
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { CatalogAvailabilityService } from './catalog/catalog-availability.service';
|
||||||
import { CheckoutService } from './checkout.service';
|
import { CheckoutService } from './checkout.service';
|
||||||
|
|
||||||
describe('CheckoutService', () => {
|
describe('CheckoutService', () => {
|
||||||
@@ -39,6 +40,45 @@ describe('CheckoutService', () => {
|
|||||||
await expect(purchasePromise).resolves.toMatchObject({ id: 55 });
|
await expect(purchasePromise).resolves.toMatchObject({ id: 55 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('refreshes catalog availability when starting checkout fails', async () => {
|
||||||
|
const catalogAvailabilityService = TestBed.inject(CatalogAvailabilityService);
|
||||||
|
const notifyAvailabilityChanged = vi.spyOn(
|
||||||
|
catalogAvailabilityService,
|
||||||
|
'notifyAvailabilityChanged',
|
||||||
|
);
|
||||||
|
const purchasePromise = service.startCheckout('desfile', { cart_id: 12 });
|
||||||
|
const request = httpMock.expectOne(`${environment.url}tenants/desfile/compras/start-checkout`);
|
||||||
|
|
||||||
|
request.flush(
|
||||||
|
{ message: 'No hay stock disponible.' },
|
||||||
|
{ status: 422, statusText: 'Unprocessable Entity' },
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(purchasePromise).rejects.toBeTruthy();
|
||||||
|
expect(notifyAvailabilityChanged).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('waits for the expired cart refresh before refreshing catalog availability', async () => {
|
||||||
|
const catalogAvailabilityService = TestBed.inject(CatalogAvailabilityService);
|
||||||
|
const notifyAvailabilityChanged = vi.spyOn(
|
||||||
|
catalogAvailabilityService,
|
||||||
|
'notifyAvailabilityChanged',
|
||||||
|
);
|
||||||
|
const purchasePromise = service.startCheckout('desfile', { cart_id: 12 });
|
||||||
|
const request = httpMock.expectOne(`${environment.url}tenants/desfile/compras/start-checkout`);
|
||||||
|
|
||||||
|
request.flush(
|
||||||
|
{
|
||||||
|
code: 'stock_reservation.expired',
|
||||||
|
message: 'La reserva de stock venció.',
|
||||||
|
},
|
||||||
|
{ status: 422, statusText: 'Unprocessable Entity' },
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(purchasePromise).rejects.toBeTruthy();
|
||||||
|
expect(notifyAvailabilityChanged).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('preserves checkout timing fields when completing a purchase', async () => {
|
it('preserves checkout timing fields when completing a purchase', async () => {
|
||||||
const purchasePromise = service.completePurchase('desfile', 55);
|
const purchasePromise = service.completePurchase('desfile', 55);
|
||||||
const request = httpMock.expectOne(`${environment.url}tenants/desfile/compras/55/complete`);
|
const request = httpMock.expectOne(`${environment.url}tenants/desfile/compras/55/complete`);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { ApiPaginatedResponse } from './api-paginated-response.interface';
|
import { ApiPaginatedResponse } from './api-paginated-response.interface';
|
||||||
import { ApiPaginationQueryParams } from './api-pagination-query-params.interface';
|
import { ApiPaginationQueryParams } from './api-pagination-query-params.interface';
|
||||||
import { BaseApiService } from './base-api.service';
|
import { BaseApiService } from './base-api.service';
|
||||||
|
import { CatalogAvailabilityService } from './catalog/catalog-availability.service';
|
||||||
|
|
||||||
export interface UpdatePurchaseCustomerPayload {
|
export interface UpdatePurchaseCustomerPayload {
|
||||||
dni: string;
|
dni: string;
|
||||||
@@ -126,24 +127,34 @@ export interface PurchaseDetailResponse extends PurchaseStatusResponse {
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class CheckoutService extends BaseApiService {
|
export class CheckoutService extends BaseApiService {
|
||||||
|
private readonly catalogAvailabilityService = inject(CatalogAvailabilityService);
|
||||||
|
|
||||||
async startCheckout(
|
async startCheckout(
|
||||||
tenantCode: string,
|
tenantCode: string,
|
||||||
payload: StartCheckoutPayload,
|
payload: StartCheckoutPayload,
|
||||||
): Promise<PurchaseDetailResponse> {
|
): Promise<PurchaseDetailResponse> {
|
||||||
const response = await firstValueFrom(
|
try {
|
||||||
this.http.post<{ data?: PurchaseDetailResponse } | PurchaseDetailResponse>(
|
const response = await firstValueFrom(
|
||||||
`${environment.url}tenants/${tenantCode}/compras/start-checkout`,
|
this.http.post<{ data?: PurchaseDetailResponse } | PurchaseDetailResponse>(
|
||||||
payload,
|
`${environment.url}tenants/${tenantCode}/compras/start-checkout`,
|
||||||
),
|
payload,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const purchase = this.extractResponseData<PurchaseDetailResponse>(response);
|
const purchase = this.extractResponseData<PurchaseDetailResponse>(response);
|
||||||
|
|
||||||
if (!purchase?.id) {
|
if (!purchase?.id) {
|
||||||
throw new Error('Error al crear la compra.');
|
throw new Error('Error al crear la compra.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return purchase;
|
||||||
|
} catch (error) {
|
||||||
|
const responseBody = (error as { error?: unknown } | null)?.error;
|
||||||
|
if (!isExpiredStockReservationResponse(responseBody)) {
|
||||||
|
this.catalogAvailabilityService.notifyAvailabilityChanged();
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return purchase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async generatePaymentIntent(
|
async generatePaymentIntent(
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export class CategoryItemsPageComponent {
|
|||||||
this.toastService.danger(message);
|
this.toastService.danger(message);
|
||||||
|
|
||||||
if (error instanceof HttpErrorResponse && isExpiredStockReservationResponse(error.error)) {
|
if (error instanceof HttpErrorResponse && isExpiredStockReservationResponse(error.error)) {
|
||||||
this.cartService.loadCart().subscribe({
|
this.cartService.loadCart(true).subscribe({
|
||||||
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (this.isStockReservationExpiredError(error)) {
|
if (this.isStockReservationExpiredError(error)) {
|
||||||
this.showRequestError(error, 'La reserva de stock venció.');
|
this.showRequestError(error, 'La reserva de stock venció.');
|
||||||
this.cartService.loadCart().subscribe({
|
this.cartService.loadCart(true).subscribe({
|
||||||
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
||||||
});
|
});
|
||||||
this.createdPurchaseId.set(null);
|
this.createdPurchaseId.set(null);
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ export class SearchPageComponent {
|
|||||||
this.toastService.danger(message);
|
this.toastService.danger(message);
|
||||||
|
|
||||||
if (error instanceof HttpErrorResponse && isExpiredStockReservationResponse(error.error)) {
|
if (error instanceof HttpErrorResponse && isExpiredStockReservationResponse(error.error)) {
|
||||||
this.cartService.loadCart().subscribe({
|
this.cartService.loadCart(true).subscribe({
|
||||||
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (error instanceof HttpErrorResponse && isExpiredStockReservationResponse(error.error)) {
|
if (error instanceof HttpErrorResponse && isExpiredStockReservationResponse(error.error)) {
|
||||||
this.toastService.danger(error.error.message);
|
this.toastService.danger(error.error.message);
|
||||||
this.cartService.loadCart().subscribe({
|
this.cartService.loadCart(true).subscribe({
|
||||||
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ export class CartComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cartService.loadCart().subscribe({
|
this.cartService.loadCart(true).subscribe({
|
||||||
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
error: (refreshError) => console.error('Error refreshing expired cart', refreshError),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user