tsapay-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ export interface TinguilinConfig {
2
+ apiKey: string;
3
+ baseUrl?: string;
4
+ }
5
+ export interface CreatePaymentParams {
6
+ amount: number;
7
+ currency?: string;
8
+ provider: "mtn_momo" | "orange_money";
9
+ phoneNumber: string;
10
+ description?: string;
11
+ reference?: string;
12
+ callbackUrl?: string;
13
+ metadata?: Record<string, string>;
14
+ }
15
+ export interface PaymentResponse {
16
+ payment: {
17
+ id: string;
18
+ merchant_id: string;
19
+ amount: number;
20
+ currency: string;
21
+ provider: string;
22
+ phone_number: string;
23
+ status: "created" | "pending" | "success" | "failed" | "expired";
24
+ description?: string;
25
+ reference?: string;
26
+ created_at: string;
27
+ updated_at: string;
28
+ };
29
+ }
30
+ export interface ListPaymentsParams {
31
+ status?: string;
32
+ limit?: number;
33
+ offset?: number;
34
+ }
35
+ export interface ListPaymentsResponse {
36
+ payments: PaymentResponse["payment"][];
37
+ total: number;
38
+ }
39
+ export declare class TinguilinError extends Error {
40
+ status: number;
41
+ data: any;
42
+ constructor(status: number, message: string, data?: any);
43
+ }
44
+ export declare class TinguilinClient {
45
+ private apiKey;
46
+ private baseUrl;
47
+ constructor(config: TinguilinConfig);
48
+ private request;
49
+ /**
50
+ * Create a new Mobile Money payment.
51
+ * This sends a USSD push to the user's phone.
52
+ */
53
+ createPayment(params: CreatePaymentParams, idempotencyKey?: string): Promise<PaymentResponse>;
54
+ /**
55
+ * Retrieve a payment by its ID.
56
+ */
57
+ getPayment(paymentId: string): Promise<PaymentResponse>;
58
+ /**
59
+ * List payments with optional filtering.
60
+ */
61
+ listPayments(params?: ListPaymentsParams): Promise<ListPaymentsResponse>;
62
+ /**
63
+ * Verify an incoming webhook signature using your webhook secret.
64
+ * Helps ensure the webhook actually came from TsaPay.
65
+ */
66
+ verifyWebhookSignature(payload: string, signatureHeader: string, webhookSecret: string): boolean;
67
+ }
package/dist/index.js ADDED
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TinguilinClient = exports.TinguilinError = void 0;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ class TinguilinError extends Error {
9
+ status;
10
+ data;
11
+ constructor(status, message, data) {
12
+ super(message);
13
+ this.name = "TinguilinError";
14
+ this.status = status;
15
+ this.data = data;
16
+ }
17
+ }
18
+ exports.TinguilinError = TinguilinError;
19
+ class TinguilinClient {
20
+ apiKey;
21
+ baseUrl;
22
+ constructor(config) {
23
+ if (!config.apiKey) {
24
+ throw new Error("TsaPay API key is required");
25
+ }
26
+ this.apiKey = config.apiKey;
27
+ // Default to the local gateway if not provided
28
+ this.baseUrl = config.baseUrl || "http://localhost:8080/v1";
29
+ }
30
+ async request(endpoint, options = {}) {
31
+ const url = `${this.baseUrl}${endpoint}`;
32
+ const headers = new Headers(options.headers || {});
33
+ headers.set("Authorization", `Bearer ${this.apiKey}`);
34
+ headers.set("Content-Type", "application/json");
35
+ const response = await fetch(url, { ...options, headers });
36
+ if (!response.ok) {
37
+ let errorData;
38
+ try {
39
+ errorData = await response.json();
40
+ }
41
+ catch (e) {
42
+ errorData = { message: response.statusText };
43
+ }
44
+ throw new TinguilinError(response.status, errorData.message || `API error ${response.status}`, errorData);
45
+ }
46
+ return response.json();
47
+ }
48
+ /**
49
+ * Create a new Mobile Money payment.
50
+ * This sends a USSD push to the user's phone.
51
+ */
52
+ async createPayment(params, idempotencyKey) {
53
+ const key = idempotencyKey || crypto_1.default.randomUUID();
54
+ const body = {
55
+ amount: params.amount,
56
+ currency: params.currency || "XAF",
57
+ provider: params.provider,
58
+ phone_number: params.phoneNumber,
59
+ description: params.description,
60
+ reference: params.reference,
61
+ callback_url: params.callbackUrl,
62
+ metadata: params.metadata,
63
+ };
64
+ return this.request("/payments", {
65
+ method: "POST",
66
+ headers: {
67
+ "Idempotency-Key": key,
68
+ },
69
+ body: JSON.stringify(body),
70
+ });
71
+ }
72
+ /**
73
+ * Retrieve a payment by its ID.
74
+ */
75
+ async getPayment(paymentId) {
76
+ return this.request(`/payments/${paymentId}`);
77
+ }
78
+ /**
79
+ * List payments with optional filtering.
80
+ */
81
+ async listPayments(params) {
82
+ const urlParams = new URLSearchParams();
83
+ if (params?.status)
84
+ urlParams.set("status", params.status);
85
+ if (params?.limit)
86
+ urlParams.set("limit", params.limit.toString());
87
+ if (params?.offset)
88
+ urlParams.set("offset", params.offset.toString());
89
+ const queryString = urlParams.toString();
90
+ const endpoint = queryString ? `/payments?${queryString}` : "/payments";
91
+ return this.request(endpoint);
92
+ }
93
+ /**
94
+ * Verify an incoming webhook signature using your webhook secret.
95
+ * Helps ensure the webhook actually came from TsaPay.
96
+ */
97
+ verifyWebhookSignature(payload, signatureHeader, webhookSecret) {
98
+ const expectedSignature = crypto_1.default
99
+ .createHmac("sha256", webhookSecret)
100
+ .update(payload)
101
+ .digest("hex");
102
+ return expectedSignature === signatureHeader;
103
+ }
104
+ }
105
+ exports.TinguilinClient = TinguilinClient;
@@ -0,0 +1,30 @@
1
+ import { TinguilinClient } from "../src/index";
2
+
3
+ async function run() {
4
+ const client = new TinguilinClient({
5
+ apiKey: "sk_test_1234567890", // Replace with your real test key
6
+ baseUrl: "http://localhost:8080/v1" // Use production URL when going live
7
+ });
8
+
9
+ try {
10
+ console.log("Initiating payment...");
11
+
12
+ // Create a payment using a Sandbox test number
13
+ const result = await client.createPayment({
14
+ amount: 15000,
15
+ currency: "XAF",
16
+ provider: "mtn_momo",
17
+ phoneNumber: "+237600000001", // Instant success number in sandbox
18
+ description: "Order #999"
19
+ });
20
+
21
+ console.log("Payment created successfully:", result.payment.id);
22
+ console.log("Status:", result.payment.status);
23
+
24
+ } catch (error: any) {
25
+ console.error("Payment failed:");
26
+ console.error(error.message);
27
+ }
28
+ }
29
+
30
+ run();
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "tsapay-sdk",
3
+ "version": "1.0.0",
4
+ "description": "The official Node.js / TypeScript SDK for TsaPay Mobile Money Aggregator",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "prepublishOnly": "npm run build"
10
+ },
11
+ "keywords": ["tsapay", "mobile-money", "payments", "mtn", "orange-money", "cemac", "cameroon", "fintech"],
12
+ "author": "TsaPay",
13
+ "license": "MIT",
14
+ "type": "commonjs",
15
+ "devDependencies": {
16
+ "@types/node": "^25.9.1",
17
+ "typescript": "^6.0.3"
18
+ }
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,158 @@
1
+ import crypto from "crypto";
2
+
3
+ export interface TinguilinConfig {
4
+ apiKey: string;
5
+ baseUrl?: string;
6
+ }
7
+
8
+ export interface CreatePaymentParams {
9
+ amount: number;
10
+ currency?: string;
11
+ provider: "mtn_momo" | "orange_money";
12
+ phoneNumber: string;
13
+ description?: string;
14
+ reference?: string;
15
+ callbackUrl?: string;
16
+ metadata?: Record<string, string>;
17
+ }
18
+
19
+ export interface PaymentResponse {
20
+ payment: {
21
+ id: string;
22
+ merchant_id: string;
23
+ amount: number;
24
+ currency: string;
25
+ provider: string;
26
+ phone_number: string;
27
+ status: "created" | "pending" | "success" | "failed" | "expired";
28
+ description?: string;
29
+ reference?: string;
30
+ created_at: string;
31
+ updated_at: string;
32
+ };
33
+ }
34
+
35
+ export interface ListPaymentsParams {
36
+ status?: string;
37
+ limit?: number;
38
+ offset?: number;
39
+ }
40
+
41
+ export interface ListPaymentsResponse {
42
+ payments: PaymentResponse["payment"][];
43
+ total: number;
44
+ }
45
+
46
+ export class TinguilinError extends Error {
47
+ public status: number;
48
+ public data: any;
49
+
50
+ constructor(status: number, message: string, data?: any) {
51
+ super(message);
52
+ this.name = "TinguilinError";
53
+ this.status = status;
54
+ this.data = data;
55
+ }
56
+ }
57
+
58
+ export class TinguilinClient {
59
+ private apiKey: string;
60
+ private baseUrl: string;
61
+
62
+ constructor(config: TinguilinConfig) {
63
+ if (!config.apiKey) {
64
+ throw new Error("TsaPay API key is required");
65
+ }
66
+ this.apiKey = config.apiKey;
67
+ // Default to the local gateway if not provided
68
+ this.baseUrl = config.baseUrl || "http://localhost:8080/v1";
69
+ }
70
+
71
+ private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
72
+ const url = `${this.baseUrl}${endpoint}`;
73
+
74
+ const headers = new Headers(options.headers || {});
75
+ headers.set("Authorization", `Bearer ${this.apiKey}`);
76
+ headers.set("Content-Type", "application/json");
77
+
78
+ const response = await fetch(url, { ...options, headers });
79
+
80
+ if (!response.ok) {
81
+ let errorData;
82
+ try {
83
+ errorData = await response.json();
84
+ } catch (e) {
85
+ errorData = { message: response.statusText };
86
+ }
87
+ throw new TinguilinError(
88
+ response.status,
89
+ errorData.message || `API error ${response.status}`,
90
+ errorData
91
+ );
92
+ }
93
+
94
+ return response.json() as Promise<T>;
95
+ }
96
+
97
+ /**
98
+ * Create a new Mobile Money payment.
99
+ * This sends a USSD push to the user's phone.
100
+ */
101
+ public async createPayment(params: CreatePaymentParams, idempotencyKey?: string): Promise<PaymentResponse> {
102
+ const key = idempotencyKey || crypto.randomUUID();
103
+
104
+ const body = {
105
+ amount: params.amount,
106
+ currency: params.currency || "XAF",
107
+ provider: params.provider,
108
+ phone_number: params.phoneNumber,
109
+ description: params.description,
110
+ reference: params.reference,
111
+ callback_url: params.callbackUrl,
112
+ metadata: params.metadata,
113
+ };
114
+
115
+ return this.request<PaymentResponse>("/payments", {
116
+ method: "POST",
117
+ headers: {
118
+ "Idempotency-Key": key,
119
+ },
120
+ body: JSON.stringify(body),
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Retrieve a payment by its ID.
126
+ */
127
+ public async getPayment(paymentId: string): Promise<PaymentResponse> {
128
+ return this.request<PaymentResponse>(`/payments/${paymentId}`);
129
+ }
130
+
131
+ /**
132
+ * List payments with optional filtering.
133
+ */
134
+ public async listPayments(params?: ListPaymentsParams): Promise<ListPaymentsResponse> {
135
+ const urlParams = new URLSearchParams();
136
+ if (params?.status) urlParams.set("status", params.status);
137
+ if (params?.limit) urlParams.set("limit", params.limit.toString());
138
+ if (params?.offset) urlParams.set("offset", params.offset.toString());
139
+
140
+ const queryString = urlParams.toString();
141
+ const endpoint = queryString ? `/payments?${queryString}` : "/payments";
142
+
143
+ return this.request<ListPaymentsResponse>(endpoint);
144
+ }
145
+
146
+ /**
147
+ * Verify an incoming webhook signature using your webhook secret.
148
+ * Helps ensure the webhook actually came from TsaPay.
149
+ */
150
+ public verifyWebhookSignature(payload: string, signatureHeader: string, webhookSecret: string): boolean {
151
+ const expectedSignature = crypto
152
+ .createHmac("sha256", webhookSecret)
153
+ .update(payload)
154
+ .digest("hex");
155
+
156
+ return expectedSignature === signatureHeader;
157
+ }
158
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "declaration": true,
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "types": ["node"],
13
+ "lib": ["ES2022", "DOM"]
14
+ },
15
+ "include": ["src/**/*"]
16
+ }