mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1993: feat(action-handler, printing, schemas)
1 commit: Bestellbestätigung drucken 2. commit: Schemas 3. commit: Action/Command handler feat(action-handler, printing, schemas): add handle command service for automated action execution Implement HandleCommandService and facade to execute order actions automatically after reward collection. Add action handler infrastructure with 23 handlers (Accepted, Arrived, Assembled, etc.). Integrate automatic receipt fetching for print commands. Add schema validation for command handling and receipt queries. Update reward confirmation to trigger actions after successful collection. - Add HandleCommandService with command orchestration - Add HandleCommandFacade as public API layer - Create schemas: HandleCommandSchema, FetchReceiptsByOrderItemSubsetIdsSchema - Add helpers: getMainActions, buildItemQuantityMap - Register 23 action handlers in reward confirmation routes - Support PRINT_SHIPPINGNOTE and PRINT_SMALLAMOUNTINVOICE auto-fetching - Update CoreCommandModule for forRoot/forChild patterns - Add comprehensive unit tests for new services and helpers - Apply prettier formatting to command and printing modules Ref: #5394
This commit is contained in:
committed by
Lorenz Hilpert
parent
53a062dcde
commit
a49ea25fd0
@@ -25,3 +25,4 @@ export * from './lib/services';
|
||||
export * from './lib/operators';
|
||||
export * from './lib/stores';
|
||||
export * from './lib/resources';
|
||||
export * from './lib/handler';
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { HandleCommandFacade } from './handle-command.facade';
|
||||
import { HandleCommandService } from '../services';
|
||||
import {
|
||||
HandleCommand,
|
||||
FetchReceiptsByOrderItemSubsetIdsInput,
|
||||
} from '../schemas';
|
||||
import { ReceiptType } from '../models/receipt-type';
|
||||
|
||||
// Mock the HandleCommandService module
|
||||
jest.mock('../services', () => ({
|
||||
HandleCommandService: jest.fn().mockImplementation(() => ({
|
||||
handle: jest.fn(),
|
||||
fetchReceiptsByOrderItemSubsetIds: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('HandleCommandFacade', () => {
|
||||
let facade: HandleCommandFacade;
|
||||
let mockHandleCommandService: {
|
||||
handle: jest.Mock;
|
||||
fetchReceiptsByOrderItemSubsetIds: jest.Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockHandleCommandService = {
|
||||
handle: jest.fn(),
|
||||
fetchReceiptsByOrderItemSubsetIds: jest.fn(),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
HandleCommandFacade,
|
||||
{
|
||||
provide: HandleCommandService,
|
||||
useValue: mockHandleCommandService,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
facade = TestBed.inject(HandleCommandFacade);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
// Assert
|
||||
expect(facade).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should delegate handle call to service', async () => {
|
||||
// Arrange
|
||||
const mockParams: HandleCommand = {
|
||||
action: { key: 'test-action', value: 'Test Action' },
|
||||
items: [],
|
||||
};
|
||||
const mockResult: HandleCommand = {
|
||||
action: { key: 'test-action', value: 'Test Action' },
|
||||
items: [],
|
||||
};
|
||||
|
||||
mockHandleCommandService.handle.mockResolvedValue(mockResult);
|
||||
|
||||
// Act
|
||||
const result = await facade.handle(mockParams);
|
||||
|
||||
// Assert
|
||||
expect(mockHandleCommandService.handle).toHaveBeenCalledWith(mockParams);
|
||||
expect(mockHandleCommandService.handle).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
|
||||
it('should pass through all parameters to service', async () => {
|
||||
// Arrange
|
||||
const mockParams: HandleCommand = {
|
||||
action: {
|
||||
key: 'complex-action',
|
||||
value: 'Complex Action',
|
||||
command: 'DO_SOMETHING',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
orderItemId: 123,
|
||||
orderItemSubsetId: 456,
|
||||
},
|
||||
] as any,
|
||||
compartmentCode: 'ABC123',
|
||||
compartmentInfo: 'Extra Info',
|
||||
itemQuantity: { 456: 2 },
|
||||
order: {
|
||||
id: 100,
|
||||
orderNumber: 'ORD-001',
|
||||
orderType: 1,
|
||||
} as any,
|
||||
};
|
||||
|
||||
mockHandleCommandService.handle.mockResolvedValue(mockParams);
|
||||
|
||||
// Act
|
||||
await facade.handle(mockParams);
|
||||
|
||||
// Assert
|
||||
expect(mockHandleCommandService.handle).toHaveBeenCalledWith(mockParams);
|
||||
});
|
||||
|
||||
it('should propagate errors from service', async () => {
|
||||
// Arrange
|
||||
const mockParams: HandleCommand = {
|
||||
action: { key: 'error-action', value: 'Error Action' },
|
||||
items: [],
|
||||
};
|
||||
const mockError = new Error('Service error');
|
||||
|
||||
mockHandleCommandService.handle.mockRejectedValue(mockError);
|
||||
|
||||
// Act & Assert
|
||||
await expect(facade.handle(mockParams)).rejects.toThrow('Service error');
|
||||
});
|
||||
|
||||
describe('fetchReceiptsByOrderItemSubsetIds', () => {
|
||||
it('should delegate call to service', async () => {
|
||||
// Arrange
|
||||
const mockParams: FetchReceiptsByOrderItemSubsetIdsInput = {
|
||||
ids: [123, 456, 789],
|
||||
};
|
||||
const mockResult = {
|
||||
receipts: [{ id: 1 }, { id: 2 }],
|
||||
};
|
||||
|
||||
mockHandleCommandService.fetchReceiptsByOrderItemSubsetIds.mockResolvedValue(
|
||||
mockResult,
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await facade.fetchReceiptsByOrderItemSubsetIds(mockParams);
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
mockHandleCommandService.fetchReceiptsByOrderItemSubsetIds,
|
||||
).toHaveBeenCalledWith(mockParams, undefined);
|
||||
expect(
|
||||
mockHandleCommandService.fetchReceiptsByOrderItemSubsetIds,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
|
||||
it('should pass all parameters with defaults to service', async () => {
|
||||
// Arrange
|
||||
const mockParams: FetchReceiptsByOrderItemSubsetIdsInput = {
|
||||
eagerLoading: 1,
|
||||
receiptType: (1 + 64 + 128) as ReceiptType,
|
||||
ids: [100, 200],
|
||||
};
|
||||
|
||||
mockHandleCommandService.fetchReceiptsByOrderItemSubsetIds.mockResolvedValue(
|
||||
{},
|
||||
);
|
||||
|
||||
// Act
|
||||
await facade.fetchReceiptsByOrderItemSubsetIds(mockParams);
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
mockHandleCommandService.fetchReceiptsByOrderItemSubsetIds,
|
||||
).toHaveBeenCalledWith(mockParams, undefined);
|
||||
});
|
||||
|
||||
it('should propagate errors from service', async () => {
|
||||
// Arrange
|
||||
const mockParams: FetchReceiptsByOrderItemSubsetIdsInput = {
|
||||
ids: [111],
|
||||
};
|
||||
const mockError = new Error('Fetch receipts error');
|
||||
|
||||
mockHandleCommandService.fetchReceiptsByOrderItemSubsetIds.mockRejectedValue(
|
||||
mockError,
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
facade.fetchReceiptsByOrderItemSubsetIds(mockParams),
|
||||
).rejects.toThrow('Fetch receipts error');
|
||||
});
|
||||
|
||||
it('should pass abort signal to service when provided', async () => {
|
||||
// Arrange
|
||||
const mockParams: FetchReceiptsByOrderItemSubsetIdsInput = {
|
||||
ids: [123],
|
||||
};
|
||||
const abortController = new AbortController();
|
||||
const mockResult = {
|
||||
receipts: [{ id: 1 }],
|
||||
};
|
||||
|
||||
mockHandleCommandService.fetchReceiptsByOrderItemSubsetIds.mockResolvedValue(
|
||||
mockResult,
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await facade.fetchReceiptsByOrderItemSubsetIds(
|
||||
mockParams,
|
||||
abortController.signal,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
mockHandleCommandService.fetchReceiptsByOrderItemSubsetIds,
|
||||
).toHaveBeenCalledWith(mockParams, abortController.signal);
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HandleCommandService } from '../services';
|
||||
import {
|
||||
FetchReceiptsByOrderItemSubsetIdsInput,
|
||||
HandleCommand,
|
||||
} from '../schemas';
|
||||
|
||||
@Injectable()
|
||||
export class HandleCommandFacade {
|
||||
#handleCommandService = inject(HandleCommandService);
|
||||
|
||||
handle(params: HandleCommand) {
|
||||
return this.#handleCommandService.handle(params);
|
||||
}
|
||||
|
||||
fetchReceiptsByOrderItemSubsetIds(
|
||||
params: FetchReceiptsByOrderItemSubsetIdsInput,
|
||||
abortSignal?: AbortSignal,
|
||||
) {
|
||||
return this.#handleCommandService.fetchReceiptsByOrderItemSubsetIds(
|
||||
params,
|
||||
abortSignal,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export { OrderCreationFacade } from './order-creation.facade';
|
||||
export { OrderRewardCollectFacade } from './order-reward-collect.facade';
|
||||
export { HandleCommandFacade } from './handle-command.facade';
|
||||
|
||||
89
libs/oms/data-access/src/lib/handler/action-handler.ts
Normal file
89
libs/oms/data-access/src/lib/handler/action-handler.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
AcceptedActionHandler,
|
||||
ArrivedActionHandler,
|
||||
AssembledActionHandler,
|
||||
AvailableForDownloadActionHandler,
|
||||
BackToStockActionHandler,
|
||||
CanceledByBuyerActionHandler,
|
||||
CanceledByRetailerActionHandler,
|
||||
CanceledBySupplierActionHandler,
|
||||
CollectOnDeliveryNoteActionHandler,
|
||||
CollectWithSmallAmountinvoiceActionHandler,
|
||||
CreateReturnItemActionHandler,
|
||||
CreateShippingNoteActionHandler,
|
||||
DeliveredActionHandler,
|
||||
DetermineSupplierActionHandler,
|
||||
DispatchedActionHandler,
|
||||
DownloadedActionHandler,
|
||||
FetchedActionHandler,
|
||||
InProcessActionHandler,
|
||||
NotAvailableActionHandler,
|
||||
NotFetchedActionHandler,
|
||||
OrderAtSupplierActionHandler,
|
||||
OrderingActionHandler,
|
||||
OverdueActionHandler,
|
||||
PackedActionHandler,
|
||||
ParkedActionHandler,
|
||||
PlacedActionHandler,
|
||||
PreparationForShippingActionHandler,
|
||||
PrintCompartmentLabelActionHandler,
|
||||
PrintPriceDiffQrCodeLabelActionHandler,
|
||||
PrintShippingNoteActionHandler,
|
||||
PrintSmallamountinvoiceActionHandler,
|
||||
ReOrderActionHandler,
|
||||
ReOrderedActionHandler,
|
||||
RedirectedInternaqllyActionHandler,
|
||||
RequestedActionHandler,
|
||||
ReserverdActionHandler,
|
||||
ReturnedByBuyerActionHandler,
|
||||
ShippingNoteActionHandler,
|
||||
ShopWithKulturpassActionHandler,
|
||||
SupplierTemporarilyOutOfStockActionHandler,
|
||||
} from '@domain/oms';
|
||||
|
||||
/**
|
||||
* Array of all available OMS action handlers.
|
||||
* Used for configuring the CoreCommandModule with all supported order item actions.
|
||||
*/
|
||||
export const OMS_ACTION_HANDLERS = [
|
||||
AcceptedActionHandler,
|
||||
ArrivedActionHandler,
|
||||
AssembledActionHandler,
|
||||
AvailableForDownloadActionHandler,
|
||||
BackToStockActionHandler,
|
||||
CanceledByBuyerActionHandler,
|
||||
CanceledByRetailerActionHandler,
|
||||
CanceledBySupplierActionHandler,
|
||||
CreateShippingNoteActionHandler,
|
||||
DeliveredActionHandler,
|
||||
DetermineSupplierActionHandler,
|
||||
DispatchedActionHandler,
|
||||
DownloadedActionHandler,
|
||||
FetchedActionHandler,
|
||||
InProcessActionHandler,
|
||||
NotAvailableActionHandler,
|
||||
NotFetchedActionHandler,
|
||||
OrderAtSupplierActionHandler,
|
||||
OrderingActionHandler,
|
||||
OverdueActionHandler,
|
||||
PackedActionHandler,
|
||||
ParkedActionHandler,
|
||||
PlacedActionHandler,
|
||||
PreparationForShippingActionHandler,
|
||||
PrintCompartmentLabelActionHandler,
|
||||
PrintShippingNoteActionHandler,
|
||||
ReOrderActionHandler,
|
||||
ReOrderedActionHandler,
|
||||
RedirectedInternaqllyActionHandler,
|
||||
RequestedActionHandler,
|
||||
ReserverdActionHandler,
|
||||
ReturnedByBuyerActionHandler,
|
||||
ShippingNoteActionHandler,
|
||||
SupplierTemporarilyOutOfStockActionHandler,
|
||||
CollectOnDeliveryNoteActionHandler,
|
||||
CreateReturnItemActionHandler,
|
||||
PrintPriceDiffQrCodeLabelActionHandler,
|
||||
CollectWithSmallAmountinvoiceActionHandler,
|
||||
PrintSmallamountinvoiceActionHandler,
|
||||
ShopWithKulturpassActionHandler,
|
||||
];
|
||||
1
libs/oms/data-access/src/lib/handler/index.ts
Normal file
1
libs/oms/data-access/src/lib/handler/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './action-handler';
|
||||
@@ -0,0 +1,177 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { getMainActions } from './get-main-actions.helper';
|
||||
import { DBHOrderItemListItem } from '../schemas';
|
||||
|
||||
describe('getMainActions', () => {
|
||||
it('should return actions from first item', () => {
|
||||
// Arrange
|
||||
const items: DBHOrderItemListItem[] = [
|
||||
{
|
||||
actions: [
|
||||
{ key: 'action1', value: 'Action 1' },
|
||||
{ key: 'action2', value: 'Action 2' },
|
||||
],
|
||||
} as DBHOrderItemListItem,
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = getMainActions(items);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([
|
||||
{ key: 'action1', value: 'Action 1' },
|
||||
{ key: 'action2', value: 'Action 2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter out actions with enabled boolean property', () => {
|
||||
// Arrange
|
||||
const items: DBHOrderItemListItem[] = [
|
||||
{
|
||||
actions: [
|
||||
{ key: 'action1', value: 'Action 1' },
|
||||
{ key: 'action2', value: 'Action 2', enabled: true },
|
||||
{ key: 'action3', value: 'Action 3', enabled: false },
|
||||
],
|
||||
} as DBHOrderItemListItem,
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = getMainActions(items);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([{ key: 'action1', value: 'Action 1' }]);
|
||||
});
|
||||
|
||||
it('should filter out actions containing FETCHED_PARTIAL when isPartial is true', () => {
|
||||
// Arrange
|
||||
const items: DBHOrderItemListItem[] = [
|
||||
{
|
||||
actions: [
|
||||
{ key: 'action1', value: 'Action 1', command: 'DO_SOMETHING' },
|
||||
{
|
||||
key: 'action2',
|
||||
value: 'Action 2',
|
||||
command: 'FETCHED_PARTIAL_ACTION',
|
||||
},
|
||||
{ key: 'action3', value: 'Action 3', command: 'DO_ANOTHER_THING' },
|
||||
],
|
||||
} as DBHOrderItemListItem,
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = getMainActions(items, true);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([
|
||||
{ key: 'action1', value: 'Action 1', command: 'DO_SOMETHING' },
|
||||
{ key: 'action3', value: 'Action 3', command: 'DO_ANOTHER_THING' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should include FETCHED_PARTIAL actions when isPartial is false', () => {
|
||||
// Arrange
|
||||
const items: DBHOrderItemListItem[] = [
|
||||
{
|
||||
actions: [
|
||||
{ key: 'action1', value: 'Action 1', command: 'DO_SOMETHING' },
|
||||
{
|
||||
key: 'action2',
|
||||
value: 'Action 2',
|
||||
command: 'FETCHED_PARTIAL_ACTION',
|
||||
},
|
||||
],
|
||||
} as DBHOrderItemListItem,
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = getMainActions(items, false);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([
|
||||
{ key: 'action1', value: 'Action 1', command: 'DO_SOMETHING' },
|
||||
{
|
||||
key: 'action2',
|
||||
value: 'Action 2',
|
||||
command: 'FETCHED_PARTIAL_ACTION',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty array when items is undefined', () => {
|
||||
// Arrange
|
||||
const items = undefined as unknown as DBHOrderItemListItem[];
|
||||
|
||||
// Act
|
||||
const result = getMainActions(items);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when items array is empty', () => {
|
||||
// Arrange
|
||||
const items: DBHOrderItemListItem[] = [];
|
||||
|
||||
// Act
|
||||
const result = getMainActions(items);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when first item has no actions', () => {
|
||||
// Arrange
|
||||
const items: DBHOrderItemListItem[] = [{} as DBHOrderItemListItem];
|
||||
|
||||
// Act
|
||||
const result = getMainActions(items);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when first item actions is undefined', () => {
|
||||
// Arrange
|
||||
const items: DBHOrderItemListItem[] = [
|
||||
{ actions: undefined } as DBHOrderItemListItem,
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = getMainActions(items);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should combine both filters correctly', () => {
|
||||
// Arrange
|
||||
const items: DBHOrderItemListItem[] = [
|
||||
{
|
||||
actions: [
|
||||
{ key: 'action1', value: 'Action 1', command: 'DO_SOMETHING' },
|
||||
{
|
||||
key: 'action2',
|
||||
value: 'Action 2',
|
||||
command: 'FETCHED_PARTIAL',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
key: 'action3',
|
||||
value: 'Action 3',
|
||||
command: 'FETCHED_PARTIAL_ACTION',
|
||||
},
|
||||
{ key: 'action4', value: 'Action 4', enabled: false },
|
||||
],
|
||||
} as DBHOrderItemListItem,
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = getMainActions(items, true);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([
|
||||
{ key: 'action1', value: 'Action 1', command: 'DO_SOMETHING' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { KeyValueOfStringAndString } from '@isa/common/data-access';
|
||||
import { DBHOrderItemListItem } from '../schemas';
|
||||
|
||||
export const getMainActions = (
|
||||
items: DBHOrderItemListItem[],
|
||||
isPartial?: boolean,
|
||||
): KeyValueOfStringAndString[] => {
|
||||
const firstItem = items?.find((_) => true);
|
||||
return (
|
||||
firstItem?.actions
|
||||
?.filter((action) => typeof action?.enabled !== 'boolean')
|
||||
?.filter((action) =>
|
||||
isPartial ? !action?.command?.includes('FETCHED_PARTIAL') : true,
|
||||
) ?? []
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './return-process';
|
||||
export * from './reward';
|
||||
export * from './get-main-actions.helper';
|
||||
|
||||
@@ -18,6 +18,35 @@ describe('getProcessingStatusState', () => {
|
||||
// Assert
|
||||
expect(result).toBe(ProcessingStatusState.Cancelled);
|
||||
});
|
||||
|
||||
it('should return Cancelled when all items are AnsLagerNichtAbgeholt', () => {
|
||||
// Arrange
|
||||
const statuses = [
|
||||
OrderItemProcessingStatusValue.AnsLagerNichtAbgeholt, // 262144
|
||||
OrderItemProcessingStatusValue.AnsLagerNichtAbgeholt, // 262144
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = getProcessingStatusState(statuses);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(ProcessingStatusState.Cancelled);
|
||||
});
|
||||
|
||||
it('should return Cancelled when items are mix of cancelled statuses', () => {
|
||||
// Arrange
|
||||
const statuses = [
|
||||
OrderItemProcessingStatusValue.StorniertKunde, // 512
|
||||
OrderItemProcessingStatusValue.AnsLagerNichtAbgeholt, // 262144
|
||||
OrderItemProcessingStatusValue.Storniert, // 1024
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = getProcessingStatusState(statuses);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(ProcessingStatusState.Cancelled);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NotFound status', () => {
|
||||
|
||||
@@ -29,7 +29,8 @@ export const getProcessingStatusState = (
|
||||
(status) =>
|
||||
status === OrderItemProcessingStatusValue.StorniertKunde ||
|
||||
status === OrderItemProcessingStatusValue.Storniert ||
|
||||
status === OrderItemProcessingStatusValue.StorniertLieferant,
|
||||
status === OrderItemProcessingStatusValue.StorniertLieferant ||
|
||||
status === OrderItemProcessingStatusValue.AnsLagerNichtAbgeholt,
|
||||
);
|
||||
if (allCancelled) {
|
||||
return ProcessingStatusState.Cancelled;
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export enum Gender {
|
||||
NotSet = 0,
|
||||
Neutrum = 1,
|
||||
Male = 2,
|
||||
Female = 4,
|
||||
}
|
||||
@@ -2,7 +2,6 @@ export * from './address-type';
|
||||
export * from './buyer';
|
||||
export * from './can-return';
|
||||
export * from './eligible-for-return';
|
||||
export * from './gender';
|
||||
export * from './logistician';
|
||||
export * from './order';
|
||||
export * from './processing-status-state';
|
||||
|
||||
27
libs/oms/data-access/src/lib/schemas/cruda.schema.ts
Normal file
27
libs/oms/data-access/src/lib/schemas/cruda.schema.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* CRUDA enum
|
||||
* Can Create, Read, Update, Delete, Archive
|
||||
*/
|
||||
export const CRUDA = {
|
||||
None: 0,
|
||||
Create: 1,
|
||||
Read: 2,
|
||||
Update: 4,
|
||||
Delete: 8,
|
||||
Archive: 16,
|
||||
} as const;
|
||||
|
||||
const ALL_FLAGS = Object.values(CRUDA).reduce<number>((a, b) => a | b, 0);
|
||||
|
||||
export const CRUDASchema = z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.refine((val) => (val & ~ALL_FLAGS) === 0, {
|
||||
message: 'Invalid CRUDA permission: contains unknown flags',
|
||||
})
|
||||
.describe('CRUDA permissions (bitflags)');
|
||||
|
||||
export type CRUDA = z.infer<typeof CRUDASchema>;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { z } from 'zod';
|
||||
import { OrderItemListItemSchema } from './order-item-list-item.schema';
|
||||
import { OrderItemTypeSchema } from './order-item-type.schema';
|
||||
|
||||
/**
|
||||
* DBH Order Item List Item DTO schema
|
||||
* Extends OrderItemListItem with additional DBH-specific fields
|
||||
*/
|
||||
export const DBHOrderItemListItemSchema = OrderItemListItemSchema.extend({
|
||||
billingZipCode: z.string().describe('Rechnungs-PLZ').optional(),
|
||||
externalRepositories: z.string().describe('Externe Lager').optional(),
|
||||
fetchOnDeliveryNote: z
|
||||
.boolean()
|
||||
.describe('Auf Lieferschein holen')
|
||||
.optional(),
|
||||
invoiceId: z.number().describe('Rechnung ID').optional(),
|
||||
logisticianId: z.number().describe('Logistiker ID').optional(),
|
||||
logisticianName: z.string().describe('Logistiker Name').optional(),
|
||||
orderItemType: OrderItemTypeSchema.describe('Bestellposten-Typ').optional(),
|
||||
orderedAtSupplier: z
|
||||
.string()
|
||||
.describe('Beim Lieferanten bestellt am')
|
||||
.optional(),
|
||||
payerId: z.number().describe('Zahler ID').optional(),
|
||||
paymentReferenceNumber: z
|
||||
.string()
|
||||
.describe('Zahlungsreferenznummer')
|
||||
.optional(),
|
||||
shippingNoteId: z.number().describe('Versandschein ID').optional(),
|
||||
});
|
||||
|
||||
export type DBHOrderItemListItem = z.infer<typeof DBHOrderItemListItemSchema>;
|
||||
@@ -0,0 +1,18 @@
|
||||
import z from 'zod';
|
||||
import { ReceiptType } from '../models/receipt-type';
|
||||
|
||||
export const FetchReceiptsByOrderItemSubsetIdsSchema = z.object({
|
||||
eagerLoading: z.number().optional().default(1).describe('Eager loading flag'),
|
||||
receiptType: z
|
||||
.custom<ReceiptType>()
|
||||
.optional()
|
||||
.default((1 + 64 + 128) as ReceiptType)
|
||||
.describe('Receipt type'),
|
||||
ids: z
|
||||
.array(z.number().describe('Order Item Subset ID'))
|
||||
.describe('List of order item subset IDs'),
|
||||
});
|
||||
|
||||
export type FetchReceiptsByOrderItemSubsetIdsInput = z.input<
|
||||
typeof FetchReceiptsByOrderItemSubsetIdsSchema
|
||||
>;
|
||||
23
libs/oms/data-access/src/lib/schemas/gender.schema.ts
Normal file
23
libs/oms/data-access/src/lib/schemas/gender.schema.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Gender/Salutation enum
|
||||
* Geschlecht/Anrede
|
||||
*/
|
||||
export const Gender = {
|
||||
NotSet: 0,
|
||||
Male: 1,
|
||||
Female: 2,
|
||||
Diverse: 4,
|
||||
} as const;
|
||||
|
||||
const ALL_FLAGS = Object.values(Gender).reduce<number>((a, b) => a | b, 0);
|
||||
|
||||
export const GenderSchema = z
|
||||
.nativeEnum(Gender)
|
||||
.refine((val) => (val & ALL_FLAGS) === val, {
|
||||
message: 'Invalid gender value',
|
||||
})
|
||||
.describe('Gender/Salutation');
|
||||
|
||||
export type Gender = z.infer<typeof GenderSchema>;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { z } from 'zod';
|
||||
import { KeyValueOfStringAndStringSchema } from '@isa/common/data-access';
|
||||
import { DBHOrderItemListItemSchema } from './dbh-order-item-list-item.schema';
|
||||
import { DisplayOrderSchema } from './display-order.schema';
|
||||
import type { Receipt } from '../models/receipt';
|
||||
|
||||
/**
|
||||
* Schema for the handle command data structure.
|
||||
* Based on ActionHandlerService.handle() method parameters.
|
||||
*/
|
||||
export const HandleCommandSchema = z.object({
|
||||
/**
|
||||
* Action to be executed (from KeyValueDTOOfStringAndString)
|
||||
*/
|
||||
action: KeyValueOfStringAndStringSchema.describe('Action to execute'),
|
||||
|
||||
/**
|
||||
* List of order item list items (DBHOrderItemListItemDTO[])
|
||||
*/
|
||||
items: z
|
||||
.array(DBHOrderItemListItemSchema)
|
||||
.describe('List of DBH order item list items'),
|
||||
|
||||
/**
|
||||
* Optional compartment code
|
||||
*/
|
||||
compartmentCode: z.string().describe('Compartment code').optional(),
|
||||
|
||||
/**
|
||||
* Optional compartment info
|
||||
*/
|
||||
compartmentInfo: z.string().describe('Compartment information').optional(),
|
||||
|
||||
/**
|
||||
* Optional item quantity map (Map<number, number>)
|
||||
* Native JavaScript Map with number keys and values
|
||||
*/
|
||||
itemQuantity: z
|
||||
.map(z.number(), z.number())
|
||||
.describe('Item quantity mapping (orderItemSubsetId -> quantity)')
|
||||
.optional(),
|
||||
|
||||
/**
|
||||
* Optional receipts (ReceiptDTO[])
|
||||
* Using the Receipt model type directly
|
||||
*/
|
||||
receipts: z
|
||||
.array(z.custom<Receipt>())
|
||||
.describe('List of receipts')
|
||||
.optional(),
|
||||
|
||||
/**
|
||||
* Optional order data (OrderDTO)
|
||||
*/
|
||||
order: DisplayOrderSchema.describe('Order information').optional(),
|
||||
});
|
||||
|
||||
export type HandleCommand = z.infer<typeof HandleCommandSchema>;
|
||||
@@ -1,13 +1,27 @@
|
||||
export * from './cruda.schema';
|
||||
export * from './dbh-order-item-list-item.schema';
|
||||
export * from './display-addressee.schema';
|
||||
export * from './display-branch.schema';
|
||||
export * from './display-logistician.schema';
|
||||
export * from './display-order-item.schema';
|
||||
export * from './display-order-item-subset.schema';
|
||||
export * from './display-order-payment.schema';
|
||||
export * from './display-order.schema';
|
||||
export * from './environment-channel.schema';
|
||||
export * from './fetch-order-item-subset.schema';
|
||||
export * from './fetch-return-details.schema';
|
||||
export * from './gender.schema';
|
||||
export * from './handle-command.schema';
|
||||
export * from './linked-record.schema';
|
||||
export * from './loyalty-collect-type.schema';
|
||||
export * from './loyalty.schema';
|
||||
export * from './order-item-list-item.schema';
|
||||
export * from './order-item-processing-status-value.schema';
|
||||
export * from './order-item-type.schema';
|
||||
export * from './order-loyalty-collect.schema';
|
||||
export * from './order-type.schema';
|
||||
export * from './payment-status.schema';
|
||||
export * from './payment-type.schema';
|
||||
export * from './price.schema';
|
||||
export * from './product.schema';
|
||||
export * from './promotion.schema';
|
||||
@@ -17,8 +31,5 @@ export * from './return-receipt-values.schema';
|
||||
export * from './shipping-type.schema';
|
||||
export * from './terms-of-delivery.schema';
|
||||
export * from './type-of-delivery.schema';
|
||||
export * from './loyalty-collect-type.schema';
|
||||
export * from './order-loyalty-collect.schema';
|
||||
export * from './fetch-order-item-subset.schema';
|
||||
export * from './display-order-item-subset.schema';
|
||||
export * from './order-item-processing-status-value.schema';
|
||||
export * from './vat-type.schema';
|
||||
export * from './fetch-receipts-by-order-item-subset-ids.schema';
|
||||
|
||||
16
libs/oms/data-access/src/lib/schemas/loyalty.schema.ts
Normal file
16
libs/oms/data-access/src/lib/schemas/loyalty.schema.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TouchBaseSchema } from '@isa/common/data-access';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Loyalty DTO schema
|
||||
*/
|
||||
export const LoyaltySchema = z
|
||||
.object({
|
||||
code: z.string().describe('Code').optional(),
|
||||
label: z.string().describe('Bezeichner').optional(),
|
||||
type: z.string().describe('Art').optional(),
|
||||
value: z.number().describe('Wert').optional(),
|
||||
})
|
||||
.extend(TouchBaseSchema.shape);
|
||||
|
||||
export type Loyalty = z.infer<typeof LoyaltySchema>;
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
KeyValueOfStringAndStringSchema,
|
||||
DateRangeSchema,
|
||||
} from '@isa/common/data-access';
|
||||
import { z } from 'zod';
|
||||
import { CRUDASchema } from './cruda.schema';
|
||||
import { EnvironmentChannelSchema } from './environment-channel.schema';
|
||||
import { GenderSchema } from './gender.schema';
|
||||
import { LoyaltySchema } from './loyalty.schema';
|
||||
import { OrderItemProcessingStatusValueSchema } from './order-item-processing-status-value.schema';
|
||||
import { OrderTypeSchema } from './order-type.schema';
|
||||
import { PaymentStatusSchema } from './payment-status.schema';
|
||||
import { PaymentTypeSchema } from './payment-type.schema';
|
||||
import { PriceSchema } from './price.schema';
|
||||
import { ProductSchema } from './product.schema';
|
||||
import { VATTypeSchema } from './vat-type.schema';
|
||||
|
||||
/**
|
||||
* Order Item List Item DTO schema
|
||||
* Bestellposten
|
||||
*/
|
||||
export const OrderItemListItemSchema = z.object({
|
||||
actions: z
|
||||
.array(KeyValueOfStringAndStringSchema)
|
||||
.describe('Mögliche Aktionen')
|
||||
.optional(),
|
||||
buyerNumber: z.string().describe('Auftraggeber-Nr').optional(),
|
||||
clientChannel: EnvironmentChannelSchema.describe('Bestellkanal').optional(),
|
||||
compartmentCode: z.string().describe('Abholfachnummer').optional(),
|
||||
compartmentInfo: z.string().describe('Abholfach-Zusatz').optional(),
|
||||
cruda: CRUDASchema.describe(
|
||||
'Can Create, Read, Update, Delete, Archive',
|
||||
).optional(),
|
||||
currency: z.string().describe('Währung').optional(),
|
||||
department: z.string().describe('Abteilung').optional(),
|
||||
estimatedDelivery: DateRangeSchema.describe(
|
||||
'Voraussichtlicher Zustellzeitraum',
|
||||
).optional(),
|
||||
estimatedShippingDate: z
|
||||
.string()
|
||||
.describe('Voraussichtliches Lieferdatum')
|
||||
.optional(),
|
||||
features: z
|
||||
.record(z.string(), z.string())
|
||||
.describe('Zusätzliche Markierungen')
|
||||
.optional(),
|
||||
firstName: z.string().describe('Vorname').optional(),
|
||||
gender: GenderSchema.describe('Anrede').optional(),
|
||||
isPrebooked: z
|
||||
.boolean()
|
||||
.describe('Bestellunposten wurde vorgemerkt')
|
||||
.optional(),
|
||||
label: z.string().describe('Label').optional(),
|
||||
labelId: z.number().describe('Label PK').optional(),
|
||||
lastName: z.string().describe('Nachname').optional(),
|
||||
loyalty: LoyaltySchema.describe('Loyalty').optional(),
|
||||
orderBranchId: z.number().describe('Bestellfiliale').optional(),
|
||||
orderDate: z.string().describe('Bestelldatum').optional(),
|
||||
orderId: z.number().describe('Bestellung ID').optional(),
|
||||
orderItemId: z.number().describe('Bestellposten ID').optional(),
|
||||
orderItemNumber: z.string().describe('Bestellpostennummer').optional(),
|
||||
orderItemPId: z.string().describe('Bestellposten PId').optional(),
|
||||
orderItemSubsetId: z
|
||||
.number()
|
||||
.describe('Bestellposten Teilmenge ID')
|
||||
.optional(),
|
||||
orderItemSubsetPId: z
|
||||
.string()
|
||||
.describe('Bestellposten Teilmenge PId')
|
||||
.optional(),
|
||||
orderItemSubsetUId: z
|
||||
.string()
|
||||
.describe('Bestellposten Teilmenge UId')
|
||||
.optional(),
|
||||
orderItemUId: z.string().describe('Bestellposten UId').optional(),
|
||||
orderNumber: z.string().describe('Bestellnummer').optional(),
|
||||
orderPId: z.string().describe('Bestellung PId').optional(),
|
||||
orderType: OrderTypeSchema.describe('Art der Bestellung').optional(),
|
||||
orderUId: z.string().describe('Bestellung UId').optional(),
|
||||
organisation: z.string().describe('Firma / Organisation').optional(),
|
||||
overallQuantity: z.number().describe('Menge gesamt').optional(),
|
||||
paymentProcessing: z.string().describe('Zahungsabwicklung').optional(),
|
||||
paymentStatus: PaymentStatusSchema.describe('Zahlungsstatus').optional(),
|
||||
paymentType: PaymentTypeSchema.describe('Zahlungsart').optional(),
|
||||
pickUpDeadline: z.string().describe('Abholfrist').optional(),
|
||||
price: z.number().describe('Preis').optional(),
|
||||
processingStatus:
|
||||
OrderItemProcessingStatusValueSchema.describe(
|
||||
'Bearbeitungsstatus',
|
||||
).optional(),
|
||||
processingStatusDate: z
|
||||
.string()
|
||||
.describe('Bearbeitungsstatus wurde gesetzt am')
|
||||
.optional(),
|
||||
product: ProductSchema.describe('Artikel-/Produktdaten').optional(),
|
||||
quantity: z.number().describe('Menge').optional(),
|
||||
readyForPickUp: z.string().describe('Im Abholfach seit').optional(),
|
||||
retailPrice: PriceSchema.describe('VK').optional(),
|
||||
shopName: z.string().describe('Bestellfiliale').optional(),
|
||||
specialComment: z.string().describe('Bemerkung zum Bestellposten').optional(),
|
||||
ssc: z.string().describe('Verfügbarkeitsstatus-Code').optional(),
|
||||
sscText: z.string().describe('Verfügbarkeitsstatus-Beschreibung').optional(),
|
||||
supplier: z.string().describe('Lieferant').optional(),
|
||||
supplierId: z.number().describe('Lieferant PK').optional(),
|
||||
targetBranch: z.string().describe('Zielfiliale').optional(),
|
||||
targetBranchId: z.number().describe('Zielfiliale PK').optional(),
|
||||
title: z.string().describe('Titel').optional(),
|
||||
vatType: VATTypeSchema.describe('Mehrwertsteuer-Art').optional(),
|
||||
});
|
||||
|
||||
export type OrderItemListItem = z.infer<typeof OrderItemListItemSchema>;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Order Item Type enum
|
||||
* Bestellposten-Typ
|
||||
*/
|
||||
export const OrderItemType = {
|
||||
NotSet: 0,
|
||||
Single: 1,
|
||||
Head: 2,
|
||||
Child: 4,
|
||||
Service: 8,
|
||||
Discount: 16,
|
||||
Shipping: 32,
|
||||
Gift: 64,
|
||||
Return: 256,
|
||||
Cancellation: 512,
|
||||
Replacement: 1024,
|
||||
} as const;
|
||||
|
||||
const ALL_FLAGS = Object.values(OrderItemType).reduce<number>(
|
||||
(a, b) => a | b,
|
||||
0,
|
||||
);
|
||||
|
||||
export const OrderItemTypeSchema = z
|
||||
.nativeEnum(OrderItemType)
|
||||
.refine((val) => (val & ALL_FLAGS) === val, {
|
||||
message: 'Invalid order item type',
|
||||
})
|
||||
.describe('Order item type');
|
||||
|
||||
export type OrderItemType = z.infer<typeof OrderItemTypeSchema>;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Payment Status enum
|
||||
* Zahlungsstatus
|
||||
*/
|
||||
export const PaymentStatus = {
|
||||
NotSet: 0,
|
||||
Open: 1,
|
||||
InProcess: 2,
|
||||
Paid: 4,
|
||||
Refunded: 8,
|
||||
PartiallyRefunded: 16,
|
||||
Cancelled: 32,
|
||||
Failed: 64,
|
||||
Pending: 128,
|
||||
Authorized: 256,
|
||||
Captured: 512,
|
||||
Voided: 1024,
|
||||
} as const;
|
||||
|
||||
const ALL_FLAGS = Object.values(PaymentStatus).reduce<number>(
|
||||
(a, b) => a | b,
|
||||
0,
|
||||
);
|
||||
|
||||
export const PaymentStatusSchema = z
|
||||
.nativeEnum(PaymentStatus)
|
||||
.refine((val) => (val & ALL_FLAGS) === val, {
|
||||
message: 'Invalid payment status',
|
||||
})
|
||||
.describe('Payment status');
|
||||
|
||||
export type PaymentStatus = z.infer<typeof PaymentStatusSchema>;
|
||||
36
libs/oms/data-access/src/lib/schemas/payment-type.schema.ts
Normal file
36
libs/oms/data-access/src/lib/schemas/payment-type.schema.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Payment Type enum
|
||||
* Zahlungsart
|
||||
*/
|
||||
export const PaymentType = {
|
||||
NotSet: 0,
|
||||
Cash: 1,
|
||||
Card: 2,
|
||||
Invoice: 4,
|
||||
DirectDebit: 8,
|
||||
PayPal: 16,
|
||||
CreditCard: 32,
|
||||
DebitCard: 64,
|
||||
BankTransfer: 128,
|
||||
Prepayment: 256,
|
||||
OnAccount: 512,
|
||||
GiftCard: 1024,
|
||||
Voucher: 2048,
|
||||
Financing: 4096,
|
||||
ApplePay: 8192,
|
||||
GooglePay: 16384,
|
||||
AmazonPay: 32768,
|
||||
} as const;
|
||||
|
||||
const ALL_FLAGS = Object.values(PaymentType).reduce<number>((a, b) => a | b, 0);
|
||||
|
||||
export const PaymentTypeSchema = z
|
||||
.nativeEnum(PaymentType)
|
||||
.refine((val) => (val & ALL_FLAGS) === val, {
|
||||
message: 'Invalid payment type',
|
||||
})
|
||||
.describe('Payment type');
|
||||
|
||||
export type PaymentType = z.infer<typeof PaymentTypeSchema>;
|
||||
28
libs/oms/data-access/src/lib/schemas/vat-type.schema.ts
Normal file
28
libs/oms/data-access/src/lib/schemas/vat-type.schema.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* VAT Type enum
|
||||
* Art des Mehrwertsteuer-Satzes
|
||||
*/
|
||||
export const VATType = {
|
||||
NotSet: 0,
|
||||
Standard: 1,
|
||||
Reduced: 2,
|
||||
SuperReduced: 4,
|
||||
Zero: 8,
|
||||
Exempt: 16,
|
||||
ReverseCharge: 32,
|
||||
MarginScheme: 64,
|
||||
Unknown: 128,
|
||||
} as const;
|
||||
|
||||
const ALL_FLAGS = Object.values(VATType).reduce<number>((a, b) => a | b, 0);
|
||||
|
||||
export const VATTypeSchema = z
|
||||
.nativeEnum(VATType)
|
||||
.refine((val) => (val & ALL_FLAGS) === val, {
|
||||
message: 'Invalid VAT type',
|
||||
})
|
||||
.describe('VAT Type');
|
||||
|
||||
export type VATType = z.infer<typeof VATTypeSchema>;
|
||||
@@ -0,0 +1,374 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { HandleCommandService } from './handle-command.service';
|
||||
import { ReceiptService } from '@generated/swagger/oms-api';
|
||||
import { provideLogging, LogLevel } from '@isa/core/logging';
|
||||
import { FetchReceiptsByOrderItemSubsetIdsInput } from '../schemas';
|
||||
import { ReceiptType } from '../models';
|
||||
|
||||
// Create simple mock object
|
||||
const mockCommandService = {
|
||||
handleCommand: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock the module
|
||||
jest.mock('@core/command', () => ({
|
||||
CommandService: jest.fn().mockImplementation(() => mockCommandService),
|
||||
}));
|
||||
|
||||
// Import after mock
|
||||
import { CommandService } from '@core/command';
|
||||
|
||||
describe('HandleCommandService', () => {
|
||||
let service: HandleCommandService;
|
||||
let mockReceiptService: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockReceiptService = {
|
||||
ReceiptGetReceiptsByOrderItemSubset: jest.fn(),
|
||||
};
|
||||
|
||||
// Reset mock before each test
|
||||
mockCommandService.handleCommand.mockReset();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
HandleCommandService,
|
||||
{
|
||||
provide: CommandService,
|
||||
useValue: mockCommandService,
|
||||
},
|
||||
{
|
||||
provide: ReceiptService,
|
||||
useValue: mockReceiptService,
|
||||
},
|
||||
provideLogging({ level: LogLevel.Off }),
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(HandleCommandService);
|
||||
});
|
||||
|
||||
describe('handle', () => {
|
||||
it('should handle PRINT_SHIPPINGNOTE command and fetch receipts automatically', async () => {
|
||||
// Arrange
|
||||
const mockParams = {
|
||||
action: { key: 'test', command: 'PRINT_SHIPPINGNOTE' },
|
||||
items: [{ orderItemSubsetId: 123 }, { orderItemSubsetId: 456 }],
|
||||
};
|
||||
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset.mockReturnValue(
|
||||
of({
|
||||
result: [
|
||||
{ item3: { data: { receiptNumber: 'R001' } } },
|
||||
{ item3: { data: { receiptNumber: 'R002' } } },
|
||||
],
|
||||
error: null,
|
||||
}),
|
||||
);
|
||||
|
||||
mockCommandService.handleCommand.mockResolvedValue({});
|
||||
|
||||
// Act
|
||||
await service.handle(mockParams as any);
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset,
|
||||
).toHaveBeenCalledWith({
|
||||
payload: expect.objectContaining({
|
||||
ids: [123, 456],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(mockCommandService.handleCommand).toHaveBeenCalledWith(
|
||||
'PRINT_SHIPPINGNOTE',
|
||||
expect.objectContaining({
|
||||
receipts: expect.arrayContaining([
|
||||
expect.objectContaining({ receiptNumber: 'R001' }),
|
||||
expect.objectContaining({ receiptNumber: 'R002' }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use provided receipts if already available', async () => {
|
||||
// Arrange
|
||||
const providedReceipts = [{ receiptNumber: 'R999' }];
|
||||
const mockParams = {
|
||||
action: { key: 'test', command: 'PRINT_SHIPPINGNOTE' },
|
||||
items: [{ orderItemSubsetId: 123 }],
|
||||
receipts: providedReceipts,
|
||||
};
|
||||
|
||||
mockCommandService.handleCommand.mockResolvedValue({});
|
||||
|
||||
// Act
|
||||
await service.handle(mockParams as any);
|
||||
|
||||
// Assert
|
||||
// Should NOT call the receipt service
|
||||
expect(
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset,
|
||||
).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockCommandService.handleCommand).toHaveBeenCalledWith(
|
||||
'PRINT_SHIPPINGNOTE',
|
||||
expect.objectContaining({
|
||||
receipts: providedReceipts,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle PRINT_SMALLAMOUNTINVOICE command and fetch receipts', async () => {
|
||||
// Arrange
|
||||
const mockParams = {
|
||||
action: { key: 'test', command: 'PRINT_SMALLAMOUNTINVOICE' },
|
||||
items: [{ orderItemSubsetId: 789 }],
|
||||
};
|
||||
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset.mockReturnValue(
|
||||
of({
|
||||
result: [{ item3: { data: { receiptNumber: 'R003' } } }],
|
||||
error: null,
|
||||
}),
|
||||
);
|
||||
|
||||
mockCommandService.handleCommand.mockResolvedValue({});
|
||||
|
||||
// Act
|
||||
await service.handle(mockParams as any);
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset,
|
||||
).toHaveBeenCalled();
|
||||
|
||||
expect(mockCommandService.handleCommand).toHaveBeenCalledWith(
|
||||
'PRINT_SMALLAMOUNTINVOICE',
|
||||
expect.objectContaining({
|
||||
receipts: expect.any(Array),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should omit receipts for other commands', async () => {
|
||||
// Arrange
|
||||
const mockParams = {
|
||||
action: { key: 'test', command: 'SOME_OTHER_COMMAND' },
|
||||
items: [{ orderItemSubsetId: 123 }],
|
||||
receipts: [{ receiptNumber: 'R001' }],
|
||||
};
|
||||
|
||||
mockCommandService.handleCommand.mockResolvedValue({});
|
||||
|
||||
// Act
|
||||
await service.handle(mockParams as any);
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset,
|
||||
).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockCommandService.handleCommand).toHaveBeenCalledWith(
|
||||
'SOME_OTHER_COMMAND',
|
||||
expect.not.objectContaining({
|
||||
receipts: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove PRINT_COMPARTMENTLABEL when compartmentCode is present', async () => {
|
||||
// Arrange
|
||||
const mockParams = {
|
||||
action: { key: 'test', command: 'SOME_COMMAND|PRINT_COMPARTMENTLABEL' },
|
||||
items: [{ orderItemSubsetId: 123 }],
|
||||
compartmentCode: 'ABC123',
|
||||
};
|
||||
|
||||
mockCommandService.handleCommand.mockResolvedValue({});
|
||||
|
||||
// Act
|
||||
await service.handle(mockParams as any);
|
||||
|
||||
// Assert
|
||||
expect(mockCommandService.handleCommand).toHaveBeenCalledWith(
|
||||
'SOME_COMMAND',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty items array gracefully', async () => {
|
||||
// Arrange
|
||||
const mockParams = {
|
||||
action: { key: 'test', command: 'PRINT_SHIPPINGNOTE' },
|
||||
items: [],
|
||||
};
|
||||
|
||||
mockCommandService.handleCommand.mockResolvedValue({});
|
||||
|
||||
// Act
|
||||
await service.handle(mockParams as any);
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset,
|
||||
).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockCommandService.handleCommand).toHaveBeenCalledWith(
|
||||
'PRINT_SHIPPINGNOTE',
|
||||
expect.objectContaining({
|
||||
receipts: [],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchReceiptsByOrderItemSubsetIds', () => {
|
||||
it('should fetch receipts successfully with minimal params', async () => {
|
||||
// Arrange
|
||||
const mockParams: FetchReceiptsByOrderItemSubsetIdsInput = {
|
||||
ids: [123, 456, 789],
|
||||
};
|
||||
|
||||
const mockReceipts = [
|
||||
{ id: 1, data: { receiptNumber: 'R001' } },
|
||||
{ id: 2, data: { receiptNumber: 'R002' } },
|
||||
];
|
||||
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset.mockReturnValue(
|
||||
of({
|
||||
result: [
|
||||
{ item3: { data: mockReceipts[0].data } },
|
||||
{ item3: { data: mockReceipts[1].data } },
|
||||
],
|
||||
error: null,
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
const result =
|
||||
await service.fetchReceiptsByOrderItemSubsetIds(mockParams);
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset,
|
||||
).toHaveBeenCalledWith({
|
||||
payload: expect.objectContaining({
|
||||
ids: [123, 456, 789],
|
||||
}),
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual(mockReceipts[0].data);
|
||||
expect(result[1]).toEqual(mockReceipts[1].data);
|
||||
});
|
||||
|
||||
it('should fetch receipts with all params including defaults', async () => {
|
||||
// Arrange
|
||||
const mockParams: FetchReceiptsByOrderItemSubsetIdsInput = {
|
||||
eagerLoading: 1,
|
||||
receiptType: (1 + 64 + 128) as ReceiptType,
|
||||
ids: [100, 200],
|
||||
};
|
||||
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset.mockReturnValue(
|
||||
of({
|
||||
result: [{ item3: { data: { receiptNumber: 'R003' } } }],
|
||||
error: null,
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
const result =
|
||||
await service.fetchReceiptsByOrderItemSubsetIds(mockParams);
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset,
|
||||
).toHaveBeenCalledWith({
|
||||
payload: expect.objectContaining({
|
||||
eagerLoading: 1,
|
||||
receiptType: 193, // 1 + 64 + 128
|
||||
ids: [100, 200],
|
||||
}),
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should filter out null/undefined receipts from result', async () => {
|
||||
// Arrange
|
||||
const mockParams: FetchReceiptsByOrderItemSubsetIdsInput = {
|
||||
ids: [111, 222],
|
||||
};
|
||||
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset.mockReturnValue(
|
||||
of({
|
||||
result: [
|
||||
{ item3: { data: { receiptNumber: 'R004' } } },
|
||||
{ item3: null },
|
||||
{ item3: { data: null } },
|
||||
{ item3: { data: { receiptNumber: 'R005' } } },
|
||||
],
|
||||
error: null,
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
const result =
|
||||
await service.fetchReceiptsByOrderItemSubsetIds(mockParams);
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].receiptNumber).toBe('R004');
|
||||
expect(result[1].receiptNumber).toBe('R005');
|
||||
});
|
||||
|
||||
it('should return empty array when no receipts found', async () => {
|
||||
// Arrange
|
||||
const mockParams: FetchReceiptsByOrderItemSubsetIdsInput = {
|
||||
ids: [999],
|
||||
};
|
||||
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset.mockReturnValue(
|
||||
of({
|
||||
result: [],
|
||||
error: null,
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
const result =
|
||||
await service.fetchReceiptsByOrderItemSubsetIds(mockParams);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle abort signal when provided', async () => {
|
||||
// Arrange
|
||||
const mockParams: FetchReceiptsByOrderItemSubsetIdsInput = {
|
||||
ids: [123],
|
||||
};
|
||||
const abortController = new AbortController();
|
||||
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset.mockReturnValue(
|
||||
of({
|
||||
result: [{ item3: { data: { receiptNumber: 'R006' } } }],
|
||||
error: null,
|
||||
}),
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await service.fetchReceiptsByOrderItemSubsetIds(
|
||||
mockParams,
|
||||
abortController.signal,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveLength(1);
|
||||
expect(
|
||||
mockReceiptService.ReceiptGetReceiptsByOrderItemSubset,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
102
libs/oms/data-access/src/lib/services/handle-command.service.ts
Normal file
102
libs/oms/data-access/src/lib/services/handle-command.service.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { CommandService } from '@core/command';
|
||||
import {
|
||||
FetchReceiptsByOrderItemSubsetIdsInput,
|
||||
FetchReceiptsByOrderItemSubsetIdsSchema,
|
||||
HandleCommand,
|
||||
HandleCommandSchema,
|
||||
} from '../schemas';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { ReceiptService } from '@generated/swagger/oms-api';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import { Receipt } from '../models';
|
||||
|
||||
@Injectable()
|
||||
export class HandleCommandService {
|
||||
#logger = logger(() => ({ service: 'HandleCommandService' }));
|
||||
|
||||
#receiptService = inject(ReceiptService);
|
||||
|
||||
// TODO: Befindet sich in der alten ISA (@core/command) - Als Lib auslagern
|
||||
#commandService = inject(CommandService);
|
||||
|
||||
async handle(params: HandleCommand): Promise<HandleCommand> {
|
||||
const parsed = HandleCommandSchema.parse(params);
|
||||
|
||||
this.#logger.debug('Handle command', () => ({ parsed }));
|
||||
|
||||
let context: HandleCommand;
|
||||
|
||||
// Fetch receipts if needed for shipping note or small amount invoice printing
|
||||
if (
|
||||
parsed?.action?.command?.includes('PRINT_SHIPPINGNOTE') ||
|
||||
parsed?.action?.command === 'PRINT_SMALLAMOUNTINVOICE'
|
||||
) {
|
||||
// Fetch receipts if not already provided
|
||||
let receipts = parsed.receipts ?? [];
|
||||
|
||||
if (receipts.length === 0 && parsed.items?.length > 0) {
|
||||
const orderItemSubsetIds = parsed.items
|
||||
.map((item) => item.orderItemSubsetId)
|
||||
.filter((id): id is number => id !== undefined && id !== null);
|
||||
|
||||
if (orderItemSubsetIds.length > 0) {
|
||||
receipts = await this.fetchReceiptsByOrderItemSubsetIds({
|
||||
ids: orderItemSubsetIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Keep receipts - use params with receipts
|
||||
context = { ...parsed, receipts };
|
||||
} else {
|
||||
// Omit receipts from context
|
||||
const { receipts, ...restParams } = parsed;
|
||||
context = restParams;
|
||||
}
|
||||
|
||||
// #2737 Bei Zubuchen kein Abholfachzettel ausdrucken
|
||||
let command = parsed?.action?.command ?? '';
|
||||
if (parsed?.compartmentCode && !!parsed?.action?.command) {
|
||||
command = parsed.action.command?.replace('|PRINT_COMPARTMENTLABEL', '');
|
||||
}
|
||||
return this.#commandService.handleCommand(command, context);
|
||||
}
|
||||
|
||||
async fetchReceiptsByOrderItemSubsetIds(
|
||||
params: FetchReceiptsByOrderItemSubsetIdsInput,
|
||||
abortSignal?: AbortSignal,
|
||||
) {
|
||||
const parsed = FetchReceiptsByOrderItemSubsetIdsSchema.parse(params);
|
||||
|
||||
this.#logger.debug('Fetch receipts by order item subset IDs', () => ({
|
||||
parsed,
|
||||
}));
|
||||
|
||||
let req$ = this.#receiptService.ReceiptGetReceiptsByOrderItemSubset({
|
||||
payload: parsed, // Payload Default from old Implementation, eagerLoading: 1 and receiptType: (1 + 64 + 128) set as Schema default
|
||||
});
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
const err = new ResponseArgsError(res);
|
||||
this.#logger.error(
|
||||
'Failed to fetch receipts by order item subset IDs',
|
||||
err,
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Mapping Logic from old implementation
|
||||
const mappedReceipts =
|
||||
res?.result?.map((r) => r.item3?.data).filter((f) => !!f) ?? [];
|
||||
|
||||
return mappedReceipts as Receipt[];
|
||||
}
|
||||
}
|
||||
@@ -9,3 +9,4 @@ export * from './return-process.service';
|
||||
export * from './return-search.service';
|
||||
export * from './return-task-list.service';
|
||||
export * from './order-reward-collect.service';
|
||||
export * from './handle-command.service';
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import {
|
||||
DBHOrderItemListItem,
|
||||
DisplayOrderItemSubset,
|
||||
FetchOrderItemSubsetSchema,
|
||||
FetchOrderItemSubsetSchemaInput,
|
||||
@@ -42,7 +43,7 @@ export class OrderRewardCollectService {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.result;
|
||||
return res.result as DBHOrderItemListItem[];
|
||||
}
|
||||
|
||||
async fetchOrderItemSubset(
|
||||
|
||||
Reference in New Issue
Block a user