Skip to content

Commit

Permalink
Nedbank implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Ruben committed Jan 9, 2025
1 parent 164d47b commit 55c3085
Show file tree
Hide file tree
Showing 63 changed files with 2,337 additions and 356 deletions.
2 changes: 2 additions & 0 deletions e2e/test-registration-data/test-registrations-SARC.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
referenceId,programFinancialServiceProviderConfigurationName,phoneNumber,preferredLanguage,paymentAmountMultiplier,fullName,nationalId
,Nedbank,31600000000,en,1,Sample Name,1234567890
27 changes: 25 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 All @@ -44,6 +47,9 @@ PORT_MOCK_SERVICE=3001
# Note: Also used in Mock-Service
EXTERNAL_121_SERVICE_URL=http://localhost:3000/

# Public IP-address of the API. Currently only used for our Nedbank integration.
PUBLIC_IP=

# API Access rate-limits.
# Provided values are set high for local development/testing.
# Generic throttling is applied to all requests.
Expand Down Expand Up @@ -171,6 +177,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 @@ -251,8 +262,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 @@ -286,6 +295,20 @@ 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 viseble in the Nedbank API documentation
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
##TODO: Add some documentation about the certificate
NEDBANK_CERTIFICATE_PATH=cert/APIMTPP_redcross_sandbox.pfx
NEDBANK_CERTIFICATE_PASSWORD=

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

# certificates
cert/DigiCertGlobalRootCA.crt.pem
cert/APIMTPP_redcross_sandbox.pfx

# Compodoc documentation
documentation

# AppMap
tmp
sql_warning.txt
sql_warning.txt
6 changes: 6 additions & 0 deletions services/121-service/module-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ graph LR
PaymentsModule-->SafaricomModule
SafaricomModule-->RedisModule
SafaricomModule-->QueuesRegistryModule
PaymentsModule-->NedbankModule
NedbankModule-->RedisModule
NedbankModule-->QueuesRegistryModule
PaymentsModule-->ExcelModule
ExcelModule-->TransactionsModule
ExcelModule-->RegistrationsModule
Expand Down Expand Up @@ -103,6 +106,8 @@ graph LR
MessageIncomingModule-->MessageTemplateModule
MessageIncomingModule-->RegistrationDataModule
MessageIncomingModule-->QueuesRegistryModule
FinancialSyncModule-->NedbankModule
FinancialSyncModule-->TransactionsModule
NoteModule-->RegistrationsModule
NoteModule-->UserModule
ActivitiesModule-->NoteModule
Expand All @@ -113,6 +118,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 @@ -14,6 +14,7 @@ import { THROTTLING_LIMIT_GENERIC } from '@121-service/src/config';
import { CronjobModule } from '@121-service/src/cronjob/cronjob.module';
import { EmailsModule } from '@121-service/src/emails/emails.module';
import { FinancialServiceProviderCallbackJobProcessorsModule } from '@121-service/src/financial-service-provider-callback-job-processors/financial-service-provider-callback-job-processors.module';
import { FinancialSyncModule } from '@121-service/src/financial-sync/financial-sync.module';
import { HealthModule } from '@121-service/src/health/health.module';
import { MetricsModule } from '@121-service/src/metrics/metrics.module';
import { NoteModule } from '@121-service/src/notes/notes.module';
Expand Down Expand Up @@ -43,6 +44,7 @@ import { TypeOrmModule } from '@121-service/src/typeorm.module';
MessageModule,
MetricsModule,
MessageIncomingModule,
FinancialSyncModule,
NoteModule,
EmailsModule,
ScheduleModule.forRoot(),
Expand Down
11 changes: 11 additions & 0 deletions services/121-service/src/cronjob/cronjob.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ export class CronjobService {
await this.httpService.post(url, {}, headers);
}

@Cron(CronExpression.EVERY_DAY_AT_4AM, {
disabled: !shouldBeEnabled(process.env.CRON_NEDBANK_VOUCHERS),
})
public async cronRetrieveAndUpdateNedbankVouchers(): 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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,23 @@ export const FINANCIAL_SERVICE_PROVIDER_SETTINGS: FinancialServiceProviderDto[]
attributes: [],
configurationProperties: [],
},
{
name: FinancialServiceProviders.nedbank,
integrationType: FinancialServiceProviderIntegrationType.api,
defaultLabel: {
en: 'Nedbank',
},
notifyOnTransaction: false,
attributes: [
{
name: FinancialServiceProviderAttributes.fullName,
isRequired: true,
},
{
name: FinancialServiceProviderAttributes.nationalId,
isRequired: true,
},
],
configurationProperties: [],
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Controller, HttpStatus, Patch } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';

