Bruce Nduko
All projects
LivePaymentsWeb

Tija Labs Billing Engine

Full subscription invoicing and billing system.

Problem

Tija Labs needed to bill subscriptions and issue invoices without relying on a third-party SaaS billing platform.

What I built

A complete subscription invoicing and billing system: Node.js/Express backend, SQLite storage, Pesapal API v3 for payments, PDF invoice generation with pdfkit, and email delivery via nodemailer. I evaluated payment gateway availability in Kenya and chose Pesapal.

Stack

Node.jsExpressSQLitePesapal API v3pdfkitnodemailer

The kind of code behind it

1import { Router } from "express";
2import PDFDocument from "pdfkit";
3import { createPesapalOrder } from "./pesapal";
4import { sendInvoiceEmail } from "./mailer";
5import { saveInvoice } from "./db";
6
7const router = Router();
8
9router.post("/invoices", async (req, res) => {
10 const { customerId, amount, currency, email } = req.body;
11
12 const order = await createPesapalOrder({
13 amount,
14 currency,
15 description: `Subscription invoice for ${customerId}`,
16 });
17
18 const doc = new PDFDocument({ margin: 50 });
19 const chunks: Buffer[] = [];
20 doc.on("data", (chunk) => chunks.push(chunk));
21
22 doc.fontSize(20).text("Tija Labs", { align: "left" });
23 doc.moveDown();
24 doc.fontSize(11).text(`Invoice for ${customerId}`);
25 doc.text(`Amount: ${currency} ${amount.toFixed(2)}`);
26 doc.text(`Reference: ${order.trackingId}`);
27 doc.end();
28
29 const pdf = await new Promise<Buffer>((resolve) => {
30 doc.on("end", () => resolve(Buffer.concat(chunks)));
31 });
32
33 const invoice = await saveInvoice({
34 customerId,
35 amount,
36 trackingId: order.trackingId,
37 status: "pending",
38 });
39
40 await sendInvoiceEmail({ to: email, pdf, invoiceId: invoice.id });
41
42 res.json({ invoiceId: invoice.id, redirectUrl: order.redirectUrl });
43});
44
45export default router;