diff --git a/src/app/core/services/cart/cart.interface.ts b/src/app/core/services/cart/cart.interface.ts new file mode 100644 index 0000000..d4916aa --- /dev/null +++ b/src/app/core/services/cart/cart.interface.ts @@ -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; +} diff --git a/src/app/core/services/cart/cart.service.spec.ts b/src/app/core/services/cart/cart.service.spec.ts new file mode 100644 index 0000000..ef040ea --- /dev/null +++ b/src/app/core/services/cart/cart.service.spec.ts @@ -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 }); + }); +}); diff --git a/src/app/core/services/cart/cart.service.ts b/src/app/core/services/cart/cart.service.ts new file mode 100644 index 0000000..16c3f20 --- /dev/null +++ b/src/app/core/services/cart/cart.service.ts @@ -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(null); + readonly cart = this.cartState.asReadonly(); + + private get tenantApiUrl(): string { + return this.tenantService.getTenantApiUrl(); + } + + loadCart(): Observable { + return this.http + .get>(`${this.tenantApiUrl}/cart`, { + withCredentials: true + }) + .pipe( + map((response) => response.data), + tap((cart) => this.cartState.set(cart)) + ); + } + + addItem(productVariantId: number, cantidad: number): Observable { + return this.http + .post>( + `${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 { + return this.http + .patch>( + `${this.tenantApiUrl}/cart/items/${productVariantId}`, + { cantidad }, + { withCredentials: true } + ) + .pipe( + map((response) => response.data), + tap((cart) => this.cartState.set(cart)) + ); + } + + removeItem(productVariantId: number): Observable { + return this.http + .delete>( + `${this.tenantApiUrl}/cart/items/${productVariantId}`, + { withCredentials: true } + ) + .pipe( + map((response) => response.data), + tap((cart) => this.cartState.set(cart)) + ); + } +}