Files

176 lines
6.2 KiB
JavaScript

"use strict";
/**
* @qbirr/sdk — Node.js client for the qbirr payment verification API.
*
* import { Qbirr } from '@qbirr/sdk';
* const qb = new Qbirr({ apiKey: process.env.QBIRR_API_KEY! });
* const r = await qb.verify({
* provider: 'cbe',
* ref: 'FT23001234ABC',
* amount: 500,
* receiverName: 'YOUR NAME',
* receiverAccount: '1000017692643',
* });
* console.log(r.verified, r.payer);
*
* Works on Node 18+ (uses the built-in fetch). No dependencies.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Qbirr = exports.QbirrError = void 0;
/** Thrown for network failures, HTTP errors, or non-JSON responses. */
class QbirrError extends Error {
status;
body;
constructor(message, status, body) {
super(message);
this.status = status;
this.body = body;
this.name = 'QbirrError';
}
}
exports.QbirrError = QbirrError;
const DEFAULT_BASE_URL = 'https://verify.qbirr.com';
class Qbirr {
apiKey;
baseUrl;
timeoutMs;
fetchFn;
constructor(opts) {
if (!opts.apiKey)
throw new QbirrError('apiKey is required');
this.apiKey = opts.apiKey;
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
this.timeoutMs = opts.timeoutMs ?? 30_000;
this.fetchFn = opts.fetch ?? globalThis.fetch;
if (!this.fetchFn)
throw new QbirrError('fetch is not available — pass options.fetch on Node < 18');
}
/**
* Verify a single transaction. Returns `{ verified: false, error: '...' }`
* on a real verification failure (wrong amount, wrong receiver, expired
* receipt). Throws QbirrError for network/auth problems.
*/
async verify(req) {
const body = {
provider: req.provider,
ref: req.ref,
amount: req.amount,
receiver_name: req.receiverName,
};
if (req.receiverAccount)
body.receiver_account = req.receiverAccount;
const data = await this.request('POST', '/api/v1/verify', body);
return data;
}
/**
* Verify by uploading the receipt itself — PDF or an image (PNG/JPEG).
* Images with a QR code (most bank receipts have one) are decoded and
* routed through the URL-based flow automatically.
*
* const buf = await fs.readFile('receipt.pdf');
* await qb.verifyUpload({
* provider: 'cbe',
* amount: 999,
* receiverName: 'YOUR NAME',
* receiverAccount: '1000017692643',
* file: buf,
* filename: 'receipt.pdf',
* contentType: 'application/pdf',
* });
*/
async verifyUpload(opts) {
const form = new FormData();
form.append('provider', opts.provider);
form.append('amount', String(opts.amount));
form.append('receiver_name', opts.receiverName);
if (opts.receiverAccount)
form.append('receiver_account', opts.receiverAccount);
const blob = opts.file instanceof Blob
? opts.file
: new Blob([opts.file], { type: opts.contentType || 'application/octet-stream' });
form.append('file', blob, opts.filename || 'upload.pdf');
return this.requestForm('POST', '/api/v1/verify/upload', form);
}
/** Returns the current month's quota usage for this API key. */
async usage() {
return this.request('GET', '/api/v1/usage');
}
async requestForm(method, path, form) {
const url = `${this.baseUrl}${path}`;
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
try {
const resp = await this.fetchFn(url, {
method,
headers: { 'X-API-Key': this.apiKey, 'User-Agent': '@qbirr/sdk-node/1.0.0' },
body: form,
signal: ctrl.signal,
});
const text = await resp.text();
let parsed;
try {
parsed = text ? JSON.parse(text) : {};
}
catch {
throw new QbirrError(`Non-JSON response from qbirr (HTTP ${resp.status})`, resp.status, text);
}
if (!resp.ok)
throw new QbirrError(parsed?.error || `HTTP ${resp.status}`, resp.status, parsed);
return parsed;
}
catch (e) {
if (e instanceof QbirrError)
throw e;
if (e?.name === 'AbortError')
throw new QbirrError(`Upload timed out after ${this.timeoutMs}ms`);
throw new QbirrError(`Network error: ${e.message}`);
}
finally {
clearTimeout(timer);
}
}
async request(method, path, body) {
const url = `${this.baseUrl}${path}`;
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
try {
const resp = await this.fetchFn(url, {
method,
headers: {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json',
Accept: 'application/json',
'User-Agent': '@qbirr/sdk-node/1.0.0',
},
body: body ? JSON.stringify(body) : undefined,
signal: ctrl.signal,
});
let parsed;
const text = await resp.text();
try {
parsed = text ? JSON.parse(text) : {};
}
catch {
throw new QbirrError(`Non-JSON response from qbirr (HTTP ${resp.status})`, resp.status, text);
}
if (!resp.ok) {
const msg = parsed?.error || `HTTP ${resp.status}`;
throw new QbirrError(msg, resp.status, parsed);
}
return parsed;
}
catch (e) {
if (e instanceof QbirrError)
throw e;
if (e?.name === 'AbortError')
throw new QbirrError(`Request timed out after ${this.timeoutMs}ms`);
throw new QbirrError(`Network error: ${e.message}`);
}
finally {
clearTimeout(timer);
}
}
}
exports.Qbirr = Qbirr;
exports.default = Qbirr;