UNPKG

voko-sdk

Version:
168 lines (167 loc) 6.71 kB
"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('&timestamp=')[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}&timestamp=${timestamp}`; return crypto_1.default.createHmac('sha256', this.apiSecret).update(signedFields).digest('hex'); } } exports.ZenopayAdapter = ZenopayAdapter;