UNPKG

addpay-js

Version:

TypeScript SDK for AddPay Cloud API - CNP payment processing

959 lines (952 loc) 23 kB
import crypto from 'crypto'; // src/types/common.ts var AddPayError = class extends Error { code; details; timestamp; constructor(code, message, details) { super(message); this.name = "AddPayError"; this.code = code; this.details = details; this.timestamp = Date.now(); } }; var CryptoUtils = class { static signWithRSA(data, privateKey) { const sign = crypto.createSign("RSA-SHA256"); sign.update(data); sign.end(); return sign.sign(privateKey, "base64"); } static verifyWithRSA(data, signature, publicKey) { try { const verify = crypto.createVerify("RSA-SHA256"); verify.update(data); verify.end(); return verify.verify(publicKey, signature, "base64"); } catch { return false; } } static encryptWithRSA(data, publicKey) { const buffer = Buffer.from(data, "utf-8"); const encrypted = crypto.publicEncrypt( { key: publicKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, buffer ); return encrypted.toString("base64"); } static decryptWithRSA(encryptedData, privateKey) { const buffer = Buffer.from(encryptedData, "base64"); const decrypted = crypto.privateDecrypt( { key: privateKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, buffer ); return decrypted.toString("utf-8"); } static generateNonce() { return crypto.randomBytes(16).toString("hex"); } static generateTimestamp() { return Date.now(); } static sortObjectKeys(obj) { const sorted = {}; Object.keys(obj).sort().forEach((key) => { if (obj[key] !== void 0 && obj[key] !== null && obj[key] !== "") { sorted[key] = obj[key]; } }); return sorted; } static buildSignatureString(params) { const sorted = this.sortObjectKeys(params); const pairs = []; for (const [key, value] of Object.entries(sorted)) { if (key === "sign") continue; if (value === void 0 || value === null || value === "") continue; if (typeof value === "object") { pairs.push(`${key}=${JSON.stringify(value)}`); } else { pairs.push(`${key}=${value}`); } } return pairs.join("&"); } }; // src/lib/http-client.ts var HttpClient = class { config; baseUrl; constructor(config) { this.config = config; this.baseUrl = config.baseUrl || (config.sandbox ? "http://gw.wisepaycloud.com" : "https://api.paycloud.africa"); } async request(endpoint, method = "POST", data, options) { const url = `${this.baseUrl}${endpoint}`; const timeout = options?.timeout || this.config.timeout || 3e4; const retries = options?.retries || 0; const requestData = this.prepareRequestData(data || {}); const signature = this.generateSignature(requestData); const body = { ...requestData, app_id: this.config.appId, merchant_no: this.config.merchantNo, store_no: this.config.storeNo, sign: signature, timestamp: CryptoUtils.generateTimestamp(), nonce: CryptoUtils.generateNonce() }; let lastError; for (let attempt = 0; attempt <= retries; attempt++) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); const response = await fetch(url, { method, headers: { "Content-Type": "application/json", "Accept": "application/json", ...options?.headers }, body: JSON.stringify(body), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new AddPayError( "HTTP_ERROR", `HTTP ${response.status}: ${response.statusText}`, { status: response.status, statusText: response.statusText } ); } const result = await response.json(); if (result.code !== "0" && result.code !== "SUCCESS") { throw new AddPayError(result.code, result.msg || "Unknown error", result); } if (result.sign && !this.verifySignature(result)) { throw new AddPayError("INVALID_SIGNATURE", "Response signature verification failed"); } return result; } catch (error) { lastError = error; if (error instanceof AddPayError) { throw error; } if (error?.name === "AbortError") { if (attempt === retries) { throw new AddPayError("TIMEOUT", `Request timeout after ${timeout}ms`); } continue; } if (attempt === retries) { throw new AddPayError( "NETWORK_ERROR", `Network error: ${lastError?.message || "Unknown error"}`, lastError ); } await this.delay(Math.min(1e3 * Math.pow(2, attempt), 5e3)); } } throw lastError || new AddPayError("UNKNOWN_ERROR", "Unknown error occurred"); } prepareRequestData(data) { const cleaned = {}; for (const [key, value] of Object.entries(data)) { if (value !== void 0 && value !== null) { if (typeof value === "object" && !Array.isArray(value)) { cleaned[key] = JSON.stringify(value); } else { cleaned[key] = value; } } } return cleaned; } generateSignature(data) { const signString = CryptoUtils.buildSignatureString({ ...data, app_id: this.config.appId, merchant_no: this.config.merchantNo, store_no: this.config.storeNo }); return CryptoUtils.signWithRSA(signString, this.config.privateKey); } verifySignature(response) { const { sign, ...data } = response; if (!sign) return true; const signString = CryptoUtils.buildSignatureString(data); return CryptoUtils.verifyWithRSA(signString, sign, this.config.gatewayPublicKey); } delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } }; // src/resources/checkout.ts var CheckoutResource = class { constructor(client) { this.client = client; } /** * Create a checkout session for hosted payment page */ async create(request) { const response = await this.client.request( "/api/entry/checkout", "POST", request ); if (!response.data) { throw new Error("No checkout data returned"); } return response.data; } /** * Get the status of a checkout session */ async getStatus(request) { if (!request.merchant_order_no && !request.order_no) { throw new Error("Either merchant_order_no or order_no is required"); } const response = await this.client.request( "/api/entry/checkout/status", "POST", request ); if (!response.data) { throw new Error("No status data returned"); } return response.data; } /** * Cancel a checkout session */ async cancel(merchantOrderNo) { const response = await this.client.request( "/api/entry/checkout/cancel", "POST", { merchant_order_no: merchantOrderNo } ); if (!response.data) { throw new Error("No cancellation data returned"); } return response.data; } /** * Refund a completed checkout transaction */ async refund(merchantOrderNo, refundAmount, reason) { const request = { merchant_order_no: merchantOrderNo }; if (refundAmount !== void 0) { request.refund_amount = refundAmount; } if (reason) { request.refund_reason = reason; } const response = await this.client.request( "/api/entry/checkout/refund", "POST", request ); if (!response.data) { throw new Error("No refund data returned"); } return response.data; } /** * Create a checkout session with fluent API */ createCheckout() { return new CheckoutBuilder(this); } }; var CheckoutBuilder = class { constructor(resource) { this.resource = resource; } request = {}; merchantOrderNo(value) { this.request.merchant_order_no = value; return this; } amount(value) { this.request.order_amount = value; return this; } currency(value) { this.request.price_currency = value; return this; } notifyUrl(value) { this.request.notify_url = value; return this; } returnUrl(value) { this.request.return_url = value; return this; } description(value) { this.request.description = value; return this; } attach(value) { this.request.attach = value; return this; } expireTime(value) { this.request.expire_time = value; return this; } goods(value) { this.request.goods_info = value; return this; } terminal(value) { this.request.terminal = value; return this; } sceneInfo(value) { this.request.scene_info = value; return this; } customerInfo(value) { this.request.customer_info = value; return this; } async execute() { this.validate(); return this.resource.create(this.request); } validate() { const required = [ "merchant_order_no", "order_amount", "price_currency", "notify_url", "return_url" ]; for (const field of required) { if (!(field in this.request)) { throw new Error(`Missing required field: ${field}`); } } } }; // src/resources/debicheck.ts var DebiCheckResource = class { constructor(client) { this.client = client; } /** * Create a DebiCheck mandate */ async createMandate(request) { const response = await this.client.request( "/api/entry/debicheck/mandate", "POST", request ); if (!response.data) { throw new Error("No mandate data returned"); } return response.data; } /** * Initiate a collection against an approved mandate */ async collect(request) { const response = await this.client.request( "/api/entry/debicheck/collect", "POST", request ); if (!response.data) { throw new Error("No collection data returned"); } return response.data; } /** * Get the status of a mandate and its collections */ async getStatus(request) { if (!request.mandate_reference && !request.merchant_order_no) { throw new Error("Either mandate_reference or merchant_order_no is required"); } const response = await this.client.request( "/api/entry/debicheck/status", "POST", request ); if (!response.data) { throw new Error("No status data returned"); } return response.data; } /** * Cancel a mandate */ async cancelMandate(mandateReference) { const response = await this.client.request( "/api/entry/debicheck/mandate/cancel", "POST", { mandate_reference: mandateReference } ); if (!response.data) { throw new Error("No cancellation data returned"); } return response.data; } /** * Create a mandate with fluent API */ createMandateBuilder() { return new DebiCheckMandateBuilder(this); } /** * Create a collection with fluent API */ createCollectionBuilder() { return new DebiCheckCollectionBuilder(this); } }; var DebiCheckMandateBuilder = class { constructor(resource) { this.resource = resource; } request = {}; merchantOrderNo(value) { this.request.merchant_order_no = value; return this; } customerInfo(value) { this.request.customer_info = value; return this; } customer(builder) { const customerBuilder = new CustomerInfoBuilder(); builder(customerBuilder); this.request.customer_info = customerBuilder.build(); return this; } mandateInfo(value) { this.request.mandate_info = value; return this; } mandate(builder) { const mandateBuilder = new MandateInfoBuilder(); builder(mandateBuilder); this.request.mandate_info = mandateBuilder.build(); return this; } notifyUrl(value) { this.request.notify_url = value; return this; } returnUrl(value) { this.request.return_url = value; return this; } async execute() { this.validate(); return this.resource.createMandate(this.request); } validate() { const required = ["merchant_order_no", "customer_info", "mandate_info", "notify_url"]; for (const field of required) { if (!(field in this.request)) { throw new Error(`Missing required field: ${field}`); } } } }; var CustomerInfoBuilder = class { info = {}; id(value) { this.info.customer_id = value; return this; } name(value) { this.info.customer_name = value; return this; } email(value) { this.info.customer_email = value; return this; } phone(value) { this.info.customer_phone = value; return this; } idNumber(value) { this.info.id_number = value; return this; } accountNumber(value) { this.info.account_number = value; return this; } accountType(value) { this.info.account_type = value; return this; } bankName(value) { this.info.bank_name = value; return this; } branchCode(value) { this.info.branch_code = value; return this; } build() { const required = [ "customer_id", "customer_name", "id_number", "account_number", "account_type", "bank_name", "branch_code" ]; for (const field of required) { if (!(field in this.info)) { throw new Error(`Missing required customer field: ${field}`); } } return this.info; } }; var MandateInfoBuilder = class { info = {}; type(value) { this.info.mandate_type = value; return this; } maxAmount(value) { this.info.max_amount = value; return this; } currency(value) { this.info.currency = value; return this; } startDate(value) { this.info.start_date = value; return this; } endDate(value) { this.info.end_date = value; return this; } frequency(value) { this.info.frequency = value; return this; } installments(value) { this.info.installments = value; return this; } trackingDays(value) { this.info.tracking_days = value; return this; } description(value) { this.info.description = value; return this; } build() { const required = ["mandate_type", "max_amount", "currency", "start_date"]; for (const field of required) { if (!(field in this.info)) { throw new Error(`Missing required mandate field: ${field}`); } } return this.info; } }; var DebiCheckCollectionBuilder = class { constructor(resource) { this.resource = resource; } request = {}; mandateReference(value) { this.request.mandate_reference = value; return this; } merchantOrderNo(value) { this.request.merchant_order_no = value; return this; } amount(value) { this.request.collection_amount = value; return this; } currency(value) { this.request.currency = value; return this; } collectionDate(value) { this.request.collection_date = value; return this; } notifyUrl(value) { this.request.notify_url = value; return this; } async execute() { this.validate(); return this.resource.collect(this.request); } validate() { const required = [ "mandate_reference", "merchant_order_no", "collection_amount", "currency", "notify_url" ]; for (const field of required) { if (!(field in this.request)) { throw new Error(`Missing required field: ${field}`); } } } }; // src/resources/token.ts var TokenResource = class { constructor(client) { this.client = client; } /** * Tokenize a card for future payments */ async tokenize(request) { const response = await this.client.request( "/api/entry/token/create", "POST", request ); if (!response.data) { throw new Error("No tokenization data returned"); } return response.data; } /** * Process a payment using a stored token */ async pay(request) { const response = await this.client.request( "/api/entry/token/pay", "POST", request ); if (!response.data) { throw new Error("No payment data returned"); } return response.data; } /** * Delete a stored token */ async delete(request) { const response = await this.client.request( "/api/entry/token/delete", "POST", request ); if (!response.data) { throw new Error("No deletion data returned"); } return response.data; } /** * List all tokens for a customer */ async list(request) { const response = await this.client.request( "/api/entry/token/list", "POST", request ); if (!response.data) { throw new Error("No token list data returned"); } return response.data; } /** * Get token details */ async get(token) { const response = await this.client.request( "/api/entry/token/get", "POST", { token } ); if (!response.data) { throw new Error("No token data returned"); } return response.data; } /** * Create a tokenization request with fluent API */ createTokenization() { return new TokenizationBuilder(this); } /** * Create a token payment with fluent API */ createPayment() { return new TokenPaymentBuilder(this); } }; var TokenizationBuilder = class { constructor(resource) { this.resource = resource; } request = {}; merchantOrderNo(value) { this.request.merchant_order_no = value; return this; } cardInfo(value) { this.request.card_info = value; return this; } card(builder) { const cardBuilder = new CardInfoBuilder(); builder(cardBuilder); this.request.card_info = cardBuilder.build(); return this; } customerInfo(value) { this.request.customer_info = value; return this; } customer(builder) { const customerBuilder = new TokenCustomerBuilder(); builder(customerBuilder); this.request.customer_info = customerBuilder.build(); return this; } notifyUrl(value) { this.request.notify_url = value; return this; } returnUrl(value) { this.request.return_url = value; return this; } verificationAmount(value) { this.request.verification_amount = value; return this; } currency(value) { this.request.currency = value; return this; } async execute() { this.validate(); return this.resource.tokenize(this.request); } validate() { const required = ["merchant_order_no", "notify_url"]; for (const field of required) { if (!(field in this.request)) { throw new Error(`Missing required field: ${field}`); } } } }; var CardInfoBuilder = class { info = {}; cardNumber(value) { this.info.card_number = value; return this; } cardHolderName(value) { this.info.card_holder_name = value; return this; } expiryMonth(value) { this.info.expiry_month = value; return this; } expiryYear(value) { this.info.expiry_year = value; return this; } cvv(value) { this.info.cvv = value; return this; } build() { return this.info; } }; var TokenCustomerBuilder = class { info = {}; id(value) { this.info.customer_id = value; return this; } email(value) { this.info.customer_email = value; return this; } phone(value) { this.info.customer_phone = value; return this; } name(value) { this.info.customer_name = value; return this; } billingAddress(value) { this.info.billing_address = value; return this; } address(builder) { const addressBuilder = new BillingAddressBuilder(); builder(addressBuilder); this.info.billing_address = addressBuilder.build(); return this; } build() { if (!this.info.customer_id) { throw new Error("customer_id is required"); } return this.info; } }; var BillingAddressBuilder = class { address = {}; line1(value) { this.address.address_line1 = value; return this; } line2(value) { this.address.address_line2 = value; return this; } city(value) { this.address.city = value; return this; } state(value) { this.address.state = value; return this; } postalCode(value) { this.address.postal_code = value; return this; } country(value) { this.address.country = value; return this; } build() { return this.address; } }; var TokenPaymentBuilder = class { constructor(resource) { this.resource = resource; } request = {}; token(value) { this.request.token = value; return this; } merchantOrderNo(value) { this.request.merchant_order_no = value; return this; } amount(value) { this.request.order_amount = value; return this; } currency(value) { this.request.currency = value; return this; } cvv(value) { this.request.cvv = value; return this; } description(value) { this.request.description = value; return this; } notifyUrl(value) { this.request.notify_url = value; return this; } returnUrl(value) { this.request.return_url = value; return this; } customerInfo(value) { this.request.customer_info = value; return this; } async execute() { this.validate(); return this.resource.pay(this.request); } validate() { const required = [ "token", "merchant_order_no", "order_amount", "currency", "notify_url" ]; for (const field of required) { if (!(field in this.request)) { throw new Error(`Missing required field: ${field}`); } } } }; // src/client.ts var AddPayClient = class _AddPayClient { httpClient; checkout; debicheck; token; constructor(config) { this.validateConfig(config); this.httpClient = new HttpClient(config); this.checkout = new CheckoutResource(this.httpClient); this.debicheck = new DebiCheckResource(this.httpClient); this.token = new TokenResource(this.httpClient); } validateConfig(config) { const required = ["appId", "merchantNo", "storeNo", "privateKey", "publicKey", "gatewayPublicKey"]; for (const field of required) { if (!config[field]) { throw new Error(`Missing required configuration: ${field}`); } } } /** * Create a new AddPay client instance */ static create(config) { return new _AddPayClient(config); } /** * Create a sandbox client with test credentials */ }; export { AddPayClient, AddPayError, CheckoutBuilder, CheckoutResource, DebiCheckCollectionBuilder, DebiCheckMandateBuilder, DebiCheckResource, TokenPaymentBuilder, TokenResource, TokenizationBuilder }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map