import { FinancialSyncService } from '@121-service/src/financial-sync/financial-sync-service';
import { AuthenticatedUser } from '@121-service/src/guards/authenticated-user.decorator';

@Controller()
export class FinancialSyncController {
public constructor(private financialSyncService: FinancialSyncService) {}
@ApiTags('financial-service-providers/nedbank')
@AuthenticatedUser({ isAdmin: true })
@ApiOperation({
summary:
'[CRON] Retrieve and update Nedbank voucher and transaction statusses',
})
@ApiResponse({
status: HttpStatus.CREATED,
description: 'Cached unused vouchers',
})
@Patch('financial-service-providers/nedbank')
public async syncNedbankVoucherAndTransactionStatusses(): Promise<void> {
console.info(
'CronjobService - Started: updateNedbankVoucherAndTransactionStatusses',
);
this.financialSyncService
.syncNedbankVoucherAndTransactionStatusses()
.then(() => {
console.info(
'CronjobService - Complete: updateNedbankVoucherAndTransactionStatusses',
);
return;
})
.catch((error) => {
throw new Error(
`CronjobService - Failed: updateNedbankVoucherAndTransactionStatusses - ${error}`,
);
});
}
}
72 changes: 72 additions & 0 deletions services/121-service/src/financial-sync/financial-sync-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Injectable } from '@nestjs/common';
import { In, IsNull, Not } from 'typeorm';

import { NedbankVoucherStatus } from '@121-service/src/payments/fsp-integration/nedbank/enums/nedbank-voucher-status.enum';
import { NedbankError } from '@121-service/src/payments/fsp-integration/nedbank/errors/nedbank.error';
import { NedbankService } from '@121-service/src/payments/fsp-integration/nedbank/nedbank.service';
import { NedbankVoucherScopedRepository } from '@121-service/src/payments/fsp-integration/nedbank/repositories/nedbank-voucher.scoped.repository';
import { TransactionStatusEnum } from '@121-service/src/payments/transactions/enums/transaction-status.enum';
import { TransactionScopedRepository } from '@121-service/src/payments/transactions/transaction.repository';

@Injectable()
export class FinancialSyncService {
public constructor(
private readonly nedbankService: NedbankService,
private readonly nedbankVoucherScopedRepository: NedbankVoucherScopedRepository,
private readonly transactionScopedRepository: TransactionScopedRepository,
) {}

public async syncNedbankVoucherAndTransactionStatusses(): Promise<void> {
const vouchers = await this.nedbankVoucherScopedRepository.find({
select: ['id', 'orderCreateReference', 'transactionId'],
where: [
{ status: IsNull() },
{
status: Not(
In([NedbankVoucherStatus.REDEEMED, NedbankVoucherStatus.REFUNDED]),
),
},
],
});

for (const voucher of vouchers) {
let voucherStatus: NedbankVoucherStatus;
try {
voucherStatus =
await this.nedbankService.retrieveAndUpdateVoucherStatus(
voucher.orderCreateReference,
voucher.id,
);
} catch (error) {
// ##TODO what extend should end the loop if something goes wrong?
if (error instanceof NedbankError) {
console.error(
`Error while getting order for voucher ${voucher.id}: ${error.message}`,
);
continue;
} else {
throw error;
}
}

// ##TODO: Should the NedbankService know about the TransactionModule?
// It is the case in https://miro.com/app/board/uXjVLVYmSPM=/?moveToWidget=3458764603767347191&cot=14 however I am not sure about it
if (voucherStatus === NedbankVoucherStatus.REDEEMED) {
await this.transactionScopedRepository.update(
{ id: voucher.transactionId },
{ status: TransactionStatusEnum.success },
);
}
if (voucherStatus === NedbankVoucherStatus.REFUNDED) {
await this.transactionScopedRepository.update(
{ id: voucher.transactionId },
{
status: TransactionStatusEnum.error,
errorMessage:
'Voucher has been refunded by Nedbank. Please contact Nedbank support.', // TODO: is this the correct ux copy?
},
);
}
}
}
}
13 changes: 13 additions & 0 deletions services/121-service/src/financial-sync/financial-sync.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';

import { FinancialSyncController } from '@121-service/src/financial-sync/financial-sync-controller';
import { FinancialSyncService } from '@121-service/src/financial-sync/financial-sync-service';
import { NedbankModule } from '@121-service/src/payments/fsp-integration/nedbank/nedbank.module';
import { TransactionsModule } from '@121-service/src/payments/transactions/transactions.module';

@Module({
imports: [NedbankModule, TransactionsModule],
providers: [FinancialSyncService],
controllers: [FinancialSyncController],
})
export class FinancialSyncModule {}
37 changes: 30 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}`;
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,22 @@ 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',
},
],
];
Expand Down
Loading

0 comments on commit 55c3085

Please sign in to comment.