diff --git a/src/app/core/facades/checkout.facade.ts b/src/app/core/facades/checkout.facade.ts index d3e1a2ca9b..1d739af806 100644 --- a/src/app/core/facades/checkout.facade.ts +++ b/src/app/core/facades/checkout.facade.ts @@ -11,7 +11,6 @@ import { LineItemUpdate } from 'ish-core/models/line-item-update/line-item-updat import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-instrument.model'; import { selectRouteData } from 'ish-core/store/core/router'; import { getServerConfigParameter } from 'ish-core/store/core/server-config'; -import { getAllAddresses } from 'ish-core/store/customer/addresses'; import { addMessageToMerchant, addPromotionCodeToBasket, @@ -25,6 +24,7 @@ import { deleteBasketItems, deleteBasketPayment, deleteBasketShippingAddress, + getBasketEligibleAddresses, getBasketEligiblePaymentMethods, getBasketEligibleShippingMethods, getBasketError, @@ -38,6 +38,7 @@ import { getCurrentBasket, getSubmittedBasket, isBasketInvoiceAndShippingAddressEqual, + loadBasketEligibleAddresses, loadBasketEligiblePaymentMethods, loadBasketEligibleShippingMethods, loadBasketWithId, @@ -283,18 +284,27 @@ export class CheckoutFacade { select( createSelector( getLoggedInUser, - getAllAddresses, + getBasketEligibleAddresses, getBasketShippingAddress, (user, addresses, shippingAddress): boolean => !!shippingAddress && !!user && - addresses.length > 1 && + addresses?.length > 1 && (!user.preferredInvoiceToAddressUrn || user.preferredInvoiceToAddressUrn !== shippingAddress.urn) && (!user.preferredShipToAddressUrn || user.preferredShipToAddressUrn !== shippingAddress.urn) ) ) ); + eligibleAddresses$() { + return this.basket$.pipe( + whenTruthy(), + take(1), + tap(() => this.store.dispatch(loadBasketEligibleAddresses())), + switchMap(() => this.store.pipe(select(getBasketEligibleAddresses))) + ); + } + assignBasketAddress(addressId: string, scope: 'invoice' | 'shipping' | 'any') { this.store.dispatch(assignBasketAddress({ addressId, scope })); } diff --git a/src/app/core/services/basket/basket.service.spec.ts b/src/app/core/services/basket/basket.service.spec.ts index b89fc0deb9..fb076897ef 100644 --- a/src/app/core/services/basket/basket.service.spec.ts +++ b/src/app/core/services/basket/basket.service.spec.ts @@ -195,6 +195,15 @@ describe('Basket Service', () => { }); }); + it("should get eligible addresses for a basket when 'getBasketEligibleAddresses' is called", done => { + when(apiService.get(anything(), anything())).thenReturn(of({ data: [] })); + + basketService.getBasketEligibleAddresses().subscribe(() => { + verify(apiService.get('eligible-addresses', anything())).once(); + done(); + }); + }); + it("should get eligible shipping methods for a basket when 'getBasketEligibleShippingMethods' is called", done => { when(apiService.get(anything(), anything())).thenReturn(of({ data: [] })); diff --git a/src/app/core/services/basket/basket.service.ts b/src/app/core/services/basket/basket.service.ts index beae39c4f3..f4c8afd0a8 100644 --- a/src/app/core/services/basket/basket.service.ts +++ b/src/app/core/services/basket/basket.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@angular/core'; import { Observable, of, throwError } from 'rxjs'; import { catchError, concatMap, map } from 'rxjs/operators'; +import { AddressData } from 'ish-core/models/address/address.interface'; import { AddressMapper } from 'ish-core/models/address/address.mapper'; import { Address } from 'ish-core/models/address/address.model'; import { Attribute } from 'ish-core/models/attribute/attribute.model'; @@ -32,41 +33,6 @@ export type BasketUpdateType = | { invoiceToAddress: string } | { messageToMerchant: string }; -type BasketIncludeType = - | 'invoiceToAddress' - | 'commonShipToAddress' - | 'commonShippingMethod' - | 'discounts' - | 'lineItems_discounts' - | 'lineItems' - | 'payments' - | 'payments_paymentMethod' - | 'payments_paymentInstrument'; - -type MergeBasketIncludeType = - | 'targetBasket' - | 'targetBasket_invoiceToAddress' - | 'targetBasket_commonShipToAddress' - | 'targetBasket_commonShippingMethod' - | 'targetBasket_discounts' - | 'targetBasket_lineItems_discounts' - | 'targetBasket_lineItems' - | 'targetBasket_payments' - | 'targetBasket_payments_paymentMethod' - | 'targetBasket_payments_paymentInstrument'; - -type ValidationBasketIncludeType = - | 'basket' - | 'basket_invoiceToAddress' - | 'basket_commonShipToAddress' - | 'basket_commonShippingMethod' - | 'basket_discounts' - | 'basket_lineItems_discounts' - | 'basket_lineItems' - | 'basket_payments' - | 'basket_payments_paymentMethod' - | 'basket_payments_paymentInstrument'; - /** * The Basket Service handles the interaction with the 'baskets' REST API. * Methods related to basket-items are handled in the basket-items.service. @@ -84,7 +50,7 @@ export class BasketService { Accept: 'application/vnd.intershop.basket.v1+json', }); - private allBasketIncludes: BasketIncludeType[] = [ + private readonly allBasketIncludes = [ 'invoiceToAddress', 'commonShipToAddress', 'commonShippingMethod', @@ -96,7 +62,7 @@ export class BasketService { 'payments_paymentInstrument', ]; - private allTargetBasketIncludes: MergeBasketIncludeType[] = [ + private readonly allTargetBasketIncludes = [ 'targetBasket', 'targetBasket_invoiceToAddress', 'targetBasket_commonShipToAddress', @@ -109,7 +75,7 @@ export class BasketService { 'targetBasket_payments_paymentInstrument', ]; - private allBasketValidationIncludes: ValidationBasketIncludeType[] = [ + private readonly allBasketValidationIncludes = [ 'basket', 'basket_invoiceToAddress', 'basket_commonShipToAddress', @@ -309,6 +275,23 @@ export class BasketService { }); } + /** + * Get eligible addresses for the currently used basket. + * + * @returns The eligible addresses. + */ + getBasketEligibleAddresses(): Observable { + return this.apiService + .currentBasketEndpoint() + .get('eligible-addresses', { + headers: this.basketHeaders, + }) + .pipe( + unpackEnvelope('data'), + map(addressesData => addressesData.map(AddressMapper.fromData)) + ); + } + /** * Create a basket address for the currently used basket of an anonymous user. * diff --git a/src/app/core/store/customer/addresses/addresses.reducer.ts b/src/app/core/store/customer/addresses/addresses.reducer.ts index 8bcbc60cbf..60100ff27c 100644 --- a/src/app/core/store/customer/addresses/addresses.reducer.ts +++ b/src/app/core/store/customer/addresses/addresses.reducer.ts @@ -3,12 +3,7 @@ import { createReducer, on } from '@ngrx/store'; import { Address } from 'ish-core/models/address/address.model'; import { HttpError } from 'ish-core/models/http-error/http-error.model'; -import { - createBasketAddress, - createBasketAddressSuccess, - deleteBasketShippingAddress, - updateBasketAddress, -} from 'ish-core/store/customer/basket'; +import { deleteBasketShippingAddress, updateBasketAddress } from 'ish-core/store/customer/basket'; import { setErrorOn, setLoadingOn, unsetLoadingAndErrorOn } from 'ish-core/utils/ngrx-creators'; import { @@ -43,7 +38,6 @@ export const addressesReducer = createReducer( setLoadingOn( loadAddresses, createCustomerAddress, - createBasketAddress, updateCustomerAddress, updateBasketAddress, deleteCustomerAddress, @@ -53,12 +47,11 @@ export const addressesReducer = createReducer( unsetLoadingAndErrorOn( loadAddressesSuccess, createCustomerAddressSuccess, - createBasketAddressSuccess, updateCustomerAddressSuccess, deleteCustomerAddressSuccess ), on(loadAddressesSuccess, (state, action) => addressAdapter.setAll(action.payload.addresses, state)), - on(createCustomerAddressSuccess, createBasketAddressSuccess, updateCustomerAddressSuccess, (state, action) => + on(createCustomerAddressSuccess, updateCustomerAddressSuccess, (state, action) => addressAdapter.upsertOne(action.payload.address, state) ), on(deleteCustomerAddressSuccess, (state, action) => addressAdapter.removeOne(action.payload.addressId, state)) diff --git a/src/app/core/store/customer/addresses/addresses.selectors.spec.ts b/src/app/core/store/customer/addresses/addresses.selectors.spec.ts index 33d464f7a2..9038cf1d5b 100644 --- a/src/app/core/store/customer/addresses/addresses.selectors.spec.ts +++ b/src/app/core/store/customer/addresses/addresses.selectors.spec.ts @@ -2,7 +2,7 @@ import { TestBed } from '@angular/core/testing'; import { Address } from 'ish-core/models/address/address.model'; import { CoreStoreModule } from 'ish-core/store/core/core-store.module'; -import { createBasketAddress, createBasketAddressSuccess, updateBasketAddress } from 'ish-core/store/customer/basket'; +import { updateBasketAddress } from 'ish-core/store/customer/basket'; import { CustomerStoreModule } from 'ish-core/store/customer/customer-store.module'; import { makeHttpError } from 'ish-core/utils/dev/api-service-utils'; import { BasketMockData } from 'ish-core/utils/dev/basket-mock-data'; @@ -125,46 +125,6 @@ describe('Addresses Selectors', () => { }); }); - describe('create basket addresses', () => { - const address = BasketMockData.getAddress(); - - beforeEach(() => { - store$.dispatch(createBasketAddress({ address, scope: 'invoice' })); - }); - - it('should set the state to loading', () => { - expect(getAddressesLoading(store$.state)).toBeTrue(); - }); - - describe('and reporting success', () => { - beforeEach(() => { - store$.dispatch(createBasketAddressSuccess({ address, scope: 'invoice' })); - }); - - it('should set loading to false and add basket address', () => { - expect(getAddressesLoading(store$.state)).toBeFalse(); - expect(getAllAddresses(store$.state)).toEqual([address]); - }); - }); - - describe('and reporting failure', () => { - beforeEach(() => { - store$.dispatch(createCustomerAddressFail({ error: makeHttpError({ message: 'error' }) })); - }); - - it('should not have loaded addresses on error', () => { - expect(getAddressesLoading(store$.state)).toBeFalse(); - expect(getAllAddresses(store$.state)).toBeEmpty(); - expect(getAddressesError(store$.state)).toMatchInlineSnapshot(` - { - "message": "error", - "name": "HttpErrorResponse", - } - `); - }); - }); - }); - describe('update basket addresses', () => { const address = BasketMockData.getAddress(); diff --git a/src/app/core/store/customer/basket/basket-addresses.effects.spec.ts b/src/app/core/store/customer/basket/basket-addresses.effects.spec.ts index 89891f090f..1994bdb3c9 100644 --- a/src/app/core/store/customer/basket/basket-addresses.effects.spec.ts +++ b/src/app/core/store/customer/basket/basket-addresses.effects.spec.ts @@ -27,6 +27,9 @@ import { createBasketAddressSuccess, deleteBasketShippingAddress, loadBasket, + loadBasketEligibleAddresses, + loadBasketEligibleAddressesFail, + loadBasketEligibleAddressesSuccess, resetBasketErrors, updateBasket, updateBasketAddress, @@ -57,6 +60,45 @@ describe('Basket Addresses Effects', () => { store = TestBed.inject(Store); }); + describe('loadBasketEligibleAddresses$', () => { + beforeEach(() => { + when(basketServiceMock.getBasketEligibleAddresses()).thenReturn(of([])); + }); + + it('should call the basketService for loadBasketEligibleAddresses', done => { + const action = loadBasketEligibleAddresses(); + actions$ = of(action); + + effects.loadBasketEligibleAddresses$.subscribe(() => { + verify(basketServiceMock.getBasketEligibleAddresses()).once(); + done(); + }); + }); + + it('should map to action of type LoadBasketEligibleAddressesSuccess', () => { + const action = loadBasketEligibleAddresses(); + const completion = loadBasketEligibleAddressesSuccess({ addresses: [] }); + + actions$ = hot('-a-a-a', { a: action }); + const expected$ = cold('-c-c-c', { c: completion }); + + expect(effects.loadBasketEligibleAddresses$).toBeObservable(expected$); + }); + + it('should map invalid request to action of type LoadBasketEligibleAddressesFail', () => { + when(basketServiceMock.getBasketEligibleAddresses()).thenReturn( + throwError(() => makeHttpError({ message: 'invalid' })) + ); + + const action = loadBasketEligibleAddresses(); + const completion = loadBasketEligibleAddressesFail({ error: makeHttpError({ message: 'invalid' }) }); + actions$ = hot('-a-a-a', { a: action }); + const expected$ = cold('-c-c-c', { c: completion }); + + expect(effects.loadBasketEligibleAddresses$).toBeObservable(expected$); + }); + }); + describe('createAddressForBasket$ for a logged in user', () => { beforeEach(() => { when(addressServiceMock.createCustomerAddress('-', anything())).thenReturn(of(BasketMockData.getAddress())); @@ -212,9 +254,10 @@ describe('Basket Addresses Effects', () => { const action = updateBasketAddress({ address }); const completion1 = updateCustomerAddressSuccess({ address }); const completion2 = loadBasket(); - const completion3 = resetBasketErrors(); + const completion3 = loadBasketEligibleAddresses(); + const completion4 = resetBasketErrors(); actions$ = hot('-a', { a: action }); - const expected$ = cold('-(cde)', { c: completion1, d: completion2, e: completion3 }); + const expected$ = cold('-(cdef)', { c: completion1, d: completion2, e: completion3, f: completion4 }); expect(effects.updateBasketAddress$).toBeObservable(expected$); }); @@ -298,8 +341,9 @@ describe('Basket Addresses Effects', () => { const action = deleteBasketShippingAddress({ addressId }); const completion1 = deleteCustomerAddressSuccess({ addressId }); const completion2 = loadBasket(); + const completion3 = loadBasketEligibleAddresses(); actions$ = hot('-a', { a: action }); - const expected$ = cold('-(cd)', { c: completion1, d: completion2 }); + const expected$ = cold('-(cde)', { c: completion1, d: completion2, e: completion3 }); expect(effects.deleteBasketShippingAddress$).toBeObservable(expected$); }); diff --git a/src/app/core/store/customer/basket/basket-addresses.effects.ts b/src/app/core/store/customer/basket/basket-addresses.effects.ts index 178acf3a4a..fbe0110f10 100644 --- a/src/app/core/store/customer/basket/basket-addresses.effects.ts +++ b/src/app/core/store/customer/basket/basket-addresses.effects.ts @@ -6,7 +6,6 @@ import { map, mergeMap } from 'rxjs/operators'; import { AddressService } from 'ish-core/services/address/address.service'; import { BasketService, BasketUpdateType } from 'ish-core/services/basket/basket.service'; import { - createCustomerAddressFail, deleteCustomerAddressFail, deleteCustomerAddressSuccess, updateCustomerAddressFail, @@ -18,9 +17,13 @@ import { mapErrorToAction, mapToPayload, mapToPayloadProperty } from 'ish-core/u import { assignBasketAddress, createBasketAddress, + createBasketAddressFail, createBasketAddressSuccess, deleteBasketShippingAddress, loadBasket, + loadBasketEligibleAddresses, + loadBasketEligibleAddressesFail, + loadBasketEligibleAddressesSuccess, resetBasketErrors, updateBasket, updateBasketAddress, @@ -35,6 +38,21 @@ export class BasketAddressesEffects { private addressService: AddressService ) {} + /** + * The load basket eligible addresses effect. + */ + loadBasketEligibleAddresses$ = createEffect(() => + this.actions$.pipe( + ofType(loadBasketEligibleAddresses), + mergeMap(() => + this.basketService.getBasketEligibleAddresses().pipe( + map(result => loadBasketEligibleAddressesSuccess({ addresses: result })), + mapErrorToAction(loadBasketEligibleAddressesFail) + ) + ) + ) + ); + /** * Creates a new invoice/shipping address which is assigned to the basket later on * if the user is logged in a customer address will be created, otherwise a new basket address will be created @@ -49,13 +67,13 @@ export class BasketAddressesEffects { if (customer) { return this.addressService.createCustomerAddress('-', action.payload.address).pipe( map(newAddress => createBasketAddressSuccess({ address: newAddress, scope: action.payload.scope })), - mapErrorToAction(createCustomerAddressFail) + mapErrorToAction(createBasketAddressFail) ); // create address at basket for anonymous user } else { return this.basketService.createBasketAddress(action.payload.address).pipe( map(newAddress => createBasketAddressSuccess({ address: newAddress, scope: action.payload.scope })), - mapErrorToAction(createCustomerAddressFail) + mapErrorToAction(createBasketAddressFail) ); } }) @@ -119,7 +137,12 @@ export class BasketAddressesEffects { // create address at customer for logged in user if (customer) { return this.addressService.updateCustomerAddress('-', address).pipe( - mergeMap(() => [updateCustomerAddressSuccess({ address }), loadBasket(), resetBasketErrors()]), + mergeMap(() => [ + updateCustomerAddressSuccess({ address }), + loadBasket(), + loadBasketEligibleAddresses(), + resetBasketErrors(), + ]), mapErrorToAction(updateCustomerAddressFail) ); // create address at basket for anonymous user @@ -142,7 +165,7 @@ export class BasketAddressesEffects { mapToPayloadProperty('addressId'), mergeMap(addressId => this.addressService.deleteCustomerAddress('-', addressId).pipe( - mergeMap(() => [deleteCustomerAddressSuccess({ addressId }), loadBasket()]), + mergeMap(() => [deleteCustomerAddressSuccess({ addressId }), loadBasket(), loadBasketEligibleAddresses()]), mapErrorToAction(deleteCustomerAddressFail) ) ) diff --git a/src/app/core/store/customer/basket/basket.actions.ts b/src/app/core/store/customer/basket/basket.actions.ts index 389970f9e5..7a34d2aeb5 100644 --- a/src/app/core/store/customer/basket/basket.actions.ts +++ b/src/app/core/store/customer/basket/basket.actions.ts @@ -48,6 +48,8 @@ export const createBasketAddressSuccess = createAction( payload<{ address: Address; scope: 'invoice' | 'shipping' | 'any' }>() ); +export const createBasketAddressFail = createAction('[Basket API] Create Basket Address Fail', httpError()); + export const assignBasketAddress = createAction( '[Basket] Assign an Address to the Basket', payload<{ addressId: string; scope: 'invoice' | 'shipping' | 'any' }>() @@ -205,6 +207,18 @@ export const deleteBasketAttributeFail = createAction('[Basket API] Delete Baske export const deleteBasketAttributeSuccess = createAction('[Basket API] Delete Basket Attribute Success'); +export const loadBasketEligibleAddresses = createAction('[Basket Internal] Load Basket Eligible Addresses'); + +export const loadBasketEligibleAddressesFail = createAction( + '[Basket API] Load Basket Eligible Addresses Fail', + httpError() +); + +export const loadBasketEligibleAddressesSuccess = createAction( + '[Basket API] Load Basket Eligible Addresses Success', + payload<{ addresses: Address[] }>() +); + export const loadBasketEligibleShippingMethods = createAction( '[Basket Internal] Load Basket Eligible Shipping Methods' ); diff --git a/src/app/core/store/customer/basket/basket.reducer.ts b/src/app/core/store/customer/basket/basket.reducer.ts index fbb2b872bd..aba81c5485 100644 --- a/src/app/core/store/customer/basket/basket.reducer.ts +++ b/src/app/core/store/customer/basket/basket.reducer.ts @@ -1,6 +1,7 @@ import { createReducer, on } from '@ngrx/store'; import { unionBy } from 'lodash-es'; +import { Address } from 'ish-core/models/address/address.model'; import { BasketInfo } from 'ish-core/models/basket-info/basket-info.model'; import { BasketValidationResultType } from 'ish-core/models/basket-validation/basket-validation.model'; import { Basket } from 'ish-core/models/basket/basket.model'; @@ -23,6 +24,9 @@ import { continueCheckoutFail, continueCheckoutSuccess, continueCheckoutWithIssues, + createBasketAddress, + createBasketAddressFail, + createBasketAddressSuccess, createBasketPayment, createBasketPaymentFail, createBasketPaymentSuccess, @@ -39,6 +43,9 @@ import { loadBasket, loadBasketByAPIToken, loadBasketByAPITokenFail, + loadBasketEligibleAddresses, + loadBasketEligibleAddressesFail, + loadBasketEligibleAddressesSuccess, loadBasketEligiblePaymentMethods, loadBasketEligiblePaymentMethodsFail, loadBasketEligiblePaymentMethodsSuccess, @@ -86,6 +93,7 @@ import { export interface BasketState { basket: Basket; + eligibleAddresses: Address[]; eligibleShippingMethods: ShippingMethod[]; eligiblePaymentMethods: PaymentMethod[]; loading: boolean; @@ -105,6 +113,7 @@ const initialValidationResults: BasketValidationResultType = { const initialState: BasketState = { basket: undefined, + eligibleAddresses: undefined, eligibleShippingMethods: undefined, eligiblePaymentMethods: undefined, loading: false, @@ -134,6 +143,8 @@ export const basketReducer = createReducer( deleteBasketItem, setBasketAttribute, deleteBasketAttribute, + createBasketAddress, + loadBasketEligibleAddresses, loadBasketEligibleShippingMethods, loadBasketEligiblePaymentMethods, setBasketPayment, @@ -158,12 +169,14 @@ export const basketReducer = createReducer( deleteBasketItemSuccess, addItemsToBasketSuccess, setBasketPaymentSuccess, + createBasketAddressSuccess, createBasketPaymentSuccess, updateBasketPaymentSuccess, deleteBasketPaymentSuccess, removePromotionCodeFromBasketSuccess, continueCheckoutSuccess, continueCheckoutWithIssues, + loadBasketEligibleAddressesSuccess, loadBasketEligibleShippingMethodsSuccess, loadBasketEligiblePaymentMethodsSuccess, updateConcardisCvcLastUpdatedSuccess, @@ -182,6 +195,8 @@ export const basketReducer = createReducer( deleteBasketItemFail, setBasketAttributeFail, deleteBasketAttributeFail, + createBasketAddressFail, + loadBasketEligibleAddressesFail, loadBasketEligibleShippingMethodsFail, loadBasketEligiblePaymentMethodsFail, setBasketPaymentFail, @@ -255,6 +270,22 @@ export const basketReducer = createReducer( validationResults: validation?.results, }; }), + on( + loadBasketEligibleAddressesSuccess, + (state, action): BasketState => ({ + ...state, + eligibleAddresses: action.payload.addresses, + }) + ), + on( + createBasketAddressSuccess, + (state, action): BasketState => ({ + ...state, + eligibleAddresses: state.eligibleAddresses + ? [...state.eligibleAddresses, action.payload.address] + : [action.payload.address], + }) + ), on( loadBasketEligibleShippingMethodsSuccess, (state, action): BasketState => ({ diff --git a/src/app/core/store/customer/basket/basket.selectors.spec.ts b/src/app/core/store/customer/basket/basket.selectors.spec.ts index 1cb5781556..cc6430aa1e 100644 --- a/src/app/core/store/customer/basket/basket.selectors.spec.ts +++ b/src/app/core/store/customer/basket/basket.selectors.spec.ts @@ -20,6 +20,9 @@ import { continueCheckoutSuccess, createBasketSuccess, loadBasket, + loadBasketEligibleAddresses, + loadBasketEligibleAddressesFail, + loadBasketEligibleAddressesSuccess, loadBasketEligiblePaymentMethods, loadBasketEligiblePaymentMethodsFail, loadBasketEligiblePaymentMethodsSuccess, @@ -31,6 +34,7 @@ import { submitBasketSuccess, } from './basket.actions'; import { + getBasketEligibleAddresses, getBasketEligiblePaymentMethods, getBasketEligibleShippingMethods, getBasketError, @@ -183,6 +187,44 @@ describe('Basket Selectors', () => { }); }); + describe('loading eligible addresses', () => { + beforeEach(() => { + store$.dispatch(loadBasketEligibleAddresses()); + }); + + it('should set the state to loading', () => { + expect(getBasketLoading(store$.state)).toBeTrue(); + }); + + describe('and reporting success', () => { + beforeEach(() => { + store$.dispatch(loadBasketEligibleAddressesSuccess({ addresses: [BasketMockData.getAddress()] })); + }); + + it('should set loading to false', () => { + expect(getBasketLoading(store$.state)).toBeFalse(); + expect(getBasketEligibleAddresses(store$.state)).toEqual([BasketMockData.getAddress()]); + }); + }); + + describe('and reporting failure', () => { + beforeEach(() => { + store$.dispatch(loadBasketEligibleAddressesFail({ error: makeHttpError({ message: 'error' }) })); + }); + + it('should not have loaded addresses on error', () => { + expect(getBasketLoading(store$.state)).toBeFalse(); + expect(getBasketEligibleAddresses(store$.state)).toBeUndefined(); + expect(getBasketError(store$.state)).toMatchInlineSnapshot(` + { + "message": "error", + "name": "HttpErrorResponse", + } + `); + }); + }); + }); + describe('loading eligible shipping methods', () => { beforeEach(() => { store$.dispatch(loadBasketEligibleShippingMethods()); diff --git a/src/app/core/store/customer/basket/basket.selectors.ts b/src/app/core/store/customer/basket/basket.selectors.ts index a8e97f3305..4ad95b80db 100644 --- a/src/app/core/store/customer/basket/basket.selectors.ts +++ b/src/app/core/store/customer/basket/basket.selectors.ts @@ -69,6 +69,8 @@ export const getBasketPromotionError = createSelector(getBasketState, basket => export const getBasketLastTimeProductAdded = createSelector(getBasketState, basket => basket.lastTimeProductAdded); +export const getBasketEligibleAddresses = createSelector(getBasketState, basket => basket.eligibleAddresses); + export const getBasketEligibleShippingMethods = createSelector( getBasketState, basket => basket.eligibleShippingMethods diff --git a/src/app/pages/checkout-address/checkout-address-anonymous/checkout-address-anonymous.component.ts b/src/app/pages/checkout-address/checkout-address-anonymous/checkout-address-anonymous.component.ts index 9de8aad462..60336560c6 100644 --- a/src/app/pages/checkout-address/checkout-address-anonymous/checkout-address-anonymous.component.ts +++ b/src/app/pages/checkout-address/checkout-address-anonymous/checkout-address-anonymous.component.ts @@ -95,13 +95,15 @@ export class CheckoutAddressAnonymousComponent implements OnChanges { ? undefined : this.form.get('shippingAddress').value.address; - if (this.form.get('additionalAddressAttributes').value.taxationID) { - this.checkoutFacade.setBasketCustomAttribute({ - name: 'taxationID', - value: this.form.get('additionalAddressAttributes').value.taxationID, - }); - } else { - this.checkoutFacade.deleteBasketCustomAttribute('taxationID'); + if (this.form.get('additionalAddressAttributes').get('taxationID')) { + if (this.form.get('additionalAddressAttributes').value.taxationID) { + this.checkoutFacade.setBasketCustomAttribute({ + name: 'taxationID', + value: this.form.get('additionalAddressAttributes').value.taxationID, + }); + } else { + this.checkoutFacade.deleteBasketCustomAttribute('taxationID'); + } } if (shippingAddress) { diff --git a/src/app/pages/checkout-address/checkout-address/checkout-address.component.html b/src/app/pages/checkout-address/checkout-address/checkout-address.component.html index 4e904909ad..46872de995 100644 --- a/src/app/pages/checkout-address/checkout-address/checkout-address.component.html +++ b/src/app/pages/checkout-address/checkout-address/checkout-address.component.html @@ -16,6 +16,7 @@
{ let element: HTMLElement; beforeEach(async () => { + const checkoutFacade = mock(CheckoutFacade); + when(checkoutFacade.eligibleAddresses$()).thenReturn(of([])); await TestBed.configureTestingModule({ declarations: [ CheckoutAddressComponent, @@ -32,6 +37,7 @@ describe('Checkout Address Component', () => { MockDirective(ServerHtmlDirective), ], imports: [TranslateModule.forRoot()], + providers: [{ provide: CheckoutFacade, useFactory: () => instance(checkoutFacade) }], }).compileComponents(); }); diff --git a/src/app/pages/checkout-address/checkout-address/checkout-address.component.ts b/src/app/pages/checkout-address/checkout-address/checkout-address.component.ts index 2c501551ea..d259a743c2 100644 --- a/src/app/pages/checkout-address/checkout-address/checkout-address.component.ts +++ b/src/app/pages/checkout-address/checkout-address/checkout-address.component.ts @@ -1,5 +1,8 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Observable, shareReplay } from 'rxjs'; +import { CheckoutFacade } from 'ish-core/facades/checkout.facade'; +import { Address } from 'ish-core/models/address/address.model'; import { Basket } from 'ish-core/models/basket/basket.model'; import { HttpError } from 'ish-core/models/http-error/http-error.model'; @@ -11,15 +14,23 @@ import { HttpError } from 'ish-core/models/http-error/http-error.model'; templateUrl: './checkout-address.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CheckoutAddressComponent { +export class CheckoutAddressComponent implements OnInit { @Input({ required: true }) basket: Basket; @Input() error: HttpError; @Output() nextStep = new EventEmitter(); + eligibleAddresses$: Observable; + submitted = false; active: 'invoice' | 'shipping'; + constructor(private checkoutFacade: CheckoutFacade) {} + + ngOnInit(): void { + this.eligibleAddresses$ = this.checkoutFacade.eligibleAddresses$().pipe(shareReplay(1)); + } + /** * leads to next checkout page (checkout shipping) */ diff --git a/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.spec.ts b/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.spec.ts index 86252a11ae..468468f4ac 100644 --- a/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.spec.ts +++ b/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.spec.ts @@ -34,7 +34,6 @@ describe('Basket Invoice Address Widget Component', () => { when(checkoutFacade.basketInvoiceAddress$).thenReturn(EMPTY); accountFacade = mock(AccountFacade); - when(accountFacade.addresses$()).thenReturn(EMPTY); when(accountFacade.isLoggedIn$).thenReturn(of(true)); await TestBed.configureTestingModule({ @@ -85,7 +84,6 @@ describe('Basket Invoice Address Widget Component', () => { describe('with address on basket', () => { beforeEach(() => { when(checkoutFacade.basketInvoiceAddress$).thenReturn(of(BasketMockData.getAddress())); - when(accountFacade.addresses$()).thenReturn(of([BasketMockData.getAddress()])); }); it('should render if invoice is set', () => { @@ -165,7 +163,7 @@ describe('Basket Invoice Address Widget Component', () => { beforeEach(() => { when(checkoutFacade.basketInvoiceAddress$).thenReturn(of(addresses[1])); - when(accountFacade.addresses$()).thenReturn(of(addresses)); + component.eligibleAddresses$ = of(addresses); }); it('should only use valid addresses for selection display', done => { diff --git a/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.ts b/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.ts index bce5370518..ae9b943563 100644 --- a/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.ts +++ b/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.ts @@ -3,7 +3,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { FormlyFieldConfig } from '@ngx-formly/core'; import { BehaviorSubject, Observable, combineLatest } from 'rxjs'; -import { filter, map, shareReplay, take } from 'rxjs/operators'; +import { filter, map, take } from 'rxjs/operators'; import { AccountFacade } from 'ish-core/facades/account.facade'; import { CheckoutFacade } from 'ish-core/facades/checkout.facade'; @@ -22,6 +22,7 @@ import { FormsService } from 'ish-shared/forms/utils/forms.service'; changeDetection: ChangeDetectionStrategy.Default, }) export class BasketInvoiceAddressWidgetComponent implements OnInit { + @Input({ required: true }) eligibleAddresses$: Observable; @Input() showErrors = true; @Output() collapseChange = new BehaviorSubject(true); @@ -35,7 +36,6 @@ export class BasketInvoiceAddressWidgetComponent implements OnInit { } invoiceAddress$: Observable
; addresses$: Observable; - customerAddresses$: Observable; isLoggedIn$: Observable; form = new UntypedFormGroup({}); @@ -53,7 +53,6 @@ export class BasketInvoiceAddressWidgetComponent implements OnInit { ) {} ngOnInit() { - this.customerAddresses$ = this.accountFacade.addresses$().pipe(shareReplay(1)); this.invoiceAddress$ = this.checkoutFacade.basketInvoiceAddress$; this.invoiceAddress$ @@ -68,7 +67,7 @@ export class BasketInvoiceAddressWidgetComponent implements OnInit { .subscribe(label => (this.emptyOptionLabel = label)); // prepare data for invoice select drop down - this.addresses$ = combineLatest([this.customerAddresses$, this.invoiceAddress$]).pipe( + this.addresses$ = combineLatest([this.eligibleAddresses$, this.invoiceAddress$]).pipe( map(([addresses, invoiceAddress]) => addresses?.filter(address => address.invoiceToAddress).filter(address => address.id !== invoiceAddress?.id) ) diff --git a/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.html b/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.html index ec27c23cb8..4b18e1a24d 100644 --- a/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.html +++ b/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.html @@ -6,11 +6,12 @@

{{ 'checkout.address.shipping.label' | translate }}

@@ -18,10 +19,11 @@

{{ 'checkout.address.shipping.label' | translate }}

diff --git a/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.spec.ts b/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.spec.ts index bb800dc284..1df143e956 100644 --- a/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.spec.ts +++ b/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.spec.ts @@ -1,6 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { RouterTestingModule } from '@angular/router/testing'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; @@ -38,15 +37,9 @@ describe('Basket Shipping Address Widget Component', () => { when(checkoutFacade.basketInvoiceAndShippingAddressEqual$).thenReturn(of(false)); when(accountFacade.isLoggedIn$).thenReturn(of(true)); - when(accountFacade.addresses$()).thenReturn(EMPTY); await TestBed.configureTestingModule({ - imports: [ - FeatureToggleModule.forTesting('addressDoctor'), - FormlyTestingModule, - RouterTestingModule, - TranslateModule.forRoot(), - ], + imports: [FeatureToggleModule.forTesting('addressDoctor'), FormlyTestingModule, TranslateModule.forRoot()], declarations: [ BasketShippingAddressWidgetComponent, MockComponent(AddressComponent), @@ -102,7 +95,7 @@ describe('Basket Shipping Address Widget Component', () => { beforeEach(() => { const address = BasketMockData.getAddress(); when(checkoutFacade.basketShippingAddress$).thenReturn(of(address)); - when(accountFacade.addresses$()).thenReturn(of([address, { ...address, id: 'test' }])); + component.eligibleAddresses$ = of([address, { ...address, id: 'test' }]); }); it('should render if shipping is set', () => { @@ -226,7 +219,7 @@ describe('Basket Shipping Address Widget Component', () => { beforeEach(() => { when(checkoutFacade.basketShippingAddress$).thenReturn(of(addresses[1])); - when(accountFacade.addresses$()).thenReturn(of(addresses)); + component.eligibleAddresses$ = of(addresses); }); it('should only use valid addresses for selection display', done => { diff --git a/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.ts b/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.ts index 49cf024553..99f290eddc 100644 --- a/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.ts +++ b/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.ts @@ -3,7 +3,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { FormlyFieldConfig } from '@ngx-formly/core/lib/core'; import { BehaviorSubject, Observable, combineLatest } from 'rxjs'; -import { filter, map, shareReplay, take } from 'rxjs/operators'; +import { filter, map, take } from 'rxjs/operators'; import { AccountFacade } from 'ish-core/facades/account.facade'; import { CheckoutFacade } from 'ish-core/facades/checkout.facade'; @@ -22,6 +22,7 @@ import { FormsService } from 'ish-shared/forms/utils/forms.service'; changeDetection: ChangeDetectionStrategy.Default, }) export class BasketShippingAddressWidgetComponent implements OnInit { + @Input({ required: true }) eligibleAddresses$: Observable; @Input() showErrors = true; @Output() collapseChange = new BehaviorSubject(true); @@ -36,7 +37,6 @@ export class BasketShippingAddressWidgetComponent implements OnInit { shippingAddress$: Observable
; addresses$: Observable; - customerAddresses$: Observable; displayAddAddressLink$: Observable; basketInvoiceAndShippingAddressEqual$: Observable; @@ -61,7 +61,6 @@ export class BasketShippingAddressWidgetComponent implements OnInit { } ngOnInit() { - this.customerAddresses$ = this.accountFacade.addresses$().pipe(shareReplay(1)); this.shippingAddress$ = this.checkoutFacade.basketShippingAddress$; this.basketInvoiceAndShippingAddressEqual$ = this.checkoutFacade.basketInvoiceAndShippingAddressEqual$; this.basketShippingAddressDeletable$ = this.checkoutFacade.basketShippingAddressDeletable$; @@ -78,7 +77,7 @@ export class BasketShippingAddressWidgetComponent implements OnInit { .subscribe(label => (this.emptyOptionLabel = label)); // prepare data for shipping select drop down - this.addresses$ = combineLatest([this.customerAddresses$, this.shippingAddress$]).pipe( + this.addresses$ = combineLatest([this.eligibleAddresses$, this.shippingAddress$]).pipe( map(([addresses, shippingAddress]) => addresses?.filter(address => address.shipToAddress).filter(address => address.id !== shippingAddress?.id) )