mpesa-mz-sdk
Version:
SDK para integração com a API M-Pesa de Moçambique
341 lines (340 loc) • 15.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MpesaService = exports.MpesaError = void 0;
// src/MpesaService.ts
const axios_1 = __importDefault(require("axios"));
const utils_1 = require("./utils");
const types_1 = require("./types");
/**
* Custom Error class for M-Pesa API errors.
*/
class MpesaError extends Error {
constructor(message, details) {
super(message);
this.name = 'MpesaError';
this.details = details;
Object.setPrototypeOf(this, MpesaError.prototype);
}
}
exports.MpesaError = MpesaError;
class MpesaService {
constructor(config) {
if (!config.apiKey || !config.publicKey || !config.serviceProviderCode || !config.origin) {
throw new Error('As configurações da API M-Pesa (apiKey, publicKey, serviceProviderCode, origin) são obrigatórias.');
}
// Definir host padrão conforme ambiente
let apiHost = config.apiHost;
if (config.env === 'sandbox') {
apiHost = 'api.sandbox.vm.co.mz:18352';
}
else if (config.env === 'live') {
apiHost = 'api.vm.co.mz:18352';
}
else if (!apiHost) {
throw new Error('É necessário definir o ambiente (env: "sandbox" ou "live") ou fornecer apiHost.');
}
this.config = {
...config,
apiHost,
timeout: config.timeout || 30000 // Default to 30 seconds
};
// Gera o Bearer token criptografando o apiKey com a publicKey (sempre no formato PEM)
this.encodedPublicKey = (0, utils_1.generateBearerToken)(this.config.apiKey, (0, utils_1.formatPublicKey)(this.config.publicKey));
// Construct the baseURL including the host and port
this.httpClient = axios_1.default.create({
baseURL: `https://${this.config.apiHost}/`,
headers: {
'Content-Type': 'application/json',
// The Authorization and X-Api-Key headers will be added per request
},
timeout: this.config.timeout,
});
}
/**
* Helper method to handle M-Pesa specific API errors.
* This method ensures consistent error reporting across all API calls.
* @param error The AxiosError object or a generic error.
* @param context A string describing the context of the error (e.g., "Token", "C2B").
* @throws MpesaError
*/
handleMpesaError(error, context) {
console.log('local error: ', error);
let httpStatus = 500;
let mpesaCode = 'INS-1';
let mpesaDesc = types_1.MPESA_ERROR_MESSAGES['INS-1'] || 'Internal Error';
let transactionId;
let conversationId;
let thirdPartyReference;
let errorMessage;
if (axios_1.default.isAxiosError(error) && error.response) {
httpStatus = error.response.status;
if (error.response.data) {
mpesaCode = error.response.data.output_ResponseCode || mpesaCode;
// Usar mensagem descritiva específica da M-Pesa se disponível
mpesaDesc = types_1.MPESA_ERROR_MESSAGES[mpesaCode] || error.response.data.output_ResponseDesc || mpesaDesc;
transactionId = error.response.data.output_TransactionID;
conversationId = error.response.data.output_ConversationID;
thirdPartyReference = error.response.data.output_ThirdPartyReference;
}
errorMessage = `M-Pesa ${context} Error: ${mpesaDesc} (Code: ${mpesaCode}, HTTP: ${httpStatus})`;
}
else {
// Para erros de rede ou outros erros não relacionados à API
const networkErrorDesc = types_1.MPESA_ERROR_MESSAGES['INS-27'] || 'Network error';
errorMessage = `M-Pesa ${context} Error: ${networkErrorDesc} - ${error.message}`;
}
const errorDetails = {
code: mpesaCode,
description: mpesaDesc,
httpStatus: httpStatus,
transactionId: transactionId,
conversationId: conversationId,
thirdPartyReference: thirdPartyReference,
};
console.error(`Error in ${context} operation:`, errorMessage, errorDetails);
throw new MpesaError(errorMessage, errorDetails);
}
/**
* Helper method to transform M-Pesa API responses into a clean, readable format.
* @param response The raw M-Pesa API response
* @param context The operation context (C2B, B2C, B2B, Query, Reversal)
* @returns A clean, readable response object
*/
transformResponse(response, context, transformData) {
const isSuccess = response.output_ResponseCode === 'INS-0';
return {
status: isSuccess ? 'success' : 'error',
message: response.output_ResponseDesc || 'No description provided',
data: isSuccess ? transformData(response) : undefined,
code: response.output_ResponseCode,
httpStatus: isSuccess ? 200 : 400,
transactionId: response.output_TransactionID,
conversationId: response.output_ConversationID,
thirdPartyReference: response.output_ThirdPartyReference,
timestamp: new Date().toISOString()
};
}
/**
* Initiates a C2B (Customer to Business) transaction.
* @param args Object containing amount, number, transactionReference, thirdPartyReference
* @returns Clean, readable M-Pesa response for the C2B transaction.
* @throws MpesaError if the C2B transaction fails.
*/
async c2b(args) {
const payload = {
input_Amount: args.amount.toFixed(2),
input_CustomerMSISDN: args.number,
input_ThirdPartyReference: args.thirdPartyReference,
input_TransactionReference: args.transactionReference,
input_ServiceProviderCode: this.config.serviceProviderCode,
};
const headers = {
'Authorization': `Bearer ${this.encodedPublicKey}`,
'Origin': this.config.origin,
'Content-Type': 'application/json',
};
// Log detalhado para depuração
console.log('C2B Request - Payload:', JSON.stringify(payload, null, 2));
console.log('C2B Request - Headers:', JSON.stringify(headers, null, 2));
try {
const response = await this.httpClient.post('/ipg/v1x/c2bPayment/singleStage/', payload, {
headers
});
// Verificar se a resposta é de sucesso
if (response.data.output_ResponseCode === 'INS-0') {
return this.transformResponse(response.data, 'C2B', (data) => ({
transactionId: data.output_TransactionID || '',
conversationId: data.output_ConversationID || '',
thirdPartyReference: data.output_ThirdPartyReference || '',
amount: args.amount.toFixed(2),
customerMsisdn: args.number,
transactionReference: args.transactionReference
}));
}
else {
// Lançar erro para respostas não bem-sucedidas
this.handleMpesaError({ response: { status: response.status, data: response.data } }, 'C2B');
}
}
catch (error) {
this.handleMpesaError(error, 'C2B');
}
}
/**
* Initiates a B2C (Business to Customer) transaction.
* @param args Object containing amount, number, transactionReference, thirdPartyReference, paymentServices
* @returns Clean, readable M-Pesa response for the B2C transaction.
* @throws MpesaError if the B2C transaction fails.
*/
async b2c(args) {
const payload = {
input_Amount: args.amount.toFixed(2),
input_CustomerMsisdn: args.number,
input_ThirdPartyReference: args.thirdPartyReference,
input_TransactionReference: args.transactionReference,
input_ServiceProviderCode: this.config.serviceProviderCode,
input_PaymentServices: args.paymentServices || "BusinessPayBill",
};
try {
const response = await this.httpClient.post('/ipg/v1x/b2cPayment/singleStage/', payload, {
headers: {
'Authorization': `Bearer ${this.encodedPublicKey}`,
'Origin': this.config.origin,
'X-Api-Key': this.config.apiKey
}
});
// Verificar se a resposta é de sucesso
if (response.data.output_ResponseCode === 'INS-0') {
return this.transformResponse(response.data, 'B2C', (data) => ({
transactionId: data.output_TransactionID || '',
conversationId: data.output_ConversationID || '',
thirdPartyReference: data.output_ThirdPartyReference || '',
amount: args.amount.toFixed(2),
customerMsisdn: args.number,
transactionReference: args.transactionReference,
recipientFirstName: data.output_RecipientFirstName,
recipientLastName: data.output_RecipientLastName,
settlementAmount: data.output_SettlementAmount
}));
}
else {
// Lançar erro para respostas não bem-sucedidas
this.handleMpesaError({ response: { status: response.status, data: response.data } }, 'B2C');
}
}
catch (error) {
this.handleMpesaError(error, 'B2C');
}
}
/**
* Initiates a B2B (Business to Business) transaction.
* @param args Object containing amount, primaryPartyCode, recipientPartyCode, transactionReference, thirdPartyReference, paymentServices
* @returns Clean, readable M-Pesa response for the B2B transaction.
* @throws MpesaError if the B2B transaction fails.
*/
async b2b(args) {
const payload = {
input_Amount: args.amount.toFixed(2),
input_PrimaryPartyCode: args.primaryPartyCode,
input_RecipientPartyCode: args.recipientPartyCode,
input_ThirdPartyReference: args.thirdPartyReference,
input_TransactionReference: args.transactionReference,
input_ServiceProviderCode: this.config.serviceProviderCode,
input_PaymentServices: args.paymentServices || "BusinessToBusinessTransfer",
};
try {
const response = await this.httpClient.post('/ipg/v1x/b2bPayment/singleStage/', payload, {
headers: {
'Authorization': `Bearer ${this.encodedPublicKey}`,
'Origin': this.config.origin,
'X-Api-Key': this.config.apiKey
}
});
// Verificar se a resposta é de sucesso
if (response.data.output_ResponseCode === 'INS-0') {
return this.transformResponse(response.data, 'B2B', (data) => ({
transactionId: data.output_TransactionID || '',
conversationId: data.output_ConversationID || '',
thirdPartyReference: data.output_ThirdPartyReference || '',
amount: args.amount.toFixed(2),
primaryPartyCode: args.primaryPartyCode,
recipientPartyCode: args.recipientPartyCode,
transactionReference: args.transactionReference,
settlementAmount: data.output_SettlementAmount
}));
}
else {
// Lançar erro para respostas não bem-sucedidas
this.handleMpesaError({ response: { status: response.status, data: response.data } }, 'B2B');
}
}
catch (error) {
this.handleMpesaError(error, 'B2B');
}
}
/**
* Queries the status of an M-Pesa transaction.
* @param args Object containing queryReference, thirdPartyReference
* @returns Clean, readable M-Pesa response with the transaction status.
* @throws MpesaError if the query fails.
*/
async query(args) {
const payload = {
input_QueryReference: args.queryReference,
input_ServiceProviderCode: this.config.serviceProviderCode,
input_ThirdPartyReference: args.thirdPartyReference,
};
try {
const response = await this.httpClient.post('/ipg/v1x/queryPaymentStatus/', payload, {
headers: {
'Authorization': `Bearer ${this.encodedPublicKey}`,
'Origin': this.config.origin,
'X-Api-Key': this.config.apiKey
}
});
// Verificar se a resposta é de sucesso
if (response.data.output_ResponseCode === 'INS-0') {
return this.transformResponse(response.data, 'Query', (data) => ({
transactionId: data.output_TransactionID || '',
conversationId: data.output_ConversationID || '',
thirdPartyReference: data.output_ThirdPartyReference || '',
queryReference: args.queryReference,
transactionStatus: data.output_ResponseTransactionStatus,
paymentStatusCode: data.output_ResponsePaymentStatusCode,
paymentStatusDesc: data.output_ResponsePaymentStatusDesc
}));
}
else {
// Lançar erro para respostas não bem-sucedidas
this.handleMpesaError({ response: { status: response.status, data: response.data } }, 'Query');
}
}
catch (error) {
this.handleMpesaError(error, 'Query');
}
}
/**
* Reverses an M-Pesa transaction.
* @param args Object containing originalTransactionId, reversalAmount, thirdPartyReference
* @returns Clean, readable M-Pesa response for the reversal.
* @throws MpesaError if the reversal fails.
*/
async reversal(args) {
const payload = {
input_ReversalAmount: args.reversalAmount.toFixed(2),
input_TransactionID: args.originalTransactionId,
input_ThirdPartyReference: args.thirdPartyReference,
input_ServiceProviderCode: this.config.serviceProviderCode,
};
try {
const response = await this.httpClient.post('/ipg/v1x/reversal/', payload, {
headers: {
'Authorization': `Bearer ${this.encodedPublicKey}`,
'Origin': this.config.origin,
'X-Api-Key': this.config.apiKey
}
});
// Verificar se a resposta é de sucesso
if (response.data.output_ResponseCode === 'INS-0') {
return this.transformResponse(response.data, 'Reversal', (data) => ({
transactionId: data.output_TransactionID || '',
conversationId: data.output_ConversationID || '',
thirdPartyReference: data.output_ThirdPartyReference || '',
originalTransactionId: args.originalTransactionId,
reversalAmount: args.reversalAmount.toFixed(2)
}));
}
else {
// Lançar erro para respostas não bem-sucedidas
this.handleMpesaError({ response: { status: response.status, data: response.data } }, 'Reversal');
}
}
catch (error) {
this.handleMpesaError(error, 'Reversal');
}
}
}
exports.MpesaService = MpesaService;