mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1978: feat(checkout): implement hierarchical grouping on rewards order confirmation...
feat(checkout): implement hierarchical grouping on rewards order confirmation page Implements correct grouping by delivery option and target address on the rewards order confirmation page (Prämien-Abschlussseite). Changes: - Add hierarchical grouping: primary by delivery type, secondary by branch - Show branch name only when multiple branches exist within same delivery type - Remove duplicate "Abholfiliale" section from addresses component - Fix undefined shoppingCartItem error by providing fallback with DisplayOrderItem features - Fix partial order creation error handling in checkout orchestrator Implementation: - New helpers: groupDisplayOrdersByDeliveryType, groupDisplayOrdersByBranch - Updated reward-order-confirmation component with groupedOrders computed signal - Added comprehensive unit tests (15 new tests, all passing) - Graceful error handling for backend responses with partial order creation Bug Fixes: - Prevent undefined features error when shopping cart item not found - Extract orders from HTTP error responses when backend returns warnings - Add German documentation for error handling with TODO for user feedback Related to: #5397 Related work items: #5397
This commit is contained in:
committed by
Nino Righi
parent
4a0fbf010b
commit
1c5bc8de12
@@ -0,0 +1,272 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { groupDisplayOrdersByBranch } from './group-display-orders-by-branch.helper';
|
||||||
|
import { DisplayOrder, DisplayOrderItem } from '@isa/oms/data-access';
|
||||||
|
|
||||||
|
describe('groupDisplayOrdersByBranch', () => {
|
||||||
|
it('should not group orders by branch for Versand order type', () => {
|
||||||
|
const orders: DisplayOrder[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
items: [{ id: 1, quantityUnitType: { id: 1 } } as DisplayOrderItem],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
items: [{ id: 2, quantityUnitType: { id: 1 } } as DisplayOrderItem],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = groupDisplayOrdersByBranch('Versand', orders);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].branchId).toBeUndefined();
|
||||||
|
expect(result[0].branchName).toBeUndefined();
|
||||||
|
expect(result[0].orders).toHaveLength(2);
|
||||||
|
expect(result[0].allItems).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should group orders by branch for Abholung order type', () => {
|
||||||
|
const orders: DisplayOrder[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
targetBranch: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Filiale München',
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
},
|
||||||
|
items: [{ id: 1, quantityUnitType: { id: 1 } } as DisplayOrderItem],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
targetBranch: {
|
||||||
|
id: 2,
|
||||||
|
name: 'Filiale Berlin',
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
},
|
||||||
|
items: [{ id: 2, quantityUnitType: { id: 1 } } as DisplayOrderItem],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
targetBranch: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Filiale München',
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
},
|
||||||
|
items: [{ id: 3, quantityUnitType: { id: 1 } } as DisplayOrderItem],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = groupDisplayOrdersByBranch('Abholung', orders);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0].branchId).toBe(1);
|
||||||
|
expect(result[0].branchName).toBe('Filiale München');
|
||||||
|
expect(result[0].orders).toHaveLength(2);
|
||||||
|
expect(result[0].allItems).toHaveLength(2);
|
||||||
|
expect(result[1].branchId).toBe(2);
|
||||||
|
expect(result[1].branchName).toBe('Filiale Berlin');
|
||||||
|
expect(result[1].orders).toHaveLength(1);
|
||||||
|
expect(result[1].allItems).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should group orders by branch for Rücklage order type', () => {
|
||||||
|
const orders: DisplayOrder[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
targetBranch: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Filiale Hamburg',
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{ id: 1, quantityUnitType: { id: 1 } } as DisplayOrderItem,
|
||||||
|
{ id: 2, quantityUnitType: { id: 1 } } as DisplayOrderItem,
|
||||||
|
],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = groupDisplayOrdersByBranch('Rücklage', orders);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].branchId).toBe(1);
|
||||||
|
expect(result[0].branchName).toBe('Filiale Hamburg');
|
||||||
|
expect(result[0].orders).toHaveLength(1);
|
||||||
|
expect(result[0].allItems).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort branch groups by branch ID', () => {
|
||||||
|
const orders: DisplayOrder[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
targetBranch: {
|
||||||
|
id: 3,
|
||||||
|
name: 'Branch 3',
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
},
|
||||||
|
items: [{ id: 1, quantityUnitType: { id: 1 } } as DisplayOrderItem],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
targetBranch: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Branch 1',
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
},
|
||||||
|
items: [{ id: 2, quantityUnitType: { id: 1 } } as DisplayOrderItem],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
targetBranch: {
|
||||||
|
id: 2,
|
||||||
|
name: 'Branch 2',
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
},
|
||||||
|
items: [{ id: 3, quantityUnitType: { id: 1 } } as DisplayOrderItem],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = groupDisplayOrdersByBranch('Abholung', orders);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result[0].branchId).toBe(1);
|
||||||
|
expect(result[1].branchId).toBe(2);
|
||||||
|
expect(result[2].branchId).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle orders without targetBranch for pickup types', () => {
|
||||||
|
const orders: DisplayOrder[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
targetBranch: undefined,
|
||||||
|
items: [{ id: 1, quantityUnitType: { id: 1 } } as DisplayOrderItem],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = groupDisplayOrdersByBranch('Abholung', orders);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].branchId).toBeUndefined();
|
||||||
|
expect(result[0].branchName).toBeUndefined();
|
||||||
|
expect(result[0].orders).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flatten items from multiple orders in same branch', () => {
|
||||||
|
const orders: DisplayOrder[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
targetBranch: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Branch 1',
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{ id: 1, quantityUnitType: { id: 1 } } as DisplayOrderItem,
|
||||||
|
{ id: 2, quantityUnitType: { id: 1 } } as DisplayOrderItem,
|
||||||
|
],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
targetBranch: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Branch 1',
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{ id: 3, quantityUnitType: { id: 1 } } as DisplayOrderItem,
|
||||||
|
{ id: 4, quantityUnitType: { id: 1 } } as DisplayOrderItem,
|
||||||
|
{ id: 5, quantityUnitType: { id: 1 } } as DisplayOrderItem,
|
||||||
|
],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = groupDisplayOrdersByBranch('Abholung', orders);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].allItems).toHaveLength(5);
|
||||||
|
expect(result[0].allItems.map((item) => item.id)).toEqual([1, 2, 3, 4, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle orders without items', () => {
|
||||||
|
const orders: DisplayOrder[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
targetBranch: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Branch 1',
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
},
|
||||||
|
items: undefined,
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = groupDisplayOrdersByBranch('Abholung', orders);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].allItems).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty orders array', () => {
|
||||||
|
const result = groupDisplayOrdersByBranch('Abholung', []);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not group DIG-Versand by branch', () => {
|
||||||
|
const orders: DisplayOrder[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
targetBranch: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Branch 1',
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
},
|
||||||
|
items: [{ id: 1, quantityUnitType: { id: 1 } } as DisplayOrderItem],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
targetBranch: {
|
||||||
|
id: 2,
|
||||||
|
name: 'Branch 2',
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
},
|
||||||
|
items: [{ id: 2, quantityUnitType: { id: 1 } } as DisplayOrderItem],
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = groupDisplayOrdersByBranch('DIG-Versand', orders);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].branchId).toBeUndefined();
|
||||||
|
expect(result[0].orders).toHaveLength(2);
|
||||||
|
expect(result[0].allItems).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { DisplayOrder, DisplayOrderItem } from '@isa/oms/data-access';
|
||||||
|
import { OrderType } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a group of orders sharing the same branch (for pickup/in-store)
|
||||||
|
* or all orders for delivery types that don't have branch-specific grouping.
|
||||||
|
*/
|
||||||
|
export type OrderBranchGroup = {
|
||||||
|
/** Branch ID (undefined for delivery types without branch grouping) */
|
||||||
|
branchId?: number;
|
||||||
|
/** Branch name (undefined for delivery types without branch grouping) */
|
||||||
|
branchName?: string;
|
||||||
|
/** Array of orders in this branch group */
|
||||||
|
orders: DisplayOrder[];
|
||||||
|
/** Flattened array of all items from all orders in this group */
|
||||||
|
allItems: DisplayOrderItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order types that require grouping by branch/filiale.
|
||||||
|
* Other order types (Versand, DIG-Versand, etc.) are grouped together without branch subdivision.
|
||||||
|
*/
|
||||||
|
const ORDER_TYPES_WITH_BRANCH_GROUPING = [
|
||||||
|
OrderType.Pickup,
|
||||||
|
OrderType.InStore,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts orders by branch ID (ascending).
|
||||||
|
*/
|
||||||
|
const sortByBranchId = (
|
||||||
|
entries: [number, DisplayOrder[]][],
|
||||||
|
): [number, DisplayOrder[]][] => {
|
||||||
|
return [...entries].sort(([a], [b]) => a - b);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups display orders by their target branch.
|
||||||
|
* Only applies branch-level grouping for Abholung (Pickup) and Rücklage (InStore) order types.
|
||||||
|
* For other delivery types (Versand, DIG-Versand, etc.), returns a single group with all orders.
|
||||||
|
*
|
||||||
|
* @param orderType - The order type to determine if branch grouping is needed
|
||||||
|
* @param orders - Array of DisplayOrder objects to group
|
||||||
|
* @returns Array of OrderBranchGroup objects, each containing orders and items for a specific branch
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // For Abholung (pickup) orders
|
||||||
|
* const pickupOrders = [
|
||||||
|
* { targetBranch: { id: 1, name: 'München' }, items: [item1, item2] },
|
||||||
|
* { targetBranch: { id: 2, name: 'Berlin' }, items: [item3] }
|
||||||
|
* ];
|
||||||
|
* const groups = groupDisplayOrdersByBranch('Abholung', pickupOrders);
|
||||||
|
* // [
|
||||||
|
* // { branchId: 1, branchName: 'München', orders: [...], allItems: [item1, item2] },
|
||||||
|
* // { branchId: 2, branchName: 'Berlin', orders: [...], allItems: [item3] }
|
||||||
|
* // ]
|
||||||
|
*
|
||||||
|
* // For Versand (delivery) orders
|
||||||
|
* const deliveryOrders = [
|
||||||
|
* { items: [item1] },
|
||||||
|
* { items: [item2, item3] }
|
||||||
|
* ];
|
||||||
|
* const groups = groupDisplayOrdersByBranch('Versand', deliveryOrders);
|
||||||
|
* // [
|
||||||
|
* // { branchId: undefined, branchName: undefined, orders: [...], allItems: [item1, item2, item3] }
|
||||||
|
* // ]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function groupDisplayOrdersByBranch(
|
||||||
|
orderType: OrderType | string,
|
||||||
|
orders: DisplayOrder[],
|
||||||
|
): OrderBranchGroup[] {
|
||||||
|
const needsBranchGrouping =
|
||||||
|
orderType === OrderType.Pickup || orderType === OrderType.InStore;
|
||||||
|
|
||||||
|
if (!needsBranchGrouping) {
|
||||||
|
// For delivery types without branch grouping, return single group with all orders
|
||||||
|
const allItems = orders.flatMap((order) => order.items ?? []);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
branchId: undefined,
|
||||||
|
branchName: undefined,
|
||||||
|
orders,
|
||||||
|
allItems,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Abholung/Rücklage, group by targetBranch.id
|
||||||
|
const branchGroups = orders.reduce((map, order) => {
|
||||||
|
const branchId = order.targetBranch?.id ?? 0;
|
||||||
|
if (!map.has(branchId)) {
|
||||||
|
map.set(branchId, []);
|
||||||
|
}
|
||||||
|
map.get(branchId)!.push(order);
|
||||||
|
return map;
|
||||||
|
}, new Map<number, DisplayOrder[]>());
|
||||||
|
|
||||||
|
// Convert Map to array of OrderBranchGroup, sorted by branch ID
|
||||||
|
return sortByBranchId(Array.from(branchGroups.entries())).map(
|
||||||
|
([branchId, branchOrders]) => {
|
||||||
|
const branch = branchOrders[0]?.targetBranch;
|
||||||
|
const allItems = branchOrders.flatMap((order) => order.items ?? []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
branchId: branchId || undefined,
|
||||||
|
branchName: branch?.name,
|
||||||
|
orders: branchOrders,
|
||||||
|
allItems,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { groupDisplayOrdersByDeliveryType } from './group-display-orders-by-delivery-type.helper';
|
||||||
|
import { DisplayOrder } from '@isa/oms/data-access';
|
||||||
|
|
||||||
|
describe('groupDisplayOrdersByDeliveryType', () => {
|
||||||
|
it('should group orders by delivery type', () => {
|
||||||
|
const orders: DisplayOrder[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
features: { orderType: 'Versand' },
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
features: { orderType: 'Abholung' },
|
||||||
|
orderType: { id: 2 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
features: { orderType: 'Versand' },
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
features: { orderType: 'Rücklage' },
|
||||||
|
orderType: { id: 3 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = groupDisplayOrdersByDeliveryType(orders);
|
||||||
|
|
||||||
|
expect(result.size).toBe(3);
|
||||||
|
expect(result.get('Versand')).toHaveLength(2);
|
||||||
|
expect(result.get('Abholung')).toHaveLength(1);
|
||||||
|
expect(result.get('Rücklage')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle orders without order type feature', () => {
|
||||||
|
const orders: DisplayOrder[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
features: {},
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
features: undefined,
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = groupDisplayOrdersByDeliveryType(orders);
|
||||||
|
|
||||||
|
expect(result.size).toBe(1);
|
||||||
|
expect(result.has('Unbekannt')).toBe(true);
|
||||||
|
expect(result.get('Unbekannt')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty map for empty orders array', () => {
|
||||||
|
const result = groupDisplayOrdersByDeliveryType([]);
|
||||||
|
|
||||||
|
expect(result.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single order', () => {
|
||||||
|
const orders: DisplayOrder[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
features: { orderType: 'Abholung' },
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = groupDisplayOrdersByDeliveryType(orders);
|
||||||
|
|
||||||
|
expect(result.size).toBe(1);
|
||||||
|
expect(result.get('Abholung')).toHaveLength(1);
|
||||||
|
expect(result.get('Abholung')?.[0]?.id).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all orders with same order type', () => {
|
||||||
|
const orders: DisplayOrder[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
features: { orderType: 'Versand' },
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
features: { orderType: 'Versand' },
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
features: { orderType: 'Versand' },
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = groupDisplayOrdersByDeliveryType(orders);
|
||||||
|
|
||||||
|
expect(result.size).toBe(1);
|
||||||
|
expect(result.get('Versand')).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all supported order types', () => {
|
||||||
|
const orders: DisplayOrder[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
features: { orderType: 'Abholung' },
|
||||||
|
orderType: { id: 1 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
features: { orderType: 'Rücklage' },
|
||||||
|
orderType: { id: 2 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
features: { orderType: 'Versand' },
|
||||||
|
orderType: { id: 3 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
features: { orderType: 'DIG-Versand' },
|
||||||
|
orderType: { id: 4 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
features: { orderType: 'B2B-Versand' },
|
||||||
|
orderType: { id: 5 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
features: { orderType: 'Download' },
|
||||||
|
orderType: { id: 6 },
|
||||||
|
quantityUnitType: { id: 1 },
|
||||||
|
} as DisplayOrder,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = groupDisplayOrdersByDeliveryType(orders);
|
||||||
|
|
||||||
|
expect(result.size).toBe(6);
|
||||||
|
expect(result.has('Abholung')).toBe(true);
|
||||||
|
expect(result.has('Rücklage')).toBe(true);
|
||||||
|
expect(result.has('Versand')).toBe(true);
|
||||||
|
expect(result.has('DIG-Versand')).toBe(true);
|
||||||
|
expect(result.has('B2B-Versand')).toBe(true);
|
||||||
|
expect(result.has('Download')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { DisplayOrder } from '@isa/oms/data-access';
|
||||||
|
import { getOrderTypeFeature } from './get-order-type-feature.helper';
|
||||||
|
import { OrderType } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups display orders by their delivery type (orderType feature).
|
||||||
|
*
|
||||||
|
* @param orders - Array of DisplayOrder objects to group
|
||||||
|
* @returns Map where keys are order types (Abholung, Rücklage, Versand, etc.)
|
||||||
|
* and values are arrays of orders with that type
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const orders = [
|
||||||
|
* { id: 1, features: { orderType: 'Abholung' }, ... },
|
||||||
|
* { id: 2, features: { orderType: 'Abholung' }, ... },
|
||||||
|
* { id: 3, features: { orderType: 'Versand' }, ... }
|
||||||
|
* ];
|
||||||
|
*
|
||||||
|
* const grouped = groupDisplayOrdersByDeliveryType(orders);
|
||||||
|
* // Map {
|
||||||
|
* // 'Abholung' => [order1, order2],
|
||||||
|
* // 'Versand' => [order3]
|
||||||
|
* // }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function groupDisplayOrdersByDeliveryType(
|
||||||
|
orders: DisplayOrder[],
|
||||||
|
): Map<OrderType | string, DisplayOrder[]> {
|
||||||
|
return orders.reduce((map, order) => {
|
||||||
|
const orderType = getOrderTypeFeature(order.features) ?? 'Unbekannt';
|
||||||
|
if (!map.has(orderType)) {
|
||||||
|
map.set(orderType, []);
|
||||||
|
}
|
||||||
|
map.get(orderType)!.push(order);
|
||||||
|
return map;
|
||||||
|
}, new Map<OrderType | string, DisplayOrder[]>());
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ export * from './format-address.helper';
|
|||||||
export * from './get-order-type-icon.helper';
|
export * from './get-order-type-icon.helper';
|
||||||
export * from './group-by-branch.helper';
|
export * from './group-by-branch.helper';
|
||||||
export * from './group-by-order-type.helper';
|
export * from './group-by-order-type.helper';
|
||||||
|
export * from './group-display-orders-by-branch.helper';
|
||||||
|
export * from './group-display-orders-by-delivery-type.helper';
|
||||||
export * from './item-selection-changed.helper';
|
export * from './item-selection-changed.helper';
|
||||||
export * from './merge-reward-selection-items.helper';
|
export * from './merge-reward-selection-items.helper';
|
||||||
export * from './should-show-grouping.helper';
|
export * from './should-show-grouping.helper';
|
||||||
|
|||||||
@@ -29,18 +29,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (hasTargetBranchFeature()) {
|
|
||||||
@for (branch of targetBranches(); track branch) {
|
|
||||||
<div>
|
|
||||||
<h3 class="isa-text-body-1-regular">Abholfiliale</h3>
|
|
||||||
<div class="isa-text-body-1-bold mt-1">
|
|
||||||
{{ branch.name }}
|
|
||||||
</div>
|
|
||||||
<shared-address
|
|
||||||
class="isa-text-body-1-bold"
|
|
||||||
[address]="branch.address"
|
|
||||||
></shared-address>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,13 +62,25 @@ export class OrderConfirmationItemListItemComponent {
|
|||||||
const shoppingCart = this.#orderConfiramtionStore.shoppingCart();
|
const shoppingCart = this.#orderConfiramtionStore.shoppingCart();
|
||||||
const item = this.item();
|
const item = this.item();
|
||||||
if (!shoppingCart) {
|
if (!shoppingCart) {
|
||||||
return undefined;
|
// Fallback: use DisplayOrderItem features directly
|
||||||
|
return {
|
||||||
|
features: item.features,
|
||||||
|
availability: undefined,
|
||||||
|
destination: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return shoppingCart.items.find(
|
const foundItem = shoppingCart.items.find(
|
||||||
(scItem) =>
|
(scItem) =>
|
||||||
scItem?.data?.product?.catalogProductNumber ===
|
scItem?.data?.product?.catalogProductNumber ===
|
||||||
item.product?.catalogProductNumber,
|
item.product?.catalogProductNumber,
|
||||||
)?.data;
|
)?.data;
|
||||||
|
|
||||||
|
// Fallback: use DisplayOrderItem features if not found in cart
|
||||||
|
return foundItem ?? {
|
||||||
|
features: item.features,
|
||||||
|
availability: undefined,
|
||||||
|
destination: undefined,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,44 @@
|
|||||||
<checkout-order-confirmation-header></checkout-order-confirmation-header>
|
<checkout-order-confirmation-header></checkout-order-confirmation-header>
|
||||||
<div class="w-full h-[0.0625rem] bg-[#DEE2E6]"></div>
|
<div class="w-full h-[0.0625rem] bg-[#DEE2E6]"></div>
|
||||||
<checkout-order-confirmation-addresses></checkout-order-confirmation-addresses>
|
<checkout-order-confirmation-addresses></checkout-order-confirmation-addresses>
|
||||||
@for (order of orders(); track order.id) {
|
|
||||||
<checkout-order-confirmation-item-list
|
@for (group of groupedOrders(); track group.orderType) {
|
||||||
[order]="order"
|
<!-- Delivery type section -->
|
||||||
></checkout-order-confirmation-item-list>
|
<div class="flex flex-col gap-2 self-stretch" data-what="delivery-type-group" [attr.data-which]="group.orderType">
|
||||||
|
@for (branchGroup of group.branchGroups; track branchGroup.branchId ?? 0) {
|
||||||
|
<!-- Branch header (only shown when multiple branches exist within same delivery type) -->
|
||||||
|
@if (branchGroup.branchName && group.branchGroups.length > 1) {
|
||||||
|
<div
|
||||||
|
class="flex p-2 items-center gap-[0.625rem] self-stretch rounded-2xl bg-isa-neutral-200 w-full isa-text-body-2-bold"
|
||||||
|
data-what="branch-header"
|
||||||
|
[attr.data-which]="branchGroup.branchName"
|
||||||
|
>
|
||||||
|
<ng-icon [name]="group.icon" size="1.5rem"></ng-icon>
|
||||||
|
<div>
|
||||||
|
{{ group.orderType }} - {{ branchGroup.branchName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<!-- Order type header (for single branch or Versand types) -->
|
||||||
|
<div
|
||||||
|
class="flex p-2 items-center gap-[0.625rem] self-stretch rounded-2xl bg-isa-neutral-200 w-full isa-text-body-2-bold"
|
||||||
|
data-what="order-type-header"
|
||||||
|
[attr.data-which]="group.orderType"
|
||||||
|
>
|
||||||
|
<ng-icon [name]="group.icon" size="1.5rem"></ng-icon>
|
||||||
|
<div>
|
||||||
|
{{ group.orderType }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Items from all orders in this (orderType + branch) group -->
|
||||||
|
@for (item of branchGroup.allItems; track item.id) {
|
||||||
|
<checkout-order-confirmation-item-list-item
|
||||||
|
[item]="item"
|
||||||
|
></checkout-order-confirmation-item-list-item>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,9 +11,22 @@ import { toSignal } from '@angular/core/rxjs-interop';
|
|||||||
import { OrderConfirmationAddressesComponent } from './order-confirmation-addresses/order-confirmation-addresses.component';
|
import { OrderConfirmationAddressesComponent } from './order-confirmation-addresses/order-confirmation-addresses.component';
|
||||||
import { OrderConfirmationHeaderComponent } from './order-confirmation-header/order-confirmation-header.component';
|
import { OrderConfirmationHeaderComponent } from './order-confirmation-header/order-confirmation-header.component';
|
||||||
import { OrderConfirmationItemListComponent } from './order-confirmation-item-list/order-confirmation-item-list.component';
|
import { OrderConfirmationItemListComponent } from './order-confirmation-item-list/order-confirmation-item-list.component';
|
||||||
|
import { OrderConfirmationItemListItemComponent } from './order-confirmation-item-list/order-confirmation-item-list-item/order-confirmation-item-list-item.component';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { TabService } from '@isa/core/tabs';
|
import { TabService } from '@isa/core/tabs';
|
||||||
import { OrderConfiramtionStore } from './reward-order-confirmation.store';
|
import { OrderConfiramtionStore } from './reward-order-confirmation.store';
|
||||||
|
import {
|
||||||
|
groupDisplayOrdersByDeliveryType,
|
||||||
|
groupDisplayOrdersByBranch,
|
||||||
|
getOrderTypeIcon,
|
||||||
|
} from '@isa/checkout/data-access';
|
||||||
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||||
|
import {
|
||||||
|
isaDeliveryVersand,
|
||||||
|
isaDeliveryRuecklage2,
|
||||||
|
isaDeliveryRuecklage1,
|
||||||
|
isaDeliveryB2BVersand1,
|
||||||
|
} from '@isa/icons';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'checkout-reward-order-confirmation',
|
selector: 'checkout-reward-order-confirmation',
|
||||||
@@ -24,8 +37,18 @@ import { OrderConfiramtionStore } from './reward-order-confirmation.store';
|
|||||||
OrderConfirmationHeaderComponent,
|
OrderConfirmationHeaderComponent,
|
||||||
OrderConfirmationAddressesComponent,
|
OrderConfirmationAddressesComponent,
|
||||||
OrderConfirmationItemListComponent,
|
OrderConfirmationItemListComponent,
|
||||||
|
OrderConfirmationItemListItemComponent,
|
||||||
|
NgIcon,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
OrderConfiramtionStore,
|
||||||
|
provideIcons({
|
||||||
|
isaDeliveryVersand,
|
||||||
|
isaDeliveryRuecklage2,
|
||||||
|
isaDeliveryRuecklage1,
|
||||||
|
isaDeliveryB2BVersand1,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
providers: [OrderConfiramtionStore],
|
|
||||||
})
|
})
|
||||||
export class RewardOrderConfirmationComponent {
|
export class RewardOrderConfirmationComponent {
|
||||||
#store = inject(OrderConfiramtionStore);
|
#store = inject(OrderConfiramtionStore);
|
||||||
@@ -45,6 +68,26 @@ export class RewardOrderConfirmationComponent {
|
|||||||
|
|
||||||
orders = this.#store.orders;
|
orders = this.#store.orders;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups orders hierarchically by delivery type and branch.
|
||||||
|
* - Primary grouping: By delivery type (Abholung, Rücklage, Versand, etc.)
|
||||||
|
* - Secondary grouping: By branch (only for Abholung and Rücklage)
|
||||||
|
*/
|
||||||
|
groupedOrders = computed(() => {
|
||||||
|
const orders = this.orders();
|
||||||
|
if (!orders || orders.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const byDeliveryType = groupDisplayOrdersByDeliveryType(orders);
|
||||||
|
|
||||||
|
return Array.from(byDeliveryType.entries()).map(([orderType, typeOrders]) => ({
|
||||||
|
orderType,
|
||||||
|
icon: getOrderTypeIcon(orderType),
|
||||||
|
branchGroups: groupDisplayOrdersByBranch(orderType, typeOrders),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const tabId = this.#tabId() || undefined;
|
const tabId = this.#tabId() || undefined;
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
OrderCreationFacade,
|
OrderCreationFacade,
|
||||||
OmsMetadataService,
|
OmsMetadataService,
|
||||||
|
DisplayOrder,
|
||||||
} from '@isa/oms/data-access';
|
} from '@isa/oms/data-access';
|
||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
import type { Order } from '@isa/checkout/data-access';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { isResponseArgs } from '@isa/common/data-access';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Orchestrates checkout completion and order creation.
|
* Orchestrates checkout completion and order creation.
|
||||||
@@ -63,7 +65,7 @@ export class CheckoutCompletionOrchestratorService {
|
|||||||
params: CompleteCrmOrderParams,
|
params: CompleteCrmOrderParams,
|
||||||
tabId?: number,
|
tabId?: number,
|
||||||
abortSignal?: AbortSignal,
|
abortSignal?: AbortSignal,
|
||||||
): Promise<Order[]> {
|
): Promise<DisplayOrder[]> {
|
||||||
this.#logger.info('Starting checkout completion and order creation');
|
this.#logger.info('Starting checkout completion and order creation');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -79,22 +81,84 @@ export class CheckoutCompletionOrchestratorService {
|
|||||||
abortSignal,
|
abortSignal,
|
||||||
);
|
);
|
||||||
|
|
||||||
const orders =
|
let orders: DisplayOrder[] = [];
|
||||||
await this.#orderCreationFacade.createOrdersFromCheckout(checkoutId);
|
|
||||||
|
|
||||||
// Step 2: Update OMS metadata with created orders
|
try {
|
||||||
if (tabId && shoppingCart) {
|
orders =
|
||||||
this.#checkoutMetadataService.addCompletedShoppingCart(
|
await this.#orderCreationFacade.createOrdersFromCheckout(checkoutId);
|
||||||
tabId,
|
} catch (error) {
|
||||||
shoppingCart,
|
/**
|
||||||
);
|
* Fehlerbehandlung für partielle Bestellungserstellung
|
||||||
|
*
|
||||||
|
* Hintergrund:
|
||||||
|
* Das Backend kann HTTP-Fehler zurückgeben (z.B. Validierungswarnungen),
|
||||||
|
* obwohl die Bestellungen erfolgreich erstellt wurden. In solchen Fällen
|
||||||
|
* enthält die Fehlerantwort die erstellten Bestellungen im 'result'-Feld.
|
||||||
|
*
|
||||||
|
* Verhalten:
|
||||||
|
* - Wenn Bestellungen erstellt wurden (orders.length > 0):
|
||||||
|
* → Warnung loggen, aber mit den erstellten Bestellungen fortfahren
|
||||||
|
* - Wenn keine Bestellungen erstellt wurden (orders.length === 0):
|
||||||
|
* → Fehler loggen und Exception werfen
|
||||||
|
*
|
||||||
|
* Dies verhindert Datenverlust bei partiellen Erfolgen und verbessert
|
||||||
|
* die Benutzererfahrung, indem erfolgreiche Bestellungen nicht verworfen werden.
|
||||||
|
*
|
||||||
|
* TODO: Benutzer-Feedback implementieren
|
||||||
|
* Aktuell wird nur geloggt, aber dem Benutzer wird kein visuelles Feedback
|
||||||
|
* über potenzielle Probleme bei der Bestellungserstellung gegeben.
|
||||||
|
* Mögliche Lösungen:
|
||||||
|
* - Toast-Benachrichtigung mit Warnhinweis
|
||||||
|
* - Banner auf der Bestätigungsseite mit Details zu Validierungswarnungen
|
||||||
|
* - Dialog mit Zusammenfassung der Probleme
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
error instanceof HttpErrorResponse &&
|
||||||
|
isResponseArgs<DisplayOrder[]>(error.error)
|
||||||
|
) {
|
||||||
|
const responseArgs = error.error;
|
||||||
|
orders = responseArgs.result;
|
||||||
|
// Wenn Bestellungen erstellt wurden, loggen wir eine Warnung aber fahren fort
|
||||||
|
if (orders.length > 0) {
|
||||||
|
this.#logger.warn(
|
||||||
|
'Order creation encountered issues but returned partial results',
|
||||||
|
() => ({
|
||||||
|
createdOrderCount: orders.length,
|
||||||
|
errorMessage: responseArgs.message,
|
||||||
|
invalidProperties: responseArgs.invalidProperties,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.#logger.error('Order creation failed with no orders created', {
|
||||||
|
errorMessage: responseArgs.message,
|
||||||
|
invalidProperties: responseArgs.invalidProperties,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.#logger.error('Order creation failed', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (tabId && orders.length > 0 && shoppingCart) {
|
|
||||||
this.#omsMetadataService.addDisplayOrders(
|
if (tabId) {
|
||||||
tabId,
|
// Step 2: Update OMS metadata with created orders
|
||||||
orders,
|
if (shoppingCart) {
|
||||||
shoppingCart.id,
|
this.#checkoutMetadataService.addCompletedShoppingCart(
|
||||||
);
|
tabId,
|
||||||
|
shoppingCart,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (orders.length > 0 && shoppingCart?.id) {
|
||||||
|
this.#omsMetadataService.addDisplayOrders(
|
||||||
|
tabId,
|
||||||
|
orders,
|
||||||
|
shoppingCart.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Cleanup the reward shopping cart
|
||||||
|
this.#checkoutMetadataService.setRewardShoppingCartId(tabId, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#logger.info(
|
this.#logger.info(
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './create-esc-abort-controller.helper';
|
export * from './create-esc-abort-controller.helper';
|
||||||
export * from './zod-error.helper';
|
export * from './is-response-args.helper';
|
||||||
|
export * from './zod-error.helper';
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ResponseArgs } from '../models';
|
||||||
|
|
||||||
|
export function isResponseArgs<T>(args: any): args is ResponseArgs<T> {
|
||||||
|
return (
|
||||||
|
args &&
|
||||||
|
typeof args === 'object' &&
|
||||||
|
'error' in args &&
|
||||||
|
'invalidProperties' in args &&
|
||||||
|
'message' in args
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user