Skip to content
Closed
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
188 changes: 185 additions & 3 deletions scripts/provider-mock-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import express = require("express");
type MockScenario = "success" | "failed" | "pending";

interface StoredTransaction {
provider: "mtn" | "airtel";
provider: "mtn" | "airtel" | "tigo" | "vodacom";
scenario: MockScenario;
createdAt: string;
}
Expand Down Expand Up @@ -95,6 +95,19 @@ function getAirtelStatus(scenario: MockScenario): "TS" | "TF" | "TP" {
return "TS";
}

function getTigoStatus(scenario: MockScenario): "SUCCESS" | "FAILED" | "PENDING" {
if (scenario === "failed") return "FAILED";
if (scenario === "pending") return "PENDING";
return "SUCCESS";
}

function getVodacomStatus(scenario: MockScenario): "0" | "1" | "2" {
// Vodacom M-Pesa status codes: 0=Success, 1=Failed, 2=Pending
if (scenario === "failed") return "1";
if (scenario === "pending") return "2";
return "0";
}

async function applyDelay(
req: Request<unknown, unknown, MockRequestBody>,
): Promise<void> {
Expand Down Expand Up @@ -127,7 +140,7 @@ export function createProviderMockApp() {
app.get("/health", (_req: Request, res: Response) => {
res.json({
status: "ok",
providers: ["mtn", "airtel"],
providers: ["mtn", "airtel", "tigo", "vodacom"],
});
});

Expand Down Expand Up @@ -366,6 +379,175 @@ export function createProviderMockApp() {
},
);

// ─── Tigo Tanzania ────────────────────────────────────────────────────
app.post(
"/tigo/auth/token",
async (req: Request<unknown, unknown, MockRequestBody>, res: Response) => {
await applyDelay(req);

res.json({
access_token: "mock-tigo-access-token",
token_type: "Bearer",
expires_in: 3600,
});
},
);

app.post(
"/tigo/collection/v1/payments",
async (req: Request<unknown, unknown, MockRequestBody>, res: Response) => {
await applyDelay(req);

const scenario = getScenario(req);
const referenceId = getReferenceId(req, "tigo-pay");

transactions.set(referenceId, {
provider: "tigo",
scenario,
createdAt: new Date().toISOString(),
});

if (scenario === "failed") {
return res.status(400).json({
status: "FAILED",
referenceId,
message: "Mock Tigo payment failure",
});
}

return res.status(200).json({
status: getTigoStatus(scenario),
referenceId,
message: "Mock Tigo payment accepted",
});
},
);

app.get(
"/tigo/collection/v1/payments/:referenceId",
async (
req: Request<{ referenceId: string }, unknown, MockRequestBody>,
res: Response,
) => {
await applyDelay(req);

const stored = transactions.get(req.params.referenceId);
const scenario = stored?.scenario || getScenario(req);

return res.json({
referenceId: req.params.referenceId,
status: getTigoStatus(scenario),
});
},
);

app.get(
"/tigo/collection/v1/balance",
async (req: Request<unknown, unknown, MockRequestBody>, res: Response) => {
await applyDelay(req);

const scenario = getScenario(req);
if (scenario === "failed") {
return res.status(503).json({
message: "Mock Tigo balance service unavailable",
});
}

return res.json({
availableBalance: DEFAULT_BALANCE,
currency: "TZS",
});
},
);

// ─── Vodacom M-Pesa Tanzania ──────────────────────────────────────────
app.post(
"/vodacom/auth/token",
async (req: Request<unknown, unknown, MockRequestBody>, res: Response) => {
await applyDelay(req);

res.json({
access_token: "mock-vodacom-access-token",
token_type: "Bearer",
expires_in: 3600,
});
},
);

app.post(
"/vodacom/mpesa/c2b/v1/payments",
async (req: Request<unknown, unknown, MockRequestBody>, res: Response) => {
await applyDelay(req);

const scenario = getScenario(req);
const referenceId = getReferenceId(req, "vodacom-pay");

transactions.set(referenceId, {
provider: "vodacom",
scenario,
createdAt: new Date().toISOString(),
});

if (scenario === "failed") {
return res.status(400).json({
ResponseCode: "1",
ResponseDesc: "Mock Vodacom payment failure",
ConversationID: referenceId,
OriginatorConversationID: `orig-${referenceId}`,
});
}

return res.status(200).json({
ResponseCode: getVodacomStatus(scenario),
ResponseDesc: scenario === "pending" ? "Pending" : "Success",
ConversationID: referenceId,
OriginatorConversationID: `orig-${referenceId}`,
});
},
);

app.get(
"/vodacom/mpesa/c2b/v1/payments/:referenceId",
async (
req: Request<{ referenceId: string }, unknown, MockRequestBody>,
res: Response,
) => {
await applyDelay(req);

const stored = transactions.get(req.params.referenceId);
const scenario = stored?.scenario || getScenario(req);

return res.json({
ResponseCode: getVodacomStatus(scenario),
ResponseDesc: scenario === "failed" ? "Failed" : scenario === "pending" ? "Pending" : "Success",
ConversationID: req.params.referenceId,
OriginatorConversationID: `orig-${req.params.referenceId}`,
});
},
);

app.get(
"/vodacom/mpesa/v1/balance",
async (req: Request<unknown, unknown, MockRequestBody>, res: Response) => {
await applyDelay(req);

const scenario = getScenario(req);
if (scenario === "failed") {
return res.status(503).json({
ResponseCode: "1",
ResponseDesc: "Mock Vodacom balance service unavailable",
});
}

return res.json({
ResponseCode: "0",
ResponseDesc: "Success",
availableBalance: DEFAULT_BALANCE,
currency: "TZS",
});
},
);

return app;
}

Expand All @@ -374,7 +556,7 @@ export function startProviderMockServer(port = DEFAULT_PORT): Server {

return app.listen(port, () => {
console.info(
`[provider-mock] listening on port ${port} for MTN and Airtel mock traffic`,
`[provider-mock] listening on port ${port} for MTN, Airtel, Tigo, and Vodacom mock traffic`,
);
});
}
Expand Down
Loading