From 8154ca7a4b254342901a8479c980997fd65cd80a Mon Sep 17 00:00:00 2001 From: unsiqasik Date: Sat, 30 May 2026 13:20:53 +0000 Subject: [PATCH] feat: add Tigo and Vodacom mock API endpoints to provider mock server - Add Tigo Tanzania endpoints: auth, collection payments, balance - Add Vodacom M-Pesa Tanzania endpoints: auth, C2B payments, balance - Update health endpoint to list all 4 providers - Add status helper functions for Tigo and Vodacom response formats Fixes #1036 --- scripts/provider-mock-server.ts | 188 +++++++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 3 deletions(-) diff --git a/scripts/provider-mock-server.ts b/scripts/provider-mock-server.ts index 01adc67f..dfd872e3 100644 --- a/scripts/provider-mock-server.ts +++ b/scripts/provider-mock-server.ts @@ -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; } @@ -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, ): Promise { @@ -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"], }); }); @@ -366,6 +379,175 @@ export function createProviderMockApp() { }, ); + // ─── Tigo Tanzania ──────────────────────────────────────────────────── + app.post( + "/tigo/auth/token", + async (req: Request, 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, 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, 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, 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, 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, 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; } @@ -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`, ); }); }