Backup Filter Ben if is Ary
Backup Filter Ben if is Ary
ts
import { BehaviorSubject, EMPTY, catchError, filter, iif, map, of, switchMap, tap } from 'rxjs';
import { Component, EventEmitter, Output, ChangeDetectionStrategy } from '@angular/core';
import { FormBuilder, FormControl, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Beneficiary } from '@bsf/beneficiary-manager-http-ang';
import { TransactionQualificationRequest } from '@bsf/payment-support-http-ang';
import { BsfP2pInitiatePaymentService } from '../../services/bsf-p2p-initiate-payment.service';
import {
BeneficiaryCategory,
CustomerSegmentType,
AccountSelectorItem,
TransferTabsTypes,
PurposesListItem,
AmountType,
} from '../../models/bsf-p2p-initiate-payment.model';
import { BsfTransferFormService } from '../../services/bsf-transfer-form.service';
import { paymentAmountValidator } from '../../validators/amount-validator';
import { BeneficiaryTypeDetails } from 'libs/beneficiary-manager-journey/util/src/lib/enums';
@Component({
selector: 'bsf-normal-transfer',
templateUrl: './bsf-normal-transfer.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BsfNormalTransfersComponent {
@Output() transactionQualificationEvent = new EventEmitter<{ reqbody:
TransactionQualificationRequest }>();
private readonly isEditView = this.route.snapshot.queryParams?.isEdit;
readonly accounts$ =
this.bsfP2pInitiatePaymentService.getArrangements(TransferTabsTypes.normal).pipe(
catchError((error) => {
this.error = this.bsfP2pInitiatePaymentService.handleError(error);
return EMPTY;
}),
);
beneficiaryTypeDetails = BeneficiaryTypeDetails;
activeType: string = '';
beneficiariesResponse: Beneficiary[];
currentBeneficiaryType: string = '';
filterBeneficiaries!: Beneficiary[];
beneficiaries$ = new BehaviorSubject<Beneficiary[]>([]);
getActiveBeneficiariesList() {
this.bsfP2pInitiatePaymentService.getActiveBeneficiaries$
.pipe(
tap((results) => {
this.beneficiariesResponse = results;
if (this.isEditView) {
const transferForm = this.bsfP2pInitiatePaymentService.getFormState();
this.transferForm.setValue(transferForm);
this.selectedFromAccount = transferForm.fromAccount;
this.activeType = this.bsfP2pInitiatePaymentService.activeBeneficiaryType;
}
this.beneficiaries$.next(results);
}),
catchError((error) => {
this.error = this.bsfP2pInitiatePaymentService.handleError(error);
return EMPTY;
}),
)
.subscribe();
}
onBeneficiaryTypeFilter(beneficiaryType: string) {
this.setBeneficiaryTypeIcons(beneficiaryType);
setBeneficiaryTypeIcons(beneficiaryType: string) {
this.beneficiaryTypeDetails.forEach((value) => {
value.iconName =
value.type === beneficiaryType && beneficiaryType !== this.activeType
? value.activeIconName
: value.inActiveIconName;
});
this.activeType = this.activeType === beneficiaryType ? '' : beneficiaryType;
this.bsfP2pInitiatePaymentService.activeBeneficiaryType = this.activeType;
}
// to use enum in template
readonly beneficiaryCategory = BeneficiaryCategory;
error!: { message: string };
selectedFromAccount!: AccountSelectorItem;
readonly transferForm = this.formBuilder.group({
fromAccount: [null as AccountSelectorItem, Validators.required],
toAccount: [null as Beneficiary, Validators.required],
transferAmount: ['', [Validators.required, paymentAmountValidator({ invalidAmount: true })]],
crossCurrencyIntrabank: this.formBuilder.group({
amount: [''],
currency: [''],
amountType: [AmountType.SOURCE],
}),
crossCurrencyInternational: this.formBuilder.group({
payment: {
amount: '',
currency: '',
},
amountType: [AmountType.SOURCE],
isTransferFeeIncluded: [false],
}),
purposeOfPayment: [
{
id: '',
description: '',
isOther: false,
type: '',
},
Validators.required,
],
otherPurpose: [''],
});
this.transferForm.controls.crossCurrencyIntrabank.controls.amount.updateValueAndValidity();
} else {
this.transferForm.get('crossCurrencyIntrabank.currency').patchValue(currencies as string);
this.transferForm.controls.crossCurrencyInternational.controls.payment.clearValidators();
this.transferForm.controls.crossCurrencyInternational.controls.payment.updateValueAndValidity();
}
}),
catchError((error) => {
this.error = this.bsfP2pInitiatePaymentService.handleError(error);
// return empty "toCurrency" for simple amount group component to load
return of([]);
}),
),
),
);
constructor(
private readonly route: ActivatedRoute,
private readonly formBuilder: FormBuilder,
private readonly temporaryFormService: BsfTransferFormService,
private readonly bsfP2pInitiatePaymentService: BsfP2pInitiatePaymentService,
){
this.getActiveBeneficiariesList();
if (!this.isEditView) {
this.setBeneficiaryTypeIcons(this.activeType);
}
}
hideAlert() {
this.error = undefined;
}
onBeneficiarySelect() {
this.transferAmountControl.reset();
this.fromAccountControl.reset(undefined, {
emitEvent: false,
});
this.transferForm.controls.purposeOfPayment.reset();
this.transferForm.controls.otherPurpose.reset();
this.transferForm.controls.crossCurrencyInternational.controls.payment.reset();
this.transferForm.controls.crossCurrencyIntrabank?.controls?.amount.reset();
this.transferForm.controls.transferAmount.setValidators([
Validators.required,
paymentAmountValidator({ invalidAmount: true }),
]);
this.transferForm.updateValueAndValidity();
this.selectedFromAccount = undefined;
}
onFromAccountSelect(account: AccountSelectorItem) {
this.selectedFromAccount = account;
}
onPaymentContinue() {
const debitAccount = this.fromAccountControl.value?.number;
const creditAccount = this.toAccountControl.value?.accountNumber;
const bankCode = this.toAccountControl.value?.bankCode;
const amount = this.transferForm.get('transferAmount').value;
const requestParam = {
customerSegment: CustomerSegmentType.CLC,
debitAccount,
creditAccount,
bankCode,
amount,
transactionType: TransactionQualificationRequest.TransactionTypeEnum.IBAN,
};
this.makePaymentReviewWork();
this.bsfP2pInitiatePaymentService.setFormState(this.transferForm.value);
// check for transaction qualification (only applicable for Domestic Transfers, check logic inside
container component)
this.transactionQualificationEvent.emit({
reqbody: requestParam,
});
}
resetForm() {
this.transferForm.reset(undefined, { emitEvent: false });
this.selectedFromAccount = undefined;
this.setBeneficiaryTypeIcons('');
}
/**
* TODO:
* Below method is to isolate technical debt which is needed to make payment review work
correctly.
* Can be removed when payment container, review component and initiate payment service is
refactored to remove state manipulation.
*/
private makePaymentReviewWork() {
this.bsfP2pInitiatePaymentService.selectedBeneficiary = this.toAccountControl.value;
this.bsfP2pInitiatePaymentService.selectedItems = this.fromAccountControl.value;
this.temporaryFormService.normalTransferForm.controls.transferAmount.setValue(
this.transferForm.get('transferAmount').value,
);
this.temporaryFormService.normalTransferForm.controls.transferPurpose.setValue(
this.transferForm.get('purposeOfPayment').value,
);
this.temporaryFormService.normalTransferForm.controls.isTransferFeeIncluded.setValue(
this.transferForm.controls.crossCurrencyInternational.controls.isTransferFeeIncluded.value,
);
this.transferForm.controls.crossCurrencyIntrabank?.controls?.amountType.value
? this.transferForm.controls.crossCurrencyIntrabank?.controls?.amountType.value
:
this.transferForm.controls.crossCurrencyIntrabank?.controls?.amountType.setValue(AmountType.S
OURCE);
}
}
}
bsf-normal-transfer.component.spec.ts
const purposes = [
{
id: '6',
type: 'TRANSFER',
isOther: false,
description: 'Personal / Family Remittance',
},
{
id: '1',
type: 'TRANSFER',
isOther: false,
description: 'Import Finance Payment',
},
{
id: '2',
type: 'TRANSFER',
isOther: false,
description: 'Education / Medical / Travel Expenses',
},
{
id: '7',
type: 'TRANSFER',
isOther: true,
description: 'Others',
},
];
const beneficiary = {
cpt: '011988',
beneficiarySequence: '7772370',
category: BeneficiaryCategory.INT,
active: false,
beneficiaryName: 'International',
alias: null,
beneficiaryCountry: 'UNITED KINGDOM',
beneficiaryAddress: 'Qa',
accountNumber: 'GB33MIDL40060751663739',
bankName: 'HSBC BANK PLC',
bankBranch: 'HSBC BANK PLC',
bankAddressLine1: '62 - 76 PARK STREET',
bankAddressLine2: '',
bankCode: 'MIDLGB2137G',
bankCountry: 'GB',
isIban: false,
createdUser: 'MBK',
createdDate: '2024-01-11',
modifiedUser: 'MBK',
modifiedDate: '2024-01-11',
};
const mockForm = {
fromAccount: {
id: '123',
balance: 100,
favorite: false,
bankBranchCode: 'abc',
name: 'Savings Account',
number: 'SA5655000000001198802533',
},
toAccount: {
cpt: '011988',
beneficiarySequence: '7772370',
category: BeneficiaryCategory.BSF,
active: false,
beneficiaryName: 'International',
alias: null,
beneficiaryCountry: 'UNITED KINGDOM',
beneficiaryAddress: 'Qa',
accountNumber: 'GB33MIDL40060751663739',
bankName: 'HSBC BANK PLC',
bankBranch: 'HSBC BANK PLC',
bankAddressLine1: '62 - 76 PARK STREET',
bankCode: 'MIDLGB2137G',
isIban: false,
createdUser: 'MBK',
createdDate: '2024-01-11',
modifiedUser: 'MBK',
modifiedDate: '2024-01-11',
},
transferAmount: '100',
crossCurrencyIntrabank: {
amount: '100',
currency: 'USD',
amountType: 'type1',
},
crossCurrencyInternational: {
payment: {
amount: '200',
currency: 'EUR',
},
amountType: 'type2',
isTransferFeeIncluded: false,
},
purposeOfPayment: {
id: '1',
description: 'Personal',
isOther: false,
type: '',
},
otherPurpose: 'Other purpose',
};
const spyBsfP2pInitiatePaymentService = jasmine.createSpyObj('BsfP2pInitiatePaymentService', [
'activeBeneficiaryType'
]);
describe('BsfNormalTransfersComponent', () => {
let component: BsfNormalTransfersComponent;
let fixture: ComponentFixture<BsfNormalTransfersComponent>;
const mockActivatedRoute = {
snapshot: {
queryParams: { isEdit: false },
},
};
const mockBsfP2pInitiatePaymentService = {
getArrangements: () => of([]),
handleError: () => ({ message: 'Sorry, this service is currently unavailable. Please try again
later.' }),
getActiveBeneficiaries$: of([beneficiary]),
getCurrencies: () => of({ allowedCurrencies: ['USD'] }),
getBsfAccountCurrency: () => of({ currencyCode: 'SAR' }),
getPaymentCategory: () => 'category',
getPurposes: () => of([{ id: '1', description: 'Personal', isOther: false, type: '' }]),
getFormState: () => mockForm,
setFormState: jasmine.createSpy('setFormState'),
selectedBeneficiary: {},
selectedItems: {},
};
const mockBsfTransferFormService = {
normalTransferForm: {
controls: {
transferAmount: { setValue: jasmine.createSpy('setValue') },
transferPurpose: { setValue: jasmine.createSpy('setValue') },
otherPurpose: { setValue: jasmine.createSpy('setValue') },
},
},
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [BsfNormalTransfersComponent],
providers: [
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
{
provide: FormBuilder,
useValue: {
group() {
return new FormGroup({
fromAccount: new FormControl(''),
toAccount: new FormControl(''),
transferAmount: new FormControl(''),
crossCurrencyIntrabank: new FormGroup({
amount: new FormControl(''),
currency: new FormControl(''),
amountType: new FormControl(''),
}),
crossCurrencyInternational: new FormGroup({
payment: new FormControl({
amount: '',
currency: '',
}),
amountType: new FormControl(''),
isTransferFeeIncluded: new FormControl(false),
}),
purposeOfPayment: new FormControl({
id: '',
description: '',
isOther: false,
type: '',
}),
otherPurpose: new FormControl(''),
});
},
},
},
{ provide: BsfP2pInitiatePaymentService, useValue: mockBsfP2pInitiatePaymentService },
{ provide: BsfTransferFormService, useValue: mockBsfTransferFormService },
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
fixture = TestBed.createComponent(BsfNormalTransfersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should set the first currency returned in list of currencies when toCurrency$ is subscribed to
and beneficiary is international', () => {
spyOn(mockBsfP2pInitiatePaymentService, 'getCurrencies').and.returnValue(
of({ allowedCurrencies: ['USD', 'EUR', 'SAR'] }),
);
component.transferForm.get('toAccount').setValue(beneficiary as any);
component.toCurrency$.subscribe();
expect(component.transferForm.get('crossCurrencyInternational.payment').value).toEqual({
amount: '',
currency: 'USD',
});
});
it('should set iconName to activeIconName for the matching beneficiaryType and inactive for
others', () => {
component.setBeneficiaryTypeIcons('BSF');
expect(component.beneficiaryTypeDetails[0].iconName).toBe('benf-bsf-white');
expect(component.beneficiaryTypeDetails[1].iconName).toBe('benf-ksa');
});
it('should set the currency returned in list of currencies when toCurrency$ is subscribed to and
beneficiary is intrabank', () => {
const intrabankBeneficiary = {
cpt: '011988',
beneficiarySequence: '7772370',
category: BeneficiaryCategory.BSF,
active: false,
beneficiaryName: 'International',
alias: null,
beneficiaryCountry: 'UNITED KINGDOM',
beneficiaryAddress: 'Qa',
accountNumber: 'GB33MIDL40060751663739',
bankName: 'HSBC BANK PLC',
bankBranch: 'HSBC BANK PLC',
bankAddressLine1: '62 - 76 PARK STREET',
bankAddressLine2: '',
bankCode: 'MIDLGB2137G',
bankCountry: 'GB',
isIban: false,
createdUser: 'MBK',
createdDate: '2024-01-11',
modifiedUser: 'MBK',
modifiedDate: '2024-01-11',
};
spyOn(mockBsfP2pInitiatePaymentService, 'getBsfAccountCurrency').and.returnValue(of({
currencyCode: 'SAR' }));
component.transferForm.get('toAccount').setValue(intrabankBeneficiary as any);
component.toCurrency$.subscribe();
expect(component.transferForm.get('crossCurrencyIntrabank.currency').value).toEqual('SAR');
});
it('should set the purpose of payment with a default value as Personal', () => {
spyOn(mockBsfP2pInitiatePaymentService, 'getPurposes').and.returnValue(of(purposes));
component.transferForm.get('toAccount').setValue(beneficiary as any);
component.purposeOfPayment$.subscribe();
expect(component.transferForm.get('purposeOfPayment').value).toEqual({
id: '6',
description: 'Personal / Family Remittance',
isOther: false,
type: 'TRANSFER',
});
});
it('should set the purpose of payment with a default value as Personal', () => {
spyOn(mockBsfP2pInitiatePaymentService, 'getPurposes').and.returnValue(of(null));
component.transferForm.get('toAccount').setValue(beneficiary as any);
component.purposeOfPayment$.subscribe();
expect(component.error).toEqual({
message: `Sorry, this service is currently unavailable. Please try again later.`,
});
});
it('should reset form fields except toAccount control when a beneficiary is selected', () => {
spyOn(component.transferAmountControl, 'reset');
spyOn(component.fromAccountControl, 'reset');
spyOn(component.transferForm.get('purposeOfPayment'), 'reset');
spyOn(component.transferForm.get('otherPurpose'), 'reset');
component.onBeneficiarySelect();
expect(component.transferAmountControl.reset).toHaveBeenCalled();
expect(component.fromAccountControl.reset).toHaveBeenCalledWith(undefined, { emitEvent:
false });
expect(component.transferForm.get('purposeOfPayment').reset).toHaveBeenCalled();
expect(component.transferForm.get('otherPurpose').reset).toHaveBeenCalled();
expect(component.selectedFromAccount).toBeUndefined();
});
it('should set the from account property when an account is selected from the dropdown', () => {
const account = {
favorite: false,
bankBranchCode: '123',
id: '456',
balance: 100.23,
};
component.onFromAccountSelect(account);
expect(component.selectedFromAccount).toEqual(account);
});
it('should set iconName to activeIconName for matching type and inactive for others', () => {
component.onBeneficiaryTypeFilter('BSF');
expect(component.beneficiaryTypeDetails[0].iconName).toBe('benf-bsf-white');
expect(component.beneficiaryTypeDetails[1].iconName).toBe('benf-ksa');
});
it('should change iconName based on the new beneficiaryType and reset icons if same type is
selected again', () => {
component.onBeneficiaryTypeFilter('BSF');
expect(component.beneficiaryTypeDetails[0].iconName).toBe('benf-bsf-white');
component.onBeneficiaryTypeFilter('BSF');
expect(component.beneficiaryTypeDetails[0].iconName).toBe('benf-bsf');
});
});
bsf-normal-transfer.component.html
<div class="bb-card">
<div class="bb-card__body">
<bb-alert-ui
*ngIf="error"
i18n-title="Service error@@bsf-retail.p2p-payment-journey.transfer.service.error"
title="Service error alert"
[dismissible]="true"
(close)="hideAlert()"
>
<p>{{ error.message }}</p>
</bb-alert-ui>
[crossCurrencyAmountControl]="transferForm.controls.crossCurrencyIntrabank?.controls?.amount"
[crossCurrencytoCurrencyControl]="transferForm.controls.crossCurrencyIntrabank?.controls?.curre
ncy"
[crossCurrencyAmountTypeControl]="
transferForm.controls.crossCurrencyIntrabank?.controls?.amountType
"
[fromAccountControl]="transferForm.controls.fromAccount"
[transferAmountControl]="transferAmountControl"
></bsf-cross-currency-intratransfer-amount-group>
</ng-container>
</ng-container>
[crossCurrencyPaymentControl]="transferForm.controls.crossCurrencyInternational.controls.payme
nt"
[crossCurrencyAmountTypeControl]="
transferForm.controls.crossCurrencyInternational.controls.amountType
"
[crossCurrencyTransferFeeControl]="
transferForm.controls.crossCurrencyInternational.controls.isTransferFeeIncluded
"
[fromAccountControl]="fromAccountControl"
[toCurrencies]="toCurrency"
[transferAmountControl]="transferAmountControl"
></bsf-cross-currency-international-amount-group>
</ng-container>
</div>
</ng-container>
</ng-container>
<ng-template #simpleAmountGroup>
<bsf-transfers-simple-amount-group
[transferAmount]="transferForm.controls.transferAmount"
[currency]="fromAccountControl.value?.currency"
></bsf-transfers-simple-amount-group>
</ng-template>
<ng-template #loading>
<bb-loading-indicator-ui loaderSize="lg"></bb-loading-indicator-ui>
</ng-template>
beneficiaryTypes.ts - /Users/dthorat/Documents/Projects/web-retail-us-portalless/src/libs/
beneficiary-manager-journey/util/src/lib/enums/beneficiaryTypes.ts
import '@angular/localize/init';
export const BeneficiaryTypeDetails = [
{
name: $localize`:BSF@@bsf-retail.beneficiary-manager.beneficiary-list-container.beneficiary-
type.label.within-bsf:BSF`,
type: 'BSF',
iconName: 'benf-bsf',
activeIconName: 'benf-bsf-white',
inActiveIconName: 'benf-bsf',
},
{
name: $localize`:Local@@bsf-retail.beneficiary-manager.beneficiary-list-container.beneficiary-
type.label.local-transfer:Local`,
type: 'KSA',
iconName: 'benf-ksa',
activeIconName: 'benf-ksa-white',
inActiveIconName: 'benf-ksa',
},
{
name: $localize`:International@@bsf-retail.beneficiary-manager.beneficiary-list-
container.beneficiary-type.label.international:International`,
type: 'INT',
iconName: 'benf-int',
activeIconName: 'benf-int-white',
inActiveIconName: 'benf-int',
},
{
name: $localize`:Western union@@bsf-retail.beneficiary-manager.beneficiary-list-
container.beneficiary-type.label.western-union:Western union`,
type: 'WU',
iconName: 'benf-wu',
activeIconName: 'benf-wu-white',
inActiveIconName: 'benf-wu',
},
{
name: $localize`:Brokerage@@bsf-retail.beneficiary-manager.beneficiary-list-
container.beneficiary-type.label.brokerage:Brokerage`,
type: 'BROKERAGE',
iconName: 'benf-brokerage',
activeIconName: 'benf-brokerage-white',
inActiveIconName: 'benf-brokerage',
},
{
name: $localize`:Company@@bsf-retail.beneficiary-manager.beneficiary-list-
container.beneficiary-type.label.company:Company`,
type: 'COMPANY',
iconName: 'benf-company',
activeIconName: 'benf-company-white',
inActiveIconName: 'benf-company',
},
];
src/libs/beneficiary-manager-journey/util/src/lib/enums/index.ts
export * from './accountCreateType';
export * from './view';
export * from './beneficiaryActions';
export * from './beneficiaryTypes';
beneficiary-list-container.component.html
<div
class="row bb-block bb-block--md beneficiary-all-beneficiaries-container"
*ngIf="beneficiaries.length > 0 || firstBeneficiaryEdit; else emptyTemplate"
>
<div
class="col beneficiary-list-container"
[ngClass]="{
'd-block': isListView | async,
'd-lg-block d-none': (isListView | async) === false
}"
>
<bb-beneficiary-list
[items]="beneficiaries"
[selectedItemId]="(selectedItemId | async) || ''"
(selectedChange)="select($event)"
>
</bb-beneficiary-list>
</div>
<div
#detailsContainer
class="col col-lg-6 beneficiary-details-container"
[ngClass]="{
'd-none d-lg-block': isListView | async
}"
>
<ng-content></ng-content>
</div>
</div>
</ng-container>
</ng-container>
</div>
</div>
<ng-template #loadingTemplate>
<ng-container *ngIf="!error; else errorTemplate">
<div class="bb-state-container">
<bb-loading-indicator-ui
i18n-text="@@beneficiary-manager.loading.label"
text="Loading beneficiaries..."
data-role="beneficiary-manager-loading"
></bb-loading-indicator-ui>
</div>
</ng-container>
</ng-template>
<ng-template #emptyTemplate>
<ng-container *ngIf="!searchTerm.value; else emptySearchTemplate">
<div class="bb-state-container">
<bb-empty-state-ui
i18n-title="@@beneficiary-manager.state.noBeneficiaryLoaded.title"
title="No beneficiary"
i18n-subtitle="@@beneficiary-manager.state.noBeneficiaryLoaded.subtitle"
subtitle="You don't have any beneficiary to display yet.<br>Try adding a new beneficiary
using the 'New Beneficiary' button"
iconClasses="d-flex justify-content-center mb-3"
iconModifier="beneficiaries"
iconColor="primary"
iconSize="xxl"
data-role="beneficiary-manager-empty"
></bb-empty-state-ui>
</div>
</ng-container>
</ng-template>
<ng-template #emptySearchTemplate>
<div class="bb-state-container">
<bb-empty-state-ui
i18n-title="@@beneficiary-manager.state.empty-search.title"
title="No results found"
i18n-subtitle="@@beneficiary-manager.state.empty-search.subtitle"
subtitle="We can't find what you're looking for. Please try a different criteria."
iconClasses="d-flex justify-content-center mb-3"
iconModifier="search"
iconColor="primary"
iconSize="xxl"
data-role="beneficiary-manager-list-search-empty"
></bb-empty-state-ui>
</div>
</ng-template>
<ng-template #errorTemplate>
<bb-alert-ui
*ngIf="listError.shouldShow"
[message]="listError.message"
title=""
[dismissible]="true"
(close)="onAlertClose()"
>
</bb-alert-ui>
</ng-template>
beneficiary-list-container.component.spec.ts
describe('BeneficiaryListContainerComponent', () => {
let component: BeneficiaryListContainerComponent;
let fixture: ComponentFixture<BeneficiaryListContainerComponent>;
const paramsSource = new Subject<ParamMap>();
const activatedRouteStub = {
queryParamMap: paramsSource.asObservable(),
};
let injectedNavigationService: BeneficiaryNavigationService;
beforeEach(() => {
jest.resetAllMocks();
const beneficiaryServiceStub = {
updateItemList: () => ({}),
dataService: {
getBeneficiaries: () => of({ body: [] }),
},
items: of({ beneficiaries: [] }),
getAccountType: () => of('IBAN'),
};
const BeneficiaryNavigationServiceStub = {
list: () => ({}),
select: () => ({}),
currentView: {},
edit: () => ({}),
newBeneficiary: () => ({}),
};
const routerStub = {
events: of('load-more'),
};
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [BeneficiaryListContainerComponent],
schemas: [NO_ERRORS_SCHEMA],
providers: [
{ provide: BeneficiaryService, useValue: beneficiaryServiceStub },
{ provide: BeneficiaryNavigationService, useValue: BeneficiaryNavigationServiceStub },
{ provide: Router, useValue: routerStub },
{ provide: ActivatedRoute, useValue: activatedRouteStub },
BeneficiaryManagerJourneyConfigurationService,
],
});
fixture = TestBed.createComponent(BeneficiaryListContainerComponent);
TestBed.inject(BeneficiaryService);
injectedNavigationService = TestBed.inject(BeneficiaryNavigationService);
component = fixture.componentInstance;
fixture.detectChanges();
});
describe('backToList', () => {
it('makes expected calls', () => {
const BeneficiaryNavigationServiceStub: BeneficiaryNavigationService =
fixture.debugElement.injector.get(BeneficiaryNavigationService);
jest.spyOn(BeneficiaryNavigationServiceStub, 'list');
component.backToList();
expect(BeneficiaryNavigationServiceStub.list).toHaveBeenCalled();
});
});
it('should set iconName to activeIconName for matching type and inactive for others', () => {
component.onBeneficiaryTypeFilter('BSF');
expect(component.beneficiaryTypeDetails[0].iconName).toBe('benf-bsf-white');
expect(component.beneficiaryTypeDetails[1].iconName).toBe('benf-ksa');
});
it('should change iconName based on the new beneficiaryType and reset icons if same type is
selected again', () => {
component.onBeneficiaryTypeFilter('BSF');
expect(component.beneficiaryTypeDetails[0].iconName).toBe('benf-bsf-white');
component.onBeneficiaryTypeFilter('BSF');
expect(component.beneficiaryTypeDetails[0].iconName).toBe('benf-bsf');
});
it('should set iconName to activeIconName for matching type and inactive for others', () => {
component.onBeneficiaryTypeFilter('BSF');
expect(component.beneficiaryTypeDetails[0].iconName).toBe('benf-bsf-white');
expect(component.beneficiaryTypeDetails[1].iconName).toBe('benf-ksa');
});
it('should change iconName based on the new beneficiaryType and reset icons if same type is
selected again', () => {
component.onBeneficiaryTypeFilter('BSF');
expect(component.beneficiaryTypeDetails[0].iconName).toBe('benf-bsf-white');
component.onBeneficiaryTypeFilter('BSF');
expect(component.beneficiaryTypeDetails[0].iconName).toBe('benf-bsf');
});
});
beneficiary-list-container.component.ts
/**
* Container component for showing beneficiary list.
*
* @usageNotes
*
* ### Display list of all the beneficiary with details / edit form.
*
* ```html
* <bb-beneficiary-list-container>
* <router-outlet></router-outlet>
* </bb-beneficiary-list-container>
* ```
* @ngModule BeneficiaryManagerJourneyFeatureModule
*/
@Component({
selector: 'bb-beneficiary-list-container',
templateUrl: './beneficiary-list-container.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BeneficiaryListContainerComponent implements OnInit, OnDestroy {
firstBeneficiaryEdit = false;
error!: HttpErrorResponse;
subscription: Subscription = new Subscription();
isSearchBoxVisible = true;
readonly searchTerm = new FormControl();
readonly debounceSearch$ = new Subject<string>();
readonly searchTerm$ = new Subject<string>();
readonly beneficiaries$ = new BehaviorSubject<Beneficiaries>([]);
listError: { shouldShow: boolean; message?: string } = {
shouldShow: false,
message: undefined,
};
beneficiariesResponse!: Beneficiaries;
beneficiaryTypeDetails = BeneficiaryTypeDetails;
currentBeneficiaryType: string = '';
activeType: string = '';
filterBeneficiaries!: Beneficiaries;
private readonly langKey = this.isEn ? LANG_KEYS.get('en-US') : LANG_KEYS.get('ar-SA');
private readonly errorMessage = $localize`:Error message@@bsf-retail.beneficiary-manager-
journey.list-container.error:Something went wrong`;
this.listError.shouldShow = true;
if (errors?.length) {
const message = errors.find(({ key }: { key: string }) => key === this.langKey)?.message;
this.listError.message = message;
} else {
this.listError.message = this.errorMessage;
}
return of(undefined);
}),
);
/**
* Observable of currently selected item id
*/
readonly selectedItemId: Observable<ItemId | null> = this.service.currentlySelectedItemId;
constructor(
private readonly service: BeneficiaryService,
private readonly navigation: BeneficiaryNavigationService,
private readonly router: Router,
private readonly localesService: LocalesService,
) {}
ngOnInit() {
this.subscription.add(
this.debounceSearch$
.pipe(debounceTime(300), distinctUntilChanged())
.subscribe((term: string) => this.searchBeneficiaries(term)),
);
this.subscription.add(
this.searchTerm$
.pipe(
withLatestFrom(this.items),
tap(([searchTerm, results]) => {
if (!results?.beneficiaries || !Array.isArray(results?.beneficiaries)) {
return this.beneficiaries$.next([]);
}
if (!searchTerm) {
return this.currentBeneficiaryType
? this.beneficiaries$.next(this.filterBeneficiaries)
: this.beneficiaries$.next(results.beneficiaries);
}
const filterBeneficiariesBySearchTerm = this.filterBeneficiariesByInput(searchTerm);
this.beneficiaries$.next(beneficiaries);
}),
)
.subscribe(),
);
}
onBeneficiaryTypeFilter(beneficiaryType: string) {
this.beneficiaryTypeDetails.forEach((value) => {
value.iconName =
value.type === beneficiaryType && beneficiaryType !== this.activeType
? value.activeIconName
: value.inActiveIconName;
});
if (this.beneficiariesResponse) {
this.filterBeneficiaries = this.beneficiariesResponse?.filter(
(beneficiary) => beneficiary.category === beneficiaryType,
);
}
this.beneficiaries$.next(this.filterBeneficiaries);
}
}
filterBeneficiariesByInput(searchTerm) {
return (beneficiaries: Beneficiaries) =>
beneficiaries.filter((beneficiary) => {
const lowerSearchTerm = searchTerm.toLowerCase();
return (
beneficiary.maskedFullName?.toLowerCase().includes(lowerSearchTerm) ||
beneficiary.beneficiaryName?.toLowerCase().includes(lowerSearchTerm)
);
});
}
/**
* Event handler for search box value change
*
* @param {string} phrase text to search for
*/
onSearchTermChange(phrase: string) {
this.debounceSearch$.next(phrase);
}
/**
* Event handler for search box clear
*/
clearSearch() {
this.debounceSearch$.next('');
}
/**
* Queries beneficiaries with given phrase
*
* @param {string} phrase text to search for
*/
searchBeneficiaries(phrase: string) {
this.searchTerm$.next(phrase);
}
/**
* Routes to selected beneficiary details.
*
* @param {string} id Beneficiary ID
*/
select(id: string) {
this.navigation.select(id);
this.detailsContainer?.nativeElement.scrollTo({ top: 0 });
}
/**
* Routes to new beneficiary.
*
* @param {number} totalBeneficiaries - total number of beneficiary.
*/
newBeneficiary(totalBeneficiaries: number) {
this.firstBeneficiaryEdit = !totalBeneficiaries;
this.navigation.newBeneficiaryForm();
this.navigation.currentView = View.Edit;
}
/**
* method to return back to list view.
*/
backToList() {
this.navigation.currentView = View.List;
this.navigation.list();
}
ngOnDestroy() {
this.subscription?.unsubscribe();
}
onAlertClose(): void {
this.listError = {
shouldShow: false,
message: undefined,
};
}
return currentLocale.includes(en);
}
}
src/libs/bsf-p2p-initiate-payment-journey/src/lib/services/bsf-p2p-initiate-
payment.service.ts
activeBeneficiaryType: string;
src/themes/scss/1-variables/components/_icon-custom.scss
//beneficiary-types
'bsf-benf-bsf-icon': url('#{$path-bsf-images}/benf-bsf.svg'),
'bsf-benf-ksa-icon': url('#{$path-bsf-images}/benf-ksa.svg'),
'bsf-benf-int-icon': url('#{$path-bsf-images}/benf-int.svg'),
'bsf-benf-wu-icon': url('#{$path-bsf-images}/benf-wu.svg'),
'bsf-benf-company-icon': url('#{$path-bsf-images}/benf-company.svg'),
'bsf-benf-brokerage-icon': url('#{$path-bsf-images}/benf-brokerage.svg'),
'bsf-benf-bsf-white-icon': url('#{$path-bsf-images}/benf-bsf-white.svg'),
'bsf-benf-ksa-white-icon': url('#{$path-bsf-images}/benf-ksa-white.svg'),
'bsf-benf-int-white-icon': url('#{$path-bsf-images}/benf-int-white.svg'),
'bsf-benf-wu-white-icon': url('#{$path-bsf-images}/benf-wu-white.svg'),
'bsf-benf-company-white-icon': url('#{$path-bsf-images}/benf-company-white.svg'),
'bsf-benf-brokerage-white-icon': url('#{$path-bsf-images}/benf-brokerage-white.svg'),
src/themes/scss/5-components/forms/_bb-custom-payment-journey.scss
.beneficiary-text-support {
color: $color-secondary-darkest;
}
.custom-beneficiary-type {
margin-top: $spacer-sm;
.custom-beneficiary-type-btn,
button.custom-beneficiary-type-btn.inactive {
display: inline-flex;
border: 1px solid $color-secondary;
border-radius: $border-radius;
padding: $sizer-sm;
align-items: center;
margin-right: $spacer-sm;
background-color: $color-neutral-white;
color: $color-secondary;
&:last-child {
margin-top: $spacer-sm;
}
}
.custom-beneficiary-type-btn {
&.active,
&:focus {
background-color: $color-secondary;
color: $color-neutral-white;
}
}
}
src/themes/scss/5-components/_bb-beneficiaries.scss
.beneficiary-text-support {
color: $color-secondary-darkest;
}
.custom-beneficiary-type {
margin-top: $spacer-sm;
.custom-beneficiary-type-btn,
button.custom-beneficiary-type-btn.inactive {
display: inline-flex;
border: 1px solid $color-secondary;
border-radius: $border-radius;
padding: $sizer-sm;
align-items: center;
margin-right: $spacer-sm;
background-color: $color-neutral-white;
color: $color-secondary;
&:last-child {
margin-top: $spacer-sm;
}
}
.custom-beneficiary-type-btn {
&.active,
&:focus {
background-color: $color-secondary;
color: $color-neutral-white;
}
}
}