Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added nedbank implementation #6293

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions e2e/test-registration-data/test-registrations-Nedbank.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
referenceId,programFinancialServiceProviderConfigurationName,phoneNumber,preferredLanguage,paymentAmountMultiplier,fullName
,Nedbank,27000000000,en,1,Sample Name
25 changes: 23 additions & 2 deletions services/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ NODE_ENV=development
# Set to `:windows` if on windows, otherwise leave empty/undefined
WINDOWS_DEV_STARTUP_SUFFIX=

# This is the UUID that is used as namespace when generating the UUIDs(v5) for Intersolve Visa transfers and Nedbank transfers
UUID_NAMESPACE=

# ----------
# API set up
# ----------
Expand Down Expand Up @@ -173,6 +176,11 @@ CRON_INTERSOLVE_VISA_UPDATE_WALLET_DETAILS=
# (Optional) Check CBE-accounts
CRON_CBE_ACCOUNT_ENQUIRIES_VALIDATION=

# FSP-Specific: Nedbank
# (All Require Admin-account credentials to be set!)
# (Optional) Check Nedbank-vouchers
CRON_NEDBANK_VOUCHERS=


# --------------------------
# Interface(s) configuration
Expand Down Expand Up @@ -253,8 +261,6 @@ INTERSOLVE_VISA_COVERLETTER_CODE=TESTINTERSOLVEVISACOVERLETTERCODE # Only use ca
INTERSOLVE_VISA_ASSET_CODE=test-INTERSOLVE_VISA_ASSET_CODE
INTERSOLVE_VISA_FUNDINGTOKEN_CODE=test_INTERSOLVE_VISA_FUNDINGTOKEN_CODE # Use pre-agreed code for Acceptance environment
INTERSOLVE_VISA_TENANT_ID=
# This is the UUID that is used as namespace when generating the UUIDs(v5) for Intersolve Visa transfers
INTERSOLVE_VISA_UUID_NAMESPACE=

# Sync data automatically with third parties (now only used for Intersolve Visa)
# Use: `TRUE` to enable, leave empty or out to disable.
Expand Down Expand Up @@ -288,6 +294,21 @@ COMMERCIAL_BANK_ETHIOPIA_USERNAME=test-COMMERCIAL_BANK_ETHIOPIA_USERNAME
MOCK_COMMERCIAL_BANK_ETHIOPIA=TRUE


# --------------------
# Third-party FSP: Nedbank
# --------------------
NEDBANK_ACCOUNT_NUMBER=1009000675
NEDBANK_CLIENT_ID=test-NEDBANK_CLIENT_ID
NEDBANK_CLIENT_SECRET=test-NEDBANK_CLIENT_SECRET
# This is the sandbox url which is also visible in the Nedbank API documentation
# In production and staging a different URL is used
NEDBANK_API_URL=https://b2b-api.nedbank.co.za/apimarket/b2b-sb/
# To use a mock version of the NEDBANK API, use: `TRUE` to enable, leave empty or out to disable.
MOCK_NEDBANK=TRUE
# The nedbank certificate is used to authenticate with the Nedbank API
NEDBANK_CERTIFICATE_PATH=cert/APIMTPP_redcross_sandbox.pfx.example
NEDBANK_CERTIFICATE_PASSWORD=

# ---------------------------------------------------------------------
# END of ENV-configuration
# Make sure to store this file only temporary, or in a secure location!
Expand Down
5 changes: 4 additions & 1 deletion services/121-service/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ swagger-spec.json
.idea

