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:
Lorenz Hilpert
2025-10-23 14:04:31 +00:00
committed by Nino Righi
parent 4a0fbf010b
commit 1c5bc8de12
13 changed files with 784 additions and 39 deletions

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v22.20.0

View File

@@ -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);
});
});

View File

@@ -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,
};
},
);
}

View File

@@ -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);
});
});

View File

@@ -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[]>());
}

View File

@@ -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';

View File

@@ -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>

View File

@@ -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,
};
}); });
} }

View File

@@ -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>

View File

@@ -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;

View File

@@ -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(

View File

@@ -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';

View File

@@ -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
);
}