feat: implement CartService with state management and API integration for shopping cart operations
This commit is contained in:
21
src/app/core/services/cart/cart.interface.ts
Normal file
21
src/app/core/services/cart/cart.interface.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface CartItemProduct {
|
||||
nombre: string;
|
||||
imagen: string | null;
|
||||
}
|
||||
|
||||
export interface CartItem {
|
||||
id: number;
|
||||
cantidad: number;
|
||||
precio_unitario: string;
|
||||
product_id: number;
|
||||
product_variant_id: number;
|
||||
product: CartItemProduct | null;
|
||||
}
|
||||
|
||||
export interface Cart {
|
||||
id: number | null;
|
||||
tenant_codigo: string;
|
||||
status: string;
|
||||
items: CartItem[];
|
||||
subtotal: string;
|
||||
}
|
||||
125
src/app/core/services/cart/cart.service.spec.ts
Normal file
125
src/app/core/services/cart/cart.service.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
|
||||
import { CartService } from './cart.service';
|
||||
import { TenantService } from '../tenant.service';
|
||||
import { Cart } from './cart.interface';
|
||||
|
||||
describe('CartService', () => {
|
||||
let service: CartService;
|
||||
let httpMock: HttpTestingController;
|
||||
let tenantServiceMock: any;
|
||||
|
||||
const mockCart: Cart = {
|
||||
id: 123,
|
||||
tenant_codigo: 'acme',
|
||||
status: 'active',
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
cantidad: 2,
|
||||
precio_unitario: '10.00',
|
||||
product_id: 5,
|
||||
product_variant_id: 10,
|
||||
product: {
|
||||
nombre: 'Test Product (Size: M)',
|
||||
imagen: null
|
||||
}
|
||||
}
|
||||
],
|
||||
subtotal: '20.00'
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
tenantServiceMock = {
|
||||
getTenantApiUrl: vi.fn().mockReturnValue('http://api.test/tenants/acme')
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CartService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{ provide: TenantService, useValue: tenantServiceMock }
|
||||
]
|
||||
});
|
||||
|
||||
service = TestBed.inject(CartService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should initialize with null cart signal', () => {
|
||||
expect(service.cart()).toBeNull();
|
||||
});
|
||||
|
||||
it('should load cart and update signal', () => {
|
||||
service.loadCart().subscribe((cart) => {
|
||||
expect(cart).toEqual(mockCart);
|
||||
expect(service.cart()).toEqual(mockCart);
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne('http://api.test/tenants/acme/cart');
|
||||
expect(req.request.method).toBe('GET');
|
||||
expect(req.request.withCredentials).toBe(true);
|
||||
req.flush({ data: mockCart });
|
||||
});
|
||||
|
||||
it('should add item and update signal', () => {
|
||||
service.addItem(10, 2).subscribe((cart) => {
|
||||
expect(cart).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.withCredentials).toBe(true);
|
||||
req.flush({ data: mockCart });
|
||||
});
|
||||
|
||||
it('should update item quantity and update signal', () => {
|
||||
const updatedCart = { ...mockCart, subtotal: '30.00' };
|
||||
updatedCart.items[0].cantidad = 3;
|
||||
|
||||
service.updateItemQuantity(10, 3).subscribe((cart) => {
|
||||
expect(cart).toEqual(updatedCart);
|
||||
expect(service.cart()).toEqual(updatedCart);
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne('http://api.test/tenants/acme/cart/items/10');
|
||||
expect(req.request.method).toBe('PATCH');
|
||||
expect(req.request.body).toEqual({ cantidad: 3 });
|
||||
expect(req.request.withCredentials).toBe(true);
|
||||
req.flush({ data: updatedCart });
|
||||
});
|
||||
|
||||
it('should remove item and update signal', () => {
|
||||
const emptyCart: Cart = {
|
||||
id: 123,
|
||||
tenant_codigo: 'acme',
|
||||
status: 'active',
|
||||
items: [],
|
||||
subtotal: '0.00'
|
||||
};
|
||||
|
||||
service.removeItem(10).subscribe((cart) => {
|
||||
expect(cart).toEqual(emptyCart);
|
||||
expect(service.cart()).toEqual(emptyCart);
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne('http://api.test/tenants/acme/cart/items/10');
|
||||
expect(req.request.method).toBe('DELETE');
|
||||
expect(req.request.withCredentials).toBe(true);
|
||||
req.flush({ data: emptyCart });
|
||||
});
|
||||
});
|
||||
71
src/app/core/services/cart/cart.service.ts
Normal file
71
src/app/core/services/cart/cart.service.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { map, Observable, tap } from 'rxjs';
|
||||
|
||||
import { ApiResponse } from '../api-response.interface';
|
||||
import { TenantService } from '../tenant.service';
|
||||
import { Cart } from './cart.interface';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CartService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly tenantService = inject(TenantService);
|
||||
|
||||
private readonly cartState = signal<Cart | null>(null);
|
||||
readonly cart = this.cartState.asReadonly();
|
||||
|
||||
private get tenantApiUrl(): string {
|
||||
return this.tenantService.getTenantApiUrl();
|
||||
}
|
||||
|
||||
loadCart(): Observable<Cart> {
|
||||
return this.http
|
||||
.get<ApiResponse<Cart>>(`${this.tenantApiUrl}/cart`, {
|
||||
withCredentials: true
|
||||
})
|
||||
.pipe(
|
||||
map((response) => response.data),
|
||||
tap((cart) => this.cartState.set(cart))
|
||||
);
|
||||
}
|
||||
|
||||
addItem(productVariantId: number, cantidad: number): Observable<Cart> {
|
||||
return this.http
|
||||
.post<ApiResponse<Cart>>(
|
||||
`${this.tenantApiUrl}/cart/items`,
|
||||
{ product_variant_id: productVariantId, cantidad },
|
||||
{ withCredentials: true }
|
||||
)
|
||||
.pipe(
|
||||
map((response) => response.data),
|
||||
tap((cart) => this.cartState.set(cart))
|
||||
);
|
||||
}
|
||||
|
||||
updateItemQuantity(productVariantId: number, cantidad: number): Observable<Cart> {
|
||||
return this.http
|
||||
.patch<ApiResponse<Cart>>(
|
||||
`${this.tenantApiUrl}/cart/items/${productVariantId}`,
|
||||
{ cantidad },
|
||||
{ withCredentials: true }
|
||||
)
|
||||
.pipe(
|
||||
map((response) => response.data),
|
||||
tap((cart) => this.cartState.set(cart))
|
||||
);
|
||||
}
|
||||
|
||||
removeItem(productVariantId: number): Observable<Cart> {
|
||||
return this.http
|
||||
.delete<ApiResponse<Cart>>(
|
||||
`${this.tenantApiUrl}/cart/items/${productVariantId}`,
|
||||
{ withCredentials: true }
|
||||
)
|
||||
.pipe(
|
||||
map((response) => response.data),
|
||||
tap((cart) => this.cartState.set(cart))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user