# certificates
# database certificate
cert/DigiCertGlobalRootCA.crt.pem
# nedbank integration certificate
cert/*.pfx

# Compodoc documentation
documentation

# AppMap
tmp
sql_warning.txt
sql_warning.txt
Empty file.
4 changes: 4 additions & 0 deletions services/121-service/module-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ graph LR
PaymentsModule-->SafaricomModule
SafaricomModule-->RedisModule
SafaricomModule-->QueuesRegistryModule
PaymentsModule-->NedbankModule
PaymentsModule-->ExcelModule
ExcelModule-->TransactionsModule
ExcelModule-->RegistrationsModule
Expand Down Expand Up @@ -102,6 +103,8 @@ graph LR
MessageIncomingModule-->MessageTemplateModule
MessageIncomingModule-->RegistrationDataModule
MessageIncomingModule-->QueuesRegistryModule
NedbankReconciliationModule-->NedbankModule
NedbankReconciliationModule-->TransactionsModule
NoteModule-->RegistrationsModule
NoteModule-->UserModule
ActivitiesModule-->NoteModule
Expand All @@ -112,6 +115,7 @@ graph LR
TransactionJobProcessorsModule-->RedisModule
TransactionJobProcessorsModule-->IntersolveVisaModule
TransactionJobProcessorsModule-->SafaricomModule
TransactionJobProcessorsModule-->NedbankModule
TransactionJobProcessorsModule-->ProgramFinancialServiceProviderConfigurationsModule
TransactionJobProcessorsModule-->RegistrationsModule
TransactionJobProcessorsModule-->ProgramModule
Expand Down
2 changes: 2 additions & 0 deletions services/121-service/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { OrganizationModule } from '@121-service/src/organization/organization.m
import { ProgramAidworkerAssignmentEntity } from '@121-service/src/programs/program-aidworker.entity';
import { ProgramModule } from '@121-service/src/programs/programs.module';
import { QueuesRegistryModule } from '@121-service/src/queues-registry/queues-registry.module';
import { NedbankReconciliationModule } from '@121-service/src/reconciliation/nedbank-reconciliation/nedbank-reconciliation.module';
import { ScriptsModule } from '@121-service/src/scripts/scripts.module';
import { ProgramExistenceInterceptor } from '@121-service/src/shared/interceptors/program-existence.interceptor';
import { TransactionJobProcessorsModule } from '@121-service/src/transaction-job-processors/transaction-job-processors.module';
Expand All @@ -45,6 +46,7 @@ import { TypeOrmModule } from '@121-service/src/typeorm.module';
MessageModule,
MetricsModule,
MessageIncomingModule,
NedbankReconciliationModule,
NoteModule,
EmailsModule,
ScheduleModule.forRoot(),
Expand Down
12 changes: 12 additions & 0 deletions services/121-service/src/cronjob/cronjob.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ export class CronjobService {
await this.httpService.post(url, {}, headers);
}

// Nedbank's systems are not available between 0:00 and 3:00 at night South Africa time
@Cron(CronExpression.EVERY_DAY_AT_4AM, {
RubenGeo marked this conversation as resolved.
Show resolved Hide resolved
disabled: !shouldBeEnabled(process.env.CRON_NEDBANK_VOUCHERS),
})
public async cronDoNedbankReconciliation(): Promise<void> {
// Calling via API/HTTP instead of directly the Service so scope-functionality works, which needs a HTTP request to work which a cronjob does not have
const accessToken = await this.axiosCallsService.getAccessToken();
const url = `${this.axiosCallsService.getBaseUrl()}/financial-service-providers/nedbank`;
const headers = this.axiosCallsService.accesTokenToHeaders(accessToken);
await this.httpService.patch(url, {}, headers);
}

@Cron(CronExpression.EVERY_DAY_AT_6AM, {
disabled: !shouldBeEnabled(process.env.CRON_GET_DAILY_EXCHANGE_RATES),
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum FinancialServiceProviders {
commercialBankEthiopia = 'Commercial-bank-ethiopia',
excel = 'Excel',
deprecatedJumbo = 'Intersolve-jumbo-physical',
nedbank = 'Nedbank',
}

export enum FinancialServiceProviderConfigurationProperties {
Expand All @@ -16,4 +17,5 @@ export enum FinancialServiceProviderConfigurationProperties {
brandCode = 'brandCode',
coverLetterCode = 'coverLetterCode',
fundingTokenCode = 'fundingTokenCode',
paymentReferencePrefix = 'paymentReferencePrefix',
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,24 @@ export const FINANCIAL_SERVICE_PROVIDER_SETTINGS: FinancialServiceProviderDto[]
attributes: [],
configurationProperties: [],
},
{
name: FinancialServiceProviders.nedbank,
integrationType: FinancialServiceProviderIntegrationType.api,
defaultLabel: {
en: 'Nedbank',
},
notifyOnTransaction: false,
attributes: [
{
name: FinancialServiceProviderAttributes.phoneNumber,
isRequired: true,
},
],
configurationProperties: [
{
name: FinancialServiceProviderConfigurationProperties.paymentReferencePrefix,
isRequired: true,
},
],
},
];
47 changes: 40 additions & 7 deletions services/121-service/src/metrics/metrics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ExportVisaCardDetails } from '@121-service/src/payments/fsp-integration
import { ExportVisaCardDetailsRawData } from '@121-service/src/payments/fsp-integration/intersolve-visa/interfaces/export-visa-card-details-raw-data.interface';
import { IntersolveVisaStatusMapper } from '@121-service/src/payments/fsp-integration/intersolve-visa/mappers/intersolve-visa-status.mapper';
import { IntersolveVoucherService } from '@121-service/src/payments/fsp-integration/intersolve-voucher/intersolve-voucher.service';
import { NedbankVoucherEntity } from '@121-service/src/payments/fsp-integration/nedbank/nedbank-voucher.entity';
import { SafaricomTransferEntity } from '@121-service/src/payments/fsp-integration/safaricom/entities/safaricom-transfer.entity';
import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum';
import { TransactionEntity } from '@121-service/src/payments/transactions/transaction.entity';
Expand Down Expand Up @@ -790,12 +791,15 @@ export class MetricsService {
await this.getAdditionalFspExportFields(programId);

for (const field of additionalFspExportFields) {
const joinTableAlias = `joinTable${field.entityJoinedToTransaction.name}${field.attribute}`;
transactionQuery.leftJoin(
field.entityJoinedToTransaction,
'joinTable',
'transaction.id = joinTable.transactionId',
joinTableAlias,
`transaction.id = ${joinTableAlias}.transactionId`,
);
transactionQuery.addSelect(
`"${joinTableAlias}"."${field.attribute}" as "${field.alias}"`,
);
transactionQuery.addSelect(`"${field.attribute}"`);
}

const duplicateNames = registrationDataOptions
Expand Down Expand Up @@ -863,10 +867,12 @@ export class MetricsService {
return result;
}

private async getAdditionalFspExportFields(
programId: number,
): Promise<
{ entityJoinedToTransaction: EntityClass<any>; attribute: string }[]
private async getAdditionalFspExportFields(programId: number): Promise<
{
entityJoinedToTransaction: EntityClass<any>;
attribute: string;
alias: string;
}[]
> {
const program = await this.programRepository.findOneOrFail({
where: { id: Equal(programId) },
Expand All @@ -875,6 +881,7 @@ export class MetricsService {
let fields: {
entityJoinedToTransaction: EntityClass<any>;
attribute: string;
alias: string;
}[] = [];

for (const fspConfig of program.programFinancialServiceProviderConfigurations) {
Expand All @@ -888,6 +895,32 @@ export class MetricsService {
{
entityJoinedToTransaction: SafaricomTransferEntity,
attribute: 'mpesaTransactionId',
alias: 'mpesaTransactionId',
},
],
];
}
if (
fspConfig.financialServiceProviderName ===
FinancialServiceProviders.nedbank
) {
fields = [
...fields,
...[
{
entityJoinedToTransaction: NedbankVoucherEntity, //TODO: should we move this to financial-service-providers-settings.const.ts?
attribute: 'status',
alias: 'nedbankVoucherStatus',
},
{
entityJoinedToTransaction: NedbankVoucherEntity, //TODO: should we move this to financial-service-providers-settings.const.ts?
attribute: 'orderCreateReference',
alias: 'nedbankOrderCreateReference',
},
{
entityJoinedToTransaction: NedbankVoucherEntity, //TODO: should we move this to financial-service-providers-settings.const.ts?
attribute: 'paymentReference',
alias: 'nedbankPaymentReference',
},
],
];
Expand Down
30 changes: 30 additions & 0 deletions services/121-service/src/migration/1736155425026-nedbank.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class Nedbank1736155425026 implements MigrationInterface {
name = 'Nedbank1736155425026';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "121-service"."nedbank_voucher" ("id" SERIAL NOT NULL, "created" TIMESTAMP NOT NULL DEFAULT now(), "updated" TIMESTAMP NOT NULL DEFAULT now(), "orderCreateReference" character varying NOT NULL, "paymentReference" character varying NOT NULL, "status" character varying, "transactionId" integer NOT NULL, CONSTRAINT "UQ_3a31e9cd76bd9c06826c016c130" UNIQUE ("orderCreateReference"), CONSTRAINT "REL_739b726eaa8f29ede851906edd" UNIQUE ("transactionId"), CONSTRAINT "PK_85d56d9ed997ba24b53b3aa36e7" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_0db7adee73f8cc5c9d44a77e7b" ON "121-service"."nedbank_voucher" ("created") `,
);
await queryRunner.query(
`ALTER TABLE "121-service"."nedbank_voucher" ADD CONSTRAINT "FK_739b726eaa8f29ede851906edd3" FOREIGN KEY ("transactionId") REFERENCES "121-service"."transaction"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`CREATE INDEX "IDX_3a31e9cd76bd9c06826c016c13" ON "121-service"."nedbank_voucher" ("orderCreateReference") `,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "121-service"."nedbank_voucher" DROP CONSTRAINT "FK_739b726eaa8f29ede851906edd3"`,
);
await queryRunner.query(
`DROP INDEX "121-service"."IDX_0db7adee73f8cc5c9d44a77e7b"`,
);
await queryRunner.query(`DROP TABLE "121-service"."nedbank_voucher"`);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Issuer, TokenSet } from 'openid-client';
import { v4 as uuid, v5 as uuidv5 } from 'uuid';
import { v4 as uuid } from 'uuid';

import { CreateCustomerRequestIntersolveApiDto } from '@121-service/src/payments/fsp-integration/intersolve-visa/dtos/intersolve-api/create-customer-request-intersolve-api.dto';
import { CreateCustomerResponseIntersolveApiDto } from '@121-service/src/payments/fsp-integration/intersolve-visa/dtos/intersolve-api/create-customer-response-intersolve-api.dto';
Expand Down Expand Up @@ -28,18 +28,7 @@ import { IntersolveVisaApiError } from '@121-service/src/payments/fsp-integratio
import { CustomHttpService } from '@121-service/src/shared/services/custom-http.service';
import { formatPhoneNumber } from '@121-service/src/utils/phone-number.helpers';
import { TokenValidationService } from '@121-service/src/utils/token/token-validation.service';

const INTERSOLVE_VISA_UUID_NAMESPACE =
process.env.INTERSOLVE_VISA_UUID_NAMESPACE || uuid();

/**
* Generate a UUID v5 based on a seed.
* @param seed The seed to generate the UUID.
* @returns The generated UUID.
*/
function generateUUIDFromSeed(seed: string): string {
return uuidv5(seed, INTERSOLVE_VISA_UUID_NAMESPACE);
}
import { generateUUIDFromSeed } from '@121-service/src/utils/uuid.helpers';

