Skip to content

Commit 7c849ab

Browse files
authored
Merge pull request #141 from flexcodelabs/develop
develop
2 parents e89f4e8 + c6077cd commit 7c849ab

8 files changed

Lines changed: 215 additions & 4 deletions

File tree

apps/mno/src/mno.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
66
import { ConfigModule } from '@nestjs/config';
77
import { MPesaModule } from './modules/mpesa/mpesa.module';
88
import { AzamModule } from './modules/azampay/azampay.module';
9+
import { SelcomModule } from './modules/selcom/selcom.module';
910

1011
@Module({
1112
imports: [
@@ -20,6 +21,7 @@ import { AzamModule } from './modules/azampay/azampay.module';
2021
ConfigModule.forRoot(),
2122
MPesaModule,
2223
AzamModule,
24+
SelcomModule,
2325
].filter((module) => module),
2426
controllers: [MnoController],
2527
providers: [{ provide: APP_FILTER, useClass: HttpErrorFilter }, RmqService],

apps/mno/src/modules/azampay/services/azam.service.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class AzamService {
3636
const token = await azampay.getToken(this.getTokenPayload);
3737
Logger.debug(`REQUESTING CHECKOUT VIA [${this.getTokenPayload.env}]`);
3838
if (token.success) {
39-
return await this.getMnoCheckout(payload, token);
39+
return (await this.getMnoCheckout(payload, token)) as CheckoutResponse;
4040
}
4141
return token as ErrorResponse;
4242
};
@@ -120,12 +120,20 @@ export class AzamService {
120120
payload.checkout as MnoCheckout,
121121
payload.options,
122122
);
123-
console.log(mnoCheckout);
124-
if (mnoCheckout.success) {
123+
if (mnoCheckout?.success) {
124+
Logger.debug(
125+
`MNO CHECKOUT SUCCESSFULL: ${mnoCheckout.message ?? mnoCheckout.msg}`,
126+
'MNO CHECKOUT',
127+
);
125128
return mnoCheckout;
126129
}
130+
Logger.debug(
131+
`MNO CHECKOUT FAILED: ${mnoCheckout?.message ?? mnoCheckout?.msg}`,
132+
'MNO CHECKOUT',
133+
);
134+
console.log(mnoCheckout);
127135
return {
128-
...mnoCheckout,
136+
...(mnoCheckout ?? {}),
129137
statusCode: HttpStatus.BAD_REQUEST,
130138
status: HttpStatus.BAD_REQUEST,
131139
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Body, Controller, Post, Res } from '@nestjs/common';
2+
import {
3+
CheckoutResponse,
4+
ErrorResponse,
5+
MnoCheckout,
6+
} from 'azampay/lib/shared/interfaces/base.interface';
7+
import { SelcomService } from '../services/selcom.service';
8+
import { Response } from 'express';
9+
10+
@Controller('api')
11+
export class SelcomController {
12+
constructor(private service: SelcomService) {}
13+
14+
@Post('selcomPush')
15+
async mnoCheckout(@Res() res: Response, @Body() payload: MnoCheckout) {
16+
const response: CheckoutResponse | ErrorResponse =
17+
await this.service.selcomPush(payload);
18+
return res.status(response.statusCode).send(response);
19+
}
20+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Module } from '@nestjs/common';
2+
import { SelcomController } from './controllers/selcom.controller';
3+
import { SelcomService } from './services/selcom.service';
4+
5+
@Module({
6+
controllers: [SelcomController],
7+
providers: [SelcomService],
8+
})
9+
export class SelcomModule {}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { APPENV, phoneNumber } from '@flexpay/common';
2+
import { HttpStatus, Injectable, Logger } from '@nestjs/common';
3+
import {
4+
CheckoutResponse,
5+
MnoCheckout,
6+
ErrorResponse,
7+
} from 'azampay/lib/shared/interfaces/base.interface';
8+
import crypto from 'crypto';
9+
10+
// Types
11+
type SelcomPayload = Record<string, string>;
12+
type Headers = Record<string, string>;
13+
14+
@Injectable()
15+
export class SelcomService {
16+
/**
17+
* Compute HS256 Signature
18+
* @param params - Payload parameters
19+
* @param signedFields - Comma-separated keys to be signed
20+
* @param timestamp - ISO timestamp
21+
* @param apiSecret - Your API secret
22+
* @returns Base64-encoded signature string
23+
*/
24+
computeSignature = (
25+
params: SelcomPayload,
26+
signedFields: string,
27+
timestamp: string,
28+
apiSecret: string,
29+
): string => {
30+
const fields = signedFields.split(',');
31+
const signingString = [
32+
`timestamp=${timestamp}`,
33+
...fields.map((field) => `${field}=${params[field]}`),
34+
].join('&');
35+
36+
return crypto
37+
.createHmac('sha256', apiSecret)
38+
.update(signingString)
39+
.digest('base64');
40+
};
41+
42+
/**
43+
* Send POST request to SELCOM API
44+
* @param url - API endpoint
45+
* @param payload - Payload to send
46+
* @param headers - Authorization headers
47+
* @returns JSON response
48+
*/
49+
sendSelcomRequest = async (
50+
url: string,
51+
payload: SelcomPayload,
52+
headers: Headers,
53+
): Promise<any> => {
54+
const response = await fetch(url, {
55+
method: 'POST',
56+
headers,
57+
body: JSON.stringify(payload),
58+
});
59+
60+
if (!response.ok) {
61+
const text = await response.text();
62+
Logger.debug(`SELCOM Error: ${text}`, 'SELCOM');
63+
return { text };
64+
}
65+
return response.json();
66+
};
67+
68+
selcomPush = async (
69+
request: MnoCheckout,
70+
): Promise<CheckoutResponse | ErrorResponse> => {
71+
const { valid, value, withCode } = phoneNumber(request.accountNumber);
72+
if (!valid) {
73+
return {
74+
statusCode: HttpStatus.BAD_REQUEST,
75+
message: 'Invalid account number',
76+
success: false,
77+
code: 'FAIL',
78+
} as unknown as CheckoutResponse | ErrorResponse;
79+
}
80+
// ======= Config & Execution ======= //
81+
const apiKey = APPENV.SELCOM_APIKEY;
82+
const apiSecret = APPENV.SELCOM_APISECRET;
83+
84+
const url = `${APPENV.SELCOM_APIURL}/wallet-payment`;
85+
86+
const payload: SelcomPayload = {
87+
utilityref: value,
88+
transid: request.externalId,
89+
amount: request.amount,
90+
vendor: APPENV.SELCOM_VENDOR,
91+
msisdn: withCode,
92+
};
93+
94+
const authorization = Buffer.from(apiKey).toString('hex');
95+
const timestamp = new Date().toISOString();
96+
const signedFields = Object.keys(payload).join(',');
97+
const digest = this.computeSignature(
98+
payload,
99+
signedFields,
100+
timestamp,
101+
apiSecret,
102+
);
103+
104+
const headers: Headers = {
105+
'Content-Type': 'application/json;charset=utf-8',
106+
Accept: 'application/json',
107+
'Cache-Control': 'no-cache',
108+
'Digest-Method': 'HS256',
109+
Authorization: `SELCOM ${authorization}`,
110+
Digest: digest,
111+
Timestamp: timestamp,
112+
'Signed-Fields': signedFields,
113+
};
114+
const result = await this.sendSelcomRequest(url, payload, headers);
115+
console.log(JSON.stringify(result, null, 2));
116+
return result;
117+
};
118+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export const phoneNumber = (phone: string) => {
2+
if (!phone || phone.length < 9) return { valid: false, value: phone };
3+
4+
phone = phone.replace(/[^\w\s]/gi, '');
5+
6+
let withCode = phone;
7+
let withoutCode = phone;
8+
let withOutPlus = phone;
9+
10+
if (phone.startsWith('+255')) {
11+
withoutCode = phone.replace('+255', '0');
12+
withOutPlus = phone.replace('+', '');
13+
withCode = phone;
14+
} else if (phone.startsWith('255')) {
15+
withoutCode = phone.replace(/^255/, '0');
16+
withCode = `+${phone}`;
17+
} else if (!phone.startsWith('0') && phone.length === 9) {
18+
withoutCode = `0${phone}`;
19+
withCode = `+255${phone}`;
20+
withOutPlus = `255${phone}`;
21+
} else if (phone.startsWith('0')) {
22+
withoutCode = phone;
23+
withCode = `+255${phone.substring(1)}`;
24+
withOutPlus = `255${phone.substring(1)}`;
25+
}
26+
27+
return {
28+
valid: true,
29+
value: withoutCode,
30+
withCode,
31+
withOutPlus,
32+
};
33+
};

libs/common/src/helpers/config.helper.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,24 @@ export const APPENV = {
5151
* Azam Pay App Name
5252
*/
5353
AZAMPAY_APIKEY: process.env.AZAMPAY_APIKEY,
54+
55+
/**
56+
* Selcom vendor
57+
*/
58+
SELCOM_VENDOR: process.env.SELCOM_VENDOR,
59+
60+
/**
61+
* Selcom API Key
62+
*/
63+
SELCOM_APIKEY: process.env.SELCOM_APIKEY,
64+
65+
/**
66+
* Selcom API URL
67+
*/
68+
SELCOM_APIURL: process.env.SELCOM_APIURL,
69+
70+
/**
71+
* Selcom API Secret
72+
*/
73+
SELCOM_APISECRET: process.env.SELCOM_APISECRET,
5474
};

libs/common/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ export * from './auth/services/auth.service';
2929
export * from './dto/shared.dto';
3030
export * from './constants/entity.names.constants';
3131
export * from './interfaces/mno.interface';
32+
export * from './helpers/base.helper';

0 commit comments

Comments
 (0)