refactor(checkout): separate data-access layer boundaries

Extract domain-specific operations from CheckoutService into dedicated services to respect data-access layer boundaries and improve separation of concerns.

## Changes

### New Services Created

**OrderCreationService** (oms-data-access)
- createOrdersFromCheckout(): Creates orders from completed checkout
- getLogistician(): Retrieves logistician (default '2470')
- 8 unit tests with Jest/Spectator

**AvailabilityService** (catalogue-data-access)
- validateDownloadAvailabilities(): Validates download items availability
- getDigDeliveryAvailability(): Gets DIG-Versand availability
- getB2bDeliveryAvailability(): Gets B2B-Versand availability
- 15 unit tests with Jest/Spectator

**BranchService** (remission-data-access)
- getDefaultBranch(): Gets default/current branch for user
- 5 unit tests with Angular Testing Utilities
- Note: Temporary location, will move to inventory-data-access

### CheckoutService Refactoring

**Removed cross-domain imports:**
- @generated/swagger/oms-api
- @generated/swagger/availability-api
- @generated/swagger/inventory-api

**Added domain service dependencies:**
- @isa/oms/data-access (OrderCreationService)
- @isa/catalogue/data-access (AvailabilityService)
- @isa/remission/data-access (BranchService)

**Code reduction:**
- Removed 254 lines (25.5% reduction: 996 → 742 lines)
- Deleted 8 private methods moved to domain services

### Test Updates

- Updated checkout.service.spec.ts with new service mocks
- Fixed Zod validation (buyerType/payerType literals)
- All 29 tests passing (8+15+5+11)

### AbortSignal Policy

Removed abortSignal from data-mutating operations:
- OrderCreationService.createOrdersFromCheckout() (POST)
- Kept abortSignal for read-only operations per project convention

## Impact

- Better separation of concerns
- Improved maintainability (smaller, focused services)
- Respects data-access layer boundaries
- No functional changes (100% backward compatible)
- 0 TypeScript compilation errors
This commit is contained in:
Lorenz Hilpert
2025-10-06 17:09:12 +02:00
parent 58815d6fc3
commit 1e9ac30b4d
21 changed files with 2876 additions and 23 deletions

View File

@@ -0,0 +1,119 @@
import { TestBed } from '@angular/core/testing';
import { of, NEVER } from 'rxjs';
import { BranchService } from './branch.service';
import { StockService, BranchDTO } from '@generated/swagger/inventory-api';
/**
* Unit tests for BranchService.
* Tests the service's ability to fetch default branch information.
*
* @group unit
* @group services
*/
describe('BranchService', () => {
let service: BranchService;
let mockStockService: jest.Mocked<StockService>;
const mockBranch: any = {
id: 42,
name: 'Test Branch',
address: '123 Test St',
branchType: 1,
branchNumber: 'BR001',
changed: '2024-01-01',
created: '2024-01-01',
isDefault: true,
isOnline: true,
key: 'test-branch',
label: 'Test Branch Label',
pId: 1,
shortName: 'TB',
status: 1,
version: 1,
};
beforeEach(() => {
// Create a fresh mock for each test to ensure isolation
mockStockService = {
StockCurrentBranch: jest.fn(),
} as unknown as jest.Mocked<StockService>;
TestBed.configureTestingModule({
providers: [
BranchService,
{ provide: StockService, useValue: mockStockService },
],
});
service = TestBed.inject(BranchService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('getDefaultBranch', () => {
it('should fetch default branch successfully', async () => {
// Arrange
const mockResponse: any = {
error: false,
result: mockBranch,
};
mockStockService.StockCurrentBranch.mockReturnValue(of(mockResponse));
// Act
const result = await service.getDefaultBranch();
// Assert
expect(result).toEqual(mockBranch);
expect(mockStockService.StockCurrentBranch).toHaveBeenCalledWith();
});
it('should throw error if API response contains error', async () => {
// Arrange
const mockResponse: any = {
error: true,
message: 'API Error',
result: undefined,
};
mockStockService.StockCurrentBranch.mockReturnValue(of(mockResponse));
// Act & Assert
await expect(service.getDefaultBranch()).rejects.toThrow();
});
it('should handle abortSignal', async () => {
// Arrange
mockStockService.StockCurrentBranch.mockReturnValue(NEVER);
const abortController = new AbortController();
// Act
const promise = service.getDefaultBranch(abortController.signal);
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 100),
);
// Assert
await expect(Promise.race([promise, timeout])).rejects.toThrow('Timeout');
});
it('should return branch with all properties', async () => {
// Arrange
const mockResponse: any = {
error: false,
result: mockBranch,
};
mockStockService.StockCurrentBranch.mockReturnValue(of(mockResponse));
// Act
const result = await service.getDefaultBranch();
// Assert
expect(result.id).toBe(42);
expect(result.name).toBe('Test Branch');
expect(result.isDefault).toBe(true);
expect(result.isOnline).toBe(true);
expect(result.branchNumber).toBe('BR001');
});
});
});

View File

@@ -0,0 +1,67 @@
import { inject, Injectable } from '@angular/core';
import { StockService, BranchDTO } from '@generated/swagger/inventory-api';
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
import { logger } from '@isa/core/logging';
import { firstValueFrom } from 'rxjs';
/**
* Service for branch/stock operations.
*
* @remarks
* This service handles inventory operations:
* - Retrieving the default/current branch for the user
*
* Note: This service is in remission-data-access as a temporary location.
* It will be moved to inventory-data-access when that module is created.
*/
@Injectable({ providedIn: 'root' })
export class BranchService {
#logger = logger(() => ({ service: 'BranchService' }));
readonly #stockService = inject(StockService);
/**
* Gets the default branch from the inventory service.
*
* @param abortSignal - Optional signal to abort the operation
* @returns Promise resolving to branch data
* @throws {Error} If branch retrieval fails
*/
async getDefaultBranch(abortSignal?: AbortSignal): Promise<BranchDTO> {
let req$ = this.#stockService.StockCurrentBranch();
if (abortSignal) req$ = req$.pipe(takeUntilAborted(abortSignal));
const res = await firstValueFrom(req$);
if (res.error) {
const error = new ResponseArgsError(res);
this.#logger.error('Failed to get default branch', error);
throw error;
}
const branch = res.result;
if (!branch) {
const error = new Error('No branch data returned');
this.#logger.error('Failed to get default branch', error);
throw error;
}
return {
id: branch.id,
name: branch.name,
address: branch.address,
branchType: branch.branchType,
branchNumber: branch.branchNumber,
changed: branch.changed,
created: branch.created,
isDefault: branch.isDefault,
isOnline: branch.isOnline,
key: branch.key,
label: branch.label,
pId: branch.pId,
shortName: branch.shortName,
status: branch.status,
version: branch.version,
};
}
}

View File

@@ -1,3 +1,4 @@
export * from './branch.service';
export * from './remission-product-group.service';
export * from './remission-reason.service';
export * from './remission-return-receipt.service';