diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..c004e356d --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v22.20.0 diff --git a/libs/checkout/data-access/src/lib/helpers/group-display-orders-by-branch.helper.spec.ts b/libs/checkout/data-access/src/lib/helpers/group-display-orders-by-branch.helper.spec.ts new file mode 100644 index 000000000..364179394 --- /dev/null +++ b/libs/checkout/data-access/src/lib/helpers/group-display-orders-by-branch.helper.spec.ts @@ -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); + }); +}); diff --git a/libs/checkout/data-access/src/lib/helpers/group-display-orders-by-branch.helper.ts b/libs/checkout/data-access/src/lib/helpers/group-display-orders-by-branch.helper.ts new file mode 100644 index 000000000..8f81cbeda --- /dev/null +++ b/libs/checkout/data-access/src/lib/helpers/group-display-orders-by-branch.helper.ts @@ -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()); + + // 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, + }; + }, + ); +} diff --git a/libs/checkout/data-access/src/lib/helpers/group-display-orders-by-delivery-type.helper.spec.ts b/libs/checkout/data-access/src/lib/helpers/group-display-orders-by-delivery-type.helper.spec.ts new file mode 100644 index 000000000..2ddee5404 --- /dev/null +++ b/libs/checkout/data-access/src/lib/helpers/group-display-orders-by-delivery-type.helper.spec.ts @@ -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); + }); +}); diff --git a/libs/checkout/data-access/src/lib/helpers/group-display-orders-by-delivery-type.helper.ts b/libs/checkout/data-access/src/lib/helpers/group-display-orders-by-delivery-type.helper.ts new file mode 100644 index 000000000..432a9fba4 --- /dev/null +++ b/libs/checkout/data-access/src/lib/helpers/group-display-orders-by-delivery-type.helper.ts @@ -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 { + 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()); +} diff --git a/libs/checkout/data-access/src/lib/helpers/index.ts b/libs/checkout/data-access/src/lib/helpers/index.ts index 62c12ad09..bb26cd288 100644 --- a/libs/checkout/data-access/src/lib/helpers/index.ts +++ b/libs/checkout/data-access/src/lib/helpers/index.ts @@ -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'; diff --git a/libs/checkout/feature/reward-order-confirmation/src/lib/order-confirmation-addresses/order-confirmation-addresses.component.html b/libs/checkout/feature/reward-order-confirmation/src/lib/order-confirmation-addresses/order-confirmation-addresses.component.html index ca6a7e199..004fea6db 100644 --- a/libs/checkout/feature/reward-order-confirmation/src/lib/order-confirmation-addresses/order-confirmation-addresses.component.html +++ b/libs/checkout/feature/reward-order-confirmation/src/lib/order-confirmation-addresses/order-confirmation-addresses.component.html @@ -29,18 +29,4 @@ } } } - @if (hasTargetBranchFeature()) { - @for (branch of targetBranches(); track branch) { -
-

Abholfiliale

-
- {{ branch.name }} -
- -
- } - } diff --git a/libs/checkout/feature/reward-order-confirmation/src/lib/order-confirmation-item-list/order-confirmation-item-list-item/order-confirmation-item-list-item.component.ts b/libs/checkout/feature/reward-order-confirmation/src/lib/order-confirmation-item-list/order-confirmation-item-list-item/order-confirmation-item-list-item.component.ts index 64efe7793..29557d304 100644 --- a/libs/checkout/feature/reward-order-confirmation/src/lib/order-confirmation-item-list/order-confirmation-item-list-item/order-confirmation-item-list-item.component.ts +++ b/libs/checkout/feature/reward-order-confirmation/src/lib/order-confirmation-item-list/order-confirmation-item-list-item/order-confirmation-item-list-item.component.ts @@ -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, + }; }); } diff --git a/libs/checkout/feature/reward-order-confirmation/src/lib/reward-order-confirmation.component.html b/libs/checkout/feature/reward-order-confirmation/src/lib/reward-order-confirmation.component.html index 293f8ead6..591f3362e 100644 --- a/libs/checkout/feature/reward-order-confirmation/src/lib/reward-order-confirmation.component.html +++ b/libs/checkout/feature/reward-order-confirmation/src/lib/reward-order-confirmation.component.html @@ -4,9 +4,44 @@
- @for (order of orders(); track order.id) { - + + @for (group of groupedOrders(); track group.orderType) { + +
+ @for (branchGroup of group.branchGroups; track branchGroup.branchId ?? 0) { + + @if (branchGroup.branchName && group.branchGroups.length > 1) { +
+ +
+ {{ group.orderType }} - {{ branchGroup.branchName }} +
+
+ } @else { + +
+ +
+ {{ group.orderType }} +
+
+ } + + + @for (item of branchGroup.allItems; track item.id) { + + } + } +
} diff --git a/libs/checkout/feature/reward-order-confirmation/src/lib/reward-order-confirmation.component.ts b/libs/checkout/feature/reward-order-confirmation/src/lib/reward-order-confirmation.component.ts index c81e04104..09daf8604 100644 --- a/libs/checkout/feature/reward-order-confirmation/src/lib/reward-order-confirmation.component.ts +++ b/libs/checkout/feature/reward-order-confirmation/src/lib/reward-order-confirmation.component.ts @@ -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; diff --git a/libs/checkout/feature/reward-shopping-cart/src/lib/services/checkout-completion-orchestrator.service.ts b/libs/checkout/feature/reward-shopping-cart/src/lib/services/checkout-completion-orchestrator.service.ts index 298228bd0..b507deb9a 100644 --- a/libs/checkout/feature/reward-shopping-cart/src/lib/services/checkout-completion-orchestrator.service.ts +++ b/libs/checkout/feature/reward-shopping-cart/src/lib/services/checkout-completion-orchestrator.service.ts @@ -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 { + ): Promise { 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(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( diff --git a/libs/common/data-access/src/lib/helpers/index.ts b/libs/common/data-access/src/lib/helpers/index.ts index 7aeb00952..04b696a56 100644 --- a/libs/common/data-access/src/lib/helpers/index.ts +++ b/libs/common/data-access/src/lib/helpers/index.ts @@ -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'; diff --git a/libs/common/data-access/src/lib/helpers/is-response-args.helper.ts b/libs/common/data-access/src/lib/helpers/is-response-args.helper.ts new file mode 100644 index 000000000..8832ad058 --- /dev/null +++ b/libs/common/data-access/src/lib/helpers/is-response-args.helper.ts @@ -0,0 +1,11 @@ +import { ResponseArgs } from '../models'; + +export function isResponseArgs(args: any): args is ResponseArgs { + return ( + args && + typeof args === 'object' && + 'error' in args && + 'invalidProperties' in args && + 'message' in args + ); +}