mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
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:
@@ -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'],
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -66,7 +66,7 @@ export class PurchaseOptionsModalService {
|
||||
data: PurchaseOptionsModalData,
|
||||
): Promise<UiModalRef<string, PurchaseOptionsModalData>> {
|
||||
const context: PurchaseOptionsModalContext = {
|
||||
useRedemptionPoints: !!data.useRedemptionPoints,
|
||||
p4mAccountId: data.p4mAccountId,
|
||||
...data,
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface PurchaseOptionsState {
|
||||
|
||||
fetchingAvailabilities: Array<FetchingAvailability>;
|
||||
|
||||
useRedemptionPoints: boolean;
|
||||
p4mAccountId?: string;
|
||||
|
||||
disabledPurchaseOptions: PurchaseOption[];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -9,13 +9,16 @@ export class RewardSelectionFacade {
|
||||
completeRewardSelection({
|
||||
tabId,
|
||||
rewardSelectionItems,
|
||||
p4mAccountId,
|
||||
}: {
|
||||
tabId: number;
|
||||
rewardSelectionItems: RewardSelectionItem[];
|
||||
p4mAccountId: string;
|
||||
}) {
|
||||
return this.#shoppingCartService.completeRewardSelection({
|
||||
tabId,
|
||||
rewardSelectionItems,
|
||||
p4mAccountId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
// Required providers
|
||||
SelectedShoppingCartResource,
|
||||
SelectedRewardShoppingCartResource,
|
||||
SelectedCustomerResource,
|
||||
RewardSelectionService,
|
||||
RewardSelectionPopUpService,
|
||||
],
|
||||
|
||||
@@ -53,6 +53,7 @@ export class RewardSelectionActionsComponent {
|
||||
await this.#rewardSelectionFacade.completeRewardSelection({
|
||||
tabId,
|
||||
rewardSelectionItems,
|
||||
p4mAccountId: this.host.data.p4mAccountId,
|
||||
});
|
||||
this.completeRewardSelectionLoading.set(false);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { RewardSelectionStore } from './store/reward-selection-dialog.store';
|
||||
export type RewardSelectionDialogData = {
|
||||
rewardSelectionItems: RewardSelectionItem[];
|
||||
customerRewardPoints: number;
|
||||
p4mAccountId: string;
|
||||
closeText: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user