Skip to content

Commit

Permalink
feat: recheck concardis credit card cvc if necessary (#359)
Browse files Browse the repository at this point in the history
If the user pays with a saved Concardis credit card the cvc code is requested on checkout payment page if necessary.
  • Loading branch information
rinkeshrsys authored and shauke committed Sep 29, 2020
1 parent dee2bba commit c82b493
Show file tree
Hide file tree
Showing 23 changed files with 615 additions and 10 deletions.
5 changes: 5 additions & 0 deletions src/app/core/facades/checkout.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
updateBasketAddress,
updateBasketItems,
updateBasketShippingMethod,
updateConcardisCvcLastUpdated,
} from 'ish-core/store/customer/basket';
import { getOrdersError, getSelectedOrder } from 'ish-core/store/customer/orders';
import { getLoggedInUser } from 'ish-core/store/customer/user';
Expand Down Expand Up @@ -172,4 +173,8 @@ export class CheckoutFacade {
removePromotionCodeFromBasket(code: string) {
this.store.dispatch(removePromotionCodeFromBasket({ code }));
}

updateConcardisCvcLastUpdated(paymentInstrument: PaymentInstrument) {
this.store.dispatch(updateConcardisCvcLastUpdated({ paymentInstrument }));
}
}
51 changes: 51 additions & 0 deletions src/app/core/services/payment/payment.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,26 @@ describe('Payment Service', () => {
],
};

const creditCardPaymentInstrument = {
id: 'UZUKgzzAppcAAAFzK9FDCMcG',
parameters: [
{
name: 'paymentInstrumentId',
value: '****************************',
},
{
name: 'cvcLastUpdated',
value: '2020-04-30T13:41:45Z',
},
{
name: 'token',
value: 'payment_instrument_123',
},
],
paymentMethod: 'Concardis_CreditCard',
urn: 'urn:payment-instrument:basket:_3oKgzzAfGgAAAFzuFpDCMcE:UZUKgzzAppcAAAFzK9FDCMcG',
};

beforeEach(() => {
apiService = mock(ApiService);
appFacade = mock(AppFacade);
Expand Down Expand Up @@ -193,6 +213,22 @@ describe('Payment Service', () => {
done();
});
});

it("should update payment instrument from basket when 'updateBasketPaymentInstrument' is called", done => {
when(apiService.patch(anyString(), anything(), anything())).thenReturn(of({}));
paymentService
.updateConcardisCvcLastUpdated(BasketMockData.getBasket(), creditCardPaymentInstrument)
.subscribe(() => {
verify(
apiService.patch(
`baskets/${BasketMockData.getBasket().id}/payment-instruments/${creditCardPaymentInstrument.id}`,
anything(),
anything()
)
).once();
done();
});
});
});

describe('user payment service', () => {
Expand Down Expand Up @@ -251,5 +287,20 @@ describe('Payment Service', () => {
done();
});
});

it("should update payment instrument from customer when 'updateBasketPaymentInstrument' is called", done => {
const userCreditCardPaymentInstrument: PaymentInstrument = {
...creditCardPaymentInstrument,
urn: 'urn:payment-instrument:user:fIQKAE8B4tYAAAFu7tuO4P6T:ug8KAE8B1dcAAAFvNQqSJBQg',
};

when(apiService.put(anyString(), anything())).thenReturn(of({}));
paymentService
.updateConcardisCvcLastUpdated(BasketMockData.getBasket(), userCreditCardPaymentInstrument)
.subscribe(() => {
verify(apiService.put(`customers/-/payments/${userCreditCardPaymentInstrument.id}`, anything())).once();
done();
});
});
});
});
49 changes: 49 additions & 0 deletions src/app/core/services/payment/payment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,4 +325,53 @@ export class PaymentService {
)
);
}

/**
* Update CvcLastUpdated in concardis credit card (user/basket) payment instrument.
* @param basket The basket.
* @param paymentInstrument The payment instrument, that is to be updated
*/
updateConcardisCvcLastUpdated(basket: Basket, paymentInstrument: PaymentInstrument): Observable<PaymentInstrument> {
if (!basket) {
return throwError('updateConcardisCvcLastUpdated() called without basket');
}
if (!paymentInstrument) {
return throwError('updateConcardisCvcLastUpdated() called without paymentInstrument');
}

if (paymentInstrument.urn?.includes('basket')) {
// update basket payment instrument
const body: {
parameters?: {
name: string;
value: string;
}[];
} = {
parameters: paymentInstrument.parameters
.filter(attr => attr.name === 'cvcLastUpdated')
.map(attr => ({ name: attr.name, value: attr.value })),
};

return this.apiService
.patch(`baskets/${basket.id}/payment-instruments/${paymentInstrument.id}`, body, {
headers: this.basketHeaders,
})
.pipe(map(({ data }) => data));
} else {
// update user payment instrument
const body: {
name: string;
parameters?: {
key: string;
property: string;
}[];
} = {
name: paymentInstrument.paymentMethod,
parameters: paymentInstrument.parameters.map(attr => ({ key: attr.name, property: attr.value })),
};

// TODO: Replace this PUT request with PATCH request once it is fixed in ICM
return this.apiService.put(`customers/-/payments/${paymentInstrument.id}`, body).pipe(mapTo(paymentInstrument));
}
}
}
19 changes: 19 additions & 0 deletions src/app/core/store/customer/basket/basket-payment.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import {
updateBasketPayment,
updateBasketPaymentFail,
updateBasketPaymentSuccess,
updateConcardisCvcLastUpdated,
updateConcardisCvcLastUpdatedFail,
updateConcardisCvcLastUpdatedSuccess,
} from './basket.actions';
import { getCurrentBasket, getCurrentBasketId } from './basket.selectors';

Expand Down Expand Up @@ -163,4 +166,20 @@ export class BasketPaymentEffects {
mapTo(loadBasketEligiblePaymentMethods())
)
);
/**
* Update CvcLastUpdated for Concardis Credit Card.
*/
updateConcardisCvcLastUpdated$ = createEffect(() =>
this.actions$.pipe(
ofType(updateConcardisCvcLastUpdated),
mapToPayloadProperty('paymentInstrument'),
withLatestFrom(this.store.pipe(select(getCurrentBasket))),
concatMap(([paymentInstrument, basket]) =>
this.paymentService.updateConcardisCvcLastUpdated(basket, paymentInstrument).pipe(
map(pi => updateConcardisCvcLastUpdatedSuccess({ paymentInstrument: pi })),
mapErrorToAction(updateConcardisCvcLastUpdatedFail)
)
)
)
);
}
15 changes: 15 additions & 0 deletions src/app/core/store/customer/basket/basket.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,18 @@ export const deleteBasketPaymentFail = createAction('[Basket API] Delete Basket
export const deleteBasketPaymentSuccess = createAction('[Basket API] Delete Basket Payment Success');

export const resetBasketErrors = createAction('[Basket Internal] Reset Basket and Basket Promotion Errors');

export const updateConcardisCvcLastUpdated = createAction(
'[Basket] Update CvcLastUpdated for Concardis Credit Card ',
payload<{ paymentInstrument: PaymentInstrument }>()
);

export const updateConcardisCvcLastUpdatedFail = createAction(
'[Basket API] Update CvcLastUpdated for Concardis Credit Card Fail',
httpError()
);

export const updateConcardisCvcLastUpdatedSuccess = createAction(
'[Basket API] Update CvcLastUpdated for Concardis Credit Card Success',
payload<{ paymentInstrument: PaymentInstrument }>()
);
22 changes: 20 additions & 2 deletions src/app/core/store/customer/basket/basket.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ import {
updateBasketPaymentFail,
updateBasketPaymentSuccess,
updateBasketShippingMethod,
updateConcardisCvcLastUpdated,
updateConcardisCvcLastUpdatedFail,
updateConcardisCvcLastUpdatedSuccess,
} from './basket.actions';

export interface BasketState {
Expand Down Expand Up @@ -109,7 +112,8 @@ export const basketReducer = createReducer(
setBasketPayment,
createBasketPayment,
updateBasketPayment,
deleteBasketPayment
deleteBasketPayment,
updateConcardisCvcLastUpdated
),
setErrorOn(
mergeBasketFail,
Expand All @@ -125,7 +129,8 @@ export const basketReducer = createReducer(
setBasketPaymentFail,
createBasketPaymentFail,
updateBasketPaymentFail,
deleteBasketPaymentFail
deleteBasketPaymentFail,
updateConcardisCvcLastUpdatedFail
),
on(addPromotionCodeToBasketFail, (state: BasketState, action) => {
const { error } = action.payload;
Expand Down Expand Up @@ -212,5 +217,18 @@ export const basketReducer = createReducer(
info: undefined,
promotionError: undefined,
validationResults: initialValidationResults,
})),

on(updateConcardisCvcLastUpdatedSuccess, (state: BasketState, action) => ({
...state,
basket: {
...state.basket,
payment: {
...state.basket.payment,
paymentInstrument: action.payload.paymentInstrument,
},
},
loading: false,
error: undefined,
}))
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<ish-modal-dialog-link
*ngIf="paymentInstrument.paymentMethod === 'Concardis_DirectDebit'"
(click)="showSepaMandateText()"
linkText="account.payment.view_sepa_mandate.link"
[options]="{ titleText: 'account.payment.view_sepa_mandate_text.link' | translate }"
>
<div class="col-md-12">
<dl class="row dl-horizontal dl-separator">
<dt class="col-md-4">{{ 'account.payment.sepa_mandate_Reference' | translate }}</dt>
<dd class="col-md-8">{{ mandateReference }}</dd>
</dl>
<dl data-testing-id="mandate-text">
<dd>{{ mandateText }}</dd>
</dl>
<dl class="row dl-horizontal dl-separator">
<dt class="col-md-4">{{ 'account.payment.sepa_accepted_on' | translate }}</dt>
<dd class="col-md-8">{{ mandateCreatedDateTime | ishDate }}</dd>
</dl>
</div>
</ish-modal-dialog-link>
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent, MockPipe } from 'ng-mocks';

import { DatePipe } from 'ish-core/pipes/date.pipe';
import { ModalDialogLinkComponent } from 'ish-shared/components/common/modal-dialog-link/modal-dialog-link.component';

import { AccountPaymentConcardisDirectdebitComponent } from './account-payment-concardis-directdebit.component';

describe('Account Payment Concardis Directdebit Component', () => {
let component: AccountPaymentConcardisDirectdebitComponent;
let fixture: ComponentFixture<AccountPaymentConcardisDirectdebitComponent>;
let element: HTMLElement;

const mandateTextValue = 'mandate_text';
const mandateCreatedDateTimeValue = '1597644563000';

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [
AccountPaymentConcardisDirectdebitComponent,
MockComponent(ModalDialogLinkComponent),
MockPipe(DatePipe),
],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(AccountPaymentConcardisDirectdebitComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;

component.paymentInstrument = {
id: '4321',
paymentMethod: 'Concardis_DirectDebit',
parameters: [
{
name: 'mandateReference',
value: 'mandate_reference',
},
{
name: 'mandateText',
value: mandateTextValue,
},
{
name: 'mandateCreatedDateTime',
value: mandateCreatedDateTimeValue,
},
],
};
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

it('should show sepa mandate text on executing showSepaMandateText()', () => {
fixture.detectChanges();
component.showSepaMandateText();
expect(element.querySelector('[data-testing-id="mandate-text"]')).toBeTruthy();
expect(component.mandateText).toEqual(mandateTextValue);
expect(component.mandateCreatedDateTime.toString()).toEqual(mandateCreatedDateTimeValue);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-instrument.model';

@Component({
selector: 'ish-account-payment-concardis-directdebit',
templateUrl: './account-payment-concardis-directdebit.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccountPaymentConcardisDirectdebitComponent {
@Input() paymentInstrument: PaymentInstrument;

mandateReference: string;
mandateText: string;
mandateCreatedDateTime: number;

/**
* Show SEPA mandate information
*/
showSepaMandateText() {
this.mandateReference = this.paymentInstrument.parameters
.find(param => param.name === 'mandateReference')
.value.toString();
this.mandateText = this.paymentInstrument.parameters.find(param => param.name === 'mandateText').value.toString();
const mandateCreatedDatetimeStr = this.paymentInstrument.parameters
.find(param => param.name === 'mandateCreatedDateTime')
.value.toString();
this.mandateCreatedDateTime = Number(mandateCreatedDatetimeStr) || 0;
}
}
3 changes: 2 additions & 1 deletion src/app/pages/account-payment/account-payment-page.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router';

import { SharedModule } from 'ish-shared/shared.module';

import { AccountPaymentConcardisDirectdebitComponent } from './account-payment-concardis-directdebit/account-payment-concardis-directdebit.component';
import { AccountPaymentPageComponent } from './account-payment-page.component';
import { AccountPaymentComponent } from './account-payment/account-payment.component';

Expand All @@ -15,6 +16,6 @@ const routes: Routes = [

@NgModule({
imports: [RouterModule.forChild(routes), SharedModule],
declarations: [AccountPaymentComponent, AccountPaymentPageComponent],
declarations: [AccountPaymentComponent, AccountPaymentConcardisDirectdebitComponent, AccountPaymentPageComponent],
})
export class AccountPaymentPageModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ <h4>
</a>
</div>
<p>{{ pi.accountIdentifier }}</p>
<a *ngIf="!isPreferred" (click)="setAsDefaultPayment(pi.id)">{{ 'account.payment.preferred.link' | translate }}</a>
<a *ngIf="!isPreferred" (click)="setAsDefaultPayment(pi.id)">{{ 'account.payment.preferred.link' | translate }}</a
><br />
<ish-account-payment-concardis-directdebit [paymentInstrument]="pi"></ish-account-payment-concardis-directdebit>
</div>
</ng-template>
Loading

0 comments on commit c82b493

Please sign in to comment.