Skip to content
Merged
Show file tree
Hide file tree
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
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,18 @@ REFRESH_TOKEN_EXPIRES_IN=
REFRESH_TOKEN_SECRET=
REFRESH_TOKEN_ISSUER=

# ---------------------------------------------------------------------------
# Mock Provider Server Configuration
# Used for development and testing with mock MTN/Airtel providers
# ---------------------------------------------------------------------------
# Delay in milliseconds before firing webhook callbacks - simulates realistic
# network latency and provider behavior when webhooks are delivered (default: 3000ms)
MOCK_WEBHOOK_LATENCY_MS=3000

# Whether webhook latency simulation is enabled. Set to false to fire webhooks
# immediately without delay. (default: true)
MOCK_WEBHOOK_LATENCY_ENABLED=true

# ---------------------------------------------------------------------------
# Provider Health Check
# ---------------------------------------------------------------------------
Expand Down
91 changes: 85 additions & 6 deletions scripts/provider-mock-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { randomUUID } from "crypto";
import { Server } from "http";
import { Request, Response } from "express";
import express = require("express");
import mockServerConfig from "../src/config/mockServer";

type MockScenario = "success" | "failed" | "pending";

Expand Down Expand Up @@ -105,6 +106,60 @@ async function applyDelay(
}
}

/**
* Helper function to delay execution by a specified number of milliseconds.
* Used to simulate webhook callback latency.
*
* @param ms - The number of milliseconds to delay
* @returns A Promise that resolves after the specified delay
*/
async function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
* Fires a webhook callback to a configured webhook URL if available.
* Respects the webhook latency configuration to simulate realistic delays
* before webhook delivery.
*
* @param referenceId - The transaction reference ID to include in the webhook payload
* @param provider - The payment provider (mtn or airtel)
* @param status - The transaction status
* @param webhookUrl - Optional webhook URL to POST to; if not provided, webhook is logged instead
*/
async function fireWebhookCallback(
referenceId: string,
provider: "mtn" | "airtel",
status: string,
webhookUrl?: string,
): Promise<void> {
// Apply webhook latency if enabled
if (mockServerConfig.webhookLatencyEnabled && mockServerConfig.webhookLatencyMs > 0) {
await delay(mockServerConfig.webhookLatencyMs);
}

const payload = {
referenceId,
provider,
status,
timestamp: new Date().toISOString(),
};

if (webhookUrl) {
try {
// Fire webhook asynchronously without blocking the response
// In a real scenario, this would be sent via HTTP request
console.log(`[webhook] Firing ${provider} webhook to ${webhookUrl}:`, payload);
// Actual HTTP request would go here (e.g., fetch or axios)
// await fetch(webhookUrl, { method: 'POST', body: JSON.stringify(payload) });
} catch (error) {
console.error(`[webhook] Error firing ${provider} webhook:`, error);
}
} else {
console.log(`[webhook] Webhook callback for ${provider} (no URL configured):`, payload);
}
}

function getReferenceId(
req: Request<unknown, unknown, MockRequestBody>,
fallbackPrefix: string,
Expand Down Expand Up @@ -159,18 +214,26 @@ export function createProviderMockApp() {
});

if (scenario === "failed") {
return res.status(400).json({
res.status(400).json({
status: "FAILED",
referenceId,
message: "Mock MTN request-to-pay failure",
});
// Fire webhook for failed transaction
fireWebhookCallback(referenceId, "mtn", "FAILED").catch(console.error);
return;
}

return res.status(202).json({
res.status(202).json({
status: getMtnStatus(scenario),
referenceId,
message: "Mock MTN request-to-pay accepted",
});

// Fire webhook callback asynchronously after response is sent
fireWebhookCallback(referenceId, "mtn", getMtnStatus(scenario)).catch(
console.error,
);
},
);

Expand Down Expand Up @@ -239,7 +302,7 @@ export function createProviderMockApp() {
});

if (scenario === "failed") {
return res.status(400).json({
res.status(400).json({
status: {
success: false,
code: "DP_REQUEST_FAILED",
Expand All @@ -251,9 +314,12 @@ export function createProviderMockApp() {
},
},
});
// Fire webhook for failed transaction
fireWebhookCallback(referenceId, "airtel", "TF").catch(console.error);
return;
}

return res.status(200).json({
res.status(200).json({
status: {
success: true,
code: scenario === "pending" ? "DP_PENDING" : "DP_SUCCESS",
Expand All @@ -265,6 +331,11 @@ export function createProviderMockApp() {
},
},
});

// Fire webhook callback asynchronously after response is sent
fireWebhookCallback(referenceId, "airtel", getAirtelStatus(scenario)).catch(
console.error,
);
},
);

Expand Down Expand Up @@ -337,7 +408,7 @@ export function createProviderMockApp() {
});

if (scenario === "failed") {
return res.status(400).json({
res.status(400).json({
status: {
success: false,
code: "DS_REQUEST_FAILED",
Expand All @@ -349,9 +420,12 @@ export function createProviderMockApp() {
},
},
});
// Fire webhook for failed transaction
fireWebhookCallback(referenceId, "airtel", "TF").catch(console.error);
return;
}

return res.status(200).json({
res.status(200).json({
status: {
success: true,
code: scenario === "pending" ? "DS_PENDING" : "DS_SUCCESS",
Expand All @@ -363,6 +437,11 @@ export function createProviderMockApp() {
},
},
});

// Fire webhook callback asynchronously after response is sent
fireWebhookCallback(referenceId, "airtel", getAirtelStatus(scenario)).catch(
console.error,
);
},
);

Expand Down
41 changes: 41 additions & 0 deletions src/config/mockServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Mock Provider Server Configuration
*
* Configuration for the mock provider server used in development and testing.
* Includes settings for webhook latency simulation to test realistic scenarios
* where webhook callbacks arrive with delays.
*/

/**
* Configuration for mock server webhook latency simulation
*/
export const mockServerConfig = {
/**
* Webhook latency in milliseconds - delay before firing webhook callbacks
* to simulate realistic network latency and provider behavior.
*
* Defaults to 3000ms (3 seconds) unless overridden by MOCK_WEBHOOK_LATENCY_MS env var.
* @default 3000
*/
webhookLatencyMs: (() => {
const value = process.env.MOCK_WEBHOOK_LATENCY_MS;
if (!value) return 3000;
const parsed = parseInt(value, 10);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 3000;
})(),

/**
* Whether webhook latency simulation is enabled.
* When false, webhooks fire immediately without delay.
*
* Defaults to true unless overridden by MOCK_WEBHOOK_LATENCY_ENABLED env var.
* @default true
*/
webhookLatencyEnabled: (() => {
const value = process.env.MOCK_WEBHOOK_LATENCY_ENABLED;
if (value === undefined || value === '') return true;
return value.toLowerCase() === 'true' || value === '1';
})(),
};

export default mockServerConfig;
Loading