UNPKG

pesapal3-sdk

Version:

pesapal api version 3 node library

482 lines 19.8 kB
"use strict"; /** * @file PesaPal SDK for TypeScript * @description A comprehensive SDK for integrating with PesaPal payment gateway * @module pesapal */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Pesapal = exports.stringifyIfObj = exports.logger = void 0; const tslib_1 = require("tslib"); /* eslint-disable @typescript-eslint/naming-convention */ const axios_1 = tslib_1.__importDefault(require("axios")); const fs = tslib_1.__importStar(require("fs")); const path = tslib_1.__importStar(require("path")); const tracer = tslib_1.__importStar(require("tracer")); const error_handler_1 = require("./error-handler"); /** * Logger instance for the PesaPal SDK * @type {tracer.Tracer.Logger} */ 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 = path.join(process.cwd() + '/logs/'); fs.mkdir(logDir, { recursive: true }, (err) => { if (err) { if (err) { throw err; } } }); fs.appendFile(logDir + '/pesapal.log', data.rawoutput + '\n', err => { if (err) { throw err; } }); } }); /** * Converts a value to string, handling objects by JSON stringification * @template T - The type of the input value * @param {T} val - The value to stringify * @returns {string} The stringified value */ const stringifyIfObj = (val) => (typeof val === 'string' ? val : JSON.stringify(val)); exports.stringifyIfObj = stringifyIfObj; /** * Main class for interacting with the PesaPal API * @class Pesapal * @description Provides methods to interact with PesaPal payment gateway */ class Pesapal { /** * Creates a new instance of the Pesapal class * @constructor * @param {Iconfig} config - Configuration object containing PesaPal credentials * @param {string} config.PESAPAL_ENVIRONMENT - Environment type ('live' or 'sandbox') */ constructor(config) { this.config = config; /** * Array of registered IPN (Instant Payment Notification) endpoints * @type {IipnResponse[]} */ this.ipns = []; if (config.PESAPAL_ENVIRONMENT === 'live') { this.pesapalUrl = 'https://pay.pesapal.com/v3'; } else { this.pesapalUrl = 'https://cybqa.pesapal.com/pesapalv3'; } this.axiosInstance = axios_1.default.create({ baseURL: this.pesapalUrl, headers: { 'Content-Type': 'application/json', Accept: 'application/json' } }); this.interceptAxios(); } /** * Intercepts Axios requests to add authorization headers * @private * @returns {void} */ interceptAxios() { this.axiosInstance.interceptors.request.use((config) => { // add default headers if (!this.tokenExpired()) { config.headers.Authorization = 'Bearer ' + this.token?.token; } else { config.headers.Authorization = ''; } return config; }); } /** * Registers an IPN (Instant Payment Notification) URL with PesaPal * @async * @param {string} [ipn] - The IPN URL to register * @param {TnotificationMethodType} [notificationMethodType='GET'] - The notification method type * @returns {Promise<IregisterIpnRes>} Promise that resolves when IPN is registered * @throws {Error} If token cannot be obtained or IPN registration fails */ async registerIpn(ipn, notificationMethodType = 'GET') { 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 parameters = { url: ipn, ipn_notification_type: notificationMethodType }; return new Promise((resolve, reject) => { this.axiosInstance .post('/api/URLSetup/RegisterIPN', parameters) .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))); }); }); } /** * Retrieves the list of registered IPN endpoints * @async * @returns {Promise<IgetIpnEndPointsRes>} Promise that resolves with the list of IPN endpoints * @throws {Error} If token cannot be obtained or IPN endpoints cannot be retrieved */ async getIpnEndPoints() { const gotToken = await this.relegateTokenStatus().catch(err => err); if (gotToken instanceof Error) { return new Promise((resolve, reject) => reject(gotToken)); } return new Promise((resolve, reject) => { this.axiosInstance .get('/api/URLSetup/GetIpnList') .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))); }); }); } /** * Submits an order to PesaPal * @async * @param {IpayDetails} paymentDetails - The payment details * @param {string} productId - The product ID * @param {string} description - The order description * @returns {Promise<IsubmitOrderRes>} Promise that resolves with the order submission result * @throws {Error} If input validation fails or order submission fails */ 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'); } // check if subscription details are provided if account number is provided if (paymentDetails.account_number && !paymentDetails.subscription_details) { throw new Error('Subscription details are required'); } // check each val in subscription details if provided if (paymentDetails.subscription_details) { if (!paymentDetails.subscription_details.start_date || !paymentDetails.subscription_details.end_date || !paymentDetails.subscription_details.frequency) { throw new Error('Subscription details are 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'); } if (!paymentDetails.notification_id) { // if no notification_ipn_url is provided, error if (!paymentDetails.notification_ipn_url) { throw new Error('Notification IPN URL is required'); } // check notification_id against current ipn_id if no exist error const ipnUrls = this.ipns.map(ipn => ipn.url); if (!ipnUrls.includes(paymentDetails.notification_ipn_url)) { throw new Error('Notification IPN URL does not match'); } paymentDetails.notification_id = this.ipns.find(ipn => ipn.url === paymentDetails.notification_ipn_url)?.ipn_id; } // Make API call const response = await this.axiosInstance.post('/api/Transactions/SubmitOrderRequest', this.constructParamsFromObj(paymentDetails, productId, description)); // 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'); } } /** * Gets the status of a transaction * @async * @param {string} orderTrackingId - The order tracking ID * @returns {Promise<IgetTransactionStatusRes>} Promise that resolves with the transaction status * @throws {Error} If token cannot be obtained or transaction status cannot be retrieved */ async getTransactionStatus(orderTrackingId) { const gotToken = await this.relegateTokenStatus().catch(err => err); if (gotToken instanceof Error) { return new Promise((resolve, reject) => reject(gotToken)); } return new Promise((resolve, reject) => { this.axiosInstance .get(`/api/Transactions/GetTransactionStatus?orderTrackingId=${orderTrackingId}`).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(response); } else { reject(new Error(`Getting Transaction Status Failed With ${response.payment_status_description}`)); } }).catch((err) => { exports.logger.error('PesaPalController, getToken err', err); reject(new Error((0, exports.stringifyIfObj)(err))); }); }); } /** * Submits a refund request for a transaction * @async * @param {IrefundRequestReq} refunReqObj - The refund request object * @returns {Promise<IrefundRequestResComplete>} Promise that resolves with the refund request result * @throws {Error} If token cannot be obtained or refund request fails */ async refundRequest(refunReqObj) { const gotToken = await this.relegateTokenStatus().catch(err => err); if (gotToken instanceof Error) { return new Promise((resolve, reject) => reject(gotToken)); } return new Promise((resolve, reject) => { this.axiosInstance .post('/api/Transactions/RefundRequestt', refunReqObj) .then(res => { const response = res.data; if (!response) { reject(new Error('Refund Unsuccessful')); } else { resolve({ success: true, refundRequestRes: response }); } }).catch((err) => { exports.logger.error('PesaPalController, submitOrder err', err); reject(new Error((0, exports.stringifyIfObj)(err))); }); }); } /** * Gets the PesaPal token * @async * @returns {Promise<IgetTokenRes>} Promise that resolves with the token * @throws {Error} If token cannot be obtained */ getToken() { const parameters = { consumer_key: this.config.PESAPAL_CONSUMER_KEY, consumer_secret: this.config.PESAPAL_CONSUMER_SECRET }; return new Promise((resolve, reject) => { this.axiosInstance .post('/api/Auth/RequestToken', parameters) .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))); }); }); } /** * Checks if the token is expired * @private * @returns {boolean} True if the token is expired, false otherwise */ tokenExpired() { if (!this.hasToken()) { return true; } const nowDate = new Date(); const tokenDate = new Date(this.token.expiryDate); return nowDate > tokenDate; } /** * Checks the status of the token and creates a new token if it is expired * @async * @private * @returns {Promise<IrelegateTokenStatusRes>} Promise that resolves with the token status * @throws {Error} If token cannot be obtained or 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; } /** * Constructs the parameters from the object * @private * @param {IpayDetails} paymentDetails - The payment details * @param {string} id - The ID of the payment * @param {string} description - The description of the payment * @param {string} [countryCode='UG'] - The country code * @param {string} [countryCurrency='UGA'] - The country currency * @returns {Object} The constructed parameters */ constructParamsFromObj(paymentDetails, 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: paymentDetails.notification_id, 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; } /** * Checks if the token is present * @private * @returns {boolean} True if the token is present, false otherwise */ hasToken() { return Boolean(this.token?.token); } } exports.Pesapal = Pesapal; //# sourceMappingURL=pesapal.js.map