import { describe, it, expect, vi, beforeEach } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { ZodError } from 'zod'; import { ShoppingCartFacade } from './shopping-cart.facade'; import { CheckoutService, ShoppingCartService } from '../services'; import { Customer, Payer as CrmPayer } from '@isa/crm/data-access'; import { ShippingAddressDTO as CrmShippingAddressDTO } from '@generated/swagger/crm-api'; import { Order } from '../models'; describe('ShoppingCartFacade', () => { let facade: ShoppingCartFacade; let mockCheckoutService: { complete: ReturnType; }; let mockShoppingCartService: { createShoppingCart: ReturnType; getShoppingCart: ReturnType; removeItem: ReturnType; updateItem: ReturnType; }; beforeEach(() => { mockCheckoutService = { complete: vi.fn(), }; mockShoppingCartService = { createShoppingCart: vi.fn(), getShoppingCart: vi.fn(), removeItem: vi.fn(), updateItem: vi.fn(), }; TestBed.configureTestingModule({ providers: [ ShoppingCartFacade, { provide: CheckoutService, useValue: mockCheckoutService }, { provide: ShoppingCartService, useValue: mockShoppingCartService }, ], }); facade = TestBed.inject(ShoppingCartFacade); }); describe('completeWithCrmData', () => { it('should transform CRM data and complete order successfully', async () => { // Arrange const mockOrders: Order[] = [ { id: 1, orderNumber: 'ORDER-001' } as Order, ]; mockCheckoutService.complete.mockResolvedValue(mockOrders); const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 8, // B2C firstName: 'John', lastName: 'Doe', notificationChannels: 1, features: [ { key: 'FEATURE1', value: 'value1' }, { key: 'FEATURE2', value: 'value2' }, ], } as Customer; // Act const result = await facade.completeWithCrmData({ shoppingCartId: 456, customer, }); // Assert expect(result).toEqual(mockOrders); expect(mockCheckoutService.complete).toHaveBeenCalledOnce(); expect(mockCheckoutService.complete).toHaveBeenCalledWith( expect.objectContaining({ shoppingCartId: 456, buyer: expect.objectContaining({ reference: { id: 123 }, source: 123, buyerType: 8, buyerNumber: 'CUST-123', firstName: 'John', lastName: 'Doe', }), notificationChannels: 1, customerFeatures: { FEATURE1: 'FEATURE1', FEATURE2: 'FEATURE2' }, }), undefined, ); }); it('should transform CRM shipping address when provided', async () => { // Arrange const mockOrders: Order[] = [ { id: 1, orderNumber: 'ORDER-001' } as Order, ]; mockCheckoutService.complete.mockResolvedValue(mockOrders); const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 8, firstName: 'John', lastName: 'Doe', notificationChannels: 1, features: [], } as Customer; const crmShippingAddress: CrmShippingAddressDTO = { id: 789, firstName: 'Jane', lastName: 'Doe', address: { street: 'Main St', streetNumber: '123', zipCode: '12345', city: 'Berlin', country: 'DE', }, }; // Act await facade.completeWithCrmData({ shoppingCartId: 456, customer, crmShippingAddress, }); // Assert expect(mockCheckoutService.complete).toHaveBeenCalledWith( expect.objectContaining({ shippingAddress: expect.objectContaining({ reference: { id: 789 }, source: 789, firstName: 'Jane', lastName: 'Doe', address: expect.objectContaining({ street: 'Main St', streetNumber: '123', zipCode: '12345', city: 'Berlin', country: 'DE', }), }), }), undefined, ); }); it('should transform CRM payer when provided', async () => { // Arrange const mockOrders: Order[] = [ { id: 1, orderNumber: 'ORDER-001' } as Order, ]; mockCheckoutService.complete.mockResolvedValue(mockOrders); const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 8, firstName: 'John', lastName: 'Doe', notificationChannels: 1, features: [], } as Customer; const crmPayer: CrmPayer = { id: 999, payerNumber: 'PAY-999', payerType: 16, // B2B payerStatus: 0, firstName: 'Billing', lastName: 'Company', } as CrmPayer; // Act await facade.completeWithCrmData({ shoppingCartId: 456, customer, crmPayer, }); // Assert expect(mockCheckoutService.complete).toHaveBeenCalledWith( expect.objectContaining({ payer: expect.objectContaining({ reference: { id: 999 }, source: 999, payerType: 16, payerNumber: 'PAY-999', payerStatus: 0, firstName: 'Billing', lastName: 'Company', }), }), undefined, ); }); it('should use provided notificationChannels over customer default', async () => { // Arrange mockCheckoutService.complete.mockResolvedValue([]); const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 8, firstName: 'John', lastName: 'Doe', notificationChannels: 1, // Customer default features: [], } as Customer; // Act await facade.completeWithCrmData({ shoppingCartId: 456, customer, notificationChannels: 4, // Override }); // Assert expect(mockCheckoutService.complete).toHaveBeenCalledWith( expect.objectContaining({ notificationChannels: 4, // Should use override }), undefined, ); }); it('should use customer notificationChannels when not provided', async () => { // Arrange mockCheckoutService.complete.mockResolvedValue([]); const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 8, firstName: 'John', lastName: 'Doe', notificationChannels: 2, // Customer default features: [], } as Customer; // Act await facade.completeWithCrmData({ shoppingCartId: 456, customer, }); // Assert expect(mockCheckoutService.complete).toHaveBeenCalledWith( expect.objectContaining({ notificationChannels: 2, // Should use customer default }), undefined, ); }); it('should default to 1 when customer notificationChannels is undefined', async () => { // Arrange mockCheckoutService.complete.mockResolvedValue([]); const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 8, firstName: 'John', lastName: 'Doe', notificationChannels: undefined, // Not set features: [], } as Customer; // Act await facade.completeWithCrmData({ shoppingCartId: 456, customer, }); // Assert expect(mockCheckoutService.complete).toHaveBeenCalledWith( expect.objectContaining({ notificationChannels: 1, // Should default to 1 }), undefined, ); }); it('should handle missing optional shipping address', async () => { // Arrange mockCheckoutService.complete.mockResolvedValue([]); const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 8, firstName: 'John', lastName: 'Doe', notificationChannels: 1, features: [], } as Customer; // Act await facade.completeWithCrmData({ shoppingCartId: 456, customer, crmShippingAddress: undefined, }); // Assert expect(mockCheckoutService.complete).toHaveBeenCalledWith( expect.objectContaining({ shippingAddress: undefined, }), undefined, ); }); it('should handle missing optional payer', async () => { // Arrange mockCheckoutService.complete.mockResolvedValue([]); const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 8, firstName: 'John', lastName: 'Doe', notificationChannels: 1, features: [], } as Customer; // Act await facade.completeWithCrmData({ shoppingCartId: 456, customer, crmPayer: undefined, }); // Assert expect(mockCheckoutService.complete).toHaveBeenCalledWith( expect.objectContaining({ payer: undefined, }), undefined, ); }); it('should pass abort signal to underlying service', async () => { // Arrange mockCheckoutService.complete.mockResolvedValue([]); const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 8, firstName: 'John', lastName: 'Doe', notificationChannels: 1, features: [], } as Customer; const abortController = new AbortController(); // Act await facade.completeWithCrmData( { shoppingCartId: 456, customer, }, abortController.signal, ); // Assert expect(mockCheckoutService.complete).toHaveBeenCalledWith( expect.any(Object), abortController.signal, ); }); it('should extract customer features correctly', async () => { // Arrange mockCheckoutService.complete.mockResolvedValue([]); const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 8, firstName: 'John', lastName: 'Doe', notificationChannels: 1, features: [ { key: 'FEATURE_A', value: 'some value' }, { key: 'FEATURE_B', value: 'another value' }, { key: 'FEATURE_C', value: 'third value' }, ], } as Customer; // Act await facade.completeWithCrmData({ shoppingCartId: 456, customer, }); // Assert expect(mockCheckoutService.complete).toHaveBeenCalledWith( expect.objectContaining({ customerFeatures: { FEATURE_A: 'FEATURE_A', FEATURE_B: 'FEATURE_B', FEATURE_C: 'FEATURE_C', }, }), undefined, ); }); it('should handle empty customer features', async () => { // Arrange mockCheckoutService.complete.mockResolvedValue([]); const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 8, firstName: 'John', lastName: 'Doe', notificationChannels: 1, features: [], } as Customer; // Act await facade.completeWithCrmData({ shoppingCartId: 456, customer, }); // Assert expect(mockCheckoutService.complete).toHaveBeenCalledWith( expect.objectContaining({ customerFeatures: {}, }), undefined, ); }); it('should throw error for invalid shopping cart ID', async () => { // Arrange const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 8, firstName: 'John', lastName: 'Doe', notificationChannels: 1, features: [], } as Customer; // Act & Assert try { await facade.completeWithCrmData({ shoppingCartId: -1, // Invalid ID customer, }); expect.fail('Should have thrown ZodError'); } catch (error) { expect(error).toBeInstanceOf(ZodError); } }); it('should throw error for invalid customer data', async () => { // Act & Assert try { await facade.completeWithCrmData({ shoppingCartId: 456, customer: { invalid: 'data' } as any, // Invalid customer }); expect.fail('Should have thrown ZodError'); } catch (error) { expect(error).toBeInstanceOf(ZodError); } }); it('should propagate errors from checkout service', async () => { // Arrange const checkoutError = new Error('Checkout failed'); mockCheckoutService.complete.mockRejectedValue(checkoutError); const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 8, firstName: 'John', lastName: 'Doe', notificationChannels: 1, features: [], } as Customer; // Act & Assert await expect( facade.completeWithCrmData({ shoppingCartId: 456, customer, }), ).rejects.toThrow('Checkout failed'); }); it('should handle all parameters together', async () => { // Arrange const mockOrders: Order[] = [ { id: 1, orderNumber: 'ORDER-001' } as Order, { id: 2, orderNumber: 'ORDER-002' } as Order, ]; mockCheckoutService.complete.mockResolvedValue(mockOrders); const customer: Customer = { id: 123, customerNumber: 'CUST-123', customerType: 16, // B2B firstName: 'John', lastName: 'Doe', gender: 2, title: 'Mr.', dateOfBirth: '1980-01-15', notificationChannels: 4, features: [{ key: 'B2B_CUSTOMER', value: 'true' }], communicationDetails: { email: 'john.doe@example.com', phone: '+49 123 456789', }, organisation: { name: 'ACME Corp', }, address: { street: 'Main St', streetNumber: '42', zipCode: '12345', city: 'Berlin', country: 'DE', }, } as Customer; const crmShippingAddress: CrmShippingAddressDTO = { id: 789, firstName: 'Jane', lastName: 'Doe', address: { street: 'Delivery St', streetNumber: '99', zipCode: '54321', city: 'Hamburg', country: 'DE', }, }; const crmPayer: CrmPayer = { id: 999, payerNumber: 'PAY-999', payerType: 16, payerStatus: 0, firstName: 'Billing', lastName: 'Department', address: { street: 'Billing St', streetNumber: '1', zipCode: '11111', city: 'Munich', country: 'DE', }, } as CrmPayer; const abortController = new AbortController(); // Act const result = await facade.completeWithCrmData( { shoppingCartId: 456, customer, crmShippingAddress, crmPayer, notificationChannels: 8, }, abortController.signal, ); // Assert expect(result).toEqual(mockOrders); expect(mockCheckoutService.complete).toHaveBeenCalledWith( expect.objectContaining({ shoppingCartId: 456, buyer: expect.objectContaining({ reference: { id: 123 }, source: 123, buyerType: 16, buyerNumber: 'CUST-123', firstName: 'John', lastName: 'Doe', }), shippingAddress: expect.objectContaining({ reference: { id: 789 }, source: 789, firstName: 'Jane', lastName: 'Doe', }), payer: expect.objectContaining({ reference: { id: 999 }, source: 999, payerType: 16, payerNumber: 'PAY-999', }), notificationChannels: 8, customerFeatures: { B2B_CUSTOMER: 'B2B_CUSTOMER' }, }), abortController.signal, ); }); }); describe('complete', () => { it('should delegate to checkout service', async () => { // Arrange const mockOrders: Order[] = [ { id: 1, orderNumber: 'ORDER-001' } as Order, ]; mockCheckoutService.complete.mockResolvedValue(mockOrders); const params = { shoppingCartId: 123, buyer: { buyerType: 8 } as any, notificationChannels: 1 as any, customerFeatures: {}, }; // Act const result = await facade.complete(params); // Assert expect(result).toEqual(mockOrders); expect(mockCheckoutService.complete).toHaveBeenCalledWith( params, undefined, ); }); }); });