@tecafrik/africa-payment-sdk
Version:
A single SDK to integrate all african payment providers seamlessly
384 lines • 21.9 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const libphonenumber_js_1 = require("libphonenumber-js");
const payment_provider_interface_1 = require("../payment-provider.interface");
const apisauce_1 = require("apisauce");
const payment_events_1 = require("../payment-events");
const crypto_1 = require("crypto");
const payment_error_1 = require("../payment-error");
const lodash_1 = require("lodash");
class PaydunyaPaymentProvider {
constructor(config) {
this.config = config;
this.api = (0, apisauce_1.create)({
baseURL: config.mode === "test"
? "https://app.sandbox.paydunya.com/api/"
: "https://app.paydunya.com/api/",
headers: {
"Content-Type": "application/json",
"PAYDUNYA-MASTER-KEY": config.masterKey,
"PAYDUNYA-PRIVATE-KEY": config.privateKey,
"PAYDUNYA-PUBLIC-KEY": config.publicKey,
"PAYDUNYA-TOKEN": config.token,
},
});
this.api.addResponseTransform((response) => {
var _a, _b, _c;
if (((_b = (_a = response.config) === null || _a === void 0 ? void 0 : _a.url) === null || _b === void 0 ? void 0 : _b.endsWith("/softpay/new-orange-money-senegal")) &&
response.status === 422 &&
((_c = response.data) === null || _c === void 0 ? void 0 : _c.message) === "Invalid or expired OTP code!") {
throw new payment_error_1.PaymentError(response.data.message, payment_error_1.PaymentErrorType.INVALID_AUTHORIZATION_CODE);
}
if (!response.ok) {
const defaultErrorMessage = "Paydunya error: " +
response.problem +
". Status: " +
response.status +
". Data: " +
JSON.stringify(response.data);
console.error(response);
throw new payment_error_1.PaymentError(response.data && (0, lodash_1.isObject)(response.data)
? "message" in response.data
? String(response.data.message)
: "response_text" in response.data
? String(response.data.response_text)
: defaultErrorMessage
: defaultErrorMessage);
}
});
const hash = (0, crypto_1.createHash)("sha512");
hash.update(this.config.masterKey, "utf-8");
this.masterKeySha512Hash = hash.digest("hex");
}
useEventEmitter(eventEmitter) {
this.eventEmitter = eventEmitter;
}
determineOrangeMoneyApiType(options) {
return options.authorizationCode ? "OTPCODE" : "QRCODE";
}
buildOrangeMoneyRequest(options, invoiceToken, parsedPhoneNumber) {
const apiType = this.determineOrangeMoneyApiType(options);
const basePayload = {
customer_name: `${options.customer.firstName || ""} ${options.customer.lastName || ""}`.trim(),
customer_email: `${options.customer.phoneNumber}@yopmail.com`,
phone_number: parsedPhoneNumber.nationalNumber,
invoice_token: invoiceToken,
api_type: apiType,
};
if (apiType === "OTPCODE") {
return Object.assign(Object.assign({}, basePayload), { authorization_code: options.authorizationCode, api_type: "OTPCODE" });
}
return Object.assign(Object.assign({}, basePayload), { api_type: "QRCODE" });
}
checkout(options) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
if (options.currency !== payment_provider_interface_1.Currency.XOF) {
throw new payment_error_1.PaymentError("Paydunya does not support the currency: " + options.currency, payment_error_1.PaymentErrorType.UNSUPPORTED_PAYMENT_METHOD);
}
const isMobileMoney = options.paymentMethod === payment_provider_interface_1.PaymentMethod.WAVE ||
options.paymentMethod === payment_provider_interface_1.PaymentMethod.ORANGE_MONEY;
const createInvoiceResponse = yield this.api.post("v1/checkout-invoice/create", {
invoice: {
total_amount: options.amount,
description: options.description,
},
store: {
name: this.config.store.name,
},
channels: options.paymentMethod === payment_provider_interface_1.PaymentMethod.CREDIT_CARD ? ["card"] : null,
custom_data: Object.assign({ transaction_id: options.transactionId }, options.metadata),
actions: {
cancel_url: options.failureRedirectUrl,
return_url: options.successRedirectUrl,
},
});
const invoiceData = createInvoiceResponse.data;
if (!invoiceData) {
throw new payment_error_1.PaymentError("Paydunya error: " + createInvoiceResponse.problem);
}
if (invoiceData.response_code !== "00") {
throw new payment_error_1.PaymentError("Paydunya error: " + invoiceData.response_text);
}
if (!("token" in invoiceData)) {
throw new payment_error_1.PaymentError("Missing invoice token in Paydunya response: " +
invoiceData.response_text);
}
const invoiceToken = invoiceData.token;
let paydunyaPaymentResponse = null;
if (isMobileMoney) {
const parsedCustomerPhoneNumber = (0, libphonenumber_js_1.parsePhoneNumber)(options.customer.phoneNumber, "SN");
if (!parsedCustomerPhoneNumber.isValid()) {
throw new payment_error_1.PaymentError("Invalid phone number: " + options.customer.phoneNumber, payment_error_1.PaymentErrorType.INVALID_PHONE_NUMBER);
}
if (!parsedCustomerPhoneNumber.isPossible()) {
throw new payment_error_1.PaymentError("Phone number is not possible: " + options.customer.phoneNumber, payment_error_1.PaymentErrorType.INVALID_PHONE_NUMBER);
}
if (options.paymentMethod === payment_provider_interface_1.PaymentMethod.WAVE) {
const paydunyaWaveResponse = yield this.api.post("/v1/softpay/wave-senegal", {
wave_senegal_fullName: `${options.customer.firstName || ""} ${options.customer.lastName || ""}`.trim(),
wave_senegal_email: `${options.customer.phoneNumber}@yopmail.com`,
wave_senegal_phone: parsedCustomerPhoneNumber.nationalNumber,
wave_senegal_payment_token: invoiceToken,
});
const waveData = paydunyaWaveResponse.data;
if (!waveData) {
throw new payment_error_1.PaymentError("Paydunya error: " + paydunyaWaveResponse.problem);
}
if (!waveData.success) {
throw new payment_error_1.PaymentError("Paydunya error: " + waveData.message);
}
if (!waveData.url) {
throw new payment_error_1.PaymentError("Missing wave payment url in Paydunya response: " + waveData.message);
}
paydunyaPaymentResponse = waveData;
}
else if (options.paymentMethod === payment_provider_interface_1.PaymentMethod.ORANGE_MONEY) {
const requestPayload = this.buildOrangeMoneyRequest(options, invoiceToken, parsedCustomerPhoneNumber);
const paydunyaOrangeMoneyResponse = yield this.api.post("/v1/softpay/new-orange-money-senegal", requestPayload);
const orangeMoneyData = paydunyaOrangeMoneyResponse.data;
if (!orangeMoneyData) {
throw new payment_error_1.PaymentError("Paydunya error: " + paydunyaOrangeMoneyResponse.problem);
}
if (!orangeMoneyData.success) {
throw new payment_error_1.PaymentError("Paydunya error: " + orangeMoneyData.message);
}
paydunyaPaymentResponse = orangeMoneyData;
}
}
if (options.paymentMethod !== payment_provider_interface_1.PaymentMethod.CREDIT_CARD &&
!paydunyaPaymentResponse) {
throw new payment_error_1.PaymentError("Paydunya error: no payment response data");
}
let redirectUrl = undefined;
if (options.paymentMethod === payment_provider_interface_1.PaymentMethod.CREDIT_CARD) {
redirectUrl = invoiceData.response_text;
}
else if (paydunyaPaymentResponse &&
"other_url" in paydunyaPaymentResponse &&
paydunyaPaymentResponse.other_url &&
"maxit_url" in paydunyaPaymentResponse.other_url) {
redirectUrl = paydunyaPaymentResponse.other_url.maxit_url;
}
else if (paydunyaPaymentResponse && "url" in paydunyaPaymentResponse) {
redirectUrl = paydunyaPaymentResponse.url;
}
const result = {
transactionAmount: options.amount,
transactionCurrency: options.currency,
transactionId: options.transactionId,
transactionReference: invoiceToken,
transactionStatus: payment_provider_interface_1.TransactionStatus.PENDING,
redirectUrl,
};
const paymentInitiatedEvent = {
type: payment_events_1.PaymentEventType.PAYMENT_INITIATED,
paymentMethod: options.paymentMethod,
transactionId: options.transactionId,
transactionAmount: options.amount,
transactionCurrency: options.currency,
transactionReference: invoiceToken,
redirectUrl,
metadata: options.metadata,
paymentProvider: PaydunyaPaymentProvider.name,
};
(_a = this.eventEmitter) === null || _a === void 0 ? void 0 : _a.emit(payment_events_1.PaymentEventType.PAYMENT_INITIATED, paymentInitiatedEvent);
return result;
});
}
checkoutMobileMoney(options) {
return __awaiter(this, void 0, void 0, function* () {
return this.checkout(options);
});
}
checkoutCreditCard(options) {
return __awaiter(this, void 0, void 0, function* () {
return this.checkout(options);
});
}
checkoutRedirect(options) {
return __awaiter(this, void 0, void 0, function* () {
throw new payment_error_1.PaymentError("Paydunya redirect checkout not yet implemented", payment_error_1.PaymentErrorType.UNSUPPORTED_PAYMENT_METHOD);
});
}
refund(options) {
return __awaiter(this, void 0, void 0, function* () {
const transactionReference = options.refundedTransactionReference;
const getInvoiceResponse = yield this.api.get(`/v1/checkout-invoice/confirm/${transactionReference}`);
if (!getInvoiceResponse.data) {
throw new payment_error_1.PaymentError("Paydunya error: " + getInvoiceResponse.problem);
}
if (getInvoiceResponse.data.response_code !== "00") {
throw new payment_error_1.PaymentError("Paydunya error: " + getInvoiceResponse.data.response_text);
}
if (!("invoice" in getInvoiceResponse.data)) {
throw new payment_error_1.PaymentError("Missing invoice in Paydunya response: " +
getInvoiceResponse.data.response_text);
}
const invoiceToRefund = getInvoiceResponse.data.invoice;
const amountToRefund = options.refundedAmount || parseInt(invoiceToRefund.total_amount, 10);
const createDisburseInvoiceResponse = yield this.api.post("/v2/disburse/get-invoice", {
account_alias: getInvoiceResponse.data.customer.phone,
amount: amountToRefund,
withdraw_mode: getInvoiceResponse.data.customer.payment_method.replace(/_/g, "-"),
disburse_id: options.transactionId,
callback_url: this.config.callbackUrl,
});
if (!createDisburseInvoiceResponse.data) {
throw new payment_error_1.PaymentError("Paydunya error: " + createDisburseInvoiceResponse.problem);
}
if (createDisburseInvoiceResponse.data.response_code !== "00") {
throw new payment_error_1.PaymentError("Paydunya error: " + createDisburseInvoiceResponse.data.response_text);
}
if (!("disburse_token" in createDisburseInvoiceResponse.data)) {
throw new payment_error_1.PaymentError("Missing disburse token in Paydunya response: " +
createDisburseInvoiceResponse.data.response_text);
}
const submitDisburseInvoiceResponse = yield this.api.post("/v2/disburse/submit-invoice", {
disburse_invoice: createDisburseInvoiceResponse.data.disburse_token,
disburse_id: options.transactionId,
});
if (!submitDisburseInvoiceResponse.data) {
throw new payment_error_1.PaymentError("Paydunya error: " + submitDisburseInvoiceResponse.problem);
}
if (submitDisburseInvoiceResponse.data.response_code !== "00") {
throw new payment_error_1.PaymentError("Paydunya error: " + submitDisburseInvoiceResponse.data.response_text);
}
return {
transactionAmount: amountToRefund,
transactionId: options.transactionId,
transactionReference: createDisburseInvoiceResponse.data.disburse_token,
transactionCurrency: payment_provider_interface_1.Currency.XOF,
transactionStatus: payment_provider_interface_1.TransactionStatus.SUCCESS,
};
});
}
payoutMobileMoney(options) {
return __awaiter(this, void 0, void 0, function* () {
const paymentMethodToWithdrawMode = {
[payment_provider_interface_1.PaymentMethod.WAVE]: "wave-senegal",
[payment_provider_interface_1.PaymentMethod.ORANGE_MONEY]: "orange-money-senegal",
};
const parsedRecipientPhoneNumber = (0, libphonenumber_js_1.parsePhoneNumber)(options.recipient.phoneNumber, "SN");
if (!parsedRecipientPhoneNumber.isValid()) {
throw new payment_error_1.PaymentError("Invalid phone number: " + options.recipient.phoneNumber, payment_error_1.PaymentErrorType.INVALID_PHONE_NUMBER);
}
if (!parsedRecipientPhoneNumber.isPossible()) {
throw new payment_error_1.PaymentError("Phone number is not possible: " + options.recipient.phoneNumber, payment_error_1.PaymentErrorType.INVALID_PHONE_NUMBER);
}
const createDisburseInvoiceResponse = yield this.api.post("/v2/disburse/get-invoice", {
account_alias: parsedRecipientPhoneNumber.nationalNumber,
amount: options.amount,
disburse_id: options.transactionId,
withdraw_mode: paymentMethodToWithdrawMode[options.paymentMethod],
callback_url: this.config.callbackUrl,
});
if (!createDisburseInvoiceResponse.data) {
throw new payment_error_1.PaymentError("Paydunya error: " + createDisburseInvoiceResponse.problem);
}
if (createDisburseInvoiceResponse.data.response_code !== "00") {
throw new payment_error_1.PaymentError("Paydunya error: " + createDisburseInvoiceResponse.data.response_text);
}
if (!("disburse_token" in createDisburseInvoiceResponse.data)) {
throw new payment_error_1.PaymentError("Missing disburse token in Paydunya response: " +
createDisburseInvoiceResponse.data.response_text);
}
const submitDisburseInvoiceResponse = yield this.api.post("/v2/disburse/submit-invoice", {
disburse_invoice: createDisburseInvoiceResponse.data.disburse_token,
disburse_id: options.transactionId,
});
if (!submitDisburseInvoiceResponse.data) {
throw new payment_error_1.PaymentError("Paydunya error: " + submitDisburseInvoiceResponse.problem);
}
if (submitDisburseInvoiceResponse.data.response_code !== "00") {
throw new payment_error_1.PaymentError("Paydunya error: " + submitDisburseInvoiceResponse.data.response_text);
}
return {
transactionAmount: options.amount,
transactionId: options.transactionId,
transactionReference: createDisburseInvoiceResponse.data.disburse_token,
transactionCurrency: options.currency,
transactionStatus: payment_provider_interface_1.TransactionStatus.SUCCESS,
};
});
}
handleWebhook(rawBody) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
if ((0, lodash_1.isBuffer)(rawBody) || (0, lodash_1.isString)(rawBody)) {
console.error("Paydunya webhook body must be a parsed object, not the raw body");
return null;
}
const body = rawBody;
if (!body.hash) {
console.error("Missing hash in Paydunya webhook body");
return null;
}
if (body.hash !== this.masterKeySha512Hash) {
console.error("Invalid hash in Paydunya webhook body");
return null;
}
const paymentMethod = body.customer.payment_method === "wave_senegal"
? payment_provider_interface_1.PaymentMethod.WAVE
: body.customer.payment_method === "orange_money_senegal"
? payment_provider_interface_1.PaymentMethod.ORANGE_MONEY
: null;
if (body.status === "completed" && body.response_code == "00") {
const paymentSuccessfulEvent = {
type: payment_events_1.PaymentEventType.PAYMENT_SUCCESSFUL,
paymentMethod,
transactionAmount: Number(body.invoice.total_amount),
transactionCurrency: payment_provider_interface_1.Currency.XOF,
transactionId: body.custom_data.transaction_id,
transactionReference: body.invoice.token,
metadata: body.custom_data,
paymentProvider: PaydunyaPaymentProvider.name,
};
(_a = this.eventEmitter) === null || _a === void 0 ? void 0 : _a.emit(payment_events_1.PaymentEventType.PAYMENT_SUCCESSFUL, paymentSuccessfulEvent);
return paymentSuccessfulEvent;
}
else if (body.status === "cancelled") {
const paymentCancelledEvent = {
type: payment_events_1.PaymentEventType.PAYMENT_CANCELLED,
paymentMethod,
transactionAmount: Number(body.invoice.total_amount),
transactionCurrency: payment_provider_interface_1.Currency.XOF,
transactionId: body.custom_data.transaction_id,
transactionReference: body.invoice.token,
metadata: body.custom_data,
reason: body.response_text,
paymentProvider: PaydunyaPaymentProvider.name,
};
(_b = this.eventEmitter) === null || _b === void 0 ? void 0 : _b.emit(payment_events_1.PaymentEventType.PAYMENT_CANCELLED, paymentCancelledEvent);
return paymentCancelledEvent;
}
else if (body.status === "failed") {
const paymentFailedEvent = {
type: payment_events_1.PaymentEventType.PAYMENT_FAILED,
paymentMethod,
transactionAmount: Number(body.invoice.total_amount),
transactionCurrency: payment_provider_interface_1.Currency.XOF,
transactionId: body.custom_data.transaction_id,
transactionReference: body.invoice.token,
metadata: body.custom_data,
reason: body.response_text,
paymentProvider: PaydunyaPaymentProvider.name,
};
(_c = this.eventEmitter) === null || _c === void 0 ? void 0 : _c.emit(payment_events_1.PaymentEventType.PAYMENT_FAILED, paymentFailedEvent);
return paymentFailedEvent;
}
return null;
});
}
}
exports.default = PaydunyaPaymentProvider;
//# sourceMappingURL=paydunya.js.map