UNPKG

@tecafrik/africa-payment-sdk

Version:

A single SDK to integrate all african payment providers seamlessly

384 lines 21.9 kB
"use strict"; 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