diff --git a/src/app/core/models/payment-method/payment-method.mapper.ts b/src/app/core/models/payment-method/payment-method.mapper.ts index 457c25e7a7..84f90a162a 100644 --- a/src/app/core/models/payment-method/payment-method.mapper.ts +++ b/src/app/core/models/payment-method/payment-method.mapper.ts @@ -22,7 +22,6 @@ export class PaymentMethodMapper { if (!body.data.length) { return []; } - return body.data .filter(data => PaymentMethodMapper.isPaymentMethodValid(data)) .map(data => ({ @@ -41,7 +40,10 @@ export class PaymentMethodMapper { ? data.paymentInstruments.map(id => included.paymentInstruments[id]) : undefined, parameters: data.parameterDefinitions ? PaymentMethodMapper.mapParameter(data.parameterDefinitions) : undefined, - hostedPaymentPageParameters: data.hostedPaymentPageParameters, + hostedPaymentPageParameters: + data.id === 'Concardis_DirectDebit' + ? PaymentMethodMapper.mapSEPAMandateInformation(data.hostedPaymentPageParameters) + : data.hostedPaymentPageParameters, })); } @@ -190,4 +192,26 @@ export class PaymentMethodMapper { return param; }); } + + /** + * convenience method to restructure concardis sepa mandate hosted payment page parameter + */ + private static mapSEPAMandateInformation( + hostedPaymentPageParameters: { name: string; value: string }[] + ): { name: string; value: string }[] { + const mandateEntry = hostedPaymentPageParameters.find(hppp => hppp.name === 'Concardis_SEPA_Mandate'); + + if (typeof mandateEntry.value !== 'string') { + const sepaMandateArray = mandateEntry.value as { + mandateId: string; + mandateText: string; + directDebitType: string; + }; + hostedPaymentPageParameters.push({ name: 'mandateId', value: sepaMandateArray.mandateId }); + hostedPaymentPageParameters.push({ name: 'mandateText', value: sepaMandateArray.mandateText }); + hostedPaymentPageParameters.push({ name: 'directDebitType', value: sepaMandateArray.directDebitType }); + } + + return hostedPaymentPageParameters; + } } 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 c1f5a3a719..c1b50fbb35 100644 --- a/src/app/pages/checkout-payment/checkout-payment-page.module.ts +++ b/src/app/pages/checkout-payment/checkout-payment-page.module.ts @@ -5,10 +5,18 @@ import { SharedModule } from 'ish-shared/shared.module'; import { CheckoutPaymentPageComponent } from './checkout-payment-page.component'; import { CheckoutPaymentComponent } from './checkout-payment/checkout-payment.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'; @NgModule({ imports: [SharedModule], - declarations: [CheckoutPaymentComponent, CheckoutPaymentPageComponent, PaymentConcardisCreditcardComponent], + declarations: [ + CheckoutPaymentComponent, + CheckoutPaymentPageComponent, + PaymentConcardisComponent, + PaymentConcardisCreditcardComponent, + PaymentConcardisDirectdebitComponent, + ], }) export class CheckoutPaymentPageModule { static component = CheckoutPaymentPageComponent; 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 4bba3639d3..a56e5a457d 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 @@ -107,39 +107,52 @@

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

*{{ 'account.required_field.message' | 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 6e7f40deb8..4ea797f002 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 @@ -22,6 +22,7 @@ import { ModalDialogLinkComponent } from 'ish-shared/components/common/modal-dia import { CheckboxComponent } from 'ish-shared/forms/components/checkbox/checkbox.component'; import { PaymentConcardisCreditcardComponent } from '../payment-concardis-creditcard/payment-concardis-creditcard.component'; +import { PaymentConcardisDirectdebitComponent } from '../payment-concardis-directdebit/payment-concardis-directdebit.component'; import { CheckoutPaymentComponent } from './checkout-payment.component'; @@ -51,6 +52,7 @@ describe('Checkout Payment Component', () => { MockComponent(ModalDialogLinkComponent), MockComponent(NgbCollapse), MockComponent(PaymentConcardisCreditcardComponent), + MockComponent(PaymentConcardisDirectdebitComponent), MockDirective(ServerHtmlDirective), MockPipe(PricePipe), ], @@ -152,33 +154,33 @@ describe('Checkout Payment Component', () => { describe('next button', () => { it('should set submitted if next button is clicked', () => { - expect(component.nextSubmitted).toBeFalse(); + expect(component.nextSubmitted).toBeFalsy(); component.goToNextStep(); - expect(component.nextSubmitted).toBeTrue(); + expect(component.nextSubmitted).toBeTruthy(); }); it('should not disable next button if basket payment method is set and next button is clicked', () => { - expect(component.nextDisabled).toBeFalse(); + expect(component.nextDisabled).toBeFalsy(); component.goToNextStep(); - expect(component.nextDisabled).toBeFalse(); + expect(component.nextDisabled).toBeFalsy(); }); it('should disable next button if basket payment method is missing and next button is clicked', () => { component.basket.payment = undefined; component.goToNextStep(); - expect(component.nextDisabled).toBeTrue(); + expect(component.nextDisabled).toBeTruthy(); }); }); describe('parameter forms', () => { it('should open and close payment form if open/cancel form is triggered', () => { - expect(component.formIsOpen(-1)).toBeTrue(); + expect(component.formIsOpen(-1)).toBeTruthy(); component.openPaymentParameterForm(2); - expect(component.formIsOpen(2)).toBeTrue(); + expect(component.formIsOpen(2)).toBeTruthy(); component.cancelNewPaymentInstrument(); - expect(component.formIsOpen(-1)).toBeTrue(); + expect(component.formIsOpen(-1)).toBeTruthy(); }); it('should throw createPaymentInstrument event when the user submits a valid parameter form and saving is not allowed', done => { @@ -190,7 +192,7 @@ describe('Checkout Payment Component', () => { paymentMethod: 'Concardis_CreditCard', parameters: [{ name: 'creditCardNumber', value: '123' }], }); - expect(formValue.saveForLater).toBeFalse(); + expect(formValue.saveForLater).toBeFalsy(); done(); }); @@ -200,14 +202,14 @@ describe('Checkout Payment Component', () => { it('should throw createUserPaymentInstrument event when the user submits a valid parameter form and saving is allowed', done => { component.ngOnChanges(paymentMethodChange); - component.openPaymentParameterForm(2); + component.openPaymentParameterForm(3); component.createPaymentInstrument.subscribe(formValue => { expect(formValue.paymentInstrument).toEqual({ paymentMethod: 'ISH_CreditCard', parameters: [{ name: 'creditCardNumber', value: '456' }], }); - expect(formValue.saveForLater).toBeTrue(); + expect(formValue.saveForLater).toBeTruthy(); done(); }); @@ -219,10 +221,10 @@ describe('Checkout Payment Component', () => { component.openPaymentParameterForm(1); fixture.detectChanges(); - expect(component.formSubmitted).toBeFalse(); + expect(component.formSubmitted).toBeFalsy(); component.parameterForm.addControl('IBAN', new FormControl('', Validators.required)); component.submitParameterForm(); - expect(component.formSubmitted).toBeTrue(); + expect(component.formSubmitted).toBeTruthy(); }); it('should render standard parameter form for standard parametrized form', () => { 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 155d42eb39..60b9192639 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 @@ -1,23 +1,12 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - Input, - OnChanges, - OnDestroy, - OnInit, - Output, -} from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { Subject } from 'rxjs'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; import { takeUntil } from 'rxjs/operators'; -import { Attribute } from 'ish-core/models/attribute/attribute.model'; -import { PaymentMethod } from 'ish-core/models/payment-method/payment-method.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; @@ -39,24 +28,10 @@ declare var PayEngine: any; changeDetection: ChangeDetectionStrategy.Default, }) // tslint:disable-next-line:ccp-no-intelligence-in-components -export class PaymentConcardisCreditcardComponent implements OnInit, OnChanges, OnDestroy { - /** - * concardis payment method, needed to get configuration parameters - */ - @Input() paymentMethod: PaymentMethod; - - /** - * should be set to true by the parent, if component is visible - */ - @Input() activated = false; - - @Output() cancel = new EventEmitter(); - @Output() submit = new EventEmitter<{ parameters: Attribute[]; saveAllowed: boolean }>(); - - scriptLoaded = false; // flag to make sure that the init script is executed only once - formSubmitted = false; // flag for displaying error messages after form submit - - parameterForm: FormGroup; // form for parameters which doesnt come form payment host +export class PaymentConcardisCreditcardComponent extends PaymentConcardisComponent implements OnInit { + constructor(protected scriptLoader: ScriptLoaderService, protected cd: ChangeDetectorRef) { + super(scriptLoader, cd); + } iframesReference: { // iframesReference, id needed by the payment host @@ -64,40 +39,20 @@ export class PaymentConcardisCreditcardComponent implements OnInit, OnChanges, O verificationIframeName: string; }; - // error messages from host - errorMessage = { - general: { message: '' }, - cardNumber: { messageKey: '', message: '', code: 0 }, - cvc: { messageKey: '', message: '', code: 0 }, - expiryMonth: { messageKey: '', message: '', code: 0 }, - }; - - private destroy$ = new Subject(); - - constructor(private scriptLoader: ScriptLoaderService, private cd: ChangeDetectorRef) {} - - /** - * initialize parameter form on init - */ ngOnInit() { - this.parameterForm = new FormGroup({ - expirationMonth: new FormControl('', [Validators.required, Validators.pattern('[0-9]{2}')]), - expirationYear: new FormControl('', [Validators.required, Validators.pattern('[0-9]{2}')]), - saveForLater: new FormControl(true), - }); - } - - /* ---------------------------------------- load concardis script if component is visible ------------------------------------------- */ + super.formInit(); - /** - * load concardis script if component is shown - */ - ngOnChanges() { - if (this.paymentMethod) { - this.loadScript(); - } + this.parameterForm.addControl( + 'expirationMonth', + new FormControl('', [Validators.required, Validators.pattern('[0-9]{2}')]) + ); + this.parameterForm.addControl( + 'expirationYear', + new FormControl('', [Validators.required, Validators.pattern('[0-9]{2}')]) + ); } + /* ---------------------------------------- load concardis script if component is visible ------------------------------------------- */ loadScript() { // load script only once if component becomes visible if (this.activated && !this.scriptLoaded) { @@ -119,14 +74,9 @@ export class PaymentConcardisCreditcardComponent implements OnInit, OnChanges, O return; } - const url = - this.getParamValue('ConcardisPaymentService.Environment', '') === 'LIVE' - ? 'https://pp.payengine.de/bridge/1.0/payengine.min.js' - : 'https://pptest.payengine.de/bridge/1.0/payengine.min.js'; - this.scriptLoaded = true; this.scriptLoader - .load(url) + .load(this.getPayEngineURL()) .pipe(takeUntil(this.destroy$)) .subscribe( () => { @@ -148,19 +98,6 @@ export class PaymentConcardisCreditcardComponent implements OnInit, OnChanges, O } } - /** - * gets a parameter value from payment method - * sets the general error message (key) if the parameter is not available - */ - private getParamValue(name: string, errorMessage: string): string { - const parameter = this.paymentMethod.hostedPaymentPageParameters.find(param => param.name === name); - if (!parameter || !parameter.value) { - this.errorMessage.general.message = errorMessage; - return; - } - return parameter.value; - } - /* ---------------------------------------- concardis callback functions ------------------------------------------- */ /** @@ -199,6 +136,7 @@ export class PaymentConcardisCreditcardComponent implements OnInit, OnChanges, O if (this.errorMessage.cardNumber && this.errorMessage.cardNumber.code) { this.errorMessage.cardNumber.messageKey = this.getErrorMessage( this.errorMessage.cardNumber.code, + 'credit_card', 'number', this.errorMessage.cardNumber.message ); @@ -209,6 +147,7 @@ export class PaymentConcardisCreditcardComponent implements OnInit, OnChanges, O 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 ); @@ -220,6 +159,7 @@ export class PaymentConcardisCreditcardComponent implements OnInit, OnChanges, O if (this.errorMessage.expiryMonth && this.errorMessage.expiryMonth.code) { this.errorMessage.expiryMonth.messageKey = this.getErrorMessage( this.errorMessage.expiryMonth.code, + 'credit_card', 'expiryMonth', this.errorMessage.expiryMonth.message ); @@ -242,86 +182,7 @@ export class PaymentConcardisCreditcardComponent implements OnInit, OnChanges, O this.cd.detectChanges(); } - /* ---------------------------------------- error message handling ------------------------------------------- */ - - /** - * reset errorMessages - */ - resetErrors() { - this.errorMessage.general.message = undefined; - if (this.errorMessage.cardNumber) { - this.errorMessage.cardNumber.message = undefined; - this.errorMessage.cardNumber.messageKey = undefined; - this.errorMessage.cardNumber.code = undefined; - } - if (this.errorMessage.cvc) { - this.errorMessage.cvc.message = undefined; - this.errorMessage.cvc.messageKey = undefined; - this.errorMessage.cvc.code = undefined; - } - if (this.errorMessage.expiryMonth) { - this.errorMessage.expiryMonth.message = undefined; - this.errorMessage.expiryMonth.messageKey = undefined; - this.errorMessage.expiryMonth.code = undefined; - } - } - - /** - * determine errorMessages on the basis of the error code - */ - getErrorMessage(code: number, fieldType: string, defaultMessage: string): string { - let messageKey: string; - - switch (code) { - case 4121: { - messageKey = `checkout.credit_card.${fieldType}.error.default`; - break; - } - case 4122: { - messageKey = `checkout.credit_card.${fieldType}.error.default`; - break; - } - case 4123: { - messageKey = `checkout.credit_card.${fieldType}.error.notNumber`; - break; - } - case 4126: { - messageKey = `checkout.credit_card.${fieldType}.error.length`; - break; - } - case 4127: { - messageKey = `checkout.credit_card.${fieldType}.error.invalid`; - break; - } - case 4128: { - messageKey = `checkout.credit_card.${fieldType}.error.length`; - break; - } - - case 4129: { - messageKey = `checkout.credit_card.${fieldType}.error.invalid`; - break; - } - default: { - messageKey = defaultMessage; - break; - } - } - - return messageKey; - } - - /* ---------------------------------------- cancel and submit form ------------------------------------------- */ - - /** - * cancel new payment instrument, hides and resets the parameter form - */ - cancelNewPaymentInstrument() { - this.parameterForm.reset(); - this.parameterForm.get('saveForLater').setValue(true); - this.resetErrors(); - this.cancel.emit(); - } + /* ---------------------------------------- submit form ------------------------------------------- */ /** * submit concardis payment form @@ -336,8 +197,4 @@ export class PaymentConcardisCreditcardComponent implements OnInit, OnChanges, O this.submitCallback(err, val) ); } - - ngOnDestroy() { - this.destroy$.next(); - } } diff --git a/src/app/pages/checkout-payment/payment-concardis-directdebit/payment-concardis-directdebit.component.html b/src/app/pages/checkout-payment/payment-concardis-directdebit/payment-concardis-directdebit.component.html new file mode 100644 index 0000000000..7edc22dd46 --- /dev/null +++ b/src/app/pages/checkout-payment/payment-concardis-directdebit/payment-concardis-directdebit.component.html @@ -0,0 +1,12 @@ + +
+
+ + +
+
+
diff --git a/src/app/pages/checkout-payment/payment-concardis-directdebit/payment-concardis-directdebit.component.spec.ts b/src/app/pages/checkout-payment/payment-concardis-directdebit/payment-concardis-directdebit.component.spec.ts new file mode 100644 index 0000000000..a57aaa3955 --- /dev/null +++ b/src/app/pages/checkout-payment/payment-concardis-directdebit/payment-concardis-directdebit.component.spec.ts @@ -0,0 +1,102 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; +import { FormlyForm } from '@ngx-formly/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent } from 'ng-mocks'; +import { anything, spy, verify } from 'ts-mockito'; + +import { PaymentMethod } from 'ish-core/models/payment-method/payment-method.model'; +import { CheckboxComponent } from 'ish-shared/forms/components/checkbox/checkbox.component'; + +import { PaymentConcardisDirectdebitComponent } from './payment-concardis-directdebit.component'; + +describe('Payment Concardis Directdebit Component', () => { + let component: PaymentConcardisDirectdebitComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + MockComponent(CheckboxComponent), + MockComponent(FaIconComponent), + MockComponent(FormlyForm), + MockComponent(NgbPopover), + PaymentConcardisDirectdebitComponent, + ], + imports: [ReactiveFormsModule, TranslateModule.forRoot()], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PaymentConcardisDirectdebitComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + + component.paymentMethod = { + id: 'Concardis_DirectDebit', + saveAllowed: false, + parameters: [ + { + key: 'IBAN', + name: 'IBAN', + type: 'input', + templateOptions: { label: 'input', type: 'text', disabled: false }, + }, + ], + } as PaymentMethod; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + + it('should emit submit event if submit call back returns with no error and parameter form is valid', () => { + fixture.detectChanges(); + + const emitter = spy(component.submit); + + component.submitCallback(undefined, { + paymentInstrumentId: '4711', + attributes: { + accountHolder: 'Klaus Klausen', + iban: '123456789', + mandateReference: 'ref', + mandate: { + mandateReference: 'ref', + mandateText: 'mandate text', + directDebitType: 'SINGLE', + createdDateTime: undefined, + }, + createdAt: undefined, + }, + }); + + verify(emitter.emit(anything())).once(); + }); + + 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: 'iban', code: 123, message: errorMessage, messageKey: '' }] } }, + undefined + ); + + expect(component.errorMessage.iban.message).toEqual(errorMessage); + }); + + it('should emit cancel event when cancelNewPaymentInstrument is triggered', () => { + fixture.detectChanges(); + + const emitter = spy(component.cancel); + + component.cancelNewPaymentInstrument(); + verify(emitter.emit()).once(); + }); +}); diff --git a/src/app/pages/checkout-payment/payment-concardis-directdebit/payment-concardis-directdebit.component.ts b/src/app/pages/checkout-payment/payment-concardis-directdebit/payment-concardis-directdebit.component.ts new file mode 100644 index 0000000000..efdd155282 --- /dev/null +++ b/src/app/pages/checkout-payment/payment-concardis-directdebit/payment-concardis-directdebit.component.ts @@ -0,0 +1,225 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core'; +import { Validators } from '@angular/forms'; +import { FormlyFieldConfig } from '@ngx-formly/core'; +import { takeUntil } from 'rxjs/operators'; + +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; + +/** + * The Payment Concardis Directdebit Component renders a form on which the user can enter his concardis direct debit data. Some entry fields are provided by an external host and embedded as iframes. Therefore an external javascript is loaded. See also {@link CheckoutPaymentPageComponent} + * + * @example + * + */ +@Component({ + selector: 'ish-payment-concardis-directdebit', + templateUrl: './payment-concardis-directdebit.component.html', + changeDetection: ChangeDetectionStrategy.Default, +}) +export class PaymentConcardisDirectdebitComponent extends PaymentConcardisComponent { + constructor(protected scriptLoader: ScriptLoaderService, protected cd: ChangeDetectorRef) { + super(scriptLoader, cd); + } + + handleErrors(controlName: string, message: string) { + if (this.parameterForm.controls[controlName]) { + this.parameterForm.controls[controlName].markAsDirty(); + this.parameterForm.controls[controlName].setErrors({ customError: message }); + } + } + + /* ---------------------------------------- load concardis script if component is visible ------------------------------------------- */ + + loadScript() { + // load script only once if component becomes visible + if (this.activated && !this.scriptLoaded) { + const merchantId = this.getParamValue( + 'ConcardisPaymentService.MerchantID', + 'checkout.payment.merchantId.error.notFould' + ); + + // if merchant Id 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(); + } + ); + } + } + + /** + * hide fields without labels and enrich mandate reference and mandate text with corresponding values from hosted payment page parameters + */ + getFieldConfig(): FormlyFieldConfig[] { + return this.paymentMethod.parameters.map(param => (!param.templateOptions.label ? this.modifyParam(param) : param)); + } + + private modifyParam(p: FormlyFieldConfig): FormlyFieldConfig { + const param = p; + + if (param.key === 'mandateReference') { + param.defaultValue = this.getParamValue('mandateId', ''); + } + param.hide = true; + if (param.key === 'mandateText') { + param.type = 'checkbox'; + param.fieldGroupClassName = 'offset-md-4 col-md-8'; + param.templateOptions.label = this.getParamValue('mandateText', ''); + param.defaultValue = false; + param.hide = false; + param.validators = [Validators.pattern('false')]; + } + return param; + } + + /* ---------------------------------------- concardis callback functions ------------------------------------------- */ + + /** + * call back function to submit data, get a response token from provider and send data in case of success + */ + submitCallback( + error: { message: { properties: { key: string; code: number; message: string; messageKey: string }[] } | string }, + result: { + paymentInstrumentId: string; + attributes: { + accountHolder: string; + iban: string; + mandateReference: string; + mandate: { + mandateReference: string; + createdDateTime: string; + mandateText: string; + directDebitType: string; + }; + createdAt: string; + }; + } + ) { + if (this.parameterForm.invalid) { + this.formSubmitted = true; + markAsDirtyRecursive(this.parameterForm); + } + + this.resetErrors(); + if (error) { + // map error messages + if (typeof error.message !== 'string' && error.message.properties) { + this.errorMessage.iban = error.message.properties && error.message.properties.find(prop => prop.key === 'iban'); + if (this.errorMessage.iban && this.errorMessage.iban.code) { + this.errorMessage.iban.messageKey = this.getErrorMessage( + this.errorMessage.iban.code, + 'sepa', + 'iban', + this.errorMessage.iban.message + ); + this.handleErrors('IBAN', this.errorMessage.iban.messageKey); + } + + this.errorMessage.bic = error.message.properties && error.message.properties.find(prop => prop.key === 'bic'); + if (this.errorMessage.bic && this.errorMessage.bic.code) { + this.errorMessage.bic.messageKey = this.getErrorMessage( + this.errorMessage.bic.code, + 'sepa', + 'bic', + this.errorMessage.bic.message + ); + this.handleErrors('BIC', this.errorMessage.bic.messageKey); + } + + this.errorMessage.accountholder = + error.message.properties && error.message.properties.find(prop => prop.key === 'accountholder'); + if (this.errorMessage.accountholder && this.errorMessage.accountholder.code) { + this.errorMessage.accountholder.messageKey = this.getErrorMessage( + this.errorMessage.accountholder.code, + 'sepa', + 'bic', + this.errorMessage.accountholder.message + ); + this.handleErrors('accountHolder', this.errorMessage.accountholder.messageKey); + } + } else if (typeof error.message === 'string') { + this.errorMessage.general.message = error.message; + } + } else if (!this.parameterForm.invalid) { + this.submit.emit({ + parameters: [ + { name: 'paymentInstrumentId', value: result.paymentInstrumentId }, + { name: 'accountHolder', value: result.attributes.accountHolder }, + { name: 'IBAN', value: result.attributes.iban }, + { name: 'mandateReference', value: result.attributes.mandate.mandateReference }, + { name: 'mandateText', value: result.attributes.mandate.mandateText }, + { name: 'mandateCreatedDateTime', value: result.attributes.mandate.createdDateTime }, + ], + saveAllowed: false, + }); + } + this.cd.detectChanges(); + } + + /* ---------------------------------------- submit form ------------------------------------------- */ + + /** + * submit concardis payment form + */ + submitNewPaymentInstrument() { + if (this.parameterForm.invalid) { + this.formSubmitted = true; + markAsDirtyRecursive(this.parameterForm); + return; + } + + const parameters = Object.entries(this.parameterForm.controls) + .filter(([, control]) => control.enabled && control.value) + .map(([key, control]) => ({ name: key, value: control.value })); + + let paymentData: { + accountHolder: string; + bic?: string; + iban: string; + mandate: { mandateId: string; mandateText: string; directDebitType: string }; + } = { + accountHolder: parameters.find(p => p.name === 'accountHolder') + ? parameters.find(p => p.name === 'accountHolder').value + : undefined, + iban: parameters.find(p => p.name === 'IBAN') ? parameters.find(p => p.name === 'IBAN').value : undefined, + mandate: { + mandateId: parameters.find(p => p.name === 'mandateReference') + ? parameters.find(p => p.name === 'mandateReference').value + : undefined, + mandateText: this.getParamValue('mandateText', ''), + directDebitType: this.getParamValue('directDebitType', ''), + }, + }; + + if (parameters.find(p => p.name === 'BIC')) { + paymentData = { ...paymentData, bic: parameters.find(p => p.name === 'BIC').value }; + } + // tslint:disable-next-line:no-null-keyword + PayEngine.createPaymentInstrument('sepa', paymentData, null, (err, val) => this.submitCallback(err, val)); + } +} diff --git a/src/app/pages/checkout-payment/payment-concardis/payment-concardis.component.spec.ts b/src/app/pages/checkout-payment/payment-concardis/payment-concardis.component.spec.ts new file mode 100644 index 0000000000..a498531ec9 --- /dev/null +++ b/src/app/pages/checkout-payment/payment-concardis/payment-concardis.component.spec.ts @@ -0,0 +1,37 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; + +import { PaymentMethod } from 'ish-core/models/payment-method/payment-method.model'; + +import { PaymentConcardisComponent } from './payment-concardis.component'; + +describe('Payment Concardis Component', () => { + let component: PaymentConcardisComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [PaymentConcardisComponent], + imports: [ReactiveFormsModule, TranslateModule.forRoot()], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PaymentConcardisComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + + component.paymentMethod = { + id: 'Concardis_CreditCard', + saveAllowed: false, + } as PaymentMethod; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); +}); diff --git a/src/app/pages/checkout-payment/payment-concardis/payment-concardis.component.ts b/src/app/pages/checkout-payment/payment-concardis/payment-concardis.component.ts new file mode 100644 index 0000000000..ff971944fb --- /dev/null +++ b/src/app/pages/checkout-payment/payment-concardis/payment-concardis.component.ts @@ -0,0 +1,226 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { FormlyFormOptions } from '@ngx-formly/core'; +import { Subject } from 'rxjs'; + +import { Attribute } from 'ish-core/models/attribute/attribute.model'; +import { PaymentMethod } from 'ish-core/models/payment-method/payment-method.model'; +import { ScriptLoaderService } from 'ish-core/utils/script-loader/script-loader.service'; + +@Component({ + selector: 'ish-payment-concardis', + template: ' ', + changeDetection: ChangeDetectionStrategy.Default, +}) +export class PaymentConcardisComponent implements OnInit, OnChanges, OnDestroy { + constructor(protected scriptLoader: ScriptLoaderService, protected cd: ChangeDetectorRef) {} + /** + * concardis payment method, needed to get configuration parameters + */ + @Input() paymentMethod: PaymentMethod; + + /** + * should be set to true by the parent, if component is visible + */ + @Input() activated = false; + + @Output() cancel = new EventEmitter(); + @Output() submit = new EventEmitter<{ parameters: Attribute[]; saveAllowed: boolean }>(); + + scriptLoaded = false; // flag to make sure that the init script is executed only once + formSubmitted = false; // flag for displaying error messages after form submit + + parameterForm: FormGroup; // form for parameters which doesnt come form payment host + model = {}; + options: FormlyFormOptions = {}; + + // error messages from host + errorMessage = { + general: { message: '' }, + iban: { messageKey: '', message: '', code: 0 }, + bic: { messageKey: '', message: '', code: 0 }, + accountholder: { messageKey: '', message: '', code: 0 }, + cardNumber: { messageKey: '', message: '', code: 0 }, + cvc: { messageKey: '', message: '', code: 0 }, + expiryMonth: { messageKey: '', message: '', code: 0 }, + }; + + protected destroy$ = new Subject(); + + getPayEngineURL() { + return this.getParamValue('ConcardisPaymentService.Environment', '') === 'LIVE' + ? 'https://pp.payengine.de/bridge/1.0/payengine.min.js' + : 'https://pptest.payengine.de/bridge/1.0/payengine.min.js'; + } + + /** + * initialize parameter form on init + */ + ngOnInit() { + this.formInit(); + } + + formInit() { + this.parameterForm = new FormGroup({ + saveForLater: new FormControl(true), + }); + } + + /** + * load concardis script if component is shown + */ + ngOnChanges() { + if (this.paymentMethod) { + this.loadScript(); + } + } + + ngOnDestroy() { + this.destroy$.next(); + } + + // tslint:disable-next-line:no-empty + loadScript() {} + + /** + * gets a parameter value from payment method + * sets the general error message (key) if the parameter is not available + */ + protected getParamValue(name: string, errorMessage: string): string { + const parameter = this.paymentMethod.hostedPaymentPageParameters.find(param => param.name === name); + if (!parameter || !parameter.value) { + this.errorMessage.general.message = errorMessage; + return; + } + return parameter.value; + } + + /* ---------------------------------------- error message handling ------------------------------------------- */ + + /** + * determine errorMessages on the basis of the error code + */ + getErrorMessage(code: number, paymentMethod: string, fieldType: string, defaultMessage: string): string { + let messageKey: string; + + switch (code) { + case 4121: { + messageKey = `checkout.${paymentMethod}.${fieldType}.error.default`; + break; + } + case 4122: { + messageKey = `checkout.${paymentMethod}.${fieldType}.error.default`; + break; + } + case 4123: { + messageKey = `checkout.${paymentMethod}.${fieldType}.error.notNumber`; + break; + } + case 4124: { + messageKey = `checkout.${paymentMethod}.${fieldType}.error.notAlphanumeric`; + break; + } + case 4126: { + messageKey = `checkout.${paymentMethod}.${fieldType}.error.length`; + break; + } + case 4127: { + messageKey = `checkout.${paymentMethod}.${fieldType}.error.invalid`; + break; + } + case 4128: { + messageKey = `checkout.${paymentMethod}.${fieldType}.error.length`; + break; + } + case 4129: { + messageKey = `checkout.${paymentMethod}.${fieldType}.error.invalid`; + break; + } + case 41213: { + messageKey = `checkout.${paymentMethod}.${fieldType}.error.countryNotSupported`; + break; + } + case 41214: { + messageKey = `checkout.${paymentMethod}.${fieldType}.error.length`; + break; + } + case 41215: { + messageKey = `checkout.${paymentMethod}.${fieldType}.error.invalid`; + break; + } + case 41216: { + messageKey = `checkout.${paymentMethod}.${fieldType}.error.length`; + break; + } + case 41217: { + messageKey = `checkout.${paymentMethod}.${fieldType}.error.countryNotSupported`; + break; + } + default: { + messageKey = defaultMessage; + break; + } + } + + return messageKey; + } + + /** + * reset errorMessages + */ + resetErrors() { + this.errorMessage.general.message = undefined; + if (this.errorMessage.accountholder) { + this.errorMessage.accountholder.message = undefined; + this.errorMessage.accountholder.messageKey = undefined; + this.errorMessage.accountholder.code = undefined; + } + if (this.errorMessage.iban) { + this.errorMessage.iban.message = undefined; + this.errorMessage.iban.messageKey = undefined; + this.errorMessage.iban.code = undefined; + } + if (this.errorMessage.bic) { + this.errorMessage.bic.message = undefined; + this.errorMessage.bic.messageKey = undefined; + this.errorMessage.bic.code = undefined; + } + if (this.errorMessage.cardNumber) { + this.errorMessage.cardNumber.message = undefined; + this.errorMessage.cardNumber.messageKey = undefined; + this.errorMessage.cardNumber.code = undefined; + } + if (this.errorMessage.cvc) { + this.errorMessage.cvc.message = undefined; + this.errorMessage.cvc.messageKey = undefined; + this.errorMessage.cvc.code = undefined; + } + if (this.errorMessage.expiryMonth) { + this.errorMessage.expiryMonth.message = undefined; + this.errorMessage.expiryMonth.messageKey = undefined; + this.errorMessage.expiryMonth.code = undefined; + } + } + + /** + * cancel new payment instrument, hides and resets the parameter form + */ + cancelNewPaymentInstrument() { + this.parameterForm.reset(); + if (this.parameterForm.get('saveForLater')) { + this.parameterForm.get('saveForLater').setValue(true); + } + this.resetErrors(); + this.cancel.emit(); + } +} diff --git a/src/app/shared/forms-dynamic/components/checkbox-dynamic/checkbox-dynamic.component.html b/src/app/shared/forms-dynamic/components/checkbox-dynamic/checkbox-dynamic.component.html new file mode 100644 index 0000000000..c96847d2bb --- /dev/null +++ b/src/app/shared/forms-dynamic/components/checkbox-dynamic/checkbox-dynamic.component.html @@ -0,0 +1,9 @@ +
+ +
diff --git a/src/app/shared/forms-dynamic/components/checkbox-dynamic/checkbox-dynamic.component.spec.ts b/src/app/shared/forms-dynamic/components/checkbox-dynamic/checkbox-dynamic.component.spec.ts new file mode 100644 index 0000000000..b66bd8c1fa --- /dev/null +++ b/src/app/shared/forms-dynamic/components/checkbox-dynamic/checkbox-dynamic.component.spec.ts @@ -0,0 +1,51 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; + +import { CheckboxComponent } from 'ish-shared/forms/components/checkbox/checkbox.component'; + +import { CheckboxDynamicComponent } from './checkbox-dynamic.component'; + +describe('Checkbox Dynamic Component', () => { + let component: CheckboxDynamicComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [CheckboxDynamicComponent, MockComponent(CheckboxComponent)], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(CheckboxDynamicComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + + component.field = { + key: 'test', + templateOptions: {}, + fieldGroupClassName: 'offset', + }; + }); + })); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + + it('should render a checkbox form component if field is provided', () => { + fixture.detectChanges(); + expect(element.querySelector('ish-checkbox')).toBeTruthy(); + }); + + it('should not render an checkbox form component if field is missing', () => { + component.field = { + key: 'test', + templateOptions: {}, + fieldGroupClassName: undefined, + }; + fixture.detectChanges(); + expect(element.querySelector('ish-checkbox')).toBeFalsy(); + }); +}); diff --git a/src/app/shared/forms-dynamic/components/checkbox-dynamic/checkbox-dynamic.component.ts b/src/app/shared/forms-dynamic/components/checkbox-dynamic/checkbox-dynamic.component.ts new file mode 100644 index 0000000000..c9b6fa2f38 --- /dev/null +++ b/src/app/shared/forms-dynamic/components/checkbox-dynamic/checkbox-dynamic.component.ts @@ -0,0 +1,9 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { FieldType } from '@ngx-formly/core'; + +@Component({ + selector: 'ish-checkbox-dynamic', + templateUrl: './checkbox-dynamic.component.html', + changeDetection: ChangeDetectionStrategy.Default, +}) +export class CheckboxDynamicComponent extends FieldType {} diff --git a/src/app/shared/forms-dynamic/forms-dynamic.module.ts b/src/app/shared/forms-dynamic/forms-dynamic.module.ts index d9c17c0c5c..9a0f13fb68 100644 --- a/src/app/shared/forms-dynamic/forms-dynamic.module.ts +++ b/src/app/shared/forms-dynamic/forms-dynamic.module.ts @@ -5,6 +5,7 @@ import { FormlyModule } from '@ngx-formly/core'; import { FormsSharedModule } from 'ish-shared/forms/forms.module'; +import { CheckboxDynamicComponent } from './components/checkbox-dynamic/checkbox-dynamic.component'; import { InputDynamicComponent } from './components/input-dynamic/input-dynamic.component'; import { SelectDynamicComponent } from './components/select-dynamic/select-dynamic.component'; @@ -15,12 +16,13 @@ import { SelectDynamicComponent } from './components/select-dynamic/select-dynam types: [ { name: 'input', component: InputDynamicComponent }, { name: 'select', component: SelectDynamicComponent }, + { name: 'checkbox', component: CheckboxDynamicComponent }, ], }), FormsSharedModule, ReactiveFormsModule, ], - declarations: [InputDynamicComponent, SelectDynamicComponent], - exports: [InputDynamicComponent, SelectDynamicComponent], + declarations: [CheckboxDynamicComponent, InputDynamicComponent, SelectDynamicComponent], + exports: [CheckboxDynamicComponent, InputDynamicComponent, SelectDynamicComponent], }) export class FormsDynamicModule {} diff --git a/src/assets/i18n/de_DE.json b/src/assets/i18n/de_DE.json index e464f6cf17..edf78e760d 100644 --- a/src/assets/i18n/de_DE.json +++ b/src/assets/i18n/de_DE.json @@ -2671,5 +2671,17 @@ "quote.already_in_basket.error": "Das Preisangebot befindet sich bereits im Warenkorb.", "quote.not_found.error": "Das Preisangebot konnte nicht gefunden werden.", "basket.add_quote.error": "Das Preisangebot konnte nicht in den Warenkorb gelegt werden.", - "quoterequest.not_editable.error": "Sie haben die Preisanfrage bereits gesendet. Laden Sie diese Seite neu, um die Änderungen zu sehen." + "quoterequest.not_editable.error": "Sie haben die Preisanfrage bereits gesendet. Laden Sie diese Seite neu, um die Änderungen zu sehen.", + "checkout.sepa.iban.error.default": "Die IBAN ist erforderlich.", + "checkout.sepa.iban.error.length": "Die Länge der IBAN ist ungültig.", + "checkout.sepa.iban.error.countryNotSupported": "Die IBAN ist einem Land zugeordnet, das nicht unterstützt wird.", + "checkout.sepa.iban.error.notAlphanumeric": "Die IBAN muss aus alphanumerischen Zeichen bestehen.", + "checkout.sepa.iban.error.invalid": "Die IBAN ist ungültig.", + "checkout.sepa.accountholder.error.default": "Der Kontoinhaber ist erforderlich.", + "checkout.sepa.accountholder.error.notAlphanumeric": "Der Kontoinhaber muss aus alphanumerischen Zeichen bestehen.", + "checkout.sepa.bic.error.notAlphanumeric": "Der BIC muss aus alphanumerischen Zeichen bestehen.", + "checkout.sepa.bic.error.countryNotSupported": "Der BIC ist einem Land zugeordnet, das nicht unterstützt wird.", + "checkout.sepa.bic.error.length": "Die Länge des BIC ist ungültig.", + "checkout.sepa.bic.error.invalid": "Der BIC ist ungültig.", + "checkout.sepa.mandate.error.required": "Es wurde keine Zustimmung zur Genehmigung erteilt." } diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json index 7b484ce244..5308dafdd1 100644 --- a/src/assets/i18n/en_US.json +++ b/src/assets/i18n/en_US.json @@ -2674,5 +2674,17 @@ "quote.already_in_basket.error": "The quote is already in the cart.", "quote.not_found.error": "The quote could not be found.", "basket.add_quote.error": "The quote could not be added to the cart.", - "quoterequest.not_editable.error": "You have already submitted the quote request. Please reload this page to view the changes." + "quoterequest.not_editable.error": "You have already submitted the quote request. Please reload this page to view the changes.", + "checkout.sepa.iban.error.default": "The IBAN is required.", + "checkout.sepa.iban.error.length": "The length of the IBAN is invalid.", + "checkout.sepa.iban.error.countryNotSupported": "The IBAN is associated to a country which is not supported.", + "checkout.sepa.iban.error.notAlphanumeric": "The IBAN must consist of alphanumeric characters.", + "checkout.sepa.iban.error.invalid": "The IBAN is invalid.", + "checkout.sepa.accountholder.error.default": "The account holder is required.", + "checkout.sepa.accountholder.error.notAlphanumeric": "The account holder must consist of alphanumeric characters.", + "checkout.sepa.bic.error.notAlphanumeric": "The BIC must consist of alphanumeric characters.", + "checkout.sepa.bic.error.countryNotSupported": "The BIC is associated to a country which is not supported.", + "checkout.sepa.bic.error.length": "The length of the BIC is invalid.", + "checkout.sepa.bic.error.invalid": "The BIC is invalid.", + "checkout.sepa.mandate.error.required": "No consent has been given for authorisation." } diff --git a/src/assets/i18n/fr_FR.json b/src/assets/i18n/fr_FR.json index 11631e81cd..109f51e79c 100644 --- a/src/assets/i18n/fr_FR.json +++ b/src/assets/i18n/fr_FR.json @@ -2672,5 +2672,17 @@ "quote.already_in_basket.error": "Le devis se trouve déjà dans le panier.", "quote.not_found.error": "Le devis n’a pas pu être trouvé.", "basket.add_quote.error": "Le devis n’a pas pu être ajouté au panier.", - "quoterequest.not_editable.error": "Vous avez déjà soumis la demande de devis. Veuillez rafraîchir cette page pour afficher les modifications." + "quoterequest.not_editable.error": "Vous avez déjà soumis la demande de devis. Veuillez rafraîchir cette page pour afficher les modifications.", + "checkout.sepa.iban.error.default": "L’IBAN est obligatoire.", + "checkout.sepa.iban.error.length": "La longueur de l’IBAN n’est pas valide.", + "checkout.sepa.iban.error.countryNotSupported": "L’IBAN est associé à un pays qui n’est pas pris en charge.", + "checkout.sepa.iban.error.notAlphanumeric": "L’IBAN doit être composé de caractères alphanumériques.", + "checkout.sepa.iban.error.invalid": "L’IBAN n’est pas valide.", + "checkout.sepa.accountholder.error.default": "Le titulaire du compte est obligatoire.", + "checkout.sepa.accountholder.error.notAlphanumeric": "Le titulaire du compte doit être composé de caractères alphanumériques.", + "checkout.sepa.bic.error.notAlphanumeric": "Le BIC doit être composé de caractères alphanumériques.", + "checkout.sepa.bic.error.countryNotSupported": "Le BIC est associé à un pays qui n’est pas pris en charge.", + "checkout.sepa.bic.error.length": "La longueur du BIC n’est pas valide.", + "checkout.sepa.bic.error.invalid": "Le BIC n’est pas valide.", + "checkout.sepa.mandate.error.required": "Aucune autorisation n’a été donnée." }