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

feat: Reading mpesa config data from config file instead of environment variables. #4

Merged
merged 8 commits into from
Jun 26, 2024
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