voko-sdk
Version:
Process payments with ease
168 lines (167 loc) • 6.71 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZenopayAdapter = void 0;
const crypto_1 = __importDefault(require("crypto"));
const base_adapter_1 = require("../base/base-adapter");
const types_1 = require("../../core/types");
const models_1 = require("../../core/models");
const phone_validator_1 = require("../../utils/phone-validator");
const zenopay_config_1 = require("./zenopay.config");
class ZenopayAdapter extends base_adapter_1.BaseAdapter {
provider = types_1.SupportedProvider.ZENOPAY;
vendor;
apiKey;
apiSecret;
constructor(config) {
super(config);
this.vendor = config.vendor;
this.apiKey = config.apiKey;
this.apiSecret = config.apiSecret || '';
}
getBaseUrl(environment) {
return zenopay_config_1.ZENOPAY_CONFIG[environment].baseUrl;
}
getPaymentEndpoint() {
return zenopay_config_1.ZENOPAY_CONFIG[this.config.environment].endpoints.payment;
}
getStatusEndpoint() {
return zenopay_config_1.ZENOPAY_CONFIG[this.config.environment].endpoints.status;
}
getContentHeaders() {
return {
'Content-Type': 'application/x-www-form-urlencoded',
};
}
getStatusHttpMethod() {
return 'POST';
}
transformRequest(request) {
const normalizedPhone = request.phoneNumber
? phone_validator_1.PhoneValidator.normalize(request.phoneNumber)
: '';
const phoneWithoutPrefix = normalizedPhone.replace('+255', '0');
return {
account_id: this.vendor,
buyer_email: request.email || '',
buyer_phone: phoneWithoutPrefix,
buyer_name: request.fullName || 'Customer',
amount: request.amount,
webhook_url: request.callbackUrl || '',
api_key: this.apiKey,
secret_key: this.apiSecret,
metadata: {
currency: request.currency,
reference: request.reference,
description: request.description,
payment_method: request.paymentMethod,
operator: request.operator,
},
};
}
transformResponse(providerResponse, originalRequest) {
const status = this.mapZenopayStatus(providerResponse.status.toLowerCase());
const operator = this.mapOperator(originalRequest.operator);
return new models_1.VokoPaymentResponse({
success: providerResponse.status === 'success',
transactionId: this.generateTransactionId(),
providerTransactionId: providerResponse.order_id,
status,
amount: originalRequest.amount,
currency: originalRequest.currency,
phoneNumber: originalRequest.phoneNumber,
email: originalRequest.email,
reference: originalRequest.reference,
message: providerResponse.message,
timestamp: new Date().toISOString(),
operator,
providerData: providerResponse,
});
}
transformStatusResponse(providerResponse) {
const status = this.mapZenopayStatus(providerResponse.payment_status.toLowerCase());
return {
transactionId: providerResponse.order_id,
providerTransactionId: providerResponse.order_id,
status,
amount: providerResponse.amount,
reference: providerResponse.order_id,
completedAt: new Date().toISOString(),
providerData: providerResponse,
};
}
getAuthHeaders() {
const timestamp = Math.floor(Date.now() / 1000).toString();
const digest = this.generateDigest(timestamp);
return {
Authorization: `ZENOPAY ${this.apiKey ? this.apiKey : 'voko'}`,
'Digest-Method': 'HS256',
'Signed-Fields': 'vendor,order_id,buyer_email,amount',
Timestamp: timestamp,
Digest: digest,
};
}
getStatusRequestData(request) {
return {
check_status: 1,
order_id: request.transactionId,
api_key: this.apiKey,
secret_key: this.apiSecret,
};
}
mapErrorCode(providerError) {
const errorMappings = {
'100': types_1.VokoErrorCode.AUTHENTICATION_FAILED,
'101': types_1.VokoErrorCode.INVALID_PHONE,
'102': types_1.VokoErrorCode.INSUFFICIENT_FUNDS,
'103': types_1.VokoErrorCode.TRANSACTION_NOT_FOUND,
'104': types_1.VokoErrorCode.NETWORK_ERROR,
'105': types_1.VokoErrorCode.PAYMENT_DECLINED,
'900': types_1.VokoErrorCode.PROVIDER_ERROR,
};
const errorCode = providerError?.code?.toString() || providerError?.result;
return errorMappings[errorCode] || types_1.VokoErrorCode.UNKNOWN_ERROR;
}
verifyWebhookSignature(payload, signature) {
if (!signature)
return false;
const timestamp = payload.split('×tamp=')[1].split('&')[0];
const digest = this.generateDigest(timestamp);
return signature === digest;
}
mapZenopayStatus(status) {
const statusMappings = {
success: types_1.PaymentStatus.SUCCESS,
completed: types_1.PaymentStatus.SUCCESS,
pending: types_1.PaymentStatus.PENDING,
failed: types_1.PaymentStatus.FAILED,
fail: types_1.PaymentStatus.FAILED,
cancelled: types_1.PaymentStatus.CANCELLED,
timeout: types_1.PaymentStatus.EXPIRED,
error: types_1.PaymentStatus.FAILED,
expired: types_1.PaymentStatus.EXPIRED,
};
return statusMappings[status?.toLowerCase()] || types_1.PaymentStatus.UNKNOWN;
}
mapOperator(operator) {
if (!operator)
return undefined;
const operatorMappings = {
VODACOM: types_1.MobileOperator.VODACOM_MPESA,
MPESA: types_1.MobileOperator.VODACOM_MPESA,
AIRTEL: types_1.MobileOperator.AIRTEL_MONEY,
TIGO: types_1.MobileOperator.TIGO_PESA,
HALOTEL: types_1.MobileOperator.HALO_PESA,
};
return operatorMappings[operator.toUpperCase()];
}
generateDigest(timestamp) {
if (!this.apiSecret)
return '';
const signedFields = `vendor=${this.vendor}×tamp=${timestamp}`;
return crypto_1.default.createHmac('sha256', this.apiSecret).update(signedFields).digest('hex');
}
}
exports.ZenopayAdapter = ZenopayAdapter;