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
This commit is contained in:
Nino
2025-10-27 17:56:48 +01:00
parent 2d654aa63a
commit 215cceb1c4
26 changed files with 281 additions and 57 deletions

View File

@@ -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<ItemDTO | ShoppingCartItemDTO>;
@@ -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'],
});

View File

@@ -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<number>(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)
);

View File

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

View File

@@ -66,7 +66,7 @@ export class PurchaseOptionsModalService {
data: PurchaseOptionsModalData,
): Promise<UiModalRef<string, PurchaseOptionsModalData>> {
const context: PurchaseOptionsModalContext = {
useRedemptionPoints: !!data.useRedemptionPoints,
p4mAccountId: data.p4mAccountId,
...data,
};

View File

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

View File

@@ -36,7 +36,7 @@ export interface PurchaseOptionsState {
fetchingAvailabilities: Array<FetchingAvailability>;
useRedemptionPoints: boolean;
p4mAccountId?: string;
disabledPurchaseOptions: PurchaseOption[];
}

View File

@@ -50,11 +50,11 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
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<PurchaseOptionsState> {
canAddResults: [],
customerFeatures: {},
fetchingAvailabilities: [],
useRedemptionPoints: false,
p4mAccountId: undefined,
disabledPurchaseOptions: [],
});
}
@@ -217,7 +217,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
type,
inStoreBranch,
pickupBranch,
useRedemptionPoints: showRedemptionPoints,
p4mAccountId,
disabledPurchaseOptions,
}: PurchaseOptionsModalContext) {
const defaultBranch = await this._service.fetchDefaultBranch().toPromise();
@@ -234,7 +234,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
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<PurchaseOptionsState> {
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<PurchaseOptionsState> {
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<PurchaseOptionsState> {
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;

View File

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

View File

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

View File

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

View File

@@ -9,13 +9,16 @@ export class RewardSelectionFacade {
completeRewardSelection({
tabId,
rewardSelectionItems,
p4mAccountId,
}: {
tabId: number;
rewardSelectionItems: RewardSelectionItem[];
p4mAccountId: string;
}) {
return this.#shoppingCartService.completeRewardSelection({
tabId,
rewardSelectionItems,
p4mAccountId,
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,6 +61,7 @@ import {
// Required providers
SelectedShoppingCartResource,
SelectedRewardShoppingCartResource,
SelectedCustomerResource,
RewardSelectionService,
RewardSelectionPopUpService,
],

View File

@@ -53,6 +53,7 @@ export class RewardSelectionActionsComponent {
await this.#rewardSelectionFacade.completeRewardSelection({
tabId,
rewardSelectionItems,
p4mAccountId: this.host.data.p4mAccountId,
});
this.completeRewardSelectionLoading.set(false);

View File

@@ -12,6 +12,7 @@ import { RewardSelectionStore } from './store/reward-selection-dialog.store';
export type RewardSelectionDialogData = {
rewardSelectionItems: RewardSelectionItem[];
customerRewardPoints: number;
p4mAccountId: string;
closeText: string;
};

View File

@@ -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<RewardSelectionDialogResult> {
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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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