mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
2 Commits
hotfix/dea
...
feature/52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7493425bb7 | ||
|
|
7ea8ce7401 |
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Interface representing the data required to create a remission.
|
||||
*/
|
||||
export interface CreateRemission {
|
||||
/**
|
||||
* The unique identifier of the return group.
|
||||
*/
|
||||
returnId: number;
|
||||
|
||||
/**
|
||||
* The unique identifier of the receipt.
|
||||
*/
|
||||
receiptId: number;
|
||||
|
||||
/**
|
||||
* Map of property names to error messages for validation failures
|
||||
* Keys represent property names, values contain validation error messages
|
||||
*/
|
||||
invalidProperties?: Record<string, string>;
|
||||
}
|
||||
@@ -15,4 +15,5 @@ export * from './supplier';
|
||||
export * from './receipt-return-tuple';
|
||||
export * from './receipt-return-suggestion-tuple';
|
||||
export * from './value-tuple-sting-and-integer';
|
||||
export * from './create-remission';
|
||||
export * from './remission-item-source';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TestBed } from '@angular/core/testing';
|
||||
import { RemissionReturnReceiptService } from './remission-return-receipt.service';
|
||||
import { ReturnService } from '@generated/swagger/inventory-api';
|
||||
import { RemissionStockService } from './remission-stock.service';
|
||||
import { ResponseArgsError } from '@isa/common/data-access';
|
||||
import { ResponseArgsError, ResponseArgs } from '@isa/common/data-access';
|
||||
import {
|
||||
Return,
|
||||
Stock,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ReceiptReturnTuple,
|
||||
ReceiptReturnSuggestionTuple,
|
||||
ReturnSuggestion,
|
||||
CreateRemission,
|
||||
} from '../models';
|
||||
import { subDays } from 'date-fns';
|
||||
import { of, throwError } from 'rxjs';
|
||||
@@ -334,7 +335,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
);
|
||||
const params = { returnGroup: 'group-1' };
|
||||
const result = await service.createReturn(params);
|
||||
expect(result).toEqual(mockReturn);
|
||||
expect(result).toEqual({ result: mockReturn, error: null });
|
||||
expect(mockReturnService.ReturnCreateReturn).toHaveBeenCalledWith({
|
||||
data: {
|
||||
supplier: { id: 'supplier-1' },
|
||||
@@ -377,7 +378,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
it('should return undefined when result is undefined', async () => {
|
||||
mockReturnService.ReturnCreateReturn.mockReturnValue(of({ error: null }));
|
||||
const result = await service.createReturn({ returnGroup: undefined });
|
||||
expect(result).toBeUndefined();
|
||||
expect(result).toEqual({ error: null });
|
||||
});
|
||||
|
||||
it('should handle observable errors', async () => {
|
||||
@@ -411,7 +412,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
);
|
||||
const params = { returnId: 123, receiptNumber: 'ABC-123' };
|
||||
const result = await service.createReceipt(params);
|
||||
expect(result).toEqual(mockReceipt);
|
||||
expect(result).toEqual({ result: mockReceipt, error: null });
|
||||
expect(mockReturnService.ReturnCreateReceipt).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
data: {
|
||||
@@ -452,7 +453,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnId: 123,
|
||||
receiptNumber: 'ABC-123',
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
expect(result).toEqual({ error: null });
|
||||
});
|
||||
|
||||
it('should handle observable errors', async () => {
|
||||
@@ -482,7 +483,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
const result = await service.assignPackage(params);
|
||||
expect(result).toEqual(mockReceipt);
|
||||
expect(result).toEqual({ result: mockReceipt, error: null });
|
||||
expect(
|
||||
mockReturnService.ReturnCreateAndAssignPackage,
|
||||
).toHaveBeenCalledWith({
|
||||
@@ -506,7 +507,11 @@ describe('RemissionReturnReceiptService', () => {
|
||||
|
||||
it('should throw ResponseArgsError when API returns error', async () => {
|
||||
mockReturnService.ReturnCreateAndAssignPackage.mockReturnValue(
|
||||
of({ error: 'API Error', result: null }),
|
||||
of({
|
||||
error: 'API Error',
|
||||
message: 'Failed to assign package',
|
||||
result: null,
|
||||
}),
|
||||
);
|
||||
await expect(
|
||||
service.assignPackage({
|
||||
@@ -526,7 +531,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
receiptId: 456,
|
||||
packageNumber: 'PKG-789',
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
expect(result).toEqual({ error: null });
|
||||
});
|
||||
|
||||
it('should handle observable errors', async () => {
|
||||
@@ -1043,42 +1048,50 @@ describe('RemissionReturnReceiptService', () => {
|
||||
).rejects.toThrow('Observable error');
|
||||
});
|
||||
});
|
||||
describe('startRemission', () => {
|
||||
const mockReturn: Return = { id: 123 } as Return;
|
||||
const mockReceipt: Receipt = { id: 456 } as Receipt;
|
||||
const mockAssignedPackage: any = {
|
||||
id: 456,
|
||||
packageNumber: 'PKG-789',
|
||||
|
||||
describe('createRemission', () => {
|
||||
const mockReturnResponse: ResponseArgs<Return> = {
|
||||
result: { id: 123 } as Return,
|
||||
error: false,
|
||||
invalidProperties: { returnGroup: 'Invalid group' },
|
||||
};
|
||||
const mockReceiptResponse: ResponseArgs<Receipt> = {
|
||||
result: { id: 456 } as Receipt,
|
||||
error: false,
|
||||
invalidProperties: { receiptNumber: 'Invalid number' },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the internal methods that startRemission calls
|
||||
jest.spyOn(service, 'createReturn').mockResolvedValue(mockReturn);
|
||||
jest.spyOn(service, 'createReceipt').mockResolvedValue(mockReceipt);
|
||||
// Mock the internal methods that createRemission calls
|
||||
jest.spyOn(service, 'createReturn').mockResolvedValue(mockReturnResponse);
|
||||
jest
|
||||
.spyOn(service, 'assignPackage')
|
||||
.mockResolvedValue(mockAssignedPackage);
|
||||
.spyOn(service, 'createReceipt')
|
||||
.mockResolvedValue(mockReceiptResponse);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should start remission successfully with all parameters', async () => {
|
||||
it('should create remission successfully with all parameters', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
returnGroup: 'group-1',
|
||||
receiptNumber: 'REC-001',
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await service.startRemission(params);
|
||||
const result: CreateRemission | undefined =
|
||||
await service.createRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
invalidProperties: {
|
||||
returnGroup: 'Invalid group',
|
||||
receiptNumber: 'Invalid number',
|
||||
},
|
||||
});
|
||||
|
||||
expect(service.createReturn).toHaveBeenCalledWith({
|
||||
@@ -1088,28 +1101,26 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnId: 123,
|
||||
receiptNumber: 'REC-001',
|
||||
});
|
||||
expect(service.assignPackage).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
packageNumber: 'PKG-789',
|
||||
});
|
||||
});
|
||||
|
||||
it('should start remission successfully with undefined returnGroup and receiptNumber', async () => {
|
||||
it('should create remission successfully with undefined parameters', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
returnGroup: undefined,
|
||||
receiptNumber: undefined,
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await service.startRemission(params);
|
||||
const result = await service.createRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
invalidProperties: {
|
||||
returnGroup: 'Invalid group',
|
||||
receiptNumber: 'Invalid number',
|
||||
},
|
||||
});
|
||||
|
||||
expect(service.createReturn).toHaveBeenCalledWith({
|
||||
@@ -1119,11 +1130,6 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnId: 123,
|
||||
receiptNumber: undefined,
|
||||
});
|
||||
expect(service.assignPackage).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
packageNumber: 'PKG-789',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when createReturn fails', async () => {
|
||||
@@ -1131,13 +1137,12 @@ describe('RemissionReturnReceiptService', () => {
|
||||
const params = {
|
||||
returnGroup: 'group-1',
|
||||
receiptNumber: 'REC-001',
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
(service.createReturn as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
const result = await service.startRemission(params);
|
||||
const result = await service.createRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
@@ -1145,21 +1150,23 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnGroup: 'group-1',
|
||||
});
|
||||
expect(service.createReceipt).not.toHaveBeenCalled();
|
||||
expect(service.assignPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return undefined when createReturn returns null', async () => {
|
||||
it('should return undefined when createReturn returns null result', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
returnGroup: 'group-1',
|
||||
receiptNumber: 'REC-001',
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
(service.createReturn as jest.Mock).mockResolvedValue(null);
|
||||
(service.createReturn as jest.Mock).mockResolvedValue({
|
||||
result: null,
|
||||
error: false,
|
||||
invalidProperties: {},
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await service.startRemission(params);
|
||||
const result = await service.createRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
@@ -1167,7 +1174,6 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnGroup: 'group-1',
|
||||
});
|
||||
expect(service.createReceipt).not.toHaveBeenCalled();
|
||||
expect(service.assignPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return undefined when createReceipt fails', async () => {
|
||||
@@ -1175,13 +1181,12 @@ describe('RemissionReturnReceiptService', () => {
|
||||
const params = {
|
||||
returnGroup: 'group-1',
|
||||
receiptNumber: 'REC-001',
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
(service.createReceipt as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
const result = await service.startRemission(params);
|
||||
const result = await service.createRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
@@ -1192,21 +1197,23 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnId: 123,
|
||||
receiptNumber: 'REC-001',
|
||||
});
|
||||
expect(service.assignPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return undefined when createReceipt returns null', async () => {
|
||||
it('should return undefined when createReceipt returns null result', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
returnGroup: 'group-1',
|
||||
receiptNumber: 'REC-001',
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
(service.createReceipt as jest.Mock).mockResolvedValue(null);
|
||||
(service.createReceipt as jest.Mock).mockResolvedValue({
|
||||
result: null,
|
||||
error: false,
|
||||
invalidProperties: {},
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await service.startRemission(params);
|
||||
const result = await service.createRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeUndefined();
|
||||
@@ -1217,90 +1224,6 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnId: 123,
|
||||
receiptNumber: 'REC-001',
|
||||
});
|
||||
expect(service.assignPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when createReturn throws', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
returnGroup: 'group-1',
|
||||
receiptNumber: 'REC-001',
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
const createReturnError = new Error('Failed to create return');
|
||||
(service.createReturn as jest.Mock).mockRejectedValue(createReturnError);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.startRemission(params)).rejects.toThrow(
|
||||
'Failed to create return',
|
||||
);
|
||||
|
||||
expect(service.createReturn).toHaveBeenCalledWith({
|
||||
returnGroup: 'group-1',
|
||||
});
|
||||
expect(service.createReceipt).not.toHaveBeenCalled();
|
||||
expect(service.assignPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when createReceipt throws', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
returnGroup: 'group-1',
|
||||
receiptNumber: 'REC-001',
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
const createReceiptError = new Error('Failed to create receipt');
|
||||
(service.createReceipt as jest.Mock).mockRejectedValue(
|
||||
createReceiptError,
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.startRemission(params)).rejects.toThrow(
|
||||
'Failed to create receipt',
|
||||
);
|
||||
|
||||
expect(service.createReturn).toHaveBeenCalledWith({
|
||||
returnGroup: 'group-1',
|
||||
});
|
||||
expect(service.createReceipt).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptNumber: 'REC-001',
|
||||
});
|
||||
expect(service.assignPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when assignPackage throws', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
returnGroup: 'group-1',
|
||||
receiptNumber: 'REC-001',
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
const assignPackageError = new Error('Failed to assign package');
|
||||
(service.assignPackage as jest.Mock).mockRejectedValue(
|
||||
assignPackageError,
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.startRemission(params)).rejects.toThrow(
|
||||
'Failed to assign package',
|
||||
);
|
||||
|
||||
expect(service.createReturn).toHaveBeenCalledWith({
|
||||
returnGroup: 'group-1',
|
||||
});
|
||||
expect(service.createReceipt).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptNumber: 'REC-001',
|
||||
});
|
||||
expect(service.assignPackage).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
packageNumber: 'PKG-789',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty string parameters', async () => {
|
||||
@@ -1308,16 +1231,19 @@ describe('RemissionReturnReceiptService', () => {
|
||||
const params = {
|
||||
returnGroup: '',
|
||||
receiptNumber: '',
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await service.startRemission(params);
|
||||
const result = await service.createRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
invalidProperties: {
|
||||
returnGroup: 'Invalid group',
|
||||
receiptNumber: 'Invalid number',
|
||||
},
|
||||
});
|
||||
|
||||
expect(service.createReturn).toHaveBeenCalledWith({
|
||||
@@ -1327,44 +1253,88 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnId: 123,
|
||||
receiptNumber: '',
|
||||
});
|
||||
expect(service.assignPackage).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
packageNumber: 'PKG-789',
|
||||
});
|
||||
});
|
||||
|
||||
it('should proceed even if assignPackage fails silently', async () => {
|
||||
it('should merge invalidProperties from both createReturn and createReceipt', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
returnGroup: 'group-1',
|
||||
receiptNumber: 'REC-001',
|
||||
packageNumber: 'PKG-789',
|
||||
};
|
||||
|
||||
// Mock assignPackage to resolve with undefined (but not throw)
|
||||
(service.assignPackage as jest.Mock).mockResolvedValue(undefined);
|
||||
const returnResponseWithProps: ResponseArgs<Return> = {
|
||||
result: { id: 123 } as Return,
|
||||
error: false,
|
||||
invalidProperties: {
|
||||
returnGroup: 'Return group error',
|
||||
field1: 'Error 1',
|
||||
},
|
||||
};
|
||||
|
||||
const receiptResponseWithProps: ResponseArgs<Receipt> = {
|
||||
result: { id: 456 } as Receipt,
|
||||
error: false,
|
||||
invalidProperties: {
|
||||
receiptNumber: 'Receipt number error',
|
||||
field2: 'Error 2',
|
||||
},
|
||||
};
|
||||
|
||||
(service.createReturn as jest.Mock).mockResolvedValue(
|
||||
returnResponseWithProps,
|
||||
);
|
||||
(service.createReceipt as jest.Mock).mockResolvedValue(
|
||||
receiptResponseWithProps,
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await service.startRemission(params);
|
||||
const result = await service.createRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
invalidProperties: {
|
||||
returnGroup: 'Return group error',
|
||||
field1: 'Error 1',
|
||||
receiptNumber: 'Receipt number error',
|
||||
field2: 'Error 2',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(service.createReturn).toHaveBeenCalledWith({
|
||||
it('should handle missing invalidProperties', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
returnGroup: 'group-1',
|
||||
});
|
||||
expect(service.createReceipt).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptNumber: 'REC-001',
|
||||
});
|
||||
expect(service.assignPackage).toHaveBeenCalledWith({
|
||||
};
|
||||
|
||||
const returnResponseNoProps: ResponseArgs<Return> = {
|
||||
result: { id: 123 } as Return,
|
||||
error: false,
|
||||
};
|
||||
|
||||
const receiptResponseNoProps: ResponseArgs<Receipt> = {
|
||||
result: { id: 456 } as Receipt,
|
||||
error: false,
|
||||
};
|
||||
|
||||
(service.createReturn as jest.Mock).mockResolvedValue(
|
||||
returnResponseNoProps,
|
||||
);
|
||||
(service.createReceipt as jest.Mock).mockResolvedValue(
|
||||
receiptResponseNoProps,
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await service.createRemission(params);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
packageNumber: 'PKG-789',
|
||||
invalidProperties: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { ReturnService } from '@generated/swagger/inventory-api';
|
||||
import { ResponseArgsError, takeUntilAborted } from '@isa/common/data-access';
|
||||
import {
|
||||
ResponseArgs,
|
||||
ResponseArgsError,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { subDays } from 'date-fns';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { RemissionStockService } from './remission-stock.service';
|
||||
@@ -20,6 +24,7 @@ import {
|
||||
FetchRemissionReturnReceiptsSchema,
|
||||
} from '../schemas';
|
||||
import {
|
||||
CreateRemission,
|
||||
Receipt,
|
||||
ReceiptReturnSuggestionTuple,
|
||||
ReceiptReturnTuple,
|
||||
@@ -184,23 +189,25 @@ export class RemissionReturnReceiptService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new remission return with an optional receipt number.
|
||||
* Uses CreateReturnSchema to validate parameters before making the request.
|
||||
* Creates a new remission return with the specified parameters.
|
||||
* Validates parameters using CreateReturnSchema before making the request.
|
||||
*
|
||||
* @async
|
||||
* @param {CreateReturn} params - The parameters for creating the return
|
||||
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
* @returns {Promise<Return | undefined>} The created return object if successful, undefined otherwise
|
||||
* @returns {Promise<ResponseArgs<Return> | undefined>} The created return object if successful, undefined otherwise
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
* @throws {z.ZodError} When parameter validation fails
|
||||
*
|
||||
* @example
|
||||
* const newReturn = await service.createReturn({ returnGroup: 'group1' });
|
||||
* const returnResponse = await service.createReturn({
|
||||
* returnGroup: 'group1',
|
||||
* });
|
||||
*/
|
||||
async createReturn(
|
||||
params: CreateReturn,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Return | undefined> {
|
||||
): Promise<ResponseArgs<Return> | undefined> {
|
||||
this.#logger.debug('Create remission return', () => ({ params }));
|
||||
|
||||
const suppliers =
|
||||
@@ -246,27 +253,27 @@ export class RemissionReturnReceiptService {
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const createdReturn = res?.result as Return | undefined;
|
||||
const returnResponse = res as ResponseArgs<Return> | undefined;
|
||||
this.#logger.debug('Successfully created return', () => ({
|
||||
found: !!createdReturn,
|
||||
found: !!returnResponse,
|
||||
}));
|
||||
|
||||
return createdReturn;
|
||||
return returnResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new remission return receipt with the specified parameters.
|
||||
* Validates parameters using CreateReceiptSchema before making the request.
|
||||
* Validates parameters using CreateReceipt before making the request.
|
||||
*
|
||||
* @async
|
||||
* @param {CreateReceipt} params - The parameters for creating the receipt
|
||||
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
* @returns {Promise<Receipt | undefined>} The created receipt object if successful, undefined otherwise
|
||||
* @returns {Promise<ResponseArgs<Receipt> | undefined>} The created receipt object if successful, undefined otherwise
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
* @throws {z.ZodError} When parameter validation fails
|
||||
*
|
||||
* @example
|
||||
* const receipt = await service.createReceipt({
|
||||
* const receiptResponse = await service.createReceipt({
|
||||
* returnId: 123,
|
||||
* receiptNumber: 'ABC-123',
|
||||
* });
|
||||
@@ -274,7 +281,7 @@ export class RemissionReturnReceiptService {
|
||||
async createReceipt(
|
||||
params: CreateReceipt,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Receipt | undefined> {
|
||||
): Promise<ResponseArgs<Receipt> | undefined> {
|
||||
this.#logger.debug('Create remission return receipt', () => ({ params }));
|
||||
|
||||
const stock =
|
||||
@@ -319,22 +326,22 @@ export class RemissionReturnReceiptService {
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const receipt = res?.result as Receipt | undefined;
|
||||
const receiptResponse = res as ResponseArgs<Receipt> | undefined;
|
||||
this.#logger.debug('Successfully created return receipt', () => ({
|
||||
found: !!receipt,
|
||||
found: !!receiptResponse,
|
||||
}));
|
||||
|
||||
return receipt;
|
||||
return receiptResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a package number to an existing return receipt.
|
||||
* Validates parameters using AssignPackageSchema before making the request.
|
||||
* Assigns a package to the specified return receipt.
|
||||
* Validates parameters using AssignPackage before making the request.
|
||||
*
|
||||
* @async
|
||||
* @param {AssignPackage} params - The parameters for assigning the package number
|
||||
* @param {AssignPackage} params - The parameters for assigning the package
|
||||
* @param {AbortSignal} [abortSignal] - Optional signal to abort the request
|
||||
* @returns {Promise<Receipt | undefined>} The updated receipt object if successful, undefined otherwise
|
||||
* @returns {Promise<ResponseArgs<Receipt> | undefined>} The updated receipt object if successful, undefined otherwise
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
* @throws {z.ZodError} When parameter validation fails
|
||||
*
|
||||
@@ -348,7 +355,7 @@ export class RemissionReturnReceiptService {
|
||||
async assignPackage(
|
||||
params: AssignPackage,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Receipt | undefined> {
|
||||
): Promise<ResponseArgs<Receipt> | undefined> {
|
||||
this.#logger.debug('Assign package to return receipt', () => ({ params }));
|
||||
|
||||
const { returnId, receiptId, packageNumber } = params;
|
||||
@@ -382,12 +389,14 @@ export class RemissionReturnReceiptService {
|
||||
throw new ResponseArgsError(res);
|
||||
}
|
||||
|
||||
const receipt = res?.result as Receipt | undefined;
|
||||
this.#logger.debug('Successfully assigned package', () => ({
|
||||
found: !!receipt,
|
||||
}));
|
||||
const receiptWithAssignedPackageResponse = res as
|
||||
| ResponseArgs<Receipt>
|
||||
| undefined;
|
||||
|
||||
return receipt;
|
||||
this.#logger.debug('Successfully assigned package', () => ({
|
||||
found: !!receiptWithAssignedPackageResponse,
|
||||
}));
|
||||
return receiptWithAssignedPackageResponse;
|
||||
}
|
||||
|
||||
async removeReturnItemFromReturnReceipt(params: {
|
||||
@@ -645,76 +654,69 @@ export class RemissionReturnReceiptService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new remission process by creating a return and receipt.
|
||||
* Validates parameters using FetchRemissionReturnReceiptSchema before making the request.
|
||||
* Warenbegleitschein eröffnen
|
||||
* Creates a remission by generating a return and receipt.
|
||||
* Validates parameters using CreateRemissionSchema before making the request.
|
||||
*
|
||||
* @async
|
||||
* @param {Object} params - The parameters for starting the remission
|
||||
* @param {string | undefined} params.returnGroup - Optional group identifier for the return
|
||||
* @param {string | undefined} params.receiptNumber - Optional receipt number
|
||||
* @param {string} params.packageNumber - The package number to assign
|
||||
* @returns {Promise<FetchRemissionReturnParams | undefined>} The created return and receipt identifiers if successful, undefined otherwise
|
||||
* @param {CreateRemission} params - The parameters for creating the remission
|
||||
* @returns {Promise<CreateRemission | undefined>} The created remission object if successful, undefined otherwise
|
||||
* @throws {ResponseArgsError} When the API request fails
|
||||
* @throws {z.ZodError} When parameter validation fails
|
||||
*
|
||||
* @example
|
||||
* const remission = await service.startRemission({
|
||||
* returnGroup: 'group1',
|
||||
* receiptNumber: 'ABC-123',
|
||||
* packageNumber: 'PKG-789',
|
||||
* const remission = await service.createRemission({
|
||||
* returnId: 123,
|
||||
* receiptId: 456,
|
||||
* });
|
||||
*/
|
||||
async startRemission({
|
||||
async createRemission({
|
||||
returnGroup,
|
||||
receiptNumber,
|
||||
packageNumber,
|
||||
}: {
|
||||
returnGroup: string | undefined;
|
||||
receiptNumber: string | undefined;
|
||||
packageNumber: string;
|
||||
}): Promise<FetchRemissionReturnParams | undefined> {
|
||||
this.#logger.debug('Starting remission', () => ({
|
||||
}): Promise<CreateRemission | undefined> {
|
||||
this.#logger.debug('Create remission', () => ({
|
||||
returnGroup,
|
||||
receiptNumber,
|
||||
packageNumber,
|
||||
}));
|
||||
|
||||
// Warenbegleitschein eröffnen
|
||||
const createdReturn: Return | undefined = await this.createReturn({
|
||||
returnGroup,
|
||||
});
|
||||
const createdReturn: ResponseArgs<Return> | undefined =
|
||||
await this.createReturn({
|
||||
returnGroup,
|
||||
});
|
||||
|
||||
if (!createdReturn) {
|
||||
if (!createdReturn || !createdReturn.result) {
|
||||
this.#logger.error('Failed to create return for remission');
|
||||
return;
|
||||
}
|
||||
|
||||
// Warenbegleitschein eröffnen
|
||||
const createdReceipt: Receipt | undefined = await this.createReceipt({
|
||||
returnId: createdReturn.id,
|
||||
receiptNumber,
|
||||
});
|
||||
const createdReceipt: ResponseArgs<Receipt> | undefined =
|
||||
await this.createReceipt({
|
||||
returnId: createdReturn.result.id,
|
||||
receiptNumber,
|
||||
});
|
||||
|
||||
if (!createdReceipt) {
|
||||
if (!createdReceipt || !createdReceipt.result) {
|
||||
this.#logger.error('Failed to create return receipt');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wannennummer zuweisen
|
||||
await this.assignPackage({
|
||||
returnId: createdReturn.id,
|
||||
receiptId: createdReceipt.id,
|
||||
packageNumber,
|
||||
});
|
||||
const invalidProperties = {
|
||||
...createdReturn.invalidProperties,
|
||||
...createdReceipt.invalidProperties,
|
||||
};
|
||||
|
||||
this.#logger.info('Successfully started remission', () => ({
|
||||
returnId: createdReturn.id,
|
||||
receiptId: createdReceipt.id,
|
||||
this.#logger.info('Successfully created remission', () => ({
|
||||
returnId: createdReturn.result.id,
|
||||
receiptId: createdReceipt.result.id,
|
||||
}));
|
||||
|
||||
return {
|
||||
returnId: createdReturn.id,
|
||||
receiptId: createdReceipt.id,
|
||||
returnId: createdReturn.result.id,
|
||||
receiptId: createdReceipt.result.id,
|
||||
invalidProperties,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
@if (control?.errors?.['pattern']) {
|
||||
<span>Die Wannennummmer muss 14-stellig sein</span>
|
||||
}
|
||||
|
||||
@if (control.errors?.['invalidProperties']) {
|
||||
<span>{{ control.errors.invalidProperties }}</span>
|
||||
}
|
||||
</ui-text-field-errors>
|
||||
}
|
||||
</ui-text-field-container>
|
||||
@@ -71,8 +75,8 @@
|
||||
color="primary"
|
||||
data-what="button"
|
||||
data-which="save"
|
||||
[disabled]="control.invalid || creatingReturnReceipt()"
|
||||
[pending]="creatingReturnReceipt()"
|
||||
[disabled]="control.invalid || assignPackageLoading().loading"
|
||||
[pending]="assignPackageLoading().loading"
|
||||
type="button"
|
||||
>
|
||||
Speichern
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
effect,
|
||||
input,
|
||||
output,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
|
||||
@@ -15,8 +17,43 @@ import {
|
||||
TextFieldErrorsComponent,
|
||||
} from '@isa/ui/input-controls';
|
||||
import { ScannerButtonComponent } from '@isa/shared/scanner';
|
||||
import { boolean } from 'zod';
|
||||
import { RequestStatus } from './remission-start-dialog.component';
|
||||
|
||||
/**
|
||||
* Component for assigning a package number in the remission process.
|
||||
*
|
||||
* This component provides the second step of the remission workflow, allowing users to:
|
||||
* - Manually input a 14-digit package number
|
||||
* - Scan a package number using the integrated scanner
|
||||
* - Validate package number format (14-digit requirement)
|
||||
* - Handle server-side validation errors
|
||||
*
|
||||
* The component uses reactive forms for input validation and handles server-side
|
||||
* validation errors through the RequestStatus input. It emits the package number
|
||||
* when the form is submitted successfully, or undefined if validation fails.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <remi-assign-package-number
|
||||
* [assignPackageLoading]="requestStatus"
|
||||
* (assignPackageNumber)="onAssignPackageNumber($event)"
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Handling the output event
|
||||
* onAssignPackageNumber(packageNumber: string | undefined): void {
|
||||
* if (packageNumber) {
|
||||
* // Package number provided and validated
|
||||
* console.log('Package number:', packageNumber);
|
||||
* } else {
|
||||
* // Validation failed or user cancelled
|
||||
* console.log('Package assignment cancelled or invalid');
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'remi-assign-package-number',
|
||||
templateUrl: './assign-package-number.component.html',
|
||||
@@ -34,18 +71,119 @@ import { boolean } from 'zod';
|
||||
],
|
||||
})
|
||||
export class AssignPackageNumberComponent {
|
||||
creatingReturnReceipt = input<boolean>(false);
|
||||
/**
|
||||
* Input signal containing the current request status for the assign package operation.
|
||||
* Used to display loading states and handle server-side validation errors.
|
||||
* When invalidProperties contains 'PackageNumber', the form control is updated with the error.
|
||||
*/
|
||||
assignPackageLoading = input<RequestStatus>({ loading: false });
|
||||
|
||||
/**
|
||||
* Output signal emitted when user completes the package assignment step.
|
||||
* Emits the validated package number string if successful, or undefined if validation fails
|
||||
* or the user cancels the operation.
|
||||
*/
|
||||
assignPackageNumber = output<string | undefined>();
|
||||
|
||||
/**
|
||||
* Form control for the package number input field.
|
||||
*
|
||||
* Validation rules:
|
||||
* - Required: Package number must be provided
|
||||
* - Pattern: Must be exactly 14 digits (/^\d{14}$/)
|
||||
*
|
||||
* The control also handles server-side validation errors received through
|
||||
* the assignPackageLoading input signal.
|
||||
*/
|
||||
control = new FormControl<string | undefined>(undefined, {
|
||||
validators: [Validators.required, Validators.pattern(/^\d{14}$/)],
|
||||
});
|
||||
|
||||
onScan(value: string) {
|
||||
/**
|
||||
* Constructor sets up reactive error handling for server-side validation.
|
||||
*
|
||||
* Uses an effect to monitor the assignPackageLoading signal and automatically
|
||||
* update the form control's error state when server validation fails for the
|
||||
* PackageNumber field. The untracked wrapper prevents infinite loops while
|
||||
* allowing the effect to respond to signal changes.
|
||||
*/
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const status = this.assignPackageLoading();
|
||||
untracked(() => {
|
||||
if (status?.invalidProperties?.['PackageNumber']) {
|
||||
this.control.setErrors({
|
||||
invalidProperties: status?.invalidProperties?.['PackageNumber'],
|
||||
});
|
||||
} else {
|
||||
this.control.setErrors(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles scanned package number input from the scanner component.
|
||||
*
|
||||
* When a valid value is scanned, it automatically populates the form control.
|
||||
* This provides a convenient way for users to input package numbers without
|
||||
* manual typing, reducing input errors and improving workflow efficiency.
|
||||
*
|
||||
* @param value - The scanned package number string
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <isa-scanner-button (scanned)="onScan($event)" />
|
||||
* ```
|
||||
*/
|
||||
onScan(value: string): void {
|
||||
this.control.setValue(value);
|
||||
}
|
||||
|
||||
onSave(value: string | undefined) {
|
||||
/**
|
||||
* Handles form submission and validation for package number assignment.
|
||||
*
|
||||
* This method performs comprehensive validation before emitting the result:
|
||||
* 1. Checks if a request is currently in progress (prevents double submission)
|
||||
* 2. Triggers form validation to ensure package number meets requirements
|
||||
* 3. Validates that a value was actually provided and is not null/undefined
|
||||
* 4. Emits the appropriate result based on validation outcome
|
||||
*
|
||||
* If validation fails or no value is provided, emits undefined to indicate
|
||||
* the operation should be cancelled or retried. If validation passes,
|
||||
* emits the validated package number for processing.
|
||||
*
|
||||
* @param value - The package number value to validate and process
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <form (ngSubmit)="onSave(control.value)">
|
||||
* <input [formControl]="control" />
|
||||
* <button type="submit" [disabled]="assignPackageLoading().loading">
|
||||
* Assign Package
|
||||
* </button>
|
||||
* </form>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In parent component
|
||||
* onAssignPackage(packageNumber: string | undefined): void {
|
||||
* if (packageNumber) {
|
||||
* // Process the validated package number
|
||||
* this.remissionService.assignPackage(packageNumber);
|
||||
* } else {
|
||||
* // Handle validation failure or cancellation
|
||||
* this.showError('Please provide a valid 14-digit package number');
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
onSave(value: string | undefined): void {
|
||||
if (this.assignPackageLoading().loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.control.updateValueAndValidity();
|
||||
|
||||
if (
|
||||
|
||||
@@ -57,6 +57,10 @@
|
||||
@if (control.errors?.['pattern']) {
|
||||
<span>Die Packstück ID muss 18-stellig sein</span>
|
||||
}
|
||||
|
||||
@if (control.errors?.['invalidProperties']) {
|
||||
<span>{{ control.errors.invalidProperties }}</span>
|
||||
}
|
||||
</ui-text-field-errors>
|
||||
}
|
||||
</ui-text-field-container>
|
||||
@@ -87,7 +91,8 @@
|
||||
color="primary"
|
||||
data-what="button"
|
||||
data-which="save"
|
||||
[disabled]="control.invalid"
|
||||
[disabled]="control.invalid || createRemissionLoading().loading"
|
||||
[pending]="createRemissionLoading().loading"
|
||||
(click)="
|
||||
onSave({ type: ReturnReceiptResultType.Input, value: control.value })
|
||||
"
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { ChangeDetectionStrategy, Component, output } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
effect,
|
||||
input,
|
||||
output,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
|
||||
import { ButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
|
||||
@@ -11,10 +18,46 @@ import {
|
||||
} from '@isa/ui/input-controls';
|
||||
import { ScannerButtonComponent } from '@isa/shared/scanner';
|
||||
import {
|
||||
RequestStatus,
|
||||
ReturnReceiptResult,
|
||||
ReturnReceiptResultType,
|
||||
} from './remission-start-dialog.component';
|
||||
|
||||
/**
|
||||
* Component for creating a return receipt in the remission process.
|
||||
*
|
||||
* This component provides the first step of the remission workflow, allowing users to:
|
||||
* - Generate a receipt number automatically
|
||||
* - Manually input a custom receipt number
|
||||
* - Scan a receipt number using the integrated scanner
|
||||
* - Validate receipt number format (18-digit requirement)
|
||||
*
|
||||
* The component uses reactive forms for input validation and handles server-side
|
||||
* validation errors through the RequestStatus input. It emits different result types
|
||||
* based on user interaction (generate, input with value, or close).
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <remi-create-return-receipt
|
||||
* [createRemissionLoading]="requestStatus"
|
||||
* (createReturnReceipt)="onCreateReturnReceipt($event)"
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Handling the output event
|
||||
* onCreateReturnReceipt(result: ReturnReceiptResult): void {
|
||||
* if (result?.type === ReturnReceiptResultType.Generate) {
|
||||
* // User chose to auto-generate
|
||||
* } else if (result?.type === ReturnReceiptResultType.Input) {
|
||||
* // User provided custom value: result.value
|
||||
* } else {
|
||||
* // User closed/cancelled
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'remi-create-return-receipt',
|
||||
templateUrl: './create-return-receipt.component.html',
|
||||
@@ -33,27 +76,131 @@ import {
|
||||
],
|
||||
})
|
||||
export class CreateReturnReceiptComponent {
|
||||
/**
|
||||
* Expose ReturnReceiptResultType enum to template for type checking and comparisons.
|
||||
*/
|
||||
ReturnReceiptResultType = ReturnReceiptResultType;
|
||||
|
||||
/**
|
||||
* Input signal containing the current request status for the create remission operation.
|
||||
* Used to display loading states and handle server-side validation errors.
|
||||
* When invalidProperties contains 'receiptNumber', the form control is updated with the error.
|
||||
*/
|
||||
createRemissionLoading = input<RequestStatus>({ loading: false });
|
||||
|
||||
/**
|
||||
* Output signal emitted when user completes the return receipt creation step.
|
||||
* Emits different result types based on user action:
|
||||
* - Generate: User chose automatic generation
|
||||
* - Input: User provided a custom receipt number
|
||||
* - Close: User cancelled or provided invalid input
|
||||
*/
|
||||
createReturnReceipt = output<ReturnReceiptResult>();
|
||||
|
||||
/**
|
||||
* Form control for the receipt number input field.
|
||||
*
|
||||
* Validation rules:
|
||||
* - Required: Receipt number must be provided when using manual input
|
||||
* - Pattern: Must be exactly 18 digits (/^\d{18}$/)
|
||||
*
|
||||
* The control also handles server-side validation errors received through
|
||||
* the createRemissionLoading input signal.
|
||||
*/
|
||||
control = new FormControl<string | undefined>(undefined, {
|
||||
validators: [Validators.required, Validators.pattern(/^\d{18}$/)],
|
||||
});
|
||||
|
||||
onScan(value: string | null) {
|
||||
/**
|
||||
* Constructor sets up reactive error handling for server-side validation.
|
||||
*
|
||||
* Uses an effect to monitor the createRemissionLoading signal and automatically
|
||||
* update the form control's error state when server validation fails for the
|
||||
* receiptNumber field. The untracked wrapper prevents infinite loops while
|
||||
* allowing the effect to respond to signal changes.
|
||||
*/
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const status = this.createRemissionLoading();
|
||||
untracked(() => {
|
||||
if (status?.invalidProperties?.['receiptNumber']) {
|
||||
this.control.setErrors({
|
||||
invalidProperties: status?.invalidProperties?.['receiptNumber'],
|
||||
});
|
||||
} else {
|
||||
this.control.setErrors(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles scanned receipt number input from the scanner component.
|
||||
*
|
||||
* When a valid value is scanned, it automatically populates the form control.
|
||||
* This provides a convenient way for users to input receipt numbers without
|
||||
* manual typing, reducing input errors.
|
||||
*
|
||||
* @param value - The scanned receipt number, or null if scan failed/was cancelled
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <isa-scanner-button (scanned)="onScan($event)" />
|
||||
* ```
|
||||
*/
|
||||
onScan(value: string | null): void {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
this.control.setValue(value);
|
||||
}
|
||||
|
||||
onGenerate() {
|
||||
/**
|
||||
* Handles the generate receipt number action.
|
||||
*
|
||||
* Emits a Generate result type, indicating the user wants the system
|
||||
* to automatically generate a receipt number rather than providing one manually.
|
||||
* This is typically the preferred option for most users as it eliminates
|
||||
* input errors and ensures uniqueness.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <button (click)="onGenerate()">Generate Receipt Number</button>
|
||||
* ```
|
||||
*/
|
||||
onGenerate(): void {
|
||||
return this.createReturnReceipt.emit({
|
||||
type: ReturnReceiptResultType.Generate,
|
||||
});
|
||||
}
|
||||
|
||||
onSave(value: ReturnReceiptResult) {
|
||||
/**
|
||||
* Handles form submission and validation for manual receipt number input.
|
||||
*
|
||||
* This method performs comprehensive validation before emitting the result:
|
||||
* 1. Checks if a request is currently in progress (prevents double submission)
|
||||
* 2. Triggers form validation to ensure receipt number meets requirements
|
||||
* 3. Validates that a value was actually provided
|
||||
* 4. Emits the appropriate result based on validation outcome
|
||||
*
|
||||
* If validation fails or no value is provided, emits a Close result.
|
||||
* If validation passes, emits the provided value for processing.
|
||||
*
|
||||
* @param value - The return receipt result to process (typically contains user input)
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <form (ngSubmit)="onSave({ type: ReturnReceiptResultType.Input, value: control.value })">
|
||||
* <input [formControl]="control" />
|
||||
* <button type="submit">Save Receipt Number</button>
|
||||
* </form>
|
||||
* ```
|
||||
*/
|
||||
onSave(value: ReturnReceiptResult): void {
|
||||
if (this.createRemissionLoading().loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.control.updateValueAndValidity();
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
@if (!createReturnReceipt()) {
|
||||
@if (!assignPackageStepData()) {
|
||||
<remi-create-return-receipt
|
||||
(createReturnReceipt)="onCreateReturnReceipt($event)"
|
||||
[createRemissionLoading]="createRemissionRequestStatus()"
|
||||
></remi-create-return-receipt>
|
||||
} @else {
|
||||
<remi-assign-package-number
|
||||
(assignPackageNumber)="onAssignPackageNumber($event)"
|
||||
[creatingReturnReceipt]="loadRequests()"
|
||||
[assignPackageLoading]="assignPackageRequestStatus()"
|
||||
></remi-assign-package-number>
|
||||
}
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import './test-mocks'; // Import mocks before anything else
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RemissionStartDialogComponent } from './remission-start-dialog.component';
|
||||
import {
|
||||
RemissionStartDialogComponent,
|
||||
ReturnReceiptResultType,
|
||||
} from './remission-start-dialog.component';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { RemissionReturnReceiptService } from '@isa/remission/data-access';
|
||||
import { DialogRef, DIALOG_DATA } from '@angular/cdk/dialog';
|
||||
import { DialogComponent } from '@isa/ui/dialog';
|
||||
import { vi } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
describe('RemissionStartDialogComponent', () => {
|
||||
let component: RemissionStartDialogComponent;
|
||||
let fixture: ComponentFixture<RemissionStartDialogComponent>;
|
||||
let mockRemissionService: {
|
||||
createRemission: ReturnType<typeof vi.fn>;
|
||||
assignPackage: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let mockDialogRef: {
|
||||
close: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
// Mock the dialog ref
|
||||
const mockDialogRef = {
|
||||
mockDialogRef = {
|
||||
close: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -28,30 +38,28 @@ describe('RemissionStartDialogComponent', () => {
|
||||
};
|
||||
|
||||
// Mock remission service
|
||||
const mockRemissionService = {
|
||||
createReturn: vi.fn().mockResolvedValue({ id: 1 }),
|
||||
createReceipt: vi.fn().mockResolvedValue({
|
||||
id: 1,
|
||||
receiptNumber: '12345',
|
||||
items: []
|
||||
mockRemissionService = {
|
||||
createRemission: vi.fn().mockResolvedValue({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
}),
|
||||
assignPackage: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RemissionStartDialogComponent,
|
||||
],
|
||||
imports: [RemissionStartDialogComponent],
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{ provide: RemissionReturnReceiptService, useValue: mockRemissionService },
|
||||
{
|
||||
provide: RemissionReturnReceiptService,
|
||||
useValue: mockRemissionService,
|
||||
},
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: mockDialogData },
|
||||
{ provide: DialogComponent, useValue: mockDialogComponent },
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RemissionStartDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
@@ -61,4 +69,162 @@ describe('RemissionStartDialogComponent', () => {
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('onCreateReturnReceipt', () => {
|
||||
it('should handle generate receipt type successfully', async () => {
|
||||
// Arrange
|
||||
const returnReceipt = { type: ReturnReceiptResultType.Generate } as const;
|
||||
|
||||
// Act
|
||||
await component.onCreateReturnReceipt(returnReceipt);
|
||||
|
||||
// Assert
|
||||
expect(mockRemissionService.createRemission).toHaveBeenCalledWith({
|
||||
returnGroup: 'test-group',
|
||||
receiptNumber: undefined,
|
||||
});
|
||||
expect(component.assignPackageStepData()).toEqual({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
expect(component.createRemissionRequestStatus().loading).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle input receipt type with custom receipt number', async () => {
|
||||
// Arrange
|
||||
const returnReceipt = {
|
||||
type: ReturnReceiptResultType.Input,
|
||||
value: 'CUSTOM-123',
|
||||
};
|
||||
|
||||
// Act
|
||||
await component.onCreateReturnReceipt(returnReceipt);
|
||||
|
||||
// Assert
|
||||
expect(mockRemissionService.createRemission).toHaveBeenCalledWith({
|
||||
returnGroup: 'test-group',
|
||||
receiptNumber: 'CUSTOM-123',
|
||||
});
|
||||
expect(component.assignPackageStepData()).toEqual({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
expect(component.createRemissionRequestStatus().loading).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle input receipt type with null value', async () => {
|
||||
// Arrange
|
||||
const returnReceipt = {
|
||||
type: ReturnReceiptResultType.Input,
|
||||
value: null,
|
||||
};
|
||||
|
||||
// Act
|
||||
await component.onCreateReturnReceipt(returnReceipt);
|
||||
|
||||
// Assert
|
||||
expect(mockRemissionService.createRemission).toHaveBeenCalledWith({
|
||||
returnGroup: 'test-group',
|
||||
receiptNumber: undefined,
|
||||
});
|
||||
expect(component.assignPackageStepData()).toEqual({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
});
|
||||
|
||||
it('should close dialog when receipt type is close', async () => {
|
||||
// Arrange
|
||||
const closeSpy = vi.spyOn(component, 'close');
|
||||
const returnReceipt = { type: ReturnReceiptResultType.Close } as const;
|
||||
|
||||
// Act
|
||||
await component.onCreateReturnReceipt(returnReceipt);
|
||||
|
||||
// Assert
|
||||
expect(closeSpy).toHaveBeenCalledWith(undefined);
|
||||
expect(mockRemissionService.createRemission).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onAssignPackageNumber', () => {
|
||||
it('should assign package number successfully', async () => {
|
||||
// Arrange
|
||||
const packageNumber = 'PKG-789';
|
||||
component.assignPackageStepData.set({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
const closeSpy = vi.spyOn(component, 'close');
|
||||
|
||||
// Act
|
||||
await component.onAssignPackageNumber(packageNumber);
|
||||
|
||||
// Assert
|
||||
expect(mockRemissionService.assignPackage).toHaveBeenCalledWith({
|
||||
packageNumber: 'PKG-789',
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
expect(closeSpy).toHaveBeenCalledWith({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
expect(component.assignPackageRequestStatus().loading).toBe(false);
|
||||
});
|
||||
|
||||
it('should close dialog when no package number provided', async () => {
|
||||
// Arrange
|
||||
const closeSpy = vi.spyOn(component, 'close');
|
||||
component.assignPackageStepData.set({
|
||||
returnId: 123,
|
||||
receiptId: 456,
|
||||
});
|
||||
|
||||
// Act
|
||||
await component.onAssignPackageNumber(undefined);
|
||||
|
||||
// Assert
|
||||
expect(closeSpy).toHaveBeenCalledWith(undefined);
|
||||
expect(mockRemissionService.assignPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should close dialog when no step data available', async () => {
|
||||
// Arrange
|
||||
const closeSpy = vi.spyOn(component, 'close');
|
||||
component.assignPackageStepData.set(undefined);
|
||||
|
||||
// Act
|
||||
await component.onAssignPackageNumber('PKG-789');
|
||||
|
||||
// Assert
|
||||
expect(closeSpy).toHaveBeenCalledWith(undefined);
|
||||
expect(mockRemissionService.assignPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onDialogClose', () => {
|
||||
it('should close dialog with result', () => {
|
||||
// Arrange
|
||||
const result = { returnId: 123, receiptId: 456 };
|
||||
const closeSpy = vi.spyOn(component, 'close');
|
||||
|
||||
// Act
|
||||
component.onDialogClose(result);
|
||||
|
||||
// Assert
|
||||
expect(closeSpy).toHaveBeenCalledWith(result);
|
||||
});
|
||||
|
||||
it('should close dialog with undefined result', () => {
|
||||
// Arrange
|
||||
const closeSpy = vi.spyOn(component, 'close');
|
||||
|
||||
// Act
|
||||
component.onDialogClose(undefined);
|
||||
|
||||
// Assert
|
||||
expect(closeSpy).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,32 +10,93 @@ import { provideIcons } from '@ng-icons/core';
|
||||
import { isaActionScanner } from '@isa/icons';
|
||||
import { CreateReturnReceiptComponent } from './create-return-receipt.component';
|
||||
import { AssignPackageNumberComponent } from './assign-package-number.component';
|
||||
import { RemissionReturnReceiptService } from '@isa/remission/data-access';
|
||||
import {
|
||||
CreateRemission,
|
||||
RemissionReturnReceiptService,
|
||||
} from '@isa/remission/data-access';
|
||||
|
||||
/**
|
||||
* Enumeration of possible return receipt result types.
|
||||
* Used to determine the action taken by the user in the return receipt creation step.
|
||||
*/
|
||||
export enum ReturnReceiptResultType {
|
||||
/** User closed the dialog without action */
|
||||
Close = 'close',
|
||||
/** User chose to generate a receipt number automatically */
|
||||
Generate = 'generate',
|
||||
/** User chose to input a custom receipt number */
|
||||
Input = 'input',
|
||||
}
|
||||
|
||||
/**
|
||||
* Union type representing the possible results from the return receipt creation step.
|
||||
* Each variant corresponds to a different user action in the dialog.
|
||||
*/
|
||||
export type ReturnReceiptResult =
|
||||
| { type: ReturnReceiptResultType.Close }
|
||||
| { type: ReturnReceiptResultType.Generate }
|
||||
| {
|
||||
type: ReturnReceiptResultType.Input;
|
||||
/** The custom receipt number entered by the user */
|
||||
value: string | undefined | null;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
/**
|
||||
* Request status object used to track the state of asynchronous operations.
|
||||
* Contains loading state and optional validation error information.
|
||||
*/
|
||||
export type RequestStatus = {
|
||||
/** Whether the request is currently in progress */
|
||||
loading: boolean;
|
||||
/** Map of property names to error messages for validation failures */
|
||||
invalidProperties?: Record<string, string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Input data required to initialize the remission start dialog.
|
||||
*/
|
||||
export type RemissionStartDialogData = {
|
||||
/** The return group identifier for the remission process */
|
||||
returnGroup: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Result data returned when the remission start dialog completes successfully.
|
||||
*/
|
||||
export type RemissionStartDialogResult = {
|
||||
/** The unique identifier of the created return */
|
||||
returnId: number;
|
||||
/** The unique identifier of the created receipt */
|
||||
receiptId: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dialog component for initiating the remission process.
|
||||
*
|
||||
* This component manages a two-step workflow:
|
||||
* 1. Create a return receipt (either generate automatically or accept manual input)
|
||||
* 2. Assign a package number to the created return
|
||||
*
|
||||
* The component extends DialogContentDirective to provide dialog functionality
|
||||
* and uses Angular signals for reactive state management.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Opening the dialog
|
||||
* const dialogRef = this.dialog.open(RemissionStartDialogComponent, {
|
||||
* data: { returnGroup: 'RG123' }
|
||||
* });
|
||||
*
|
||||
* // Handling the result
|
||||
* dialogRef.afterClosed().subscribe(result => {
|
||||
* if (result) {
|
||||
* console.log('Return created:', result.returnId);
|
||||
* console.log('Receipt created:', result.receiptId);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'remi-remission-start-dialog',
|
||||
templateUrl: './remission-start-dialog.component.html',
|
||||
@@ -48,65 +109,169 @@ export class RemissionStartDialogComponent extends DialogContentDirective<
|
||||
RemissionStartDialogData,
|
||||
RemissionStartDialogResult | undefined
|
||||
> {
|
||||
/** Service for handling remission and return receipt operations */
|
||||
#remissionReturnReceiptService = inject(RemissionReturnReceiptService);
|
||||
createReturnReceipt = signal<ReturnReceiptResult>(undefined);
|
||||
loadRequests = signal<boolean>(false);
|
||||
|
||||
onCreateReturnReceipt(returnReceipt: ReturnReceiptResult) {
|
||||
if (returnReceipt && returnReceipt.type !== ReturnReceiptResultType.Close) {
|
||||
this.createReturnReceipt.set(returnReceipt);
|
||||
} else {
|
||||
this.onDialogClose(undefined);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Signal tracking the request status for the create remission operation.
|
||||
* Used to show loading states and validation errors in the first step.
|
||||
*/
|
||||
createRemissionRequestStatus = signal<RequestStatus>({ loading: false });
|
||||
|
||||
/**
|
||||
* Signal tracking the request status for the assign package operation.
|
||||
* Used to show loading states and validation errors in the second step.
|
||||
*/
|
||||
assignPackageRequestStatus = signal<RequestStatus>({ loading: false });
|
||||
|
||||
/**
|
||||
* Signal containing the data needed for the package assignment step.
|
||||
* Set after successful return receipt creation and used in the second step.
|
||||
*/
|
||||
assignPackageStepData = signal<
|
||||
Omit<CreateRemission, 'invalidProperties'> | undefined
|
||||
>(undefined);
|
||||
|
||||
/**
|
||||
* Handles the completion of the return receipt creation step.
|
||||
*
|
||||
* This method processes the user's choice regarding receipt creation:
|
||||
* - If the user closes or cancels, the dialog is closed with no result
|
||||
* - If the user chooses to generate or input a receipt, a remission is created
|
||||
* - On successful creation, the workflow advances to the package assignment step
|
||||
*
|
||||
* @param returnReceipt - The result from the return receipt creation step
|
||||
* @returns Promise that resolves when the operation completes
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Called from template when user completes first step
|
||||
* onCreateReturnReceipt({ type: ReturnReceiptResultType.Generate });
|
||||
* onCreateReturnReceipt({
|
||||
* type: ReturnReceiptResultType.Input,
|
||||
* value: 'CUSTOM-123'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
async onCreateReturnReceipt(
|
||||
returnReceipt: ReturnReceiptResult,
|
||||
): Promise<void> {
|
||||
this.createRemissionRequestStatus.set({ loading: true });
|
||||
|
||||
onAssignPackageNumber(packageNumber: string | undefined) {
|
||||
const returnReceipt = this.createReturnReceipt();
|
||||
if (
|
||||
packageNumber &&
|
||||
returnReceipt &&
|
||||
returnReceipt.type !== ReturnReceiptResultType.Close
|
||||
!returnReceipt ||
|
||||
returnReceipt.type === ReturnReceiptResultType.Close
|
||||
) {
|
||||
let receiptNumber: string | undefined = undefined; // undefined -> Wird generiert;
|
||||
if (
|
||||
returnReceipt.type === ReturnReceiptResultType.Input &&
|
||||
returnReceipt.value
|
||||
) {
|
||||
receiptNumber = returnReceipt.value;
|
||||
}
|
||||
|
||||
this.startRemission({ receiptNumber, packageNumber });
|
||||
} else {
|
||||
this.onDialogClose(undefined);
|
||||
} else {
|
||||
try {
|
||||
let receiptNumber: string | undefined = undefined; // undefined -> Wird generiert;
|
||||
if (
|
||||
returnReceipt.type === ReturnReceiptResultType.Input &&
|
||||
returnReceipt.value
|
||||
) {
|
||||
receiptNumber = returnReceipt.value;
|
||||
}
|
||||
|
||||
const response =
|
||||
await this.#remissionReturnReceiptService.createRemission({
|
||||
returnGroup: this.data.returnGroup,
|
||||
receiptNumber,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
return this.onDialogClose(undefined);
|
||||
}
|
||||
|
||||
this.assignPackageStepData.set({
|
||||
returnId: response.returnId,
|
||||
receiptId: response.receiptId,
|
||||
});
|
||||
this.createRemissionRequestStatus.set({ loading: false });
|
||||
} catch (error: any) {
|
||||
console.error('Error creating remission:', error);
|
||||
if (error?.error?.invalidProperties) {
|
||||
this.createRemissionRequestStatus.set({
|
||||
loading: false,
|
||||
invalidProperties: error?.error?.invalidProperties,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async startRemission({
|
||||
receiptNumber,
|
||||
packageNumber,
|
||||
}: {
|
||||
receiptNumber: string | undefined;
|
||||
packageNumber: string;
|
||||
}) {
|
||||
this.loadRequests.set(true);
|
||||
const response = await this.#remissionReturnReceiptService.startRemission({
|
||||
returnGroup: this.data.returnGroup,
|
||||
receiptNumber,
|
||||
packageNumber,
|
||||
});
|
||||
/**
|
||||
* Handles the completion of the package assignment step.
|
||||
*
|
||||
* This method assigns a package number to the previously created return
|
||||
* and completes the remission start workflow. On successful assignment,
|
||||
* the dialog closes with the return and receipt IDs.
|
||||
*
|
||||
* @param packageNumber - The package number to assign to the return
|
||||
* @returns Promise that resolves when the operation completes
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Called from template when user completes second step
|
||||
* onAssignPackageNumber('PKG-789');
|
||||
* ```
|
||||
*/
|
||||
async onAssignPackageNumber(
|
||||
packageNumber: string | undefined,
|
||||
): Promise<void> {
|
||||
this.assignPackageRequestStatus.set({ loading: true });
|
||||
const data = this.assignPackageStepData();
|
||||
|
||||
if (!response) {
|
||||
if (!data || !packageNumber) {
|
||||
return this.onDialogClose(undefined);
|
||||
}
|
||||
|
||||
this.onDialogClose({
|
||||
returnId: response.returnId,
|
||||
receiptId: response.receiptId,
|
||||
});
|
||||
try {
|
||||
const response = await this.#remissionReturnReceiptService.assignPackage({
|
||||
packageNumber,
|
||||
returnId: data.returnId,
|
||||
receiptId: data.receiptId,
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
return this.onDialogClose(undefined);
|
||||
}
|
||||
|
||||
this.onDialogClose({
|
||||
returnId: data.returnId,
|
||||
receiptId: data.receiptId,
|
||||
});
|
||||
this.assignPackageRequestStatus.set({ loading: false });
|
||||
} catch (error: any) {
|
||||
console.error('Error assigning package:', error);
|
||||
if (error?.error?.invalidProperties) {
|
||||
this.assignPackageRequestStatus.set({
|
||||
loading: false,
|
||||
invalidProperties: error?.error?.invalidProperties,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDialogClose(result: RemissionStartDialogResult | undefined) {
|
||||
/**
|
||||
* Closes the dialog with the specified result.
|
||||
*
|
||||
* This method wraps the inherited close method from DialogContentDirective
|
||||
* to provide a consistent interface for closing the dialog from various
|
||||
* points in the workflow.
|
||||
*
|
||||
* @param result - The result to return to the dialog opener, or undefined if cancelled
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Close with successful result
|
||||
* this.onDialogClose({ returnId: 123, receiptId: 456 });
|
||||
*
|
||||
* // Close without result (cancelled)
|
||||
* this.onDialogClose(undefined);
|
||||
* ```
|
||||
*/
|
||||
onDialogClose(result: RemissionStartDialogResult | undefined): void {
|
||||
this.close(result);
|
||||
this.loadRequests.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user