Skip to content

Commit

Permalink
feat: Reading mpesa config data from config file instead of environme…
Browse files Browse the repository at this point in the history
…nt variables. (#4)

* fix: reading callback url from env

* refacor: added fail option when evironment variables are missing

* feat: reading environment vars from config file instead of .env

* refactor: use hashmap instead of array of objects

* fix: cors error for missing config requests

* refactor: remove "mpesa" keyword from callback url

---------

Co-authored-by: Amos Machora <81857018+AmosMachora@users.noreply.github.com>
  • Loading branch information
amosmachora and amosmachora committed Jun 26, 2024
1 parent c95a580 commit 538bcf2
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 69 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

mpesa-config.json
35 changes: 30 additions & 5 deletions app/api/mpesa/stk-push/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { db } from "@/app/db/drizzle-client";
import { stkPushRequest } from "@/daraja/stk-push";
import { allowedOrigins, corsOptions } from "@/utils/cors";
import { MPESA_APP_BASE_URL } from "@/config/env";
import { getHealthFacilityMpesaConfig } from "@/config/mpesa-config";

type RequestBody = {
accountReference: string;
Expand All @@ -19,7 +20,7 @@ export const POST = async (request: NextRequest) => {
const requestBody: RequestBody = {
accountReference,
amount,
callbackURL: `${MPESA_APP_BASE_URL}/api/mpesa/stk-push-callback`,
callbackURL: `${MPESA_APP_BASE_URL}/api/stk-push-callback`,
phoneNumber,
transactionDesc: "HMIS Payment",
};
Expand All @@ -30,10 +31,35 @@ export const POST = async (request: NextRequest) => {
requestBody.accountReference.indexOf("-")
);

const healthFacilityMpesaConfig = getHealthFacilityMpesaConfig(mfl);

if (!healthFacilityMpesaConfig) {
const res = NextResponse.json(
{ message: "Health facility M-PESA data not configured." },
{ status: 403 }
);

const origin = request.headers.get("origin") ?? "";
const isAllowedOrigin = allowedOrigins.includes(origin);

if (isAllowedOrigin) {
res.headers.set("Access-Control-Allow-Origin", origin);
}

Object.entries(corsOptions).forEach(([key, value]) => {
res.headers.set(key, value);
});

return res;
}

try {
const stkPushRes = await stkPushRequest(requestBody);
const stkPushRes = await stkPushRequest(
requestBody,
healthFacilityMpesaConfig
);

const res = await db
const dbRes = await db
.insert(payments)
.values({
mfl,
Expand All @@ -46,13 +72,12 @@ export const POST = async (request: NextRequest) => {
.returning({ requestId: payments.id });

const response = NextResponse.json(
{ requestId: res.at(0)?.requestId },
{ requestId: dbRes.at(0)?.requestId },
{
status: 200,
}
);

// Handling cors
const origin = request.headers.get("origin") ?? "";
const isAllowedOrigin = allowedOrigins.includes(origin);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,18 @@ const safaricomOrigins = [
];

export const POST = async (req: NextRequest, res: NextResponse) => {
console.log("request", req);

const received: STKPushSuccessfulCallbackBody = await req.json();

const origin = req.headers.get("origin") ?? "";
const isAllowedOrigin = [...allowedOrigins, ...safaricomOrigins].includes(
origin
);

if (!isAllowedOrigin) {
const tempIsAllowedOrigin = true;

if (!tempIsAllowedOrigin) {
return NextResponse.json({ message: "NOT-ALLOWED" }, { status: 401 });
}

Expand Down
15 changes: 15 additions & 0 deletions config/env.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
export const ENVIRONMENT = assertValue<"production" | "development">(
process.env.ENVIRONMENT as "production" | "development",
"Missing environment variable: ENVIRONMENT"
);

export const BASE_URL =
ENVIRONMENT === "production"
? "https://api.safaricom.co.ke"
: "https://sandbox.safaricom.co.ke";

export const MPESA_TRANSACTION_TYPE = assertValue(
process.env.MPESA_TRANSACTION_TYPE,
"Missing environment variable: MPESA_TRANSACTION_TYPE"
);

export const MPESA_APP_BASE_URL = assertValue(
process.env.MPESA_APP_BASE_URL,
"Missing environment variable: MPESA_APP_BASE_URL"
Expand Down
18 changes: 18 additions & 0 deletions config/mpesa-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import configData from "../mpesa-config.json";

export type MPESA_CONFIG = {
MPESA_BUSINESS_SHORT_CODE: string;
MPESA_CONSUMER_KEY: string;
MPESA_CONSUMER_SECRET: string;
MPESA_API_PASS_KEY: string;
};

// HASHMAP
const mpesaConfigMap: { [key: string]: MPESA_CONFIG } = configData;

export const getHealthFacilityMpesaConfig = (
mfl: string
): MPESA_CONFIG | undefined => {
const config = mpesaConfigMap[mfl];
return config;
};
10 changes: 7 additions & 3 deletions daraja/access-token.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import axios from "axios";
import { BASE_URL, CONSUMER_KEY, CONSUMER_SECRET } from "./config";
import { BASE_URL } from "../config/env";
import cache from "memory-cache";
import { AccessTokenResponse } from "daraja-kit";
import { MPESA_CONFIG } from "@/config/mpesa-config";

export const generateAccessToken = async (): Promise<AccessTokenResponse> => {
const credentials = `${CONSUMER_KEY}:${CONSUMER_SECRET}`;
export const generateAccessToken = async (
mpesaConfig: MPESA_CONFIG
): Promise<AccessTokenResponse> => {
const { MPESA_CONSUMER_KEY, MPESA_CONSUMER_SECRET } = mpesaConfig;
const credentials = `${MPESA_CONSUMER_KEY}:${MPESA_CONSUMER_SECRET}`;
const encodedAuthString = Buffer.from(credentials).toString("base64");

const token: AccessTokenResponse = cache.get("act");
Expand Down
42 changes: 0 additions & 42 deletions daraja/config.ts

This file was deleted.

32 changes: 19 additions & 13 deletions daraja/stk-push.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import axios from "axios";

import { generateTimestamp, generatePassword } from "./utils";
import { BASE_URL, BUSINESS_SHORT_CODE, ENVIRONMENT } from "./config";
import { BASE_URL, ENVIRONMENT } from "../config/env";
import { generateAccessToken } from "./access-token";

import {
Expand All @@ -13,7 +12,9 @@ import {
STKPushBody,
TransactionType,
STKPushResponse,
BusinessShortCode,
} from "daraja-kit";
import { MPESA_CONFIG } from "@/config/mpesa-config";

export type STKPushRequestParam = {
phoneNumber: PhoneNumber;
Expand All @@ -23,21 +24,26 @@ export type STKPushRequestParam = {
accountReference: AccountReference;
};

export const stkPushRequest = async ({
phoneNumber,
amount,
callbackURL,
transactionDesc,
accountReference,
}: STKPushRequestParam) => {
export const stkPushRequest = async (
{
phoneNumber,
amount,
callbackURL,
transactionDesc,
accountReference,
}: STKPushRequestParam,
mpesaConfig: MPESA_CONFIG
) => {
const { MPESA_BUSINESS_SHORT_CODE } = mpesaConfig;

try {
const timestamp = generateTimestamp();

const password = generatePassword();
const password = generatePassword(mpesaConfig);

const stkPushBody: STKPushBody = {
BusinessShortCode: BUSINESS_SHORT_CODE!,
PartyB: BUSINESS_SHORT_CODE!,
BusinessShortCode: MPESA_BUSINESS_SHORT_CODE,
PartyB: MPESA_BUSINESS_SHORT_CODE,
Timestamp: timestamp,
Password: password,
PartyA: phoneNumber,
Expand All @@ -50,7 +56,7 @@ export const stkPushRequest = async ({
AccountReference: accountReference,
};

const accessTokenResponse = await generateAccessToken();
const accessTokenResponse = await generateAccessToken(mpesaConfig);

const res = await axios.post<STKPushResponse>(
`${BASE_URL}/mpesa/stkpush/v1/processrequest`,
Expand Down
9 changes: 4 additions & 5 deletions daraja/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BUSINESS_SHORT_CODE, PASSKEY } from "./config";
import { MPESA_CONFIG } from "@/config/mpesa-config";

/**
* Generates a timestamp in the format of YEAR+MONTH+DATE+HOUR+MINUTE+SECOND (YYYYMMDDHHMMSS).
Expand All @@ -17,13 +17,12 @@ export function generateTimestamp(): string {
return `${year}${month}${date}${hours}${minutes}${seconds}`;
}

export const generatePassword = (): string => {
const businessShortCode = BUSINESS_SHORT_CODE;
const passkey = PASSKEY;
export const generatePassword = (mpesaConfig: MPESA_CONFIG): string => {
const { MPESA_BUSINESS_SHORT_CODE, MPESA_API_PASS_KEY } = mpesaConfig;

const timestamp = generateTimestamp();

const concatenatedString = `${businessShortCode}${passkey}${timestamp}`;
const concatenatedString = `${MPESA_BUSINESS_SHORT_CODE}${MPESA_API_PASS_KEY}${timestamp}`;

// Check if the environment is Node.js
if (typeof btoa === "undefined") {
Expand Down

0 comments on commit 538bcf2

Please sign in to comment.