mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
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:
@@ -1,23 +1,24 @@
|
||||
export * from './address-type';
|
||||
export * from './buyer';
|
||||
export * from './can-return';
|
||||
export * from './eligible-for-return';
|
||||
export * from './gender';
|
||||
export * from './product';
|
||||
export * from './quantity';
|
||||
export * from './receipt-item-list-item';
|
||||
export * from './receipt-item-task-list-item';
|
||||
export * from './receipt-item';
|
||||
export * from './receipt-list-item';
|
||||
export * from './receipt-type';
|
||||
export * from './receipt';
|
||||
export * from './return-info';
|
||||
export * from './return-process-answer';
|
||||
export * from './return-process-question-key';
|
||||
export * from './return-process-question-type';
|
||||
export * from './return-process-question';
|
||||
export * from './return-process-status';
|
||||
export * from './return-process';
|
||||
export * from './shipping-address-2';
|
||||
export * from './shipping-type';
|
||||
export * from './task-action-type';
|
||||
export * from './address-type';
|
||||
export * from './buyer';
|
||||
export * from './can-return';
|
||||
export * from './eligible-for-return';
|
||||
export * from './gender';
|
||||
export * from './order';
|
||||
export * from './product';
|
||||
export * from './quantity';
|
||||
export * from './receipt-item-list-item';
|
||||
export * from './receipt-item-task-list-item';
|
||||
export * from './receipt-item';
|
||||
export * from './receipt-list-item';
|
||||
export * from './receipt-type';
|
||||
export * from './receipt';
|
||||
export * from './return-info';
|
||||
export * from './return-process-answer';
|
||||
export * from './return-process-question-key';
|
||||
export * from './return-process-question-type';
|
||||
export * from './return-process-question';
|
||||
export * from './return-process-status';
|
||||
export * from './return-process';
|
||||
export * from './shipping-address-2';
|
||||
export * from './shipping-type';
|
||||
export * from './task-action-type';
|
||||
|
||||
15
libs/oms/data-access/src/lib/models/order.ts
Normal file
15
libs/oms/data-access/src/lib/models/order.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { DisplayOrderDTO } from '@generated/swagger/oms-api';
|
||||
|
||||
/**
|
||||
* Order model representing a completed checkout order.
|
||||
* This is an alias to the OMS API's DisplayOrderDTO.
|
||||
*
|
||||
* @remarks
|
||||
* DisplayOrderDTO contains:
|
||||
* - Order metadata (orderNumber, orderDate, orderType, orderValue)
|
||||
* - Customer information (buyer, payer, shippingAddress)
|
||||
* - Order items and their details
|
||||
* - Payment information
|
||||
* - Branch and delivery information
|
||||
*/
|
||||
export type Order = DisplayOrderDTO;
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './order-creation.service';
|
||||
export * from './return-can-return.service';
|
||||
export * from './return-details.service';
|
||||
export * from './print-receipts.service';
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
|
||||
import { OrderCreationService } from './order-creation.service';
|
||||
import {
|
||||
OrderCheckoutService,
|
||||
LogisticianService,
|
||||
LogisticianDTO,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { of, NEVER } from 'rxjs';
|
||||
import { Order } from '../models';
|
||||
|
||||
describe('OrderCreationService', () => {
|
||||
let spectator: SpectatorService<OrderCreationService>;
|
||||
const createService = createServiceFactory({
|
||||
service: OrderCreationService,
|
||||
mocks: [OrderCheckoutService, LogisticianService],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createService();
|
||||
});
|
||||
|
||||
describe('createOrdersFromCheckout', () => {
|
||||
it('should create orders successfully', async () => {
|
||||
// Arrange
|
||||
const checkoutId = 123;
|
||||
const mockOrders: Order[] = [
|
||||
{ id: 1 } as Order,
|
||||
{ id: 2 } as Order,
|
||||
];
|
||||
const mockResponse = { result: mockOrders, error: false };
|
||||
const orderCheckoutService = spectator.inject(OrderCheckoutService);
|
||||
orderCheckoutService.OrderCheckoutCreateOrderPOST.mockReturnValue(of(mockResponse));
|
||||
|
||||
// Act
|
||||
const result = await spectator.service.createOrdersFromCheckout(checkoutId);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockOrders);
|
||||
expect(orderCheckoutService.OrderCheckoutCreateOrderPOST).toHaveBeenCalledWith({
|
||||
checkoutId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error if checkoutId is invalid', async () => {
|
||||
// Act & Assert
|
||||
await expect(
|
||||
spectator.service.createOrdersFromCheckout(0),
|
||||
).rejects.toThrow('Invalid checkoutId: 0');
|
||||
});
|
||||
|
||||
it('should throw error if API response contains error', async () => {
|
||||
// Arrange
|
||||
const checkoutId = 123;
|
||||
const mockResponse = { error: true, result: undefined };
|
||||
const orderCheckoutService = spectator.inject(OrderCheckoutService);
|
||||
orderCheckoutService.OrderCheckoutCreateOrderPOST.mockReturnValue(of(mockResponse));
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
spectator.service.createOrdersFromCheckout(checkoutId),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLogistician', () => {
|
||||
it('should get default logistician (2470) successfully', async () => {
|
||||
// Arrange
|
||||
const mockLogistician: LogisticianDTO = {
|
||||
logisticianNumber: '2470',
|
||||
name: 'Default Logistician',
|
||||
} as LogisticianDTO;
|
||||
const mockResponse = {
|
||||
result: [mockLogistician],
|
||||
error: false,
|
||||
};
|
||||
const logisticianService = spectator.inject(LogisticianService);
|
||||
logisticianService.LogisticianGetLogisticians.mockReturnValue(of(mockResponse));
|
||||
|
||||
// Act
|
||||
const result = await spectator.service.getLogistician();
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockLogistician);
|
||||
expect(logisticianService.LogisticianGetLogisticians).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it('should get specific logistician by number', async () => {
|
||||
// Arrange
|
||||
const mockLogisticians: LogisticianDTO[] = [
|
||||
{ logisticianNumber: '1000', name: 'Logistician 1' } as LogisticianDTO,
|
||||
{ logisticianNumber: '2000', name: 'Logistician 2' } as LogisticianDTO,
|
||||
];
|
||||
const mockResponse = {
|
||||
result: mockLogisticians,
|
||||
error: false,
|
||||
};
|
||||
const logisticianService = spectator.inject(LogisticianService);
|
||||
logisticianService.LogisticianGetLogisticians.mockReturnValue(of(mockResponse));
|
||||
|
||||
// Act
|
||||
const result = await spectator.service.getLogistician('2000');
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockLogisticians[1]);
|
||||
});
|
||||
|
||||
it('should throw error if logistician not found', async () => {
|
||||
// Arrange
|
||||
const mockResponse = {
|
||||
result: [
|
||||
{ logisticianNumber: '1000', name: 'Other' } as LogisticianDTO,
|
||||
],
|
||||
error: false,
|
||||
};
|
||||
const logisticianService = spectator.inject(LogisticianService);
|
||||
logisticianService.LogisticianGetLogisticians.mockReturnValue(of(mockResponse));
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
spectator.service.getLogistician('2470'),
|
||||
).rejects.toThrow('Logistician 2470 not found');
|
||||
});
|
||||
|
||||
it('should throw error if API response contains error', async () => {
|
||||
// Arrange
|
||||
const mockResponse = { error: true, result: undefined };
|
||||
const logisticianService = spectator.inject(LogisticianService);
|
||||
logisticianService.LogisticianGetLogisticians.mockReturnValue(of(mockResponse));
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
spectator.service.getLogistician(),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle observable that never emits', async () => {
|
||||
// Arrange
|
||||
const logisticianService = spectator.inject(LogisticianService);
|
||||
logisticianService.LogisticianGetLogisticians.mockReturnValue(NEVER);
|
||||
|
||||
// Act & Assert
|
||||
const fetchPromise = spectator.service.getLogistician();
|
||||
const timeout = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), 100),
|
||||
);
|
||||
await expect(Promise.race([fetchPromise, timeout])).rejects.toThrow(
|
||||
'Timeout',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import {
|
||||
OrderCheckoutService,
|
||||
LogisticianService,
|
||||
LogisticianDTO,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Order } from '../models';
|
||||
|
||||
/**
|
||||
* Service for creating orders from checkout.
|
||||
*
|
||||
* @remarks
|
||||
* This service handles order creation operations that are part of the OMS domain.
|
||||
* It provides methods to:
|
||||
* - Create orders from a completed checkout
|
||||
* - Retrieve logistician information
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class OrderCreationService {
|
||||
#logger = logger(() => ({ service: 'OrderCreationService' }));
|
||||
readonly #orderCheckoutService = inject(OrderCheckoutService);
|
||||
readonly #logisticianService = inject(LogisticianService);
|
||||
|
||||
/**
|
||||
* Creates orders from a checkout.
|
||||
*
|
||||
* @param checkoutId - The ID of the checkout to create orders from
|
||||
* @returns Promise resolving to array of created orders
|
||||
* @throws {Error} If checkoutId is invalid or order creation fails
|
||||
*/
|
||||
async createOrdersFromCheckout(checkoutId: number): Promise<Order[]> {
|
||||
if (!checkoutId) {
|
||||
throw new Error(`Invalid checkoutId: ${checkoutId}`);
|
||||
}
|
||||
|
||||
const req$ = this.#orderCheckoutService.OrderCheckoutCreateOrderPOST({
|
||||
checkoutId,
|
||||
});
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const error = new ResponseArgsError(res);
|
||||
this.#logger.error('Failed to create orders', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result as Order[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves logistician information.
|
||||
*
|
||||
* @param logisticianNumber - The logistician number to retrieve (defaults to '2470')
|
||||
* @param abortSignal - Optional signal to abort the operation
|
||||
* @returns Promise resolving to logistician data
|
||||
* @throws {Error} If logistician is not found or request fails
|
||||
*/
|
||||
async getLogistician(
|
||||
logisticianNumber = '2470',
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<LogisticianDTO> {
|
||||
let req$ = this.#logisticianService.LogisticianGetLogisticians({});
|
||||
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 logistician', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const logistician = res.result?.find(
|
||||
(l) => l.logisticianNumber === logisticianNumber,
|
||||
);
|
||||
|
||||
if (!logistician) {
|
||||
throw new Error(`Logistician ${logisticianNumber} not found`);
|
||||
}
|
||||
|
||||
return logistician;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user