From c82b493b106facfb6f90786bc32014dff766fd24 Mon Sep 17 00:00:00 2001 From: rinkeshrsys <67738328+rinkeshrsys@users.noreply.github.com> Date: Tue, 29 Sep 2020 19:05:14 +0530 Subject: [PATCH] feat: recheck concardis credit card cvc if necessary (#359) If the user pays with a saved Concardis credit card the cvc code is requested on checkout payment page if necessary. --- src/app/core/facades/checkout.facade.ts | 5 + .../services/payment/payment.service.spec.ts | 51 ++++++ .../core/services/payment/payment.service.ts | 49 ++++++ .../customer/basket/basket-payment.effects.ts | 19 +++ .../store/customer/basket/basket.actions.ts | 15 ++ .../store/customer/basket/basket.reducer.ts | 22 ++- ...yment-concardis-directdebit.component.html | 20 +++ ...nt-concardis-directdebit.component.spec.ts | 67 ++++++++ ...payment-concardis-directdebit.component.ts | 30 ++++ .../account-payment-page.module.ts | 3 +- .../account-payment.component.html | 4 +- .../account-payment.component.spec.ts | 9 +- .../checkout-payment-page.module.ts | 2 + .../checkout-payment.component.html | 24 ++- .../checkout-payment.component.spec.ts | 7 + .../checkout-payment.component.ts | 3 +- ...ardis-creditcard-cvc-detail.component.html | 21 +++ ...is-creditcard-cvc-detail.component.spec.ts | 94 ++++++++++ ...ncardis-creditcard-cvc-detail.component.ts | 160 ++++++++++++++++++ .../payment-concardis-creditcard.component.ts | 2 + src/assets/i18n/de_DE.json | 6 + src/assets/i18n/en_US.json | 6 + src/assets/i18n/fr_FR.json | 6 + 23 files changed, 615 insertions(+), 10 deletions(-) create mode 100644 src/app/pages/account-payment/account-payment-concardis-directdebit/account-payment-concardis-directdebit.component.html create mode 100644 src/app/pages/account-payment/account-payment-concardis-directdebit/account-payment-concardis-directdebit.component.spec.ts create mode 100644 src/app/pages/account-payment/account-payment-concardis-directdebit/account-payment-concardis-directdebit.component.ts create mode 100644 src/app/pages/checkout-payment/payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component.html create mode 100644 src/app/pages/checkout-payment/payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component.spec.ts create mode 100644 src/app/pages/checkout-payment/payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component.ts diff --git a/src/app/core/facades/checkout.facade.ts b/src/app/core/facades/checkout.facade.ts index b5effb84ef..ce42ac6101 100644 --- a/src/app/core/facades/checkout.facade.ts +++ b/src/app/core/facades/checkout.facade.ts @@ -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'; @@ -172,4 +173,8 @@ export class CheckoutFacade { removePromotionCodeFromBasket(code: string) { this.store.dispatch(removePromotionCodeFromBasket({ code })); } + + updateConcardisCvcLastUpdated(paymentInstrument: PaymentInstrument) { + this.store.dispatch(updateConcardisCvcLastUpdated({ paymentInstrument })); + } } diff --git a/src/app/core/services/payment/payment.service.spec.ts b/src/app/core/services/payment/payment.service.spec.ts index ed5d8d89ed..382496e3b2 100644 --- a/src/app/core/services/payment/payment.service.spec.ts +++ b/src/app/core/services/payment/payment.service.spec.ts @@ -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); @@ -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', () => { @@ -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(); + }); + }); }); }); diff --git a/src/app/core/services/payment/payment.service.ts b/src/app/core/services/payment/payment.service.ts index d368cb9762..1f750c8896 100644 --- a/src/app/core/services/payment/payment.service.ts +++ b/src/app/core/services/payment/payment.service.ts @@ -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 { + 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)); + } + } } diff --git a/src/app/core/store/customer/basket/basket-payment.effects.ts b/src/app/core/store/customer/basket/basket-payment.effects.ts index 454be1f727..2da9e88469 100644 --- a/src/app/core/store/customer/basket/basket-payment.effects.ts +++ b/src/app/core/store/customer/basket/basket-payment.effects.ts @@ -25,6 +25,9 @@ import { updateBasketPayment, updateBasketPaymentFail, updateBasketPaymentSuccess, + updateConcardisCvcLastUpdated, + updateConcardisCvcLastUpdatedFail, + updateConcardisCvcLastUpdatedSuccess, } from './basket.actions'; import { getCurrentBasket, getCurrentBasketId } from './basket.selectors'; @@ -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) + ) + ) + ) + ); } diff --git a/src/app/core/store/customer/basket/basket.actions.ts b/src/app/core/store/customer/basket/basket.actions.ts index eb9c6a3b7c..7d4d5e3b1e 100644 --- a/src/app/core/store/customer/basket/basket.actions.ts +++ b/src/app/core/store/customer/basket/basket.actions.ts @@ -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 }>() +); diff --git a/src/app/core/store/customer/basket/basket.reducer.ts b/src/app/core/store/customer/basket/basket.reducer.ts index a0558b16a6..f77b2ac2cb 100644 --- a/src/app/core/store/customer/basket/basket.reducer.ts +++ b/src/app/core/store/customer/basket/basket.reducer.ts @@ -58,6 +58,9 @@ import { updateBasketPaymentFail, updateBasketPaymentSuccess, updateBasketShippingMethod, + updateConcardisCvcLastUpdated, + updateConcardisCvcLastUpdatedFail, + updateConcardisCvcLastUpdatedSuccess, } from './basket.actions'; export interface BasketState { @@ -109,7 +112,8 @@ export const basketReducer = createReducer( setBasketPayment, createBasketPayment, updateBasketPayment, - deleteBasketPayment + deleteBasketPayment, + updateConcardisCvcLastUpdated ), setErrorOn( mergeBasketFail, @@ -125,7 +129,8 @@ export const basketReducer = createReducer( setBasketPaymentFail, createBasketPaymentFail, updateBasketPaymentFail, - deleteBasketPaymentFail + deleteBasketPaymentFail, + updateConcardisCvcLastUpdatedFail ), on(addPromotionCodeToBasketFail, (state: BasketState, action) => { const { error } = action.payload; @@ -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, })) ); diff --git a/src/app/pages/account-payment/account-payment-concardis-directdebit/account-payment-concardis-directdebit.component.html b/src/app/pages/account-payment/account-payment-concardis-directdebit/account-payment-concardis-directdebit.component.html new file mode 100644 index 0000000000..9e9d6b910a --- /dev/null +++ b/src/app/pages/account-payment/account-payment-concardis-directdebit/account-payment-concardis-directdebit.component.html @@ -0,0 +1,20 @@ + +
+
+
{{ 'account.payment.sepa_mandate_Reference' | translate }}
+
{{ mandateReference }}
+
+
+
{{ mandateText }}
+
+
+
{{ 'account.payment.sepa_accepted_on' | translate }}
+
{{ mandateCreatedDateTime | ishDate }}
+
+
+
diff --git a/src/app/pages/account-payment/account-payment-concardis-directdebit/account-payment-concardis-directdebit.component.spec.ts b/src/app/pages/account-payment/account-payment-concardis-directdebit/account-payment-concardis-directdebit.component.spec.ts new file mode 100644 index 0000000000..b875110cdb --- /dev/null +++ b/src/app/pages/account-payment/account-payment-concardis-directdebit/account-payment-concardis-directdebit.component.spec.ts @@ -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; + 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); + }); +}); diff --git a/src/app/pages/account-payment/account-payment-concardis-directdebit/account-payment-concardis-directdebit.component.ts b/src/app/pages/account-payment/account-payment-concardis-directdebit/account-payment-concardis-directdebit.component.ts new file mode 100644 index 0000000000..84f6dd5d15 --- /dev/null +++ b/src/app/pages/account-payment/account-payment-concardis-directdebit/account-payment-concardis-directdebit.component.ts @@ -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; + } +} diff --git a/src/app/pages/account-payment/account-payment-page.module.ts b/src/app/pages/account-payment/account-payment-page.module.ts index 1bd70f953e..8521874765 100644 --- a/src/app/pages/account-payment/account-payment-page.module.ts +++ b/src/app/pages/account-payment/account-payment-page.module.ts @@ -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'; @@ -15,6 +16,6 @@ const routes: Routes = [ @NgModule({ imports: [RouterModule.forChild(routes), SharedModule], - declarations: [AccountPaymentComponent, AccountPaymentPageComponent], + declarations: [AccountPaymentComponent, AccountPaymentConcardisDirectdebitComponent, AccountPaymentPageComponent], }) export class AccountPaymentPageModule {} diff --git a/src/app/pages/account-payment/account-payment/account-payment.component.html b/src/app/pages/account-payment/account-payment/account-payment.component.html index 909bfb98be..e968174b26 100644 --- a/src/app/pages/account-payment/account-payment/account-payment.component.html +++ b/src/app/pages/account-payment/account-payment/account-payment.component.html @@ -46,6 +46,8 @@

{{ pi.accountIdentifier }}

- {{ 'account.payment.preferred.link' | translate }} + {{ 'account.payment.preferred.link' | translate }}
+ diff --git a/src/app/pages/account-payment/account-payment/account-payment.component.spec.ts b/src/app/pages/account-payment/account-payment/account-payment.component.spec.ts index f1eb5cbc96..f76abf67e2 100644 --- a/src/app/pages/account-payment/account-payment/account-payment.component.spec.ts +++ b/src/app/pages/account-payment/account-payment/account-payment.component.spec.ts @@ -8,6 +8,8 @@ import { makeHttpError } from 'ish-core/utils/dev/api-service-utils'; import { BasketMockData } from 'ish-core/utils/dev/basket-mock-data'; import { ErrorMessageComponent } from 'ish-shared/components/common/error-message/error-message.component'; +import { AccountPaymentConcardisDirectdebitComponent } from '../account-payment-concardis-directdebit/account-payment-concardis-directdebit.component'; + import { AccountPaymentComponent } from './account-payment.component'; describe('Account Payment Component', () => { @@ -18,7 +20,12 @@ describe('Account Payment Component', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], - declarations: [AccountPaymentComponent, MockComponent(ErrorMessageComponent), MockComponent(FaIconComponent)], + declarations: [ + AccountPaymentComponent, + MockComponent(AccountPaymentConcardisDirectdebitComponent), + MockComponent(ErrorMessageComponent), + MockComponent(FaIconComponent), + ], }).compileComponents(); }); diff --git a/src/app/pages/checkout-payment/checkout-payment-page.module.ts b/src/app/pages/checkout-payment/checkout-payment-page.module.ts index c1b50fbb35..e2077589a0 100644 --- a/src/app/pages/checkout-payment/checkout-payment-page.module.ts +++ b/src/app/pages/checkout-payment/checkout-payment-page.module.ts @@ -4,6 +4,7 @@ import { SharedModule } from 'ish-shared/shared.module'; import { CheckoutPaymentPageComponent } from './checkout-payment-page.component'; import { CheckoutPaymentComponent } from './checkout-payment/checkout-payment.component'; +import { PaymentConcardisCreditcardCvcDetailComponent } from './payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component'; import { PaymentConcardisCreditcardComponent } from './payment-concardis-creditcard/payment-concardis-creditcard.component'; import { PaymentConcardisDirectdebitComponent } from './payment-concardis-directdebit/payment-concardis-directdebit.component'; import { PaymentConcardisComponent } from './payment-concardis/payment-concardis.component'; @@ -15,6 +16,7 @@ import { PaymentConcardisComponent } from './payment-concardis/payment-concardis CheckoutPaymentPageComponent, PaymentConcardisComponent, PaymentConcardisCreditcardComponent, + PaymentConcardisCreditcardCvcDetailComponent, PaymentConcardisDirectdebitComponent, ], }) diff --git a/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.html b/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.html index aafc8574b3..b438602503 100644 --- a/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.html +++ b/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.html @@ -77,7 +77,7 @@

{{ 'checkout.payment.method.select.heading' | translate }}

[attr.data-testing-id]="'payment-parameter-form-' + paymentMethod.id" >
  • -
  • -
  • {{ 'checkout.payment.addPayment.link' | translate }} @@ -177,7 +189,13 @@

    {{ 'checkout.order_details.heading' | translate }}

    -
    diff --git a/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.spec.ts b/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.spec.ts index 44b42db993..11452f5c70 100644 --- a/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.spec.ts +++ b/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.spec.ts @@ -21,6 +21,7 @@ import { ErrorMessageComponent } from 'ish-shared/components/common/error-messag import { ModalDialogLinkComponent } from 'ish-shared/components/common/modal-dialog-link/modal-dialog-link.component'; import { CheckboxComponent } from 'ish-shared/forms/components/checkbox/checkbox.component'; +import { PaymentConcardisCreditcardCvcDetailComponent } from '../payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component'; import { PaymentConcardisCreditcardComponent } from '../payment-concardis-creditcard/payment-concardis-creditcard.component'; import { PaymentConcardisDirectdebitComponent } from '../payment-concardis-directdebit/payment-concardis-directdebit.component'; @@ -52,6 +53,7 @@ describe('Checkout Payment Component', () => { MockComponent(ModalDialogLinkComponent), MockComponent(NgbCollapse), MockComponent(PaymentConcardisCreditcardComponent), + MockComponent(PaymentConcardisCreditcardCvcDetailComponent), MockComponent(PaymentConcardisDirectdebitComponent), MockDirective(ServerHtmlDirective), MockPipe(PricePipe), @@ -175,6 +177,7 @@ describe('Checkout Payment Component', () => { describe('parameter forms', () => { it('should open and close payment form if open/cancel form is triggered', () => { + component.basket.payment = undefined; expect(component.formIsOpen(-1)).toBeTruthy(); component.openPaymentParameterForm(2); expect(component.formIsOpen(2)).toBeTruthy(); @@ -184,6 +187,7 @@ describe('Checkout Payment Component', () => { }); it('should throw createPaymentInstrument event when the user submits a valid parameter form and saving is not allowed', done => { + component.basket.payment = undefined; component.ngOnChanges(paymentMethodChange); component.openPaymentParameterForm(1); @@ -201,6 +205,7 @@ describe('Checkout Payment Component', () => { }); it('should throw createUserPaymentInstrument event when the user submits a valid parameter form and saving is allowed', done => { + component.basket.payment = undefined; component.ngOnChanges(paymentMethodChange); component.openPaymentParameterForm(3); @@ -218,6 +223,7 @@ describe('Checkout Payment Component', () => { }); it('should disable submit button when the user submits an invalid parameter form', () => { + component.basket.payment = undefined; component.openPaymentParameterForm(1); fixture.detectChanges(); @@ -228,6 +234,7 @@ describe('Checkout Payment Component', () => { }); it('should render standard parameter form for standard parametrized form', () => { + component.basket.payment = undefined; component.openPaymentParameterForm(1); component.ngOnChanges(paymentMethodChange); diff --git a/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.ts b/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.ts index 916c697149..80180c18e4 100644 --- a/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.ts +++ b/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.ts @@ -29,7 +29,7 @@ import { markAsDirtyRecursive } from 'ish-shared/forms/utils/form-utils'; @Component({ selector: 'ish-checkout-payment', templateUrl: './checkout-payment.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, + changeDetection: ChangeDetectionStrategy.Default, }) export class CheckoutPaymentComponent implements OnInit, OnChanges, OnDestroy { @Input() basket: Basket; @@ -157,7 +157,6 @@ export class CheckoutPaymentComponent implements OnInit, OnChanges, OnDestroy { openPaymentParameterForm(index: number) { this.formSubmitted = false; this.openFormIndex = index; - // enable / disable the appropriate parameter form controls Object.keys(this.parameterForm.controls).forEach(key => { this.filteredPaymentMethods[index].parameters.find(param => param.key === key) diff --git a/src/app/pages/checkout-payment/payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component.html b/src/app/pages/checkout-payment/payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component.html new file mode 100644 index 0000000000..9c2ba7291a --- /dev/null +++ b/src/app/pages/checkout-payment/payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component.html @@ -0,0 +1,21 @@ +
    +
    + +
    + +
    + +
    +
    diff --git a/src/app/pages/checkout-payment/payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component.spec.ts b/src/app/pages/checkout-payment/payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component.spec.ts new file mode 100644 index 0000000000..8663d73164 --- /dev/null +++ b/src/app/pages/checkout-payment/payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component.spec.ts @@ -0,0 +1,94 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent } from 'ng-mocks'; +import { instance, mock } from 'ts-mockito'; + +import { CheckoutFacade } from 'ish-core/facades/checkout.facade'; +import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-instrument.model'; +import { PaymentMethod } from 'ish-core/models/payment-method/payment-method.model'; +import { InputComponent } from 'ish-shared/forms/components/input/input.component'; + +import { PaymentConcardisCreditcardCvcDetailComponent } from './payment-concardis-creditcard-cvc-detail.component'; + +describe('Payment Concardis Creditcard Cvc Detail Component', () => { + let component: PaymentConcardisCreditcardCvcDetailComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MockComponent(InputComponent), PaymentConcardisCreditcardCvcDetailComponent], + imports: [ReactiveFormsModule, TranslateModule.forRoot()], + providers: [{ provide: CheckoutFacade, useFactory: () => instance(mock(CheckoutFacade)) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PaymentConcardisCreditcardCvcDetailComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + + component.paymentMethod = { + id: 'Concardis_CreditCard', + saveAllowed: false, + serviceId: 'Concardis_CreditCard', + } as PaymentMethod; + + component.paymentInstrument = { + accountIdentifier: 'VISA 4000007XXXX00031 10/23', + id: 'UZUKgzzAppcAAAFzK9FDCMcG', + numberOfPayments: 0, + parameters: [ + { + name: 'paymentInstrumentId', + value: '****************************', + }, + { + name: 'cardType', + value: 'VISA', + }, + { + name: 'cvcLastUpdated', + value: '2020-04-30T13:41:45Z', + }, + { + name: 'token', + value: 'payment_instrument_123', + }, + ], + paymentMethod: 'Concardis_CreditCard', + urn: 'urn:payment-instrument:basket:_3oKgzzAfGgAAAFzuFpDCMcE:UZUKgzzAppcAAAFzK9FDCMcG', + } as PaymentInstrument; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + expect(component.cvcDetailForm.get('cvcDetail')).toBeTruthy(); + }); + + it('should return true when cvc is expired', () => { + expect(component.isCvcExpired()).toBeTruthy(); + }); + + it('should return false when cvc is valid', () => { + component.validityTimeInMinutes = '20'; + component.paymentInstrument.parameters.find( + attribute => attribute.name === 'cvcLastUpdated' + ).value = new Date().toISOString(); + expect(component.isCvcExpired()).toBeFalsy(); + }); + + it('should show an error if submit call back returns with an error', () => { + const errorMessage = 'field is required'; + + fixture.detectChanges(); + component.submitCallback({ + message: { properties: [{ key: 'verification', code: 123, message: errorMessage, messageKey: '' }] }, + }); + + expect(component.errorMessage.cvc.message).toEqual(errorMessage); + }); +}); diff --git a/src/app/pages/checkout-payment/payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component.ts b/src/app/pages/checkout-payment/payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component.ts new file mode 100644 index 0000000000..3b9a37ada9 --- /dev/null +++ b/src/app/pages/checkout-payment/payment-concardis-creditcard-cvc-detail/payment-concardis-creditcard-cvc-detail.component.ts @@ -0,0 +1,160 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { takeUntil } from 'rxjs/operators'; + +import { CheckoutFacade } from 'ish-core/facades/checkout.facade'; +import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-instrument.model'; +import { ScriptLoaderService } from 'ish-core/utils/script-loader/script-loader.service'; +import { markAsDirtyRecursive } from 'ish-shared/forms/utils/form-utils'; + +import { PaymentConcardisComponent } from '../payment-concardis/payment-concardis.component'; + +// allows access to concardis js functionality +// tslint:disable-next-line:no-any +declare var PayEngine: any; + +@Component({ + selector: 'ish-payment-concardis-creditcard-cvc-detail', + templateUrl: './payment-concardis-creditcard-cvc-detail.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +// tslint:disable-next-line: rxjs-prefer-angular-takeuntil +export class PaymentConcardisCreditcardCvcDetailComponent extends PaymentConcardisComponent implements OnInit { + @Input() paymentInstrument: PaymentInstrument; + + validityTimeInMinutes: string; + cvcDetailForm: FormGroup; + + constructor( + protected scriptLoader: ScriptLoaderService, + protected cd: ChangeDetectorRef, + private checkoutFacade: CheckoutFacade + ) { + super(scriptLoader, cd); + this.cvcDetailForm = new FormGroup({ + cvcDetail: new FormControl(undefined, [Validators.required]), + }); + } + + /** + * load concardis script if component is visible + */ + loadScript() { + // load script only once if component becomes visible + if (!this.scriptLoaded) { + const merchantId = this.getParamValue( + 'ConcardisPaymentService.MerchantID', + 'checkout.credit_card.merchantId.error.notFound' + ); + // if config params are missing - don't load script + if (!merchantId) { + return; + } + + this.scriptLoaded = true; + this.scriptLoader + .load(this.getPayEngineURL()) + .pipe(takeUntil(this.destroy$)) + .subscribe( + () => { + PayEngine.setPublishableKey(merchantId); + }, + error => { + this.scriptLoaded = false; + this.errorMessage.general.message = error; + this.cd.detectChanges(); + } + ); + } + this.validityTimeInMinutes = this.getParamValue( + 'intershop.payment.Concardis_CreditCard.cvcmaxage', + 'checkout.credit_card.validityTime.error.notFound' + ); + } + + isCvcExpired() { + let isExpired = true; + // if cvc last updated timestamp is less than maximum validity in minutes then return false + if (this.paymentInstrument.parameters) { + const cvcLastUpdatedAttr = + this.paymentInstrument.parameters && + this.paymentInstrument.parameters.find(attribute => attribute.name === 'cvcLastUpdated'); + + if (cvcLastUpdatedAttr) { + const cvcLastUpdatedValue = cvcLastUpdatedAttr.value ? cvcLastUpdatedAttr.value.toString() : undefined; + if (cvcLastUpdatedValue) { + const cvcDate = new Date(cvcLastUpdatedValue); + const diffAsMinutes = (Date.now() - cvcDate.getTime()) / (1000 * 60); + if (diffAsMinutes <= parseInt(this.validityTimeInMinutes, 10)) { + isExpired = false; + } + } + } + } + return isExpired; + } + + /** + * call back function to submit data + */ + submitCallback(error) { + if (error) { + // map error messages + if (typeof error.message !== 'string' && error.message.properties) { + this.errorMessage.cvc = + error.message.properties && error.message.properties.find(prop => prop.key === 'verification'); + if (this.errorMessage.cvc && this.errorMessage.cvc.code) { + this.errorMessage.cvc.messageKey = this.getErrorMessage( + this.errorMessage.cvc.code, + 'credit_card', + 'cvc', + this.errorMessage.cvc.message + ); + this.cvcDetailForm.get('cvcDetail').setErrors({ + customError: this.errorMessage.cvc.messageKey, + }); + } + } + } else { + // update cvcLastUpdated to current timestamp + const param = + this.paymentInstrument.parameters && + this.paymentInstrument.parameters.map(attr => ({ name: attr.name, value: attr.value })); + if (param.find(attribute => attribute.name === 'cvcLastUpdated')) { + param.find(attribute => attribute.name === 'cvcLastUpdated').value = new Date().toISOString(); + } else { + param.push({ name: 'cvcLastUpdated', value: new Date().toISOString() }); + } + // TODO: Replacing encoded paymentInstrumentId with Token for put request + param.find(attribute => attribute.name === 'paymentInstrumentId').value = this.paymentInstrument.parameters.find( + attribute => attribute.name === 'token' + ).value; + const pi: PaymentInstrument = { + id: this.paymentInstrument.id, + urn: this.paymentInstrument.urn, + accountIdentifier: this.paymentInstrument.accountIdentifier, + parameters: param, + paymentMethod: this.paymentInstrument.paymentMethod, + }; + + this.checkoutFacade.updateConcardisCvcLastUpdated(pi); + } + } + + renewCVCDetails() { + if (this.paymentInstrument.parameters?.find(attribute => attribute.name === 'token')) { + const tokenAttr = this.paymentInstrument.parameters.find(attribute => attribute.name === 'token'); + + const tokenValue = tokenAttr.value ? tokenAttr.value.toString() : undefined; + if (tokenValue) { + const cvcValue = this.cvcDetailForm.get('cvcDetail').value; + if (cvcValue) { + PayEngine.verifyPaymentInstrument(tokenValue, cvcValue, err => this.submitCallback(err)); + } else { + this.cvcDetailForm.get('cvcDetail').setErrors({ required: true }); + markAsDirtyRecursive(this.cvcDetailForm); + } + } + } + } +} diff --git a/src/app/pages/checkout-payment/payment-concardis-creditcard/payment-concardis-creditcard.component.ts b/src/app/pages/checkout-payment/payment-concardis-creditcard/payment-concardis-creditcard.component.ts index 5b8d145bd1..5c44943ed7 100644 --- a/src/app/pages/checkout-payment/payment-concardis-creditcard/payment-concardis-creditcard.component.ts +++ b/src/app/pages/checkout-payment/payment-concardis-creditcard/payment-concardis-creditcard.component.ts @@ -175,6 +175,8 @@ export class PaymentConcardisCreditcardComponent extends PaymentConcardisCompone { name: 'maskedCardNumber', value: result.attributes.cardNumber }, { name: 'cardType', value: result.attributes.brand }, { name: 'expirationDate', value: `${result.attributes.expiryMonth}/${result.attributes.expiryYear}` }, + { name: 'cvcLastUpdated', value: new Date().toISOString() }, + { name: 'token', value: result.paymentInstrumentId }, ], saveAllowed: this.paymentMethod.saveAllowed && this.parameterForm.get('saveForLater').value, }); diff --git a/src/assets/i18n/de_DE.json b/src/assets/i18n/de_DE.json index b6523cff3b..e2be2e9639 100644 --- a/src/assets/i18n/de_DE.json +++ b/src/assets/i18n/de_DE.json @@ -288,6 +288,10 @@ "account.payment.payment_deleted.message": "Ihr Zahlungsmittel wurde gelöscht.", "account.payment.preferred.link": "Als bevorzugtes Zahlungsmittel speichern", "account.payment.preferred_method": "Bevorzugtes Zahlungsmittel", + "account.payment.sepa_accepted_on": "Akzeptiert am", + "account.payment.sepa_mandate_Reference": "Mandatsreferenz", + "account.payment.view_sepa_mandate.link": "SEPA-Mandat anzeigen", + "account.payment.view_sepa_mandate_text.link": "SEPA-Lastschriftmandat", "account.productnotification.login.disabled_user.error": "Ihr Benutzerkonto ist deaktiviert.", "account.productnotification.login.email.error.required": "Bitte geben Sie eine E-Mail-Adresse an.", "account.productnotification.login.email_password.error.invalid": "Ihre E-Mail-Kennwort-Kombination ist nicht korrekt. Bitte versuchen Sie es erneut.", @@ -529,6 +533,7 @@ "basket.promotioncode.not_found.error": "Der Aktionscode wurde nicht gefunden.", "basket.validation.general.error": "Ihr Warenkorb ist nicht mehr gültig. Bitte korrigieren Sie die unten angegebenen Fehler.", "captcha.incorrect": "Die Sicherheitsprüfung ist fehlgeschlagen. Korrigieren Sie den unten angegebenen Fehler.", + "checkout.account.confirm.button.label": "Bestätigen", "checkout.account.email.already_exist.error": "Die angegebene E-Mail-Adresse wird bereits verwendet.", "checkout.account.email.invalid.error": "Die E-Mail-Adresse ist ungültig.", "checkout.account.email.registered.heading": "Benutzerkonto", @@ -600,6 +605,7 @@ "checkout.credit_card.user.firstname.missing.error": "Bitte geben Sie den Vornamen des Karteninhabers an.", "checkout.credit_card.user.lastname.missing.error": "Bitte geben Sie den Nachnamen des Karteninhabers an.", "checkout.credit_card.user.name.missing.error": "Bitte geben Sie den Namen des Karteninhabers an.", + "checkout.credit_card.validityTime.error.notFound": "Die maximale Gültigkeitsdauer der Kartenprüfnummer wurde nicht gefunden.", "checkout.detail.text": "Details", "checkout.error.AuthorizationCancelled": "Ihre Bezahlung wurde abgebrochen. Wählen Sie eine andere Zahlungsart aus.", "checkout.error.AuthorizationFailed": "Ihre Zahlung konnte nicht autorisiert werden. Wählen Sie eine andere Zahlungsart aus.", diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json index d44042133b..42b6bc14a4 100644 --- a/src/assets/i18n/en_US.json +++ b/src/assets/i18n/en_US.json @@ -288,6 +288,10 @@ "account.payment.payment_deleted.message": "Your payment instrument has been deleted.", "account.payment.preferred.link": "Save as Preferred Payment Instrument", "account.payment.preferred_method": "Preferred Payment Instrument", + "account.payment.sepa_accepted_on": "Accepted on", + "account.payment.sepa_mandate_Reference": "Mandate Reference", + "account.payment.view_sepa_mandate.link": "View SEPA mandate", + "account.payment.view_sepa_mandate_text.link": "SEPA Direct Debit Mandate", "account.productnotification.login.disabled_user.error": "Your account is disabled.", "account.productnotification.login.email.error.required": "Please enter an e-mail address.", "account.productnotification.login.email_password.error.invalid": "Your E-mail/Password combination is incorrect. Please try again.", @@ -531,6 +535,7 @@ "basket.promotioncode.not_found.error": "The promotion code could not be found.", "basket.validation.general.error": "Your shopping cart is not valid anymore. Please correct the errors indicated below.", "captcha.incorrect": "The security verification failed. Please correct the error indicated below.", + "checkout.account.confirm.button.label": "Confirm", "checkout.account.email.already_exist.error": "A user with that e-mail address already exists.", "checkout.account.email.invalid.error": "The e-mail address is not valid.", "checkout.account.email.registered.heading": "Account", @@ -602,6 +607,7 @@ "checkout.credit_card.user.firstname.missing.error": "Please enter a cardholder first name", "checkout.credit_card.user.lastname.missing.error": "Please enter a cardholder last name", "checkout.credit_card.user.name.missing.error": "Please enter a cardholder name.", + "checkout.credit_card.validityTime.error.notFound": "max validity time For CVC not found.", "checkout.detail.text": "Details", "checkout.error.AuthorizationCancelled": "Your payment was canceled. Please select another payment method.", "checkout.error.AuthorizationFailed": "Your payment could not be authorized. Please select another payment method.", diff --git a/src/assets/i18n/fr_FR.json b/src/assets/i18n/fr_FR.json index 390ec7606c..fece8550b7 100644 --- a/src/assets/i18n/fr_FR.json +++ b/src/assets/i18n/fr_FR.json @@ -288,6 +288,10 @@ "account.payment.payment_deleted.message": "Votre instrument de paiement a été supprimé.", "account.payment.preferred.link": "Enregistrer comme instrument de paiement par défaut", "account.payment.preferred_method": "Instrument de paiement par défaut", + "account.payment.sepa_accepted_on": "Accepté le", + "account.payment.sepa_mandate_Reference": "Référence du mandat", + "account.payment.view_sepa_mandate.link": "Voir le mandat SEPA", + "account.payment.view_sepa_mandate_text.link": "Mandat de prélèvement SEPA", "account.productnotification.login.disabled_user.error": "Votre compte est désactivé.", "account.productnotification.login.email.error.required": "Veuillez entrer une adresse courriel.", "account.productnotification.login.email_password.error.invalid": "Votre combinaison Courriel/Mot de passe est incorrecte. Veuillez réessayer.", @@ -531,6 +535,7 @@ "basket.promotioncode.not_found.error": "Le code promotionnel n’a pas pu être trouvé.", "basket.validation.general.error": "Votre panier n’est plus valide. Veuillez corriger les erreurs signalées ci-dessous.", "captcha.incorrect": "Échec de la vérification de sécurité. Veuillez corriger l’erreur signalée ci-dessous.", + "checkout.account.confirm.button.label": "Confirmer", "checkout.account.email.already_exist.error": "Un utilisateur avec cette adresse courriel existe déjà.", "checkout.account.email.invalid.error": "L’adresse courriel n’est pas valide.", "checkout.account.email.registered.heading": "Compte", @@ -602,6 +607,7 @@ "checkout.credit_card.user.firstname.missing.error": "Veuillez entrer un prénom du titulaire de la carte", "checkout.credit_card.user.lastname.missing.error": "Veuillez entrer un nom de famille du titulaire de la carte", "checkout.credit_card.user.name.missing.error": "Veuillez entrer un nom du titulaire de la carte.", + "checkout.credit_card.validityTime.error.notFound": "La durée de validité maximale du CVC n’a pas été trouvée.", "checkout.detail.text": "Détails", "checkout.error.AuthorizationCancelled": "Votre paiement a été annulé. Veuillez sélectionner un autre mode de paiement.", "checkout.error.AuthorizationFailed": "Votre paiement n’a pas pu être autorisé. Veuillez sélectionner un autre mode de paiement.",