pesapal3-sdk
Version:
pesapal api version 3 node library
510 lines • 20.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Pesapal = exports.stringifyIfObj = exports.createMockPayDetails = exports.logger = void 0;
const tslib_1 = require("tslib");
/* eslint-disable @typescript-eslint/naming-convention */
const faker_1 = require("@faker-js/faker");
const axios_1 = tslib_1.__importDefault(require("axios"));
const fs = tslib_1.__importStar(require("fs"));
const tracer = tslib_1.__importStar(require("tracer"));
const error_handler_1 = require("./error-handler");
exports.logger = tracer.colorConsole({
format: '{{timestamp}} [{{title}}] {{message}} (in {{file}}:{{line}})',
dateformat: 'HH:MM:ss.L',
transport(data) {
// eslint-disable-next-line no-console
console.log(data.output);
const logDir = './serverLog/';
fs.mkdir(logDir, { recursive: true }, (err) => {
if (err) {
if (err) {
throw err;
}
}
});
fs.appendFile('./serverLog/pesapal.log', data.rawoutput + '\n', err => {
if (err) {
throw err;
}
});
}
});
/**
* This function creates a mock pay details object.
* The `ipnUrl` property is the IPN URL.
* The `phone` property is the phone number of the payer.
*
* The function returns an object with the following properties:
* * `id`: The ID of the pay details.
* * `currency`: The currency of the pay details.
* * `amount`: The amount of the pay details.
* * `description`: The description of the pay details.
* * `callback_url`: The callback URL of the pay details.
* * `notification_id`: The notification ID of the pay details.
* * `billing_address`: The billing address of the payer.
*/
const createMockPayDetails = (ipnUrl, phone) => ({
id: faker_1.faker.string.uuid(),
currency: 'UGX',
amount: 1000,
description: faker_1.faker.string.alphanumeric(),
callback_url: 'http://localhost:4000',
notification_id: ipnUrl,
billing_address: {
email_address: faker_1.faker.internet.email(),
phone_number: phone,
country_code: 'UGA',
first_name: faker_1.faker.internet.userName(),
middle_name: faker_1.faker.internet.userName(),
last_name: faker_1.faker.internet.userName(),
line_1: faker_1.faker.string.alphanumeric(),
line_2: faker_1.faker.string.alphanumeric(),
city: 'Kampala',
state: 'Uganda',
postal_code: '0000',
zip_code: '0000'
}
});
exports.createMockPayDetails = createMockPayDetails;
const stringifyIfObj = (val) => (typeof val === 'string' ? val : JSON.stringify(val));
exports.stringifyIfObj = stringifyIfObj;
/**
* This class is a controller for PesaPal payments.
* The `token` property is the PesaPal token.
* The `ipns` property is an array of IIPnResponse objects.
* The `defaultHeaders` property is an object with the default headers for the requests.
* The `callbackUrl` property is the main callback URL.
* The `notificationId` property is the notification ID.
*/
class Pesapal {
constructor(config) {
this.config = config;
this.defaultHeaders = {
// eslint-disable-next-line @typescript-eslint/naming-convention
Accept: 'application/json',
// eslint-disable-next-line @typescript-eslint/naming-convention
'Content-Type': 'application/json'
};
this.ipns = [];
if (config.PESAPAL_ENVIRONMENT === 'live') {
this.pesapalUrl = 'https://pay.pesapal.com/v3';
}
else {
this.pesapalUrl = 'https://cybqa.pesapal.com/pesapalv3';
}
this.interceptAxios();
}
/**
* Intercepts Axios requests and adds the PesaPal token to the Authorization header if it is available.
* This ensures that the PesaPal token is included in all outgoing requests.
*/
interceptAxios() {
axios_1.default.interceptors.request.use((config) => {
if (!this.tokenExpired()) {
config.headers.Authorization = 'Bearer ' + this.token?.token;
}
else {
config.headers.Authorization = '';
}
return config;
});
}
/**
* This method registers the IPN URL with PesaPal.
* The method returns a promise with the following properties:
*
* * `success`: Indicates whether the request was successful.
*/
async registerIpn(ipn, notificationMethodType) {
const gotToken = await this.relegateTokenStatus().catch(err => err);
if (gotToken instanceof Error) {
return new Promise((resolve, reject) => reject(gotToken));
}
if (!gotToken.success) {
return new Promise((resolve, reject) => reject(new Error('couldnt resolve getting token')));
}
const ipnUrl = ipn || this.config.PESAPAL_IPN_URL;
const ipnNotificationType = notificationMethodType || 'GET';
const parameters = {
url: ipnUrl,
ipn_notification_type: ipnNotificationType
};
const headers = {
...this.defaultHeaders,
Authorization: 'Bearer ' + this.token?.token
};
return new Promise((resolve, reject) => {
axios_1.default
.post(this.pesapalUrl +
'/api/URLSetup/RegisterIPN', parameters, { headers })
.then(res => {
const response = res.data;
if (response.error) {
if (typeof response.error === 'string') {
reject(new Error(response.error));
}
else {
reject(new error_handler_1.PesaPalError(response.error));
}
}
else {
this.ipns = [...this.ipns, response];
resolve({ success: true });
}
}).catch((err) => {
exports.logger.error('PesaPalController, registerIpn err', err);
reject(new Error((0, exports.stringifyIfObj)(err)));
});
});
}
/**
* This method gets the IPN endpoints from PesaPal.
* The method returns a promise with the following properties:
*
* * `success`: Indicates whether the request was successful.
*/
async getIpnEndPoints() {
const gotToken = await this.relegateTokenStatus().catch(err => err);
if (gotToken instanceof Error) {
return new Promise((resolve, reject) => reject(gotToken));
}
const headers = {
...this.defaultHeaders,
Authorization: 'Bearer ' + this.token.token
};
return new Promise((resolve, reject) => {
axios_1.default
.get(this.pesapalUrl +
'/api/URLSetup/GetIpnList', { headers })
.then(res => {
const response = res.data;
exports.logger.debug('PesaPalController, getIpnEndPoints response', response);
if (response[0] && response[0].error) {
if (typeof response[0].error === 'string') {
reject(new Error(response[0].error));
}
else {
reject(new error_handler_1.PesaPalError(response[0].error));
}
}
else {
this.ipns = res.data;
resolve({ success: true });
}
}).catch((err) => {
exports.logger.error('PesaPalController, getIpnEndPoints err', err);
reject(new Error((0, exports.stringifyIfObj)(err)));
});
});
}
/**
* This method submits the order to PesaPal.
* The method takes the following parameters:
* * `paymentDetails`: The payment details object.
* * `productId`: The ID of the product.
* * `description`: The description of the payment.
*
* The method returns a promise with the following properties:
* * `success`: Indicates whether the request was successful.
* * `status`: The status of the order.
* * `pesaPalOrderRes`: The PesaPal order response.
*/
async submitOrder(paymentDetails, productId, description) {
// Input validation
if (!paymentDetails) {
throw new Error('Payment details are required');
}
if (!productId) {
throw new Error('Product ID is required');
}
if (!description) {
throw new Error('Description is required');
}
// Sanitized logging
exports.logger.info('Submitting order', {
transactionType: 'order_submission'
});
try {
// Ensure token is valid
await this.relegateTokenStatus();
// Safely get notification ID
if (!this.ipns || this.ipns.length === 0) {
throw new Error('No IPN endpoints available');
}
const notifId = this.ipns[0].ipn_id;
// Prepare headers with trimmed Bearer token
const headers = {
...this.defaultHeaders,
Authorization: `Bearer ${this.token.token.trim()}`
};
// Make API call
const response = await axios_1.default.post(`${this.pesapalUrl}/api/Transactions/SubmitOrderRequest`, this.constructParamsFromObj(paymentDetails, notifId, productId, description), { headers });
// Handle response
const orderResponse = response.data;
// Robust error handling
if (orderResponse.error) {
const errorDetails = typeof orderResponse.error === 'string' ?
{ message: orderResponse.error } :
orderResponse.error || { message: 'Unknown error' };
exports.logger.error('Order submission failed', {
errorType: 'api_response_error',
errorMessage: errorDetails.message
});
throw new Error(errorDetails.message);
}
exports.logger.info('Order submitted successfully', {
transactionType: 'order_submission_complete'
});
return {
success: true,
status: response.status,
pesaPalOrderRes: orderResponse
};
}
catch (err) {
exports.logger.error('Order submission error', {
errorType: 'submission_error',
errorMessage: err instanceof Error ? err.message : 'Unknown error'
});
// Rethrow the original error or create a new one
throw err instanceof Error ?
err :
new Error('Unexpected error during order submission');
}
}
/**
* This method gets the transaction status from PesaPal.
* The method takes the following parameters:
* * `orderTrackingId`: The order tracking ID.
*
* The method returns a promise with the following properties:
* * `success`: Indicates whether the request was successful.
* * `response`: The response from PesaPal.
*/
async getTransactionStatus(orderTrackingId) {
const gotToken = await this.relegateTokenStatus().catch(err => err);
if (gotToken instanceof Error) {
return new Promise((resolve, reject) => reject(gotToken));
}
const headers = {
...this.defaultHeaders,
Authorization: 'Bearer ' + this.token.token
};
return new Promise((resolve, reject) => {
axios_1.default
.get(this.pesapalUrl +
'/api/Transactions/GetTransactionStatus' +
`?orderTrackingId=${orderTrackingId}`, { headers }).then(res => {
const response = res.data;
if (response.error) {
reject(new Error(response.error.message + ' on ' + response.error.call_back_url));
}
else if (response.payment_status_description.toLowerCase() === 'completed') {
resolve({ success: true, response });
}
else {
reject(new Error('Getting Transaction Status Failed With ' + response.payment_status_description));
resolve({ success: false, status: response.payment_status_description });
}
}).catch((err) => {
exports.logger.error('PesaPalController, getToken err', err);
reject(new Error((0, exports.stringifyIfObj)(err)));
});
});
}
/**
* Sends a refund request for a transaction.
* @param refunReqObj - The refund request object.
* @returns A promise that resolves to the refund request response.
*/
async refundRequest(refunReqObj) {
const gotToken = await this.relegateTokenStatus().catch(err => err);
if (gotToken instanceof Error) {
return new Promise((resolve, reject) => reject(gotToken));
}
const headers = {
...this.defaultHeaders,
Authorization: 'Bearer ' + this.token.token
};
return new Promise((resolve, reject) => {
axios_1.default
.post(this.pesapalUrl +
'/api/Transactions/RefundRequestt', refunReqObj, { headers })
.then(res => {
const response = res.data;
if (!response) {
reject(new Error('Refund Unsuccesful'));
}
else {
resolve({ success: true, refundRequestRes: response });
}
}).catch((err) => {
exports.logger.error('PesaPalController, submitOrder err', err);
reject(new Error((0, exports.stringifyIfObj)(err)));
});
});
}
/**
* This method gets the PesaPal token.
* The method returns a promise with the following properties:
*
* * `success`: Indicates whether the request was successful.
* * `err`: The error, if any.
*/
getToken() {
const headers = {
...this.defaultHeaders
};
const parameters = {
consumer_key: this.config.PESAPAL_CONSUMER_KEY,
consumer_secret: this.config.PESAPAL_CONSUMER_SECRET
};
return new Promise((resolve, reject) => {
axios_1.default
.post(this.pesapalUrl +
'/api/Auth/RequestToken', parameters, { headers })
.then(res => {
const data = res.data;
exports.logger.debug('response data from getToken()', data);
if (data?.error) {
exports.logger.error('PesaPalController, unknown err', data.error.message || data.error);
if (typeof data.error === 'string') {
reject(new Error(data.error));
}
else {
reject(new error_handler_1.PesaPalError(data.error));
}
}
else if (data?.token) {
this.token = data;
// set token to file
/** fs.writeFileSync(lConfig.
encryptedDirectory + 'airtltoken', JSON.stringify(token)); */
resolve({ success: true });
}
else {
exports.logger.error('PesaPalController, unknown err', 'sorry but unknwn');
this.token = null;
reject(new Error('Get token failed with unknown err'));
}
resolve({ success: true });
}).catch((err) => {
exports.logger.error('PesaPalController, getToken err', err);
reject(new Error((0, exports.stringifyIfObj)(err)));
});
});
}
tokenExpired() {
if (!this.hasToken()) {
return true;
}
const nowDate = new Date();
const tokenDate = new Date(this.token.expiryDate);
return nowDate > tokenDate;
}
/**
* This method checks the status of the token and creates a new token if it is expired.
*
* The method returns a promise with the following properties:
*
* * `success`: Indicates whether the request was successful.
* * `madeNewToken`: Indicates whether a new token was created.
*/
async relegateTokenStatus() {
const response = {
success: false,
madeNewToken: false
};
if (this.hasToken()) {
if (this.tokenExpired()) {
const tokenRes = await this.getToken().catch(err => err);
if (tokenRes instanceof Error) {
return new Promise((resolve, reject) => reject(tokenRes));
}
if (!tokenRes.success) {
response.success = false;
response.madeNewToken = false;
return response;
}
else {
response.success = true;
response.madeNewToken = true;
}
}
else {
// await this.getToken();
response.success = true;
response.madeNewToken = false;
}
}
else {
const tokenRes = await this.getToken().catch(err => err);
if (tokenRes instanceof Error) {
return new Promise((resolve, reject) => reject(tokenRes));
}
if (!tokenRes.success) {
response.success = false;
response.madeNewToken = false;
}
else {
response.success = true;
response.madeNewToken = false;
}
}
return response;
}
/**
* This method constructs the parameters from the object.
* The method takes the following parameters:
* * `paymentDetails`: The payment details object.
* * `notificationId`: The notification ID.
* * `id`: The ID of the payment.
* * `description`: The description of the payment.
*
* The method returns an object with the following properties:
* * `id`: The ID of the payment.
* * `currency`: The currency of the payment.
* * `amount`: The amount of the payment.
* * `description`: The description of the payment.
* * `callback_url`: The callback URL of the payment.
* * `notification_id`: The notification ID of the payment.
* * `billing_address`: The billing address of the payer.
* * `countryCode`: The country code to map country the payment is from.
* * `countryCurrency`: The countriesmoney currency.
*/
constructParamsFromObj(paymentDetails, notificationId, id, description, countryCode = 'UG', countryCurrency = 'UGA') {
const constructedObj = {
id: id || paymentDetails.id,
currency: paymentDetails.currency || countryCurrency,
amount: paymentDetails.amount,
description,
callback_url: paymentDetails.callback_url,
notification_id: notificationId,
billing_address: {
email_address: paymentDetails.billing_address?.email_address,
phone_number: paymentDetails.billing_address?.phone_number,
country_code: countryCode,
first_name: paymentDetails.billing_address?.first_name,
middle_name: paymentDetails.billing_address?.middle_name,
last_name: paymentDetails.billing_address?.last_name,
line_1: paymentDetails.billing_address?.line_1,
line_2: paymentDetails.billing_address?.line_2,
city: paymentDetails.billing_address?.city,
state: paymentDetails.billing_address?.state,
// postal_code: paymentRelated.shippingAddress?.zipcode,
zip_code: paymentDetails.billing_address?.zip_code
}
};
exports.logger.debug('constructParamsFromObj, constructedObj', constructedObj);
return constructedObj;
}
/**
* This method checks if the token is present.
* The method returns `true` if the token is present, and `false` otherwise.
*/
hasToken() {
return Boolean(this.token?.token);
}
}
exports.Pesapal = Pesapal;
//# sourceMappingURL=pesapal.js.map