const intersolveVisaApiUrl = process.env.MOCK_INTERSOLVE
? `${process.env.MOCK_SERVICE_URL}api/fsp/intersolve-visa`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`NedbankService retrieveVoucherInfo should return a voucher status and specific error message on an error with code NBApimResourceNotFound 1`] = `"Nedbank voucher was not found, something went wrong when creating the voucher. Please retry the transfer."`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export interface NedbankCreateOrderRequestBodyDto {
Data: {
Initiation: {
InstructionIdentification: string;
InstructedAmount: {
Amount: string; // This should be a string with two decimal places
Currency: 'ZAR'; // This should always be 'ZAR'
};
DebtorAccount: {
SchemeName: 'account'; // should always be 'account'
Identification: string;
Name: string;
};
CreditorAccount: {
SchemeName: string;
Identification: string;
Name: string;
};
};
ExpirationDateTime: string;
};
Risk: {
OrderCreateReference: string;
OrderDateTime: string;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NedbankVoucherStatus } from '@121-service/src/payments/fsp-integration/nedbank/enums/nedbank-voucher-status.enum';

// Named the interface 'CreateOrder' in line with https://apim.nedbank.co.za/static/docs/cashout-create-order
export interface CreateOrderResponseNedbankDto {
Data: {
OrderId: string;
Status: NedbankVoucherStatus;
};
Links: {
Self: string;
};
Meta: Record<string, unknown>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface ErrorReponseNedbankDto {
Message: string;
Code: string;
Id: string;
Errors: {
ErrorCode: string;
Message: string;
Path?: string;
Url?: string;
}[];
}
Loading
Loading