mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
1 Commits
hotfix-538
...
hotfix-538
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b6b726036 |
@@ -3,3 +3,4 @@ export * from './lib/models';
|
||||
export * from './lib/stores';
|
||||
export * from './lib/schemas';
|
||||
export * from './lib/helpers';
|
||||
export * from './lib/guards';
|
||||
|
||||
2
libs/remission/data-access/src/lib/guards/index.ts
Normal file
2
libs/remission/data-access/src/lib/guards/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { isReturnItem } from './is-return-item';
|
||||
export { isReturnSuggestion } from './is-return-suggestion';
|
||||
42
libs/remission/data-access/src/lib/guards/is-return-item.ts
Normal file
42
libs/remission/data-access/src/lib/guards/is-return-item.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ReturnItem } from '../models/return-item';
|
||||
|
||||
/**
|
||||
* Type guard to check if an object is a valid ReturnItem
|
||||
* @param value - The value to check
|
||||
* @returns True if the value is a ReturnItem, false otherwise
|
||||
*/
|
||||
export const isReturnItem = (value: unknown): value is ReturnItem => {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const item = value as Partial<ReturnItem>;
|
||||
|
||||
// Check required properties from ReturnItem
|
||||
return (
|
||||
// Check product exists and has required properties
|
||||
typeof item.product === 'object' &&
|
||||
item.product !== null &&
|
||||
(typeof item.product.name === 'string' ||
|
||||
typeof item.product.ean === 'string') &&
|
||||
// Check retailPrice exists and has required nested structure
|
||||
typeof item.retailPrice === 'object' &&
|
||||
item.retailPrice !== null &&
|
||||
typeof item.retailPrice.value === 'object' &&
|
||||
item.retailPrice.value !== null &&
|
||||
typeof item.retailPrice.value.value === 'number' &&
|
||||
typeof item.retailPrice.value.currency === 'string' &&
|
||||
// Check source exists and is a valid string
|
||||
typeof item.source === 'string' &&
|
||||
item.source.length > 0 &&
|
||||
// Check inherited ReturnItemDTO properties (id is a number)
|
||||
typeof item.id === 'number' &&
|
||||
// ReturnItem-specific: Must NOT have ReturnSuggestion-specific fields
|
||||
!('accepted' in item) &&
|
||||
!('rejected' in item) &&
|
||||
!('sort' in item) &&
|
||||
// ReturnItem can have predefinedReturnQuantity, quantityReturned, or neither
|
||||
// If it has none of the ReturnSuggestion-specific fields, it's a ReturnItem
|
||||
true
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ReturnSuggestion } from '../models/return-suggestion';
|
||||
|
||||
/**
|
||||
* Type guard to check if an object is a valid ReturnSuggestion
|
||||
* @param value - The value to check
|
||||
* @returns True if the value is a ReturnSuggestion, false otherwise
|
||||
*/
|
||||
export const isReturnSuggestion = (
|
||||
value: unknown,
|
||||
): value is ReturnSuggestion => {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const suggestion = value as Partial<ReturnSuggestion>;
|
||||
|
||||
// Check required properties from ReturnSuggestion
|
||||
return (
|
||||
// Check product exists and has required properties
|
||||
typeof suggestion.product === 'object' &&
|
||||
suggestion.product !== null &&
|
||||
(typeof suggestion.product.name === 'string' ||
|
||||
typeof suggestion.product.ean === 'string') &&
|
||||
// Check retailPrice exists and has required nested structure
|
||||
typeof suggestion.retailPrice === 'object' &&
|
||||
suggestion.retailPrice !== null &&
|
||||
typeof suggestion.retailPrice.value === 'object' &&
|
||||
suggestion.retailPrice.value !== null &&
|
||||
typeof suggestion.retailPrice.value.value === 'number' &&
|
||||
typeof suggestion.retailPrice.value.currency === 'string' &&
|
||||
// Check source exists and is a valid string
|
||||
typeof suggestion.source === 'string' &&
|
||||
suggestion.source.length > 0 &&
|
||||
// Check inherited ReturnSuggestionDTO properties (id is a number)
|
||||
typeof suggestion.id === 'number' &&
|
||||
// ReturnSuggestion-specific: Must have at least one distinguishing property
|
||||
('accepted' in suggestion ||
|
||||
'rejected' in suggestion ||
|
||||
'sort' in suggestion) &&
|
||||
// Additionally, must NOT have ReturnItem-specific fields
|
||||
!('predefinedReturnQuantity' in suggestion) &&
|
||||
!('quantityReturned' in suggestion)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
import { getItemType } from './get-item-type.helper';
|
||||
import { RemissionItemType } from '../models';
|
||||
import { RemissionItem } from '../stores';
|
||||
|
||||
describe('getItemType', () => {
|
||||
describe('Happy Path - ReturnItem', () => {
|
||||
it('should return ReturnItem when item has predefinedReturnQuantity', () => {
|
||||
// Arrange
|
||||
const item = {
|
||||
id: 123,
|
||||
product: {
|
||||
name: 'Test Product',
|
||||
ean: '1234567890123',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
value: 10.99,
|
||||
currency: 'EUR',
|
||||
},
|
||||
},
|
||||
source: 'manually-added',
|
||||
predefinedReturnQuantity: 1,
|
||||
quantity: 1,
|
||||
} as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.ReturnItem);
|
||||
});
|
||||
|
||||
it('should return ReturnItem when item has no suggestion-specific fields', () => {
|
||||
// Arrange
|
||||
const item = {
|
||||
id: 456,
|
||||
product: {
|
||||
name: 'Another Product',
|
||||
ean: '9876543210987',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
value: 15.5,
|
||||
currency: 'EUR',
|
||||
},
|
||||
},
|
||||
source: 'DisposalListModule',
|
||||
returnReason: 'Herstellerfehler',
|
||||
} as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.ReturnItem);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Happy Path - ReturnSuggestion', () => {
|
||||
it('should return ReturnSuggestion when item has sort field', () => {
|
||||
// Arrange
|
||||
const item = {
|
||||
id: 789,
|
||||
product: {
|
||||
name: 'Suggestion Product',
|
||||
ean: '5555555555555',
|
||||
contributors: 'Test Author',
|
||||
format: 'TB',
|
||||
formatDetail: 'Taschenbuch',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
value: 20.0,
|
||||
currency: 'EUR',
|
||||
},
|
||||
},
|
||||
source: 'manually-added',
|
||||
quantity: 3,
|
||||
sort: 1,
|
||||
} as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.ReturnSuggestion);
|
||||
});
|
||||
|
||||
it('should return ReturnSuggestion when item has accepted field', () => {
|
||||
// Arrange
|
||||
const item = {
|
||||
id: 101,
|
||||
product: {
|
||||
name: 'Accepted Product',
|
||||
ean: '1111111111111',
|
||||
contributors: 'Test Author',
|
||||
format: 'TB',
|
||||
formatDetail: 'Taschenbuch',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
value: 8.99,
|
||||
currency: 'EUR',
|
||||
},
|
||||
},
|
||||
source: 'DisposalListModule',
|
||||
quantity: 2,
|
||||
accepted: '2025-10-20T10:00:00Z',
|
||||
} as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.ReturnSuggestion);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fallback with RemissionListType', () => {
|
||||
it('should return ReturnItem when neither guard matches and remissionListType is Pflichtremission', () => {
|
||||
// Arrange - Item that doesn't match either guard (missing required fields)
|
||||
const ambiguousItem = {
|
||||
id: 202,
|
||||
product: {
|
||||
name: 'Ambiguous Product',
|
||||
ean: '1234567890',
|
||||
contributors: 'Test',
|
||||
format: 'TB',
|
||||
formatDetail: 'Taschenbuch',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
value: 12.5,
|
||||
currency: 'EUR',
|
||||
},
|
||||
},
|
||||
// Missing 'source' field - will fail both guards
|
||||
quantity: 1,
|
||||
} as unknown as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(ambiguousItem, 'Pflichtremission');
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.ReturnItem);
|
||||
});
|
||||
|
||||
it('should return ReturnSuggestion when neither guard matches and remissionListType is Abteilungsremission', () => {
|
||||
// Arrange - Item that doesn't match either guard (missing required fields)
|
||||
const ambiguousItem = {
|
||||
id: 303,
|
||||
product: {
|
||||
name: 'Another Ambiguous Product',
|
||||
ean: '9876543210',
|
||||
contributors: 'Test',
|
||||
format: 'TB',
|
||||
formatDetail: 'Taschenbuch',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
value: 7.5,
|
||||
currency: 'EUR',
|
||||
},
|
||||
},
|
||||
// Missing 'source' field - will fail both guards
|
||||
quantity: 1,
|
||||
} as unknown as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(ambiguousItem, 'Abteilungsremission');
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.ReturnSuggestion);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unknown Type', () => {
|
||||
it('should return Unknown when no guards match and no remissionListType provided', () => {
|
||||
// Arrange
|
||||
const invalidItem = {
|
||||
id: 404,
|
||||
} as RemissionItem;
|
||||
|
||||
// Act
|
||||
const result = getItemType(invalidItem);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(RemissionItemType.Unknown);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { RemissionItemType, RemissionListType } from '../models';
|
||||
import { RemissionItem } from '../stores';
|
||||
import { isReturnItem, isReturnSuggestion } from '../guards';
|
||||
|
||||
/**
|
||||
* Determines the concrete type of a RemissionItem
|
||||
* @param item - The item to check
|
||||
* @param remissionListType - Optional fallback to determine type when guards are ambiguous
|
||||
* @returns The type as a RemissionItemType enum value
|
||||
*/
|
||||
export const getItemType = (
|
||||
item: RemissionItem,
|
||||
remissionListType?: RemissionListType,
|
||||
): RemissionItemType => {
|
||||
const isReturnItemType = isReturnItem(item);
|
||||
const isReturnSuggestionItemType = isReturnSuggestion(item);
|
||||
|
||||
// If exactly one guard matches, use it directly (without remissionListType)
|
||||
if (isReturnItemType && !isReturnSuggestionItemType) {
|
||||
return RemissionItemType.ReturnItem;
|
||||
}
|
||||
|
||||
if (isReturnSuggestionItemType && !isReturnItemType) {
|
||||
return RemissionItemType.ReturnSuggestion;
|
||||
}
|
||||
|
||||
// If both are true or both are false, use remissionListType as fallback
|
||||
if (remissionListType) {
|
||||
// Pflichtremission typically contains ReturnItems
|
||||
// Abteilungsremission typically contains ReturnSuggestions
|
||||
if (remissionListType === 'Pflichtremission') {
|
||||
return RemissionItemType.ReturnItem;
|
||||
}
|
||||
|
||||
if (remissionListType === 'Abteilungsremission') {
|
||||
return RemissionItemType.ReturnSuggestion;
|
||||
}
|
||||
}
|
||||
|
||||
// If no remissionListType provided or unrecognized, return Unknown
|
||||
return RemissionItemType.Unknown;
|
||||
};
|
||||
@@ -10,3 +10,4 @@ export * from './get-package-numbers-from-return.helper';
|
||||
export * from './get-retail-price-from-item.helper';
|
||||
export * from './get-assortment-from-item.helper';
|
||||
export * from './order-by-list-items.helper';
|
||||
export * from './get-item-type.helper';
|
||||
|
||||
@@ -21,3 +21,4 @@ export * from './receipt-complete-status';
|
||||
export * from './remission-response-args-error-message';
|
||||
export * from './impediment';
|
||||
export * from './update-item';
|
||||
export * from './remission-item-type';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const RemissionItemType = {
|
||||
ReturnItem: 'ReturnItem',
|
||||
ReturnSuggestion: 'ReturnSuggestion',
|
||||
Unknown: 'Unknown',
|
||||
} as const;
|
||||
|
||||
export type RemissionItemType = keyof typeof RemissionItemType;
|
||||
export type RemissionItemTypeValue =
|
||||
(typeof RemissionItemType)[RemissionItemType];
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
Stock,
|
||||
Receipt,
|
||||
ReturnItem,
|
||||
RemissionListType,
|
||||
ReceiptReturnTuple,
|
||||
ReceiptReturnSuggestionTuple,
|
||||
ReturnSuggestion,
|
||||
CreateRemission,
|
||||
RemissionItemType,
|
||||
} from '../models';
|
||||
import { subDays } from 'date-fns';
|
||||
import { of, throwError } from 'rxjs';
|
||||
@@ -1559,12 +1559,12 @@ describe('RemissionReturnReceiptService', () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should call addReturnSuggestionItem for Abteilung type', async () => {
|
||||
it('should call addReturnSuggestionItem for ReturnSuggestion type', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Abteilung,
|
||||
type: RemissionItemType.ReturnSuggestion,
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -1578,16 +1578,18 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnSuggestionId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
impedimentComment: undefined,
|
||||
remainingQuantity: undefined,
|
||||
});
|
||||
expect(service.addReturnItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call addReturnItem for Pflicht type', async () => {
|
||||
it('should call addReturnItem for ReturnItem type', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Pflicht,
|
||||
type: RemissionItemType.ReturnItem,
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -1605,12 +1607,12 @@ describe('RemissionReturnReceiptService', () => {
|
||||
expect(service.addReturnSuggestionItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return undefined for unknown type', async () => {
|
||||
it('should return undefined for Unknown type', async () => {
|
||||
// Arrange
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: 'Unknown' as RemissionListType,
|
||||
type: RemissionItemType.Unknown,
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -1630,7 +1632,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Abteilung,
|
||||
type: RemissionItemType.ReturnSuggestion,
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
@@ -1641,6 +1643,8 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnSuggestionId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
impedimentComment: undefined,
|
||||
remainingQuantity: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1652,7 +1656,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Pflicht,
|
||||
type: RemissionItemType.ReturnItem,
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
@@ -1675,7 +1679,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Abteilung,
|
||||
type: RemissionItemType.ReturnSuggestion,
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -1689,6 +1693,8 @@ describe('RemissionReturnReceiptService', () => {
|
||||
returnSuggestionId: 3,
|
||||
quantity: 4,
|
||||
inStock: 5,
|
||||
impedimentComment: undefined,
|
||||
remainingQuantity: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1699,7 +1705,7 @@ describe('RemissionReturnReceiptService', () => {
|
||||
const params = {
|
||||
itemId: 3,
|
||||
addItem: baseAddItem,
|
||||
type: RemissionListType.Pflicht,
|
||||
type: RemissionItemType.ReturnItem,
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
Receipt,
|
||||
ReceiptReturnSuggestionTuple,
|
||||
ReceiptReturnTuple,
|
||||
RemissionListType,
|
||||
RemissionItemType,
|
||||
ReturnItem,
|
||||
ReturnSuggestion,
|
||||
} from '../models';
|
||||
@@ -905,14 +905,42 @@ export class RemissionReturnReceiptService {
|
||||
|
||||
/**
|
||||
* Remits an item to the return receipt based on its type.
|
||||
* Determines whether to add a return item or return suggestion item based on the remission list type.
|
||||
* Determines whether to add a return item or return suggestion item based on the remission item type.
|
||||
*
|
||||
* @async
|
||||
* @param {Object} params - The parameters for remitting the item
|
||||
* @param {number} params.itemId - The ID of the item to remit
|
||||
* @param {AddReturnItem | AddReturnSuggestionItem} params.addItem - The item data to add
|
||||
* @param {RemissionListType} params.type - The type of remission list (Abteilung or Pflicht)
|
||||
* @param {Omit<AddReturnItem, 'returnItemId'> | Omit<AddReturnSuggestionItem, 'returnSuggestionId'>} params.addItem - The item data to add (without the item ID field)
|
||||
* @param {RemissionItemType} params.type - The type of remission item (ReturnItem, ReturnSuggestion, or Unknown)
|
||||
* @returns {Promise<ReceiptReturnSuggestionTuple | ReceiptReturnTuple | undefined>} The updated receipt and return tuple if successful, undefined otherwise
|
||||
*
|
||||
* @example
|
||||
* // Remit a ReturnItem
|
||||
* const result = await service.remitItem({
|
||||
* itemId: 123,
|
||||
* addItem: {
|
||||
* returnId: 1,
|
||||
* receiptId: 2,
|
||||
* quantity: 10,
|
||||
* inStock: 5,
|
||||
* },
|
||||
* type: RemissionItemType.ReturnItem,
|
||||
* });
|
||||
*
|
||||
* @example
|
||||
* // Remit a ReturnSuggestion
|
||||
* const result = await service.remitItem({
|
||||
* itemId: 456,
|
||||
* addItem: {
|
||||
* returnId: 1,
|
||||
* receiptId: 2,
|
||||
* quantity: 10,
|
||||
* inStock: 5,
|
||||
* impedimentComment: 'Restmenge',
|
||||
* remainingQuantity: 5,
|
||||
* },
|
||||
* type: RemissionItemType.ReturnSuggestion,
|
||||
* });
|
||||
*/
|
||||
async remitItem({
|
||||
itemId,
|
||||
@@ -923,10 +951,17 @@ export class RemissionReturnReceiptService {
|
||||
addItem:
|
||||
| Omit<AddReturnItem, 'returnItemId'>
|
||||
| Omit<AddReturnSuggestionItem, 'returnSuggestionId'>;
|
||||
type: RemissionListType;
|
||||
type: RemissionItemType;
|
||||
}): Promise<ReceiptReturnSuggestionTuple | ReceiptReturnTuple | undefined> {
|
||||
if (type === RemissionItemType.Unknown) {
|
||||
this.#logger.error(
|
||||
'Invalid remission item type: None. Cannot remit item.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ReturnSuggestion
|
||||
if (type === RemissionListType.Abteilung) {
|
||||
if (type === RemissionItemType.ReturnSuggestion) {
|
||||
return await this.addReturnSuggestionItem({
|
||||
returnId: addItem.returnId,
|
||||
receiptId: addItem.receiptId,
|
||||
@@ -941,7 +976,7 @@ export class RemissionReturnReceiptService {
|
||||
}
|
||||
|
||||
// ReturnItem
|
||||
if (type === RemissionListType.Pflicht) {
|
||||
if (type === RemissionItemType.ReturnItem) {
|
||||
return await this.addReturnItem({
|
||||
returnId: addItem.returnId,
|
||||
receiptId: addItem.receiptId,
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
RemissionResponseArgsErrorMessage,
|
||||
UpdateItem,
|
||||
orderByListItems,
|
||||
getItemType,
|
||||
} from '@isa/remission/data-access';
|
||||
import { injectDialog, injectFeedbackErrorDialog } from '@isa/ui/dialog';
|
||||
import { SearchItemToRemitDialogComponent } from '@isa/remission/shared/search-item-to-remit-dialog';
|
||||
@@ -528,7 +529,7 @@ export class RemissionListComponent {
|
||||
? undefined
|
||||
: inStock - quantity,
|
||||
},
|
||||
type: remissionListType,
|
||||
type: getItemType(item, remissionListType),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,11 +77,10 @@ export const createRemissionListResource = (
|
||||
const isReload = params.searchTrigger === 'reload';
|
||||
|
||||
// #5273
|
||||
// #5387 Hotfix Navigation | Reload has priority over Exact Search
|
||||
if (isReload) {
|
||||
queryToken.input = {};
|
||||
}
|
||||
|
||||
if (exactSearch) {
|
||||
} else if (exactSearch) {
|
||||
queryToken.filter = {};
|
||||
queryToken.orderBy = [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user