addpay-js
Version:
TypeScript SDK for AddPay Cloud API - CNP payment processing
959 lines (952 loc) • 23 kB
JavaScript
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