Skip to content

Commit

Permalink
feat: select payment methods as default in the payment settings (#1159)
Browse files Browse the repository at this point in the history
Co-authored-by: max.kless@googlemail.com <max.kless@googlemail.com>
  • Loading branch information
2 people authored and shauke committed Jul 21, 2022
1 parent 88d5952 commit 87a19f8
Show file tree
Hide file tree
Showing 23 changed files with 433 additions and 181 deletions.
27 changes: 27 additions & 0 deletions e2e/cypress/integration/pages/account/payment.page.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { waitLoadingEnd } from '../../framework';
import { HeaderModule } from '../header.module';

export class PaymentPage {
Expand All @@ -9,7 +10,33 @@ export class PaymentPage {
return cy.get(this.tag);
}

get preferredPaymentMethod() {
return cy.get(this.tag).find('[data-testing-id="preferred-payment-method"]');
}

get noPreferredPaymentOption() {
return cy.get(this.tag).find('#paymentOption_empty');
}

static navigateTo() {
cy.visit('/account/payment');
}

selectPayment(payment: 'INVOICE' | 'CASH_ON_DELIVERY' | 'CASH_IN_ADVANCE') {
cy.get(this.tag).find(`#paymentOption_ISH_${payment}`).check();
cy.wait(1500);
waitLoadingEnd();
}

selectCreditCard() {
cy.get(this.tag).find('div[data-testing-id="paymentMethodList"] input').first().check();
cy.wait(1500);
waitLoadingEnd();
}

selectNoPreferredPayment() {
this.noPreferredPaymentOption.check();
cy.wait(1500);
waitLoadingEnd();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,10 @@ describe('Checkout Payment', () => {
});
});

it('should set first addresses automatically', () => {
it('should continue checkout up to the payment page', () => {
at(CheckoutAddressesPage, page => {
page.continueCheckout();
});
});

it('should accept default shipping option', () => {
at(CheckoutShippingPage, page => page.continueCheckout());
});

Expand Down Expand Up @@ -150,6 +147,32 @@ describe('Checkout Payment', () => {
page.content.should('contain', '********1111');
page.content.should('not.contain', '********4444');
page.content.should('not.contain', '****************0000');
page.preferredPaymentMethod.should('not.exist');
page.noPreferredPaymentOption.should('not.exist');
});
});

it('should set a unparametrized payment method as preferred payment method', () => {
at(PaymentPage, page => {
page.selectPayment('INVOICE');
page.preferredPaymentMethod.should('contain', 'Invoice');
page.noPreferredPaymentOption.should('be.visible');
});
});

it('should set a parametrized payment method as preferred payment method', () => {
at(PaymentPage, page => {
page.selectCreditCard();
page.preferredPaymentMethod.should('contain', 'ISH Demo Credit Card');
page.noPreferredPaymentOption.should('be.visible');
});
});

it('should reset preferred payment method', () => {
at(PaymentPage, page => {
page.selectNoPreferredPayment();
page.preferredPaymentMethod.should('not.exist');
page.noPreferredPaymentOption.should('not.exist');
});
});
});
Expand Down
48 changes: 46 additions & 2 deletions src/app/core/facades/account.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Customer, CustomerRegistrationType, SsoRegistrationType } from 'ish-cor
import { HttpError } from 'ish-core/models/http-error/http-error.model';
import { PasswordReminderUpdate } from 'ish-core/models/password-reminder-update/password-reminder-update.model';
import { PasswordReminder } from 'ish-core/models/password-reminder/password-reminder.model';
import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-instrument.model';
import { User } from 'ish-core/models/user/user.model';
import { MessagesPayloadType } from 'ish-core/store/core/messages';
import {
Expand Down Expand Up @@ -51,6 +52,7 @@ import {
updateUser,
updateUserPassword,
updateUserPasswordByPasswordReminder,
updateUserPreferredPayment,
} from 'ish-core/store/customer/user';
import { whenTruthy } from 'ish-core/utils/operators';

Expand Down Expand Up @@ -164,8 +166,50 @@ export class AccountFacade {
return this.eligiblePaymentMethods$;
}

deletePaymentInstrument(paymentInstrumentId: string) {
this.store.dispatch(deleteUserPaymentInstrument({ id: paymentInstrumentId }));
deletePaymentInstrument(paymentInstrumentId: string, successMessage?: MessagesPayloadType) {
this.store.dispatch(deleteUserPaymentInstrument({ id: paymentInstrumentId, successMessage }));
}

async updateUserPreferredPaymentMethod(
user: User,
paymentMethodId: string,
currentPreferredPaymentInstrument: PaymentInstrument
) {
if (currentPreferredPaymentInstrument && !currentPreferredPaymentInstrument.parameters?.length) {
this.deletePaymentInstrument(currentPreferredPaymentInstrument.id);
await new Promise(resolve => setTimeout(resolve, 600)); // prevent server conflicts
}

this.store.dispatch(
updateUserPreferredPayment({
user,
paymentMethodId,
successMessage: {
message: 'account.payment.payment_created.message',
},
})
);
}

async updateUserPreferredPaymentInstrument(
user: User,
paymentInstrumentId: string,
currentPreferredPaymentInstrument: PaymentInstrument
) {
if (currentPreferredPaymentInstrument && !currentPreferredPaymentInstrument.parameters?.length) {
this.deletePaymentInstrument(currentPreferredPaymentInstrument.id);
await new Promise(resolve => setTimeout(resolve, 600)); // prevent server conflicts
}
this.store.dispatch(
updateUser({
user: { ...user, preferredPaymentInstrumentId: paymentInstrumentId },
successMessage: {
message: paymentInstrumentId
? 'account.payment.payment_created.message'
: 'account.payment.no_preferred.message',
},
})
);
}

// ADDRESSES
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface PaymentMethodBaseData {
paymentInstruments?: string[];
parameterDefinitions?: PaymentMethodParameterType[];
hostedPaymentPageParameters?: { name: string; value: string }[];
paymentParameters?: { name: string; key: string }[]; // Needed for old payment method user REST api
}

export interface PaymentMethodData {
Expand Down
8 changes: 6 additions & 2 deletions src/app/core/models/payment-method/payment-method.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,16 @@ export class PaymentMethodMapper {
return [];
}

// return only payment methods that have also a payment instrument
const pmBlacklist = ['ISH_FASTPAY', 'ISH_INVOICE_TOTAL_ZERO'];

// return only payment methods that have either payment instruments or no parameters
return options.methods[0].payments
.map(pm => ({
id: pm.id,
serviceId: pm.id, // is missing
displayName: pm.displayName,
restrictionCauses: pm.restrictions,
hasParameters: !!pm.paymentParameters?.length,
paymentInstruments: options.instruments
.filter(i => i.name === pm.id)
.map(i => ({
Expand All @@ -78,7 +81,8 @@ export class PaymentMethodMapper {
paymentMethod: pm.id,
})),
}))
.filter(x => x.paymentInstruments?.length);
.filter(pm => !pmBlacklist.includes(pm.serviceId))
.filter(pm => !pm.hasParameters || pm.paymentInstruments?.length);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/app/core/models/payment-method/payment-method.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export interface PaymentMethod {
paymentInstruments?: PaymentInstrument[];
parameters?: FormlyFieldConfig[];
hostedPaymentPageParameters?: { name: string; value: string }[];
hasParameters?: boolean; // Needed for old payment method user REST api
}
12 changes: 5 additions & 7 deletions src/app/core/services/payment/payment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,12 +273,8 @@ export class PaymentService {
if (!customerNo) {
return throwError(() => new Error('createUserPayment called without required customer number'));
}
if (!paymentInstrument) {
return throwError(() => new Error('createUserPayment called without required payment instrument'));
}

if (!paymentInstrument.parameters || !paymentInstrument.parameters.length) {
return throwError(() => new Error('createUserPayment called without required payment parameters'));
if (!paymentInstrument?.paymentMethod) {
return throwError(() => new Error('createUserPayment called without required valid payment instrument'));
}

const body: {
Expand All @@ -289,7 +285,9 @@ export class PaymentService {
}[];
} = {
name: paymentInstrument.paymentMethod,
parameters: paymentInstrument.parameters.map(attr => ({ key: attr.name, property: attr.value })),
parameters: paymentInstrument.parameters?.length
? paymentInstrument.parameters.map(attr => ({ key: attr.name, property: attr.value }))
: undefined,
};

return this.appFacade.customerRestResource$.pipe(
Expand Down
4 changes: 3 additions & 1 deletion src/app/core/services/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ export class UserService {
...body.user,
preferredInvoiceToAddress: { urn: body.user.preferredInvoiceToAddressUrn },
preferredShipToAddress: { urn: body.user.preferredShipToAddressUrn },
preferredPaymentInstrument: { id: body.user.preferredPaymentInstrumentId },
preferredPaymentInstrument: body.user.preferredPaymentInstrumentId
? { id: body.user.preferredPaymentInstrumentId }
: {},
preferredInvoiceToAddressUrn: undefined,
preferredShipToAddressUrn: undefined,
preferredPaymentInstrumentId: undefined,
Expand Down
5 changes: 3 additions & 2 deletions src/app/core/store/core/messages/messages.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Store, select } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { ActiveToast, IndividualConfig, ToastrService } from 'ngx-toastr';
import { OperatorFunction, Subject, combineLatest } from 'rxjs';
import { map, switchMap, take, tap, withLatestFrom } from 'rxjs/operators';
import { filter, map, switchMap, take, tap, withLatestFrom } from 'rxjs/operators';

import { getDeviceType } from 'ish-core/store/core/configuration';
import { isStickyHeader } from 'ish-core/store/core/viewconf';
Expand Down Expand Up @@ -82,6 +82,7 @@ export class MessagesEffects {
this.actions$.pipe(
ofType(displaySuccessMessage),
mapToPayload(),
filter(payload => !!payload?.message),
this.composeToastServiceArguments(),
map(args => this.toastr.success(...args)),
tap(() => {
Expand Down Expand Up @@ -128,9 +129,9 @@ export class MessagesEffects {
withLatestFrom(this.store.pipe(select(getDeviceType))),
map(([payload, deviceType]) => {
const timeOut = payload.duration ?? duration ?? 5000;

return [
// message translation

this.translate.instant(payload.message, payload.messageParams),
// title translation
payload.title ? this.translate.instant(payload.title, payload.titleParams) : payload.title,
Expand Down
7 changes: 6 additions & 1 deletion src/app/core/store/customer/user/user.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export const updateUserSuccess = createAction(

export const updateUserFail = createAction('[User API] Update User Failed', httpError());

export const updateUserPreferredPayment = createAction(
'[User] Update User Preferred Payment',
payload<{ user: User; paymentMethodId: string; successMessage?: MessagesPayloadType }>()
);

export const updateUserPassword = createAction(
'[User] Update User Password',
payload<{ password: string; currentPassword: string; successMessage?: MessagesPayloadType }>()
Expand Down Expand Up @@ -91,7 +96,7 @@ export const loadUserPaymentMethodsSuccess = createAction(

export const deleteUserPaymentInstrument = createAction(
'[User] Delete User Instrument Payment ',
payload<{ id: string }>()
payload<{ id: string; successMessage?: MessagesPayloadType }>()
);

export const deleteUserPaymentInstrumentFail = createAction(
Expand Down
59 changes: 58 additions & 1 deletion src/app/core/store/customer/user/user.effects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
updateUserPassword,
updateUserPasswordFail,
updateUserPasswordSuccess,
updateUserPreferredPayment,
updateUserSuccess,
} from './user.actions';
import { UserEffects } from './user.effects';
Expand Down Expand Up @@ -94,6 +95,7 @@ describe('User Effects', () => {
when(userServiceMock.requestPasswordReminder(anything())).thenReturn(of({}));
when(userServiceMock.getEligibleCostCenters()).thenReturn(of([]));
when(paymentServiceMock.getUserPaymentMethods(anything())).thenReturn(of([]));
when(paymentServiceMock.createUserPayment(anything(), anything())).thenReturn(of({ id: 'paymentInstrumentId' }));
when(paymentServiceMock.deleteUserPaymentInstrument(anyString(), anyString())).thenReturn(of(undefined));
when(apiTokenServiceMock.hasUserApiTokenCookie()).thenReturn(false);

Expand Down Expand Up @@ -646,7 +648,10 @@ describe('User Effects', () => {
});

it('should dispatch a DeleteUserPaymentSuccess action on successful', () => {
const action = deleteUserPaymentInstrument({ id: 'paymentInstrumentId' });
const action = deleteUserPaymentInstrument({
id: 'paymentInstrumentId',
successMessage: { message: 'account.payment.payment_deleted.message' },
});
const completion1 = deleteUserPaymentInstrumentSuccess();
const completion2 = loadUserPaymentMethods();
const completion3 = displaySuccessMessage({
Expand Down Expand Up @@ -675,6 +680,58 @@ describe('User Effects', () => {
expect(effects.deleteUserPayment$).toBeObservable(expected$);
});
});

describe('updatePreferredUserPayment$', () => {
beforeEach(() => {
store$.dispatch(
loginUserSuccess({
customer,
user: {} as User,
})
);
});

it('should call the payment service when UpdateUserPreferredPayment event is called', done => {
const action = updateUserPreferredPayment({ user: {} as User, paymentMethodId: 'paymentInstrumentId' });
actions$ = of(action);
effects.updatePreferredUserPayment$.subscribe(() => {
verify(paymentServiceMock.createUserPayment(customer.customerNo, anything())).once();
done();
});
});

it('should dispatch a UpdateUser action on successful payment instrument creation', () => {
const action = updateUserPreferredPayment({
user: {} as User,
paymentMethodId: 'paymentInstrumentId',
});
const completion1 = updateUser({ user: { preferredPaymentInstrumentId: 'paymentInstrumentId' } as User });
const completion2 = loadUserPaymentMethods();

actions$ = hot('-a', { a: action });
const expected$ = cold('-(cd)', { c: completion1, d: completion2 });

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

it('should dispatch a UpdateUserFail action on failed', () => {
const error = makeHttpError({ status: 401, code: 'error' });
when(paymentServiceMock.createUserPayment(anything(), anything())).thenReturn(throwError(() => error));

const action = updateUserPreferredPayment({
user: {} as User,
paymentMethodId: 'paymentInstrumentId',
});
const completion = updateUserFail({
error,
});

actions$ = hot('-a-a-a', { a: action });
const expected$ = cold('-c-c-c', { c: completion });
expect(effects.updatePreferredUserPayment$).toBeObservable(expected$);
});
});

describe('requestPasswordReminder$', () => {
const data: PasswordReminder = {
email: 'patricia@test.intershop.de',
Expand Down
Loading

0 comments on commit 87a19f8

Please sign in to comment.