mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 14:32:10 +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 './group-by-branch.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 './merge-reward-selection-items.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>
|
||||
|
||||
@@ -62,13 +62,25 @@ export class OrderConfirmationItemListItemComponent {
|
||||
const shoppingCart = this.#orderConfiramtionStore.shoppingCart();
|
||||
const item = this.item();
|
||||
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?.data?.product?.catalogProductNumber ===
|
||||
item.product?.catalogProductNumber,
|
||||
)?.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>
|
||||
<div class="w-full h-[0.0625rem] bg-[#DEE2E6]"></div>
|
||||
<checkout-order-confirmation-addresses></checkout-order-confirmation-addresses>
|
||||
@for (order of orders(); track order.id) {
|
||||
<checkout-order-confirmation-item-list
|
||||
[order]="order"
|
||||
></checkout-order-confirmation-item-list>
|
||||
|
||||
@for (group of groupedOrders(); track group.orderType) {
|
||||
<!-- Delivery type section -->
|
||||
<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>
|
||||
|
||||
@@ -11,9 +11,22 @@ import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { OrderConfirmationAddressesComponent } from './order-confirmation-addresses/order-confirmation-addresses.component';
|
||||
import { OrderConfirmationHeaderComponent } from './order-confirmation-header/order-confirmation-header.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 { TabService } from '@isa/core/tabs';
|
||||
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({
|
||||
selector: 'checkout-reward-order-confirmation',
|
||||
@@ -24,8 +37,18 @@ import { OrderConfiramtionStore } from './reward-order-confirmation.store';
|
||||
OrderConfirmationHeaderComponent,
|
||||
OrderConfirmationAddressesComponent,
|
||||
OrderConfirmationItemListComponent,
|
||||
OrderConfirmationItemListItemComponent,
|
||||
NgIcon,
|
||||
],
|
||||
providers: [
|
||||
OrderConfiramtionStore,
|
||||
provideIcons({
|
||||
isaDeliveryVersand,
|
||||
isaDeliveryRuecklage2,
|
||||
isaDeliveryRuecklage1,
|
||||
isaDeliveryB2BVersand1,
|
||||
}),
|
||||
],
|
||||
providers: [OrderConfiramtionStore],
|
||||
})
|
||||
export class RewardOrderConfirmationComponent {
|
||||
#store = inject(OrderConfiramtionStore);
|
||||
@@ -45,6 +68,26 @@ export class RewardOrderConfirmationComponent {
|
||||
|
||||
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() {
|
||||
effect(() => {
|
||||
const tabId = this.#tabId() || undefined;
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
import {
|
||||
OrderCreationFacade,
|
||||
OmsMetadataService,
|
||||
DisplayOrder,
|
||||
} from '@isa/oms/data-access';
|
||||
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.
|
||||
@@ -63,7 +65,7 @@ export class CheckoutCompletionOrchestratorService {
|
||||
params: CompleteCrmOrderParams,
|
||||
tabId?: number,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Order[]> {
|
||||
): Promise<DisplayOrder[]> {
|
||||
this.#logger.info('Starting checkout completion and order creation');
|
||||
|
||||
try {
|
||||
@@ -79,22 +81,84 @@ export class CheckoutCompletionOrchestratorService {
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
const orders =
|
||||
await this.#orderCreationFacade.createOrdersFromCheckout(checkoutId);
|
||||
let orders: DisplayOrder[] = [];
|
||||
|
||||
// Step 2: Update OMS metadata with created orders
|
||||
if (tabId && shoppingCart) {
|
||||
this.#checkoutMetadataService.addCompletedShoppingCart(
|
||||
tabId,
|
||||
shoppingCart,
|
||||
);
|
||||
try {
|
||||
orders =
|
||||
await this.#orderCreationFacade.createOrdersFromCheckout(checkoutId);
|
||||
} catch (error) {
|
||||
/**
|
||||
* 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(
|
||||
tabId,
|
||||
orders,
|
||||
shoppingCart.id,
|
||||
);
|
||||
|
||||
if (tabId) {
|
||||
// Step 2: Update OMS metadata with created orders
|
||||
if (shoppingCart) {
|
||||
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(
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './create-esc-abort-controller.helper';
|
||||
export * from './zod-error.helper';
|
||||
export * from './create-esc-abort-controller.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