mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
1 Commits
7200eaefbf
...
feature/54
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
215cceb1c4 |
@@ -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