Skip to content

Commit

Permalink
feat: save desired delivery date at basket items (#1325)
Browse files Browse the repository at this point in the history
Co-authored-by: Marcel Eisentraut <meisentraut@intershop.de>
  • Loading branch information
SGrueber and Eisie96 authored Nov 24, 2022
1 parent 7f4d34c commit f8fba29
Show file tree
Hide file tree
Showing 14 changed files with 305 additions and 38 deletions.
6 changes: 4 additions & 2 deletions docs/guides/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ Find more information in the [Formly Upgrade Guide](https://github.com/ngx-forml
We still use deprecated form properties like 'templateOptions' and 'expressionProperties' for compatibility reasons but we are going to replace them in the next major release.

The two small black triangle images `active_catalog.png` (header: when hovering a catalog) and `budget-bar-indicator.png` (my account: budget bar) are removed and replaced by CSS styling.

The basket empty image `empty-cart.png` is removed and replaced with CSS styling.

The sprite image `product_sprite.png` is removed and replaced with localized text for "New", "Sale" and "Top" with the according CSS styling.

After entering a desired delivery date on the checkout shipping page and after submitting the order the desired delivery date will be saved at all basket items, if necessary.
In case of large basket (> 20 items) this might cause (unacceptable) long response times.
You can keep the existing behavior by modifying the updateBasketItemsDesiredDeliveryDate() method of the basket service to always return an empty array without doing anything.

## 3.0 to 3.1

The SSR environment variable 'ICM_IDENTITY_PROVIDER' will be removed in a future release ( PWA 5.0 ).
Expand Down
13 changes: 13 additions & 0 deletions src/app/core/facades/checkout.facade.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable } from '@angular/core';
import { Store, createSelector, select } from '@ngrx/store';
import { formatISO } from 'date-fns';
import { Subject, combineLatest, merge } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, sample, switchMap, take, tap } from 'rxjs/operators';

Expand Down Expand Up @@ -39,8 +40,10 @@ import {
loadBasketWithId,
removePromotionCodeFromBasket,
setBasketAttribute,
setBasketDesiredDeliveryDate,
setBasketPayment,
startCheckout,
submitOrder,
updateBasketAddress,
updateBasketCostCenter,
updateBasketItem,
Expand Down Expand Up @@ -74,6 +77,10 @@ export class CheckoutFacade {
this.store.dispatch(startCheckout());
}

submitOrder() {
this.store.dispatch(submitOrder());
}

continue(targetStep: number) {
this.store.dispatch(continueCheckout({ targetStep }));
}
Expand Down Expand Up @@ -159,6 +166,12 @@ export class CheckoutFacade {
select(getServerConfigParameter<number>('shipping.desiredDeliveryDaysMin'))
);

setDesiredDeliveryDate(date: Date) {
this.store.dispatch(
setBasketDesiredDeliveryDate({ desiredDeliveryDate: date ? formatISO(date, { representation: 'date' }) : '' })
);
}

eligibleShippingMethods$() {
return this.basket$.pipe(
whenTruthy(),
Expand Down
1 change: 1 addition & 0 deletions src/app/core/models/line-item/line-item.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ export interface LineItemData {
freeGift: boolean;
quantityFixed?: boolean;
quote?: string;
desiredDelivery?: string;
}
1 change: 1 addition & 0 deletions src/app/core/models/line-item/line-item.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class LineItemMapper {
productSKU: data.product,
editable: !data.quantityFixed,
quote: data.quote ? data.quote : undefined,
desiredDeliveryDate: data.desiredDelivery,
};
} else {
throw new Error(`'LineItemData' is required for the mapping`);
Expand Down
1 change: 1 addition & 0 deletions src/app/core/models/line-item/line-item.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface LineItem {

editable: boolean;
quote?: string;
desiredDeliveryDate?: string;
}

export interface LineItemView extends LineItem {
Expand Down
30 changes: 30 additions & 0 deletions src/app/core/services/basket/basket.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { anyString, anything, capture, instance, mock, verify, when } from 'ts-m
import { Address } from 'ish-core/models/address/address.model';
import { BasketData } from 'ish-core/models/basket/basket.interface';
import { LineItemData } from 'ish-core/models/line-item/line-item.interface';
import { LineItem } from 'ish-core/models/line-item/line-item.model';
import { ApiService } from 'ish-core/services/api/api.service';
import { OrderService } from 'ish-core/services/order/order.service';
import { getBasketIdOrCurrent } from 'ish-core/store/customer/basket';
Expand Down Expand Up @@ -271,6 +272,35 @@ describe('Basket Service', () => {
});
});

describe('Update Basket Items desired delivery date', () => {
const lineItems: LineItem[] = [
...BasketMockData.getBasket().lineItems,
{ id: 'withdesiredDeliveryDate', desiredDeliveryDate: '2022-20-02' } as LineItem,
];

it("should update the desired delivery date at all basket items when 'updateBasketItemsDesiredDeliveryDate' is called", done => {
when(apiService.patch(anyString(), anything(), anything())).thenReturn(
of({ data: { id: lineItemData.id, calculated: false } as LineItemData, infos: undefined })
);

basketService.updateBasketItemsDesiredDeliveryDate('2022-22-02', lineItems).subscribe(() => {
verify(apiService.patch(anything(), anything(), anything())).twice();
done();
});
});

it("should not update the desired delivery date at those basket items that have already the correct date when 'updateBasketItemsDesiredDeliveryDate' is called", done => {
when(apiService.patch(anyString(), anything(), anything())).thenReturn(
of({ data: { id: lineItemData.id, calculated: false } as LineItemData, infos: undefined })
);

basketService.updateBasketItemsDesiredDeliveryDate('2022-20-02', lineItems).subscribe(() => {
verify(apiService.patch(anything(), anything(), anything())).once();
done();
});
});
});

it("should create an attribute for a basket when 'createBasketAttribute' is called", done => {
when(apiService.post(anything(), anything(), anything())).thenReturn(of({}));

Expand Down
32 changes: 29 additions & 3 deletions src/app/core/services/basket/basket.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { EMPTY, Observable, of, throwError } from 'rxjs';
import { EMPTY, Observable, forkJoin, iif, of, throwError } from 'rxjs';
import { catchError, concatMap, map, take } from 'rxjs/operators';

import { AddressMapper } from 'ish-core/models/address/address.mapper';
Expand All @@ -20,7 +20,7 @@ import { Basket } from 'ish-core/models/basket/basket.model';
import { ErrorFeedback } from 'ish-core/models/http-error/http-error.model';
import { LineItemData } from 'ish-core/models/line-item/line-item.interface';
import { LineItemMapper } from 'ish-core/models/line-item/line-item.mapper';
import { LineItem } from 'ish-core/models/line-item/line-item.model';
import { LineItem, LineItemView } from 'ish-core/models/line-item/line-item.model';
import { SkuQuantityType } from 'ish-core/models/product/product.model';
import { ShippingMethodData } from 'ish-core/models/shipping-method/shipping-method.interface';
import { ShippingMethodMapper } from 'ish-core/models/shipping-method/shipping-method.mapper';
Expand All @@ -38,7 +38,9 @@ export type BasketUpdateType =

export type BasketItemUpdateType =
| { quantity?: { value: number; unit: string }; product?: string }
| { shippingMethod: { id: string } };
| { shippingMethod?: { id: string } }
| { desiredDelivery?: string }
| { calculated: boolean };

type BasketIncludeType =
| 'invoiceToAddress'
Expand Down Expand Up @@ -513,6 +515,30 @@ export class BasketService {
);
}

/**
* Updates the desired delivery date at all those line items of the current basket, whose desired delivery date differs from the given date.
*
* @param desiredDeliveryDate Desired delivery date in iso format, i.e. yyyy-mm-dd.
* @param lineItems Array of basket line items
* @returns Array of updated line items and basket
*/
updateBasketItemsDesiredDeliveryDate(
desiredDeliveryDate: string,
lineItems: LineItemView[]
): Observable<{ lineItem: LineItem; info: BasketInfo[] }[]> {
if (desiredDeliveryDate && !new RegExp(/\d{4}-\d{2}-\d{2}/).test(desiredDeliveryDate)) {
return throwError(
() => new Error('updateBasketItemsDesiredDeliveryDate() called with an invalid desiredDeliveryDate')
);
}

const obsArray = lineItems
?.filter(item => item.desiredDeliveryDate !== desiredDeliveryDate)
?.map(item => this.updateBasketItem(item.id, { desiredDelivery: desiredDeliveryDate }));

return iif(() => !!obsArray.length, forkJoin(obsArray), of([]));
}

/**
* Creates a custom attribute on the currently used basket. Default attribute type is 'String'.
*
Expand Down
16 changes: 16 additions & 0 deletions src/app/core/store/customer/basket/basket.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,19 @@ export const updateConcardisCvcLastUpdatedSuccess = createAction(
'[Basket API] Update CvcLastUpdated for Concardis Credit Card Success',
payload<{ paymentInstrument: PaymentInstrument }>()
);

export const submitOrder = createAction('[Basket] Basket Submit Order');

export const setBasketDesiredDeliveryDate = createAction(
'[Basket] Add or Update Basket Desired Delivery Date',
payload<{ desiredDeliveryDate: string }>() // international iso date format yyyy-mm-dd
);

export const setBasketDesiredDeliveryDateFail = createAction(
'[Basket API] Add or Update Basket Desired Delivery Date Fail',
httpError()
);

export const setBasketDesiredDeliveryDateSuccess = createAction(
'[Basket API] Add or Update Basket Desired Delivery Date Success'
);
133 changes: 130 additions & 3 deletions src/app/core/store/customer/basket/basket.effects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { EMPTY, Observable, of, throwError } from 'rxjs';
import { anyString, anything, instance, mock, verify, when } from 'ts-mockito';

import { Basket } from 'ish-core/models/basket/basket.model';
import { LineItem } from 'ish-core/models/line-item/line-item.model';
import { BasketService } from 'ish-core/services/basket/basket.service';
import { CoreStoreModule } from 'ish-core/store/core/core-store.module';
import { loadServerConfigSuccess } from 'ish-core/store/core/server-config';
Expand Down Expand Up @@ -37,8 +38,12 @@ import {
setBasketAttribute,
setBasketAttributeFail,
setBasketAttributeSuccess,
setBasketDesiredDeliveryDate,
setBasketDesiredDeliveryDateFail,
setBasketDesiredDeliveryDateSuccess,
submitBasket,
submitBasketFail,
submitOrder,
updateBasket,
updateBasketCostCenter,
updateBasketFail,
Expand Down Expand Up @@ -213,9 +218,9 @@ describe('Basket Effects', () => {

effects.recalculateBasketAfterCurrencyChange$.subscribe(action => {
expect(action).toMatchInlineSnapshot(`
[Basket Internal] Update Basket:
update: {"calculated":true}
`);
[Basket Internal] Update Basket:
update: {"calculated":true}
`);
done();
});
});
Expand Down Expand Up @@ -388,6 +393,58 @@ describe('Basket Effects', () => {
});
});

describe('setBasketDesiredDeliveryDate$', () => {
beforeEach(() => {
when(basketServiceMock.updateBasketItemsDesiredDeliveryDate(anything(), anything())).thenReturn(of([]));
store.dispatch(
loadBasketSuccess({
basket: {
id: 'BID',
attributes: [{ name: 'desiredDeliveryDate', value: desiredDeliveryDate }],
lineItems,
} as Basket,
})
);
});
const desiredDeliveryDate = '2022-02-20';
const lineItems: LineItem[] = [{ id: '1', desiredDeliveryDate: undefined } as LineItem];

it('should call the basketService for setBasketDesiredDeliveryDate', done => {
const action = setBasketDesiredDeliveryDate({ desiredDeliveryDate });
actions$ = of(action);

effects.setBasketDesiredDeliveryDate$.subscribe(() => {
verify(basketServiceMock.updateBasketItemsDesiredDeliveryDate(desiredDeliveryDate, anything())).once();
done();
});
});

it('should map to actions of type setBasketAttribute and setBasketDesiredDeliveryDate', () => {
const action = setBasketDesiredDeliveryDate({ desiredDeliveryDate });
const completion1 = setBasketAttribute({
attribute: { name: 'desiredDeliveryDate', value: desiredDeliveryDate },
});
const completion2 = setBasketDesiredDeliveryDateSuccess();
actions$ = hot('-a', { a: action });
const expected$ = cold('-(cd)', { c: completion1, d: completion2 });

expect(effects.setBasketDesiredDeliveryDate$).toBeObservable(expected$);
});

it('should map invalid request to action of type setBasketDesiredDeliveryDateFail', () => {
when(basketServiceMock.updateBasketItemsDesiredDeliveryDate(anything(), anything())).thenReturn(
throwError(() => makeHttpError({ message: 'invalid' }))
);

const action = setBasketDesiredDeliveryDate({ desiredDeliveryDate });
const completion = setBasketDesiredDeliveryDateFail({ error: makeHttpError({ message: 'invalid' }) });
actions$ = hot('-a-a-a', { a: action });
const expected$ = cold('-c-c-c', { c: completion });

expect(effects.setBasketDesiredDeliveryDate$).toBeObservable(expected$);
});
});

describe('setCustomAttributeToBasket$', () => {
beforeEach(() => {
when(basketServiceMock.createBasketAttribute(anything())).thenReturn(of(undefined));
Expand Down Expand Up @@ -610,4 +667,74 @@ describe('Basket Effects', () => {
expect(effects.createRequisition$).toBeObservable(expected$);
});
});

describe('submitOrder$', () => {
beforeEach(() => {
when(basketServiceMock.updateBasketItemsDesiredDeliveryDate(anything(), anything())).thenReturn(of([]));
store.dispatch(
loadBasketSuccess({
basket: {
id: 'BID',
attributes: [{ name: 'desiredDeliveryDate', value: desiredDeliveryDate }],
lineItems,
} as Basket,
})
);
});
const desiredDeliveryDate = '2022-02-20';
const lineItems: LineItem[] = [{ id: '1', desiredDeliveryDate: undefined } as LineItem];

it('should call the basketService for submitOrder if the basket has a desired delivery date', done => {
const action = submitOrder();
actions$ = of(action);

effects.submitOrder$.subscribe(() => {
verify(basketServiceMock.updateBasketItemsDesiredDeliveryDate(desiredDeliveryDate, anything())).once();
done();
});
});

it('should not call the basketService for submitOrder if the basket has no desired delivery date', done => {
store.dispatch(
loadBasketSuccess({
basket: { id: 'BID', attributes: [] } as Basket,
})
);

const action = submitOrder();
actions$ = of(action);

effects.submitOrder$.subscribe({
next: () => {
verify(basketServiceMock.updateBasketItemsDesiredDeliveryDate(anything(), anything())).never();
},
error: fail,
complete: done,
});
});

it('should map a valid request to action of type continueCheckout', done => {
actions$ = of(submitOrder());

effects.submitOrder$.subscribe(action => {
expect(action).toMatchInlineSnapshot(`
[Basket] Validate Basket and continue checkout:
targetStep: 5
`);
done();
});
});

it('should map an invalid request to action of type setBasketDesiredDeliveryDateFail', () => {
when(basketServiceMock.updateBasketItemsDesiredDeliveryDate(anything(), anything())).thenReturn(
throwError(() => makeHttpError({ message: 'invalid' }))
);
const action = submitOrder();
const completion = setBasketDesiredDeliveryDateFail({ error: makeHttpError({ message: 'invalid' }) });
actions$ = hot('-a-a-a', { a: action });
const expected$ = cold('-c-c-c', { c: completion });

expect(effects.submitOrder$).toBeObservable(expected$);
});
});
});
Loading

0 comments on commit f8fba29

Please sign in to comment.