From 215cceb1c479b911621d8fcc00563a1323d93efe Mon Sep 17 00:00:00 2001 From: Nino Date: Mon, 27 Oct 2025 17:56:48 +0100 Subject: [PATCH] feature(purchase-options, reward-dialog, reward-popup, checkout-data-access): replace useRedemptionPoints flag with p4mAccountId Replace the boolean useRedemptionPoints flag with explicit p4mAccountId string parameter throughout the reward selection and purchase options flow. This provides better traceability and explicit account identification for P4M loyalty point redemptions. Changes: - Update PurchaseOptionsModalData to use p4mAccountId instead of useRedemptionPoints - Pass p4mAccountId through purchase options store and selectors - Set loyalty.code with p4mAccountId when creating/updating cart items - Update RewardSelectionService to compute p4mAccountId from customer attributes - Add getCustomerP4mAccountId helper function with unit tests - Update getPrimaryBonusCard to sort alphabetically by code when multiple primary cards exist - Add SelectedCustomerResource provider to required modules - Update all component templates and service calls to use p4mAccountId - Enhance reward selection dialog to require and use p4mAccountId - Update README documentation with new parameter usage The p4mAccountId is now extracted from customer attributes using the key 'p4mAccountId' and passed explicitly through the entire redemption flow, replacing the implicit boolean flag approach. Ref: #5407 --- .../src/modal/purchase-options/README.md | 8 +- .../purchase-options-list-item.component.ts | 6 +- .../purchase-options-modal.data.ts | 10 +-- .../purchase-options-modal.service.ts | 2 +- .../store/purchase-options.selectors.ts | 4 +- .../store/purchase-options.state.ts | 2 +- .../store/purchase-options.store.ts | 52 +++++++---- .../article-details/article-details.module.ts | 2 + .../customer-search/customer-search.module.ts | 2 + .../entity-dtocontainer-of-attribute-dto.ts | 3 +- .../lib/facades/reward-selection.facade.ts | 3 + .../services/shopping-cart.service.spec.ts | 10 +++ .../src/lib/services/shopping-cart.service.ts | 10 ++- .../reward-action/reward-action.component.ts | 20 ++++- .../reward-shopping-cart-item.component.ts | 23 ++++- .../shared/reward-selection-dialog/README.md | 1 + .../reward-selection-actions.component.ts | 1 + .../reward-selection-dialog.component.ts | 1 + .../service/reward-selection.service.ts | 21 ++++- .../reward-selection-trigger.component.ts | 2 + ...get-customer-p4m-account-id.helper.spec.ts | 88 +++++++++++++++++++ .../get-customer-p4m-account-id.helper.ts | 18 ++++ .../get-primary-bonus-card.helper.spec.ts | 25 +++--- .../helpers/get-primary-bonus-card.helper.ts | 21 ++++- libs/crm/data-access/src/lib/helpers/index.ts | 1 + .../src/lib/schemas/attribute.schema.ts | 2 + 26 files changed, 281 insertions(+), 57 deletions(-) create mode 100644 libs/crm/data-access/src/lib/helpers/get-customer-p4m-account-id.helper.spec.ts create mode 100644 libs/crm/data-access/src/lib/helpers/get-customer-p4m-account-id.helper.ts diff --git a/apps/isa-app/src/modal/purchase-options/README.md b/apps/isa-app/src/modal/purchase-options/README.md index 37a82f245..be59bdad1 100644 --- a/apps/isa-app/src/modal/purchase-options/README.md +++ b/apps/isa-app/src/modal/purchase-options/README.md @@ -111,7 +111,7 @@ const modalRef = await this.purchaseOptionsModal.open({ shoppingCartId: 456, type: 'add', items: [rewardItem], - useRedemptionPoints: true, + p4mAccountId: 'abc-123-uuid', preSelectOption: { option: 'in-store' }, disabledPurchaseOptions: ['b2b-delivery'], // Common for rewards }); @@ -132,8 +132,8 @@ interface PurchaseOptionsModalData { /** Action type: 'add' = new items, 'update' = existing items */ type: 'add' | 'update'; - /** Enable redemption points mode */ - useRedemptionPoints?: boolean; + /** P4M account ID for loyalty point redemption */ + p4mAccountId?: string; /** Items to show in the modal */ items: Array; @@ -234,7 +234,7 @@ await modalService.open({ shoppingCartId: rewardCartId, type: 'add', items: rewardItems, - useRedemptionPoints: true, + p4mAccountId: 'customer-p4m-uuid', preSelectOption: { option: 'in-store' }, disabledPurchaseOptions: ['b2b-delivery'], }); diff --git a/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.ts b/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.ts index ab6f61d40..f906df821 100644 --- a/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.ts +++ b/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.ts @@ -99,7 +99,7 @@ export class PurchaseOptionsListItemComponent return item.redemptionPoints; }); - showRedemptionPoints = toSignal(this._store.useRedemptionPoints$); + showRedemptionPoints = toSignal(this._store.p4mAccountId$, { initialValue: undefined }); quantityFormControl = new FormControl(null); @@ -270,7 +270,7 @@ export class PurchaseOptionsListItemComponent return undefined; } - useRedemptionPoints = toSignal(this._store.useRedemptionPoints$); + p4mAccountId = toSignal(this._store.p4mAccountId$); purchaseOption = toSignal(this._store.purchaseOption$); @@ -280,7 +280,7 @@ export class PurchaseOptionsListItemComponent showLowStockMessage = computed(() => { return ( - this.useRedemptionPoints() && + !!this.p4mAccountId() && this.isReservePurchaseOption() && (!this.availability() || this.availability().inStock < 2) ); diff --git a/apps/isa-app/src/modal/purchase-options/purchase-options-modal.data.ts b/apps/isa-app/src/modal/purchase-options/purchase-options-modal.data.ts index 8739d4575..347a6cf3e 100644 --- a/apps/isa-app/src/modal/purchase-options/purchase-options-modal.data.ts +++ b/apps/isa-app/src/modal/purchase-options/purchase-options-modal.data.ts @@ -45,10 +45,10 @@ export interface PurchaseOptionsModalData { type: ActionType; /** - * Enable redemption points mode for reward items. - * When true, prices are set to 0 and loyalty points are applied. + * P4M account ID for loyalty point redemption. + * When set (typically a UUID string), prices are set to 0 and loyalty points with this account ID are applied. */ - useRedemptionPoints?: boolean; + p4mAccountId?: string; /** Items to display in the modal for purchase option selection */ items: Array; @@ -101,8 +101,8 @@ export interface PurchaseOptionsModalContext { */ type: ActionType; - /** Enable redemption points mode for reward items */ - useRedemptionPoints: boolean; + /** P4M account ID for loyalty point redemption */ + p4mAccountId?: string; /** Items to display in the modal for purchase option selection */ items: Array; diff --git a/apps/isa-app/src/modal/purchase-options/purchase-options-modal.service.ts b/apps/isa-app/src/modal/purchase-options/purchase-options-modal.service.ts index dfb8507f1..739c29ee2 100644 --- a/apps/isa-app/src/modal/purchase-options/purchase-options-modal.service.ts +++ b/apps/isa-app/src/modal/purchase-options/purchase-options-modal.service.ts @@ -66,7 +66,7 @@ export class PurchaseOptionsModalService { data: PurchaseOptionsModalData, ): Promise> { const context: PurchaseOptionsModalContext = { - useRedemptionPoints: !!data.useRedemptionPoints, + p4mAccountId: data.p4mAccountId, ...data, }; diff --git a/apps/isa-app/src/modal/purchase-options/store/purchase-options.selectors.ts b/apps/isa-app/src/modal/purchase-options/store/purchase-options.selectors.ts index fe99dfe83..ecc62b895 100644 --- a/apps/isa-app/src/modal/purchase-options/store/purchase-options.selectors.ts +++ b/apps/isa-app/src/modal/purchase-options/store/purchase-options.selectors.ts @@ -20,8 +20,8 @@ export function getType(state: PurchaseOptionsState): ActionType { return state.type; } -export function getUseRedemptionPoints(state: PurchaseOptionsState): boolean { - return state.useRedemptionPoints; +export function getP4mAccountId(state: PurchaseOptionsState): string | undefined { + return state.p4mAccountId; } export function getShoppingCartId(state: PurchaseOptionsState): number { diff --git a/apps/isa-app/src/modal/purchase-options/store/purchase-options.state.ts b/apps/isa-app/src/modal/purchase-options/store/purchase-options.state.ts index ae5bbf3f7..62ce53bb9 100644 --- a/apps/isa-app/src/modal/purchase-options/store/purchase-options.state.ts +++ b/apps/isa-app/src/modal/purchase-options/store/purchase-options.state.ts @@ -36,7 +36,7 @@ export interface PurchaseOptionsState { fetchingAvailabilities: Array; - useRedemptionPoints: boolean; + p4mAccountId?: string; disabledPurchaseOptions: PurchaseOption[]; } diff --git a/apps/isa-app/src/modal/purchase-options/store/purchase-options.store.ts b/apps/isa-app/src/modal/purchase-options/store/purchase-options.store.ts index e02d37263..e705051de 100644 --- a/apps/isa-app/src/modal/purchase-options/store/purchase-options.store.ts +++ b/apps/isa-app/src/modal/purchase-options/store/purchase-options.store.ts @@ -50,11 +50,11 @@ export class PurchaseOptionsStore extends ComponentStore { type$ = this.select(Selectors.getType); - get useRedemptionPoints() { - return this.get(Selectors.getUseRedemptionPoints); + get p4mAccountId() { + return this.get(Selectors.getP4mAccountId); } - useRedemptionPoints$ = this.select(Selectors.getUseRedemptionPoints); + p4mAccountId$ = this.select(Selectors.getP4mAccountId); get shoppingCartId() { return this.get(Selectors.getShoppingCartId); @@ -184,7 +184,7 @@ export class PurchaseOptionsStore extends ComponentStore { canAddResults: [], customerFeatures: {}, fetchingAvailabilities: [], - useRedemptionPoints: false, + p4mAccountId: undefined, disabledPurchaseOptions: [], }); } @@ -217,7 +217,7 @@ export class PurchaseOptionsStore extends ComponentStore { type, inStoreBranch, pickupBranch, - useRedemptionPoints: showRedemptionPoints, + p4mAccountId, disabledPurchaseOptions, }: PurchaseOptionsModalContext) { const defaultBranch = await this._service.fetchDefaultBranch().toPromise(); @@ -234,7 +234,7 @@ export class PurchaseOptionsStore extends ComponentStore { this.patchState({ type: type, shoppingCartId, - useRedemptionPoints: showRedemptionPoints, + p4mAccountId, items: items.map((item) => ({ ...item, quantity: item['quantity'] ?? 1, @@ -314,11 +314,17 @@ export class PurchaseOptionsStore extends ComponentStore { const itemData = mapToItemData(item, this.type); - if ((purchaseOption === 'in-store' || purchaseOption === undefined) && !this.isOptionDisabled('in-store')) { + if ( + (purchaseOption === 'in-store' || purchaseOption === undefined) && + !this.isOptionDisabled('in-store') + ) { promises.push(this._loadInStoreAvailability(itemData)); } - if ((purchaseOption === 'pickup' || purchaseOption === undefined) && !this.isOptionDisabled('pickup')) { + if ( + (purchaseOption === 'pickup' || purchaseOption === undefined) && + !this.isOptionDisabled('pickup') + ) { promises.push(this._loadPickupAvailability(itemData)); } @@ -1066,15 +1072,21 @@ export class PurchaseOptionsStore extends ComponentStore { let loyalty: Loyalty | undefined = undefined; const redemptionPoints: number | null = item.redemptionPoints || null; - // "Lesepunkte einlösen" logic - // If "Lesepunkte einlösen" is checked and item has redemption points, set price to 0 and remove promotion - if (this.useRedemptionPoints) { + // P4M loyalty point redemption logic + // If p4mAccountId is set, set price to 0 and apply loyalty + if (this.p4mAccountId) { // If loyalty is set, we need to remove promotion promotion = undefined; - // Set loyalty points from item - loyalty = { value: redemptionPoints }; + // Set loyalty points from item with P4M account ID + loyalty = { value: redemptionPoints, code: this.p4mAccountId }; // Set price to 0 - price.value.value = 0; + price.value = { + value: 0, + currency: price.value.currency ?? 'EUR', + currencySymbol: price.value.currencySymbol ?? '€', + }; + // Set VAT to undefined for loyalty redemption + price.vat = undefined; } let destination: EntityDTOContainerOfDestinationDTO; @@ -1118,10 +1130,16 @@ export class PurchaseOptionsStore extends ComponentStore { purchaseOption, ); - // If loyalty points is set we know it is a redemption item + // If P4M account ID is set we know it is a loyalty redemption item // we need to make sure we don't update the price - if (this.useRedemptionPoints) { - price.value.value = 0; + if (this.p4mAccountId) { + price.value = { + value: 0, + currency: price.value.currency ?? 'EUR', + currencySymbol: price.value.currencySymbol ?? '€', + }; + // Set VAT to undefined for loyalty redemption + price.vat = undefined; } let destination: EntityDTOContainerOfDestinationDTO; diff --git a/apps/isa-app/src/page/catalog/article-details/article-details.module.ts b/apps/isa-app/src/page/catalog/article-details/article-details.module.ts index 6b26a29e8..b843b8f7c 100644 --- a/apps/isa-app/src/page/catalog/article-details/article-details.module.ts +++ b/apps/isa-app/src/page/catalog/article-details/article-details.module.ts @@ -23,6 +23,7 @@ import { RewardSelectionService, RewardSelectionPopUpService, } from '@isa/checkout/shared/reward-selection-dialog'; +import { SelectedCustomerResource } from '@isa/crm/data-access'; @NgModule({ imports: [ @@ -46,6 +47,7 @@ import { providers: [ SelectedShoppingCartResource, SelectedRewardShoppingCartResource, + SelectedCustomerResource, RewardSelectionService, RewardSelectionPopUpService, ], diff --git a/apps/isa-app/src/page/customer/customer-search/customer-search.module.ts b/apps/isa-app/src/page/customer/customer-search/customer-search.module.ts index 9434910a3..1aa8b795a 100644 --- a/apps/isa-app/src/page/customer/customer-search/customer-search.module.ts +++ b/apps/isa-app/src/page/customer/customer-search/customer-search.module.ts @@ -20,6 +20,7 @@ import { RewardSelectionService, RewardSelectionPopUpService, } from '@isa/checkout/shared/reward-selection-dialog'; +import { SelectedCustomerResource } from '@isa/crm/data-access'; @NgModule({ imports: [ @@ -40,6 +41,7 @@ import { providers: [ SelectedShoppingCartResource, SelectedRewardShoppingCartResource, + SelectedCustomerResource, RewardSelectionService, RewardSelectionPopUpService, ], diff --git a/generated/swagger/crm-api/src/models/entity-dtocontainer-of-attribute-dto.ts b/generated/swagger/crm-api/src/models/entity-dtocontainer-of-attribute-dto.ts index cd4dcaf1d..cef4dee55 100644 --- a/generated/swagger/crm-api/src/models/entity-dtocontainer-of-attribute-dto.ts +++ b/generated/swagger/crm-api/src/models/entity-dtocontainer-of-attribute-dto.ts @@ -1,6 +1,7 @@ /* tslint:disable */ import { EntityDTOReferenceContainer } from './entity-dtoreference-container'; import { AttributeDTO } from './attribute-dto'; -export interface EntityDTOContainerOfAttributeDTO extends EntityDTOReferenceContainer { +export interface EntityDTOContainerOfAttributeDTO + extends EntityDTOReferenceContainer { data?: AttributeDTO; } diff --git a/libs/checkout/data-access/src/lib/facades/reward-selection.facade.ts b/libs/checkout/data-access/src/lib/facades/reward-selection.facade.ts index 3cd2a80cd..1ce6d2a1c 100644 --- a/libs/checkout/data-access/src/lib/facades/reward-selection.facade.ts +++ b/libs/checkout/data-access/src/lib/facades/reward-selection.facade.ts @@ -9,13 +9,16 @@ export class RewardSelectionFacade { completeRewardSelection({ tabId, rewardSelectionItems, + p4mAccountId, }: { tabId: number; rewardSelectionItems: RewardSelectionItem[]; + p4mAccountId: string; }) { return this.#shoppingCartService.completeRewardSelection({ tabId, rewardSelectionItems, + p4mAccountId, }); } } diff --git a/libs/checkout/data-access/src/lib/services/shopping-cart.service.spec.ts b/libs/checkout/data-access/src/lib/services/shopping-cart.service.spec.ts index fa2bea5d1..c039f4dcf 100644 --- a/libs/checkout/data-access/src/lib/services/shopping-cart.service.spec.ts +++ b/libs/checkout/data-access/src/lib/services/shopping-cart.service.spec.ts @@ -81,6 +81,7 @@ describe('ShoppingCartService', () => { it('should add item to regular cart when cartQuantity > 0', async () => { // Arrange const tabId = 1; + const p4mAccountId = 'P4M-12345'; const rewardSelectionItem = createMockRewardSelectionItem(2, 0); mockCheckoutMetadataService.getShoppingCartId.mockReturnValue(123); @@ -98,6 +99,7 @@ describe('ShoppingCartService', () => { await service.completeRewardSelection({ tabId, rewardSelectionItems: [rewardSelectionItem], + p4mAccountId, }); // Assert @@ -112,6 +114,7 @@ describe('ShoppingCartService', () => { it('should add item to reward cart when rewardCartQuantity > 0', async () => { // Arrange const tabId = 1; + const p4mAccountId = 'P4M-12345'; const rewardSelectionItem = createMockRewardSelectionItem(0, 3); mockCheckoutMetadataService.getShoppingCartId.mockReturnValue(123); @@ -129,6 +132,7 @@ describe('ShoppingCartService', () => { await service.completeRewardSelection({ tabId, rewardSelectionItems: [rewardSelectionItem], + p4mAccountId, }); // Assert @@ -147,6 +151,7 @@ describe('ShoppingCartService', () => { it('should update item quantity in regular cart', async () => { // Arrange const tabId = 1; + const p4mAccountId = 'P4M-12345'; const existingItem = createMockShoppingCartItem(); const rewardSelectionItem = createMockRewardSelectionItem(5, 0); @@ -167,6 +172,7 @@ describe('ShoppingCartService', () => { await service.completeRewardSelection({ tabId, rewardSelectionItems: [rewardSelectionItem], + p4mAccountId, }); // Assert @@ -182,6 +188,7 @@ describe('ShoppingCartService', () => { it('should remove item from cart when quantity is 0', async () => { // Arrange const tabId = 1; + const p4mAccountId = 'P4M-12345'; const existingItem = createMockShoppingCartItem(); const rewardSelectionItem = createMockRewardSelectionItem(0, 0); @@ -202,6 +209,7 @@ describe('ShoppingCartService', () => { await service.completeRewardSelection({ tabId, rewardSelectionItems: [rewardSelectionItem], + p4mAccountId, }); // Assert @@ -217,6 +225,7 @@ describe('ShoppingCartService', () => { it('should create shopping cart if not exists', async () => { // Arrange const tabId = 1; + const p4mAccountId = 'P4M-12345'; const rewardSelectionItem = createMockRewardSelectionItem(1, 0); mockCheckoutMetadataService.getShoppingCartId.mockReturnValue(null); @@ -238,6 +247,7 @@ describe('ShoppingCartService', () => { await service.completeRewardSelection({ tabId, rewardSelectionItems: [rewardSelectionItem], + p4mAccountId, }); // Assert diff --git a/libs/checkout/data-access/src/lib/services/shopping-cart.service.ts b/libs/checkout/data-access/src/lib/services/shopping-cart.service.ts index 669cdd9ce..0d508e362 100644 --- a/libs/checkout/data-access/src/lib/services/shopping-cart.service.ts +++ b/libs/checkout/data-access/src/lib/services/shopping-cart.service.ts @@ -171,9 +171,11 @@ export class ShoppingCartService { async completeRewardSelection({ tabId, rewardSelectionItems, + p4mAccountId, }: { tabId: number; rewardSelectionItems: RewardSelectionItem[]; + p4mAccountId: string; }) { // Fetch or create both shopping cart IDs const shoppingCartId = await this.#getOrCreateShoppingCartId(tabId); @@ -213,6 +215,7 @@ export class ShoppingCartService { itemId: currentInRewardCart?.id, currentRewardCartItem: currentInRewardCart, rewardSelectionItem: selectionItem, + p4mAccountId, }); } } @@ -294,11 +297,13 @@ export class ShoppingCartService { itemId, currentRewardCartItem, rewardSelectionItem, + p4mAccountId, }: { rewardShoppingCartId: number; itemId: number | undefined; currentRewardCartItem: ShoppingCartItem | undefined; rewardSelectionItem: RewardSelectionItem; + p4mAccountId: string; }) { const desiredRewardCartQuantity = rewardSelectionItem.rewardCartQuantity; if (currentRewardCartItem && itemId) { @@ -349,7 +354,10 @@ export class ShoppingCartService { }, quantity: desiredRewardCartQuantity, promotion: undefined, // If loyalty is set, we need to remove promotion - loyalty: { value: rewardSelectionItem.catalogRewardPoints }, // Set loyalty points from item + loyalty: { + value: rewardSelectionItem.catalogRewardPoints, + code: p4mAccountId, + }, // Set loyalty points from item availability: { ...rewardSelectionItem.item.availability, price: { diff --git a/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.ts b/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.ts index accee3a6a..2ed2dcd5c 100644 --- a/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.ts +++ b/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.ts @@ -15,7 +15,11 @@ import { PurchaseOptionsModalService } from '@modal/purchase-options'; import { firstValueFrom } from 'rxjs'; import { Router } from '@angular/router'; import { getRouteToCustomer } from '../helpers'; -import { PrimaryCustomerCardResource } from '@isa/crm/data-access'; +import { + getCustomerP4mAccountId, + PrimaryCustomerCardResource, + SelectedCustomerResource, +} from '@isa/crm/data-access'; import { NavigationStateService } from '@isa/core/navigation'; @Component({ @@ -34,8 +38,11 @@ export class RewardActionComponent { #purchasingOptionsModal = inject(PurchaseOptionsModalService); #shoppingCartFacade = inject(ShoppingCartFacade); #checkoutMetadataService = inject(CheckoutMetadataService); + #customerResource = inject(SelectedCustomerResource).resource; #primaryBonudCardResource = inject(PrimaryCustomerCardResource); + readonly customerValue = this.#customerResource.value.asReadonly(); + readonly primaryCustomerCardValue = this.#primaryBonudCardResource.primaryCustomerCard; @@ -50,14 +57,21 @@ export class RewardActionComponent { disableSelectRewardButton = computed( () => !this.hasSelectedItems() || + !this.p4mAccountId() || (!!this.primaryCustomerCardValue() && this.points() <= 0), ); + p4mAccountId = computed(() => { + const customer = this.customerValue(); + return getCustomerP4mAccountId(customer?.attributes); + }); + async continueToPurchasingOptions() { const tabId = this.#tabId(); + const p4mAccountId = this.p4mAccountId(); const items = Object.values(this.selectedItems() || {}); - if (!items?.length || !tabId) { + if (!items?.length || !tabId || !p4mAccountId) { return; } @@ -73,7 +87,7 @@ export class RewardActionComponent { tabId, shoppingCartId: rewardShoppingCartId, items, - useRedemptionPoints: true, + p4mAccountId, preSelectOption: { option: 'in-store' }, disabledPurchaseOptions: ['b2b-delivery'], hideDisabledPurchaseOptions: true, diff --git a/libs/checkout/feature/reward-shopping-cart/src/lib/reward-shopping-cart-item/reward-shopping-cart-item.component.ts b/libs/checkout/feature/reward-shopping-cart/src/lib/reward-shopping-cart-item/reward-shopping-cart-item.component.ts index 80e4ed2e4..3dd8f3ba9 100644 --- a/libs/checkout/feature/reward-shopping-cart/src/lib/reward-shopping-cart-item/reward-shopping-cart-item.component.ts +++ b/libs/checkout/feature/reward-shopping-cart/src/lib/reward-shopping-cart-item/reward-shopping-cart-item.component.ts @@ -23,6 +23,10 @@ import { RewardShoppingCartItemQuantityControlComponent } from './reward-shoppin import { RewardShoppingCartItemRemoveButtonComponent } from './reward-shopping-cart-item-remove-button.component'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { isaOtherInfo } from '@isa/icons'; +import { + getCustomerP4mAccountId, + SelectedCustomerResource, +} from '@isa/crm/data-access'; // TODO: [Next Sprint - Medium Priority] Create test file // - Test component creation and item input binding @@ -53,6 +57,10 @@ export class RewardShoppingCartItemComponent { #purchaseOptionsModalService = inject(PurchaseOptionsModalService); + #customerResource = inject(SelectedCustomerResource).resource; + + readonly customerValue = this.#customerResource.value.asReadonly(); + isBusy = signal(false); isHorizontal = breakpoint([ @@ -67,11 +75,22 @@ export class RewardShoppingCartItemComponent { shoppingCartId = computed(() => this.#rewardShoppingCartResource.value()?.id); + p4mAccountId = computed(() => { + const customer = this.customerValue(); + return getCustomerP4mAccountId(customer?.attributes); + }); + async updatePurchaseOption() { const shoppingCartItemId = this.itemId(); const shoppingCartId = this.shoppingCartId(); + const p4mAccountId = this.p4mAccountId(); - if (this.isBusy() || !shoppingCartId || !shoppingCartItemId) { + if ( + this.isBusy() || + !shoppingCartId || + !shoppingCartItemId || + !p4mAccountId + ) { return; } this.isBusy.set(true); @@ -82,7 +101,7 @@ export class RewardShoppingCartItemComponent { shoppingCartId: this.shoppingCartId()!, tabId: this.#tabId() as unknown as number, type: 'update', - useRedemptionPoints: true, + p4mAccountId, disabledPurchaseOptions: ['b2b-delivery'], hideDisabledPurchaseOptions: true, }); diff --git a/libs/checkout/shared/reward-selection-dialog/README.md b/libs/checkout/shared/reward-selection-dialog/README.md index e0f408282..eab049f5a 100644 --- a/libs/checkout/shared/reward-selection-dialog/README.md +++ b/libs/checkout/shared/reward-selection-dialog/README.md @@ -61,6 +61,7 @@ import { // Required providers SelectedShoppingCartResource, SelectedRewardShoppingCartResource, + SelectedCustomerResource, RewardSelectionService, RewardSelectionPopUpService, ], diff --git a/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/reward-selection-actions/reward-selection-actions.component.ts b/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/reward-selection-actions/reward-selection-actions.component.ts index 42d6abf92..c049723ef 100644 --- a/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/reward-selection-actions/reward-selection-actions.component.ts +++ b/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/reward-selection-actions/reward-selection-actions.component.ts @@ -53,6 +53,7 @@ export class RewardSelectionActionsComponent { await this.#rewardSelectionFacade.completeRewardSelection({ tabId, rewardSelectionItems, + p4mAccountId: this.host.data.p4mAccountId, }); this.completeRewardSelectionLoading.set(false); diff --git a/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/reward-selection-dialog.component.ts b/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/reward-selection-dialog.component.ts index d3787eaea..91d7f3084 100644 --- a/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/reward-selection-dialog.component.ts +++ b/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/reward-selection-dialog.component.ts @@ -12,6 +12,7 @@ import { RewardSelectionStore } from './store/reward-selection-dialog.store'; export type RewardSelectionDialogData = { rewardSelectionItems: RewardSelectionItem[]; customerRewardPoints: number; + p4mAccountId: string; closeText: string; }; diff --git a/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/service/reward-selection.service.ts b/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/service/reward-selection.service.ts index de6c9221d..d78e8f75d 100644 --- a/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/service/reward-selection.service.ts +++ b/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/service/reward-selection.service.ts @@ -13,7 +13,11 @@ import { RewardSelectionDialogComponent, RewardSelectionDialogResult, } from '../reward-selection-dialog.component'; -import { PrimaryCustomerCardResource } from '@isa/crm/data-access'; +import { + getCustomerP4mAccountId, + PrimaryCustomerCardResource, + SelectedCustomerResource, +} from '@isa/crm/data-access'; import { firstValueFrom } from 'rxjs'; import { toObservable } from '@angular/core/rxjs-interop'; import { filter, first } from 'rxjs/operators'; @@ -26,6 +30,7 @@ import { export class RewardSelectionService { rewardSelectionDialog = injectDialog(RewardSelectionDialogComponent); + #customer = inject(SelectedCustomerResource).resource; #primaryCustomerCardResource = inject(PrimaryCustomerCardResource); #priceAndRedemptionPointsResource = inject(PriceAndRedemptionPointsResource); #shoppingCartResource = inject(SelectedShoppingCartResource).resource; @@ -44,6 +49,13 @@ export class RewardSelectionService { readonly priceAndRedemptionPoints = this.#priceAndRedemptionPointsResource.priceAndRedemptionPoints; + readonly customerValue = this.#customer.value.asReadonly(); + + p4mAccountId = computed(() => { + const customer = this.customerValue(); + return getCustomerP4mAccountId(customer?.attributes); + }); + shoppingCartItems = computed(() => { return ( this.shoppingCartResponseValue() @@ -124,7 +136,10 @@ export class RewardSelectionService { ); canOpen = computed( - () => this.eligibleItems().length > 0 && !!this.primaryBonusCardPoints(), + () => + this.eligibleItems().length > 0 && + !!this.primaryBonusCardPoints() && + !!this.p4mAccountId(), ); constructor() { @@ -149,12 +164,14 @@ export class RewardSelectionService { }: { closeText: string; }): Promise { + const p4mAccountId = this.p4mAccountId()!; const rewardSelectionItems = this.eligibleItems(); const dialogRef = this.rewardSelectionDialog({ title: 'Ein oder mehrere Artikel sind als Prämie verfügbar', data: { rewardSelectionItems, customerRewardPoints: this.primaryBonusCardPoints(), + p4mAccountId, closeText, }, displayClose: true, diff --git a/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/trigger/reward-selection-trigger.component.ts b/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/trigger/reward-selection-trigger.component.ts index e75fffc6c..99362a0e4 100644 --- a/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/trigger/reward-selection-trigger.component.ts +++ b/libs/checkout/shared/reward-selection-dialog/src/lib/reward-selection-dialog/trigger/reward-selection-trigger.component.ts @@ -11,6 +11,7 @@ import { SelectedShoppingCartResource, } from '@isa/checkout/data-access'; import { CheckoutNavigationService } from '@shared/services/navigation'; +import { SelectedCustomerResource } from '@isa/crm/data-access'; @Component({ selector: 'lib-reward-selection-trigger', @@ -21,6 +22,7 @@ import { CheckoutNavigationService } from '@shared/services/navigation'; SelectedShoppingCartResource, SelectedRewardShoppingCartResource, RewardSelectionService, + SelectedCustomerResource, ], }) export class RewardSelectionTriggerComponent { diff --git a/libs/crm/data-access/src/lib/helpers/get-customer-p4m-account-id.helper.spec.ts b/libs/crm/data-access/src/lib/helpers/get-customer-p4m-account-id.helper.spec.ts new file mode 100644 index 000000000..acd3a4b30 --- /dev/null +++ b/libs/crm/data-access/src/lib/helpers/get-customer-p4m-account-id.helper.spec.ts @@ -0,0 +1,88 @@ +import { describe, it, expect } from 'vitest'; +import { getCustomerP4mAccountId } from './get-customer-p4m-account-id.helper'; +import { Attribute } from '../schemas'; + +describe('getCustomerP4mAccountId', () => { + it('should return p4mAccountId when attribute exists', () => { + // Arrange + const attributes = [ + { + data: { + key: 'someOtherKey', + value: 'someValue', + } as Attribute, + }, + { + data: { + key: 'p4mAccountId', + value: '12345', + } as Attribute, + }, + ]; + + // Act + const result = getCustomerP4mAccountId(attributes); + + // Assert + expect(result).toBe('12345'); + }); + + it('should return undefined when p4mAccountId attribute does not exist', () => { + // Arrange + const attributes = [ + { + data: { + key: 'someKey', + value: 'someValue', + } as Attribute, + }, + ]; + + // Act + const result = getCustomerP4mAccountId(attributes); + + // Assert + expect(result).toBeUndefined(); + }); + + it('should return undefined when attributes array is empty', () => { + // Arrange + const attributes: Array<{ data?: Attribute }> = []; + + // Act + const result = getCustomerP4mAccountId(attributes); + + // Assert + expect(result).toBeUndefined(); + }); + + it('should return undefined when attributes is undefined', () => { + // Arrange + const attributes = undefined; + + // Act + const result = getCustomerP4mAccountId(attributes); + + // Assert + expect(result).toBeUndefined(); + }); + + it('should handle null data in containers', () => { + // Arrange + const attributes = [ + { data: null }, + { + data: { + key: 'p4mAccountId', + value: '67890', + } as Attribute, + }, + ]; + + // Act + const result = getCustomerP4mAccountId(attributes); + + // Assert + expect(result).toBe('67890'); + }); +}); diff --git a/libs/crm/data-access/src/lib/helpers/get-customer-p4m-account-id.helper.ts b/libs/crm/data-access/src/lib/helpers/get-customer-p4m-account-id.helper.ts new file mode 100644 index 000000000..6414ff575 --- /dev/null +++ b/libs/crm/data-access/src/lib/helpers/get-customer-p4m-account-id.helper.ts @@ -0,0 +1,18 @@ +import { Attribute } from '../schemas'; + +export const getCustomerP4mAccountId = ( + attributes: + | Array<{ data?: Attribute | null | undefined } | null | undefined> + | undefined, +): string | undefined => { + if (!attributes) { + return undefined; + } + + const p4mAttribute = attributes + .map((container) => container?.data) + .filter((data): data is Attribute => data != null) + .find((attr) => attr.key === 'p4mAccountId'); + + return p4mAttribute?.value; +}; diff --git a/libs/crm/data-access/src/lib/helpers/get-primary-bonus-card.helper.spec.ts b/libs/crm/data-access/src/lib/helpers/get-primary-bonus-card.helper.spec.ts index 63c279166..01b74b905 100644 --- a/libs/crm/data-access/src/lib/helpers/get-primary-bonus-card.helper.spec.ts +++ b/libs/crm/data-access/src/lib/helpers/get-primary-bonus-card.helper.spec.ts @@ -7,6 +7,7 @@ describe('getPrimaryBonusCard', () => { // Arrange const bonusCards: BonusCardInfo[] = [ { + code: 'CARD-B', firstName: 'John', lastName: 'Doe', isActive: true, @@ -14,19 +15,13 @@ describe('getPrimaryBonusCard', () => { totalPoints: 100, } as BonusCardInfo, { + code: 'CARD-A', firstName: 'Jane', lastName: 'Smith', isActive: true, isPrimary: true, totalPoints: 200, } as BonusCardInfo, - { - firstName: 'Bob', - lastName: 'Johnson', - isActive: true, - isPrimary: false, - totalPoints: 50, - } as BonusCardInfo, ]; // Act @@ -35,14 +30,14 @@ describe('getPrimaryBonusCard', () => { // Assert expect(result).toBeDefined(); expect(result?.isPrimary).toBe(true); - expect(result?.firstName).toBe('Jane'); - expect(result?.lastName).toBe('Smith'); + expect(result?.code).toBe('CARD-A'); }); - it('should return undefined when no primary bonus card exists', () => { + it('should return first alphabetically when no primary card exists', () => { // Arrange const bonusCards: BonusCardInfo[] = [ { + code: 'CARD-C', firstName: 'John', lastName: 'Doe', isActive: true, @@ -50,6 +45,7 @@ describe('getPrimaryBonusCard', () => { totalPoints: 100, } as BonusCardInfo, { + code: 'CARD-A', firstName: 'Jane', lastName: 'Smith', isActive: true, @@ -62,7 +58,8 @@ describe('getPrimaryBonusCard', () => { const result = getPrimaryBonusCard(bonusCards); // Assert - expect(result).toBeUndefined(); + expect(result).toBeDefined(); + expect(result?.code).toBe('CARD-A'); }); it('should return undefined when bonus cards array is empty', () => { @@ -76,10 +73,11 @@ describe('getPrimaryBonusCard', () => { expect(result).toBeUndefined(); }); - it('should return the first primary card when multiple primary cards exist', () => { + it('should return first alphabetically when multiple primary cards exist', () => { // Arrange const bonusCards: BonusCardInfo[] = [ { + code: 'CARD-Z', firstName: 'John', lastName: 'Doe', isActive: true, @@ -87,6 +85,7 @@ describe('getPrimaryBonusCard', () => { totalPoints: 100, } as BonusCardInfo, { + code: 'CARD-A', firstName: 'Jane', lastName: 'Smith', isActive: true, @@ -101,6 +100,6 @@ describe('getPrimaryBonusCard', () => { // Assert expect(result).toBeDefined(); expect(result?.isPrimary).toBe(true); - expect(result?.firstName).toBe('John'); + expect(result?.code).toBe('CARD-A'); }); }); diff --git a/libs/crm/data-access/src/lib/helpers/get-primary-bonus-card.helper.ts b/libs/crm/data-access/src/lib/helpers/get-primary-bonus-card.helper.ts index c78461fd7..dbc980255 100644 --- a/libs/crm/data-access/src/lib/helpers/get-primary-bonus-card.helper.ts +++ b/libs/crm/data-access/src/lib/helpers/get-primary-bonus-card.helper.ts @@ -1,5 +1,22 @@ import { BonusCardInfo } from '../models'; -export function getPrimaryBonusCard(bonusCards: BonusCardInfo[]) { - return bonusCards.find((card) => card.isPrimary); +export function getPrimaryBonusCard( + bonusCards: BonusCardInfo[], +): BonusCardInfo | undefined { + if (bonusCards.length === 0) { + return undefined; + } + + // Filter primary cards if any exist + const primaryCards = bonusCards.filter((card) => card.isPrimary); + + // Use primary cards if available, otherwise use all cards + const cardsToSort = primaryCards.length > 0 ? primaryCards : bonusCards; + + // Sort alphabetically by code and return the first one + return cardsToSort.sort((a, b) => { + const codeA = a.code?.toLowerCase() ?? ''; + const codeB = b.code?.toLowerCase() ?? ''; + return codeA.localeCompare(codeB); + })[0]; } diff --git a/libs/crm/data-access/src/lib/helpers/index.ts b/libs/crm/data-access/src/lib/helpers/index.ts index 500743682..92948020f 100644 --- a/libs/crm/data-access/src/lib/helpers/index.ts +++ b/libs/crm/data-access/src/lib/helpers/index.ts @@ -6,3 +6,4 @@ export { } from './deduplicate-addressees.helper'; export * from './get-customer-name.component'; export * from './get-primary-bonus-card.helper'; +export * from './get-customer-p4m-account-id.helper'; diff --git a/libs/crm/data-access/src/lib/schemas/attribute.schema.ts b/libs/crm/data-access/src/lib/schemas/attribute.schema.ts index 856e2a31e..8bcc24ae0 100644 --- a/libs/crm/data-access/src/lib/schemas/attribute.schema.ts +++ b/libs/crm/data-access/src/lib/schemas/attribute.schema.ts @@ -17,3 +17,5 @@ export const AttributeSchema = z.object({ stop: z.string().describe('Stop').optional(), value: z.string().describe('Value').optional(), }); + +export type Attribute = z.infer;