Role Founder & Lead Product Engineer
Scope Product Architecture, Engine State Machine, Webhook & Microservice Integrations, Telemetry, GTM
Stack WhatsApp Flows, Node.js, Railway, PDFShift API, Stripe Webhooks, PostHog, Hostinger
Small service contractors, tradesmen, and solo operators across Botswana, South Africa, and Zimbabwe face severe conversion loss when forced to switch from chat apps to desktop software (Word/Excel) to write estimates. Traditional accounting platforms (Xero, QuickBooks) introduce high UX friction, requiring external app downloads or website redirects.
Invoisani is a 100% WhatsApp-native financial document engine built to solve this. It processes multi-item estimates, computes taxes, renders branded A4 PDFs, tracks financial rollups, and enforces tier paywalls entirely within WhatsApp.
Targeting micro-enterprises operate primarily on mobile devices under tight bandwidth constraints. Field contractors cannot stop manual labor to boot up laptops, log into web portals, or navigate complex multi-field accounting forms.
The core document engine executes dynamic subtotaling, dynamic tax computation across variable line items, and constructs formatted HTML rows for PDFShift API rendering.
// Action #9: Centralized Item Processing & Dynamic Calculations Node let history = []; try { if (typeof quote_history === 'string' && quote_history.trim() !== '') { history = JSON.parse(quote_history); } else if (Array.isArray(quote_history)) { history = quote_history; } } catch(e) { history = []; } // Append incoming flow line item let newDesc = String(desc || '').trim(); let newQty = Number(qty) || 0; let newRate = Number(rate) || 0; if (newDesc !== '' && (newQty > 0 || newRate > 0)) { history.push({ description: newDesc, quantity: newQty, rate: newRate }); } // Compute totals across array let subtotal = 0, summaryText = "", htmlRows = ""; history.forEach((item, index) => { let lineTotal = (item.quantity || 0) * (item.rate || 0); subtotal += lineTotal; summaryText += `${index + 1}. *${item.description}*\n ${item.quantity} x $${item.rate.toFixed(2)} = *$${lineTotal.toFixed(2)}*\n\n`; htmlRows += `<tr><td style='padding:8px;border-bottom:1px solid #ddd;'>${item.description}</td><td style='padding:8px;border-bottom:1px solid #ddd;text-align:center;'>${item.quantity}</td><td style='padding:8px;border-bottom:1px solid #ddd;text-align:right;'>$${item.rate.toFixed(2)}</td><td style='padding:8px;border-bottom:1px solid #ddd;text-align:right;'>$${lineTotal.toFixed(2)}</td></tr>`; }); let taxRatePercent = Number(tax_rate) || 0; let taxAmount = (subtotal * taxRatePercent) / 100; let grandTotal = subtotal + taxAmount; // Generate read-only preview number without incrementing user database count let isInvoice = String(document_type || '').toLowerCase() === 'invoice'; let nextCount = isInvoice ? (Number(total_invoices_generated) || 0) + 1 : (Number(total_quotes_generated) || 0) + 1; let previewDocNumber = isInvoice ? `INV-${String(nextCount).padStart(4, '0')}` : `Q-${String(nextCount).padStart(4, '0')}`; return { subtotal: Number(subtotal.toFixed(2)), tax_amount: Number(taxAmount.toFixed(2)), quote_grand_total: Number(grandTotal.toFixed(2)), quote_items_summary_text: summaryText || "No items added.", quote_table_rows_html: htmlRows, updated_history: JSON.stringify(history), document_number: previewDocNumber };
When a client accepts an estimate, the user taps [Make into Invoice]. The engine executes a post-PDF workload buffer sequence: it increments `total_invoices_generated`, converts `document_number` to `INV-XXXX`, strips the active ID out of `pending_quotes_list`, and rolls the `quote_grand_total` into `estimated_total_income`.
// Invoice Count & Revenue Rollup Sequence (Executed Post-PDF Delivery) let updatedInvoices = currentInvoices + 1; let updatedIncome = currentIncome + Number(quote_grand_total); let formattedNumber = `INV-${String(updatedInvoices).padStart(4, '0')}`; // Strip converted quote ID from zero-storage pending index let pendingArray = currentPending.split(',').map(s => s.trim()).filter(Boolean); let updatedPending = pendingArray.filter(q => q !== activeQuoteNum).join(', '); return { total_invoices_generated: updatedInvoices, estimated_total_income: Number(updatedIncome.toFixed(2)), document_number: formattedNumber, pending_quotes_list: updatedPending };
Monetization is governed by a 5-quote free tier limit enforced via system gates (`IsFreeDepleted`). Upgrades and cancellations use Stripe’s Hosted Customer Portal and real-time webhook listeners to toggle user permissions automatically.
To eliminate attribution loss when directing external campaign traffic into WhatsApp, I built a zero-data-loss redirect handler hosted on Hostinger. It utilizes `sendBeacon` to ensure analytics pings complete before the browser unloads the window.
// /chatnow/index.html Redirect Pipeline var targetUrl = "https://wa.me/17637660492?text=Hi"; var redirected = false; function redirectToWhatsApp() { if (!redirected) { redirected = true; window.location.href = targetUrl; } } posthog.init('phc_##', { api_host: 'https://us.i.posthog.com', person_profiles: 'always', transport: 'sendBeacon', // Forces async beacon delivery during page unload loaded: function(ph) { ph.capture('$pageview', {}, { send_instantly: true }); setTimeout(redirectToWhatsApp, 200); } }); setTimeout(redirectToWhatsApp, 1200); // Fallback safety timer for ad-blockers
Established production analytics filtering rules to eliminate localhost/testing noise (`$host != 127.0.0.1`) and built clean multi-channel attribution tables tracking campaign conversions by `utm_source`, `utm_medium`, and `utm_campaign`.
Simply visit the Invoisani website or send "Hi" on WhatsApp.