feat(cart): refactor cart item structure and update related components for consistency

This commit is contained in:
2026-07-16 10:02:24 -03:00
parent e4195a8567
commit 8b785fd631
10 changed files with 246 additions and 195 deletions

View File

@@ -3,12 +3,14 @@ export interface CartItemProduct {
imagen: string | null;
}
export type BuyableType = 'variant' | 'bundle';
export interface CartItem {
id: number;
cantidad: number;
precio_unitario: string;
product_id: number;
product_variant_id: number;
buyable_type: BuyableType;
buyable_id: number;
product: CartItemProduct | null;
}

View File

@@ -21,21 +21,21 @@ describe('CartService', () => {
id: 1,
cantidad: 2,
precio_unitario: '10.00',
product_id: 5,
product_variant_id: 10,
buyable_type: 'variant',
buyable_id: 10,
product: {
nombre: 'Test Product (Size: M)',
imagen: null
}
}
imagen: null,
},
},
],
subtotal: '20.00'
subtotal: '20.00',
};
beforeEach(() => {
tenantServiceMock = {
getTenantApiUrl: vi.fn().mockReturnValue('http://api.test/tenants/acme'),
tenant: vi.fn().mockReturnValue({ codigo: 'acme' })
tenant: vi.fn().mockReturnValue({ codigo: 'acme' }),
};
TestBed.configureTestingModule({
@@ -43,8 +43,8 @@ describe('CartService', () => {
CartService,
provideHttpClient(),
provideHttpClientTesting(),
{ provide: TenantService, useValue: tenantServiceMock }
]
{ provide: TenantService, useValue: tenantServiceMock },
],
});
service = TestBed.inject(CartService);
@@ -83,19 +83,23 @@ describe('CartService', () => {
tenant_codigo: 'acme',
status: 'active',
items: [],
subtotal: '0.00'
subtotal: '0.00',
});
});
it('should add item and update signal', () => {
service.addItem(10, 2).subscribe((res) => {
service.addItem('variant', 10, 2).subscribe((res) => {
expect(res.data).toEqual(mockCart);
expect(service.cart()).toEqual(mockCart);
});
const req = httpMock.expectOne('http://api.test/tenants/acme/cart/items');
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual({ product_variant_id: 10, cantidad: 2 });
expect(req.request.body).toEqual({
buyable_type: 'variant',
buyable_id: 10,
cantidad: 2,
});
expect(req.request.withCredentials).toBe(true);
req.flush({ data: mockCart });
});
@@ -122,7 +126,7 @@ describe('CartService', () => {
tenant_codigo: 'acme',
status: 'active',
items: [],
subtotal: '0.00'
subtotal: '0.00',
};
service.removeItem(10).subscribe((res) => {

View File

@@ -4,10 +4,10 @@ import { catchError, map, Observable, tap } from 'rxjs';
import { ApiResponse } from '../api-response.interface';
import { TenantService } from '../tenant.service';
import { Cart } from './cart.interface';
import { BuyableType, Cart } from './cart.interface';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class CartService {
private readonly http = inject(HttpClient);
@@ -29,29 +29,31 @@ export class CartService {
tenant_codigo: this.tenantService.tenant()?.codigo ?? '',
status: 'active',
items: [],
subtotal: '0.00'
subtotal: '0.00',
});
}
loadCart(): Observable<Cart> {
return this.http
.get<ApiResponse<Cart>>(`${this.tenantApiUrl}/cart`, {
withCredentials: true
withCredentials: true,
})
.pipe(
map((response) => response.data),
tap((cart) => this.cartState.set(cart))
tap((cart) => this.cartState.set(cart)),
);
}
addItem(productVariantId: number, cantidad: number): Observable<ApiResponse<Cart>> {
addItem(
buyableType: BuyableType,
buyableId: number,
cantidad: number,
): Observable<ApiResponse<Cart>> {
this.isUpdatingState.set(true);
return this.http
.post<ApiResponse<Cart>>(
`${this.tenantApiUrl}/cart/items`,
{ product_variant_id: productVariantId, cantidad },
{ withCredentials: true }
)
.post<
ApiResponse<Cart>
>(`${this.tenantApiUrl}/cart/items`, { buyable_type: buyableType, buyable_id: buyableId, cantidad }, { withCredentials: true })
.pipe(
tap((response) => {
this.cartState.set(response.data);
@@ -60,18 +62,16 @@ export class CartService {
catchError((error) => {
this.isUpdatingState.set(false);
throw error;
})
}),
);
}
updateItemQuantity(productVariantId: number, cantidad: number): Observable<ApiResponse<Cart>> {
updateItemQuantity(cartItemId: number, cantidad: number): Observable<ApiResponse<Cart>> {
this.isUpdatingState.set(true);
return this.http
.patch<ApiResponse<Cart>>(
`${this.tenantApiUrl}/cart/items/${productVariantId}`,
{ cantidad },
{ withCredentials: true }
)
.patch<
ApiResponse<Cart>
>(`${this.tenantApiUrl}/cart/items/${cartItemId}`, { cantidad }, { withCredentials: true })
.pipe(
tap((response) => {
this.cartState.set(response.data);
@@ -80,17 +80,16 @@ export class CartService {
catchError((error) => {
this.isUpdatingState.set(false);
throw error;
})
}),
);
}
removeItem(productVariantId: number): Observable<ApiResponse<Cart>> {
removeItem(cartItemId: number): Observable<ApiResponse<Cart>> {
this.isUpdatingState.set(true);
return this.http
.delete<ApiResponse<Cart>>(
`${this.tenantApiUrl}/cart/items/${productVariantId}`,
{ withCredentials: true }
)
.delete<
ApiResponse<Cart>
>(`${this.tenantApiUrl}/cart/items/${cartItemId}`, { withCredentials: true })
.pipe(
tap((response) => {
this.cartState.set(response.data);
@@ -99,7 +98,7 @@ export class CartService {
catchError((error) => {
this.isUpdatingState.set(false);
throw error;
})
}),
);
}
}