From selected opportunity to buildable plan — one document for the commercial founder, one for the technical founder, same product, same delivery commitments.
The selected idea, restated as a product: a Malaysian SME distributor sends 30–90 day credit to its business customers. Chasing late payment today means someone manually checking a spreadsheet, texting a buyer, then matching the bank statement by eye days later. This product automates that loop end to end, in Malaysian Ringgit, over WhatsApp, with a DuitNow QR code attached to every chase.
Late B2B payment is routine in Malaysia and the tools built to fix it (Chaser, Gaviti, Kolleno, TreviPay) are real, funded, and well-built — for a USD-billing enterprise with a live bank-API feed. None of them speak DuitNow, reconcile a Maybank or CIMB statement, or price for a RM2–20m/yr distributor. The gap is not "AR software doesn't exist," it's "AR software doesn't exist for this buyer."
SME distributors and wholesalers in Malaysia extending 30–90 day terms to other businesses, roughly RM2–20m/yr revenue. The economic buyer is the finance manager or, at this size, the owner directly.
Get paid faster without hiring a collections clerk: automatic ageing visibility, reminders that already carry a scannable DuitNow QR code, and a reconciliation view that tells you which invoices are actually settled — without replacing the accounting software already in use.
| Role | Can | Cannot |
|---|---|---|
| Owner | Everything, plus billing and user management | — |
| Finance staff | Manage customers, invoices, reminders, reconciliation | Billing, inviting/removing users |
A finance manager can load their open invoices (CSV or manual entry), see them ranked by age, trigger a WhatsApp/email reminder carrying a DuitNow QR code, and upload a bank statement to mark invoices paid. That alone is sellable — it replaces the spreadsheet-plus-manual-WhatsApp workflow on day one.
| Priority | Feature |
|---|---|
| P0 — launch | Invoice CSV import + manual entry; ageing dashboard; scheduled WhatsApp/email reminders with embedded DuitNow QR; manual bank-statement CSV upload and matching; single-currency MYR |
| P1 — after first customers | Accounting-software export/import (SQL Account, AutoCount); reminder template customisation; partial-payment handling in the UI (not just the data model); basic reporting export |
| P2 — later | Live bank-feed integration (if/when a viable MY open-banking API exists); multi-currency; dispute/credit-note workflow; multi-entity accounts for distributors with several registered companies |
Nothing here is added because a competitor has it — each P0 item is load-bearing for the MSP's single promise: see what's owed, chase it, confirm it landed.
A web tool that chases unpaid invoices for you and tells you the moment they're actually paid. Load your open invoices, it ranks them by how overdue they are, sends a WhatsApp or email reminder with a DuitNow QR code attached so the buyer can pay in two taps, and when you upload your bank statement it matches payments back to invoices automatically.
A distributor or wholesaler, roughly RM2m–RM20m a year in revenue, who gives their own customers 30 to 90 days to pay and currently chases that money by spreadsheet and manual WhatsApp messages. The person who signs is the finance manager, or the owner directly in a smaller shop.
Expect to sit with the first few pilot customers while they import their first invoice batch — the CSV template won't match their export format perfectly on the first try.
6–8 weeks to an MVP a pilot customer can actually use, assuming the DuitNow QR question above resolves quickly and the first 2–3 real bank statement samples are in hand before week 5 (statement parsing is the one part of this build that can't be estimated from a spec alone — see the technical roadmap).
Six flows, drawn to the actual product above — not a generic SaaS template.
flowchart LR A[Hears about us via
distributor assoc. or
accounting-firm referral] --> B[Books a demo with
commercial founder] B --> C{Signs up for
trial?} C -- no --> Z[Stays manual —
follow up in 30 days] C -- yes --> D[Onboards: imports
invoices + sets reminders] D --> E[Sends first reminder batch] E --> F{Gets paid faster
in first 30 days?} F -- no --> G[Churns at trial end] F -- yes --> H[Converts to paid plan] H --> I[Monthly: uploads bank
statement, reviews ageing] I --> J[Renews month over month]
flowchart TD A[Create account] --> B[Enter company + buyer
customer list] B --> C{Has invoice
export file?} C -- yes --> D[Upload CSV] C -- no --> E[Enter invoices manually] D --> F{Import
validates?} F -- no --> G[Show row-level errors,
fix and re-upload] G --> F F -- yes --> H[Set reminder schedule
+ WhatsApp/email] E --> H H --> I[Review ageing dashboard] I --> J[Send first reminder batch]
flowchart TD A["Invoice created
(import or manual)"] --> B[Scheduler computes
age bucket daily] B --> C{Due date
approaching or
passed?} C -- no --> B C -- yes --> D[Generate reminder
+ DuitNow QR] D --> E[Send via WhatsApp/email] E --> F[Log sent reminder] F --> G{Bank statement
uploaded?} G -- no --> B G -- yes --> H[Parse transactions] H --> I{Transaction matches
an open invoice?} I -- yes, confident --> J[Auto-mark invoice
paid/partially paid] I -- low confidence --> K[Queue for manual match] J --> L[Invoice leaves
ageing dashboard] K --> M[Finance staff
confirms match] M --> L
flowchart LR
subgraph Owner
O1[Manage billing]
O2[Invite/remove users]
O3[All finance-staff actions]
end
subgraph FinanceStaff[Finance staff]
F1[Manage customers]
F2[Manage invoices]
F3[Send/schedule reminders]
F4[Upload statements,
confirm matches]
end
Owner --> FinanceStaff
flowchart TD A[Trial signup] --> B[14-day trial,
full P0 feature set] B --> C{Converts before
trial ends?} C -- no --> D[Account downgrades
to read-only] C -- yes --> E[Owner enters payment
method via gateway] E --> F[One-time setup fee
RM1,500 charged] F --> G[Monthly plan billed
RM249-499 by volume] G --> H{Payment
succeeds?} H -- no --> I[Retry + email alert,
then suspend after
grace period] H -- yes --> J[Active — next billing
cycle in 30 days]
flowchart TD A[Pilot customer
signs up] --> B[Founder manually
verifies account +
reviews first CSV import] B --> C{Import clean?} C -- no --> D[Founder fixes template
or assists customer] C -- yes --> E[Monitor reminder
delivery + bank-match
accuracy for 2 weeks] E --> F{Any failed
WhatsApp sends or
mismatches?} F -- yes --> G[Manual fix +
log as a product bug] F -- no --> H[Hand off to
self-serve]
A modular monolith on managed infrastructure — nothing here justifies microservices, a job queue cluster, or a dedicated data pipeline at pilot scale.
| Layer | Choice | Why |
|---|---|---|
| Frontend + backend | Next.js (App Router), deployed to Vercel | One codebase, one deploy target, matches the house stack already used on other projects — no new tooling to learn mid-build |
| Database | Postgres via Neon or Supabase | Relational fits this domain exactly (invoices, payments, matches are all foreign-keyed records); Supabase additionally gives auth and storage for free if chosen |
| ORM | Drizzle | Explicit schema, cheap to reason about tenant-scoping by hand in every query |
| Auth | Supabase Auth or Clerk | Don't hand-roll auth for a product that stores other companies' financial data |
| File storage | Vercel Blob or Supabase Storage | CSV imports and uploaded bank statements are small files, no need for a dedicated object-storage setup |
| Background jobs | Vercel Cron for the daily ageing/reminder scheduler; a simple queue table for reminder sends | Reminder sending and nightly ageing recompute are the only async work — a full queue product (SQS, BullMQ+Redis) is overkill at this volume |
| WhatsApp delivery | WhatsApp Business Platform API at paid-customer scale; a manual "copy to WhatsApp" wa.me link for the free pilot phase | Avoids paying for API access and Meta Business verification before a single customer has paid |
| Payments (our own billing) | CHIP or Stripe | Per-transaction pricing, no large upfront integration cost |
| Hosting/deploy | Vercel | Zero-ops deploy, generous free tier through the pilot phase |
flowchart LR
subgraph Client
Browser
end
subgraph Vercel
NextApp["Next.js app
(UI + API routes)"]
Cron["Vercel Cron
(daily ageing + reminder scheduler)"]
end
subgraph DataLayer[Data layer]
PG[(Postgres
Neon/Supabase)]
Blob[(File storage
CSV + statements)]
end
subgraph External[External services]
WA[WhatsApp Business
Platform API]
Email[Email provider
e.g. Resend]
Pay[Billing gateway
CHIP/Stripe]
QR[DuitNow QR issuer
— TBD, see B.10]
end
Browser -->|HTTPS| NextApp
NextApp --> PG
NextApp --> Blob
Cron --> PG
Cron --> WA
Cron --> Email
NextApp --> Pay
NextApp --> QR
account_id column; every query is scoped to the authenticated user's account_id — enforce this at the database layer with row-level security, not only in application code.Multi-tenant by design: every distributor is an account, and every other table hangs off it.
erDiagram
ACCOUNTS ||--o{ USERS : has
ACCOUNTS ||--o{ BUYER_CUSTOMERS : has
ACCOUNTS ||--o{ INVOICES : has
ACCOUNTS ||--o{ BANK_IMPORTS : has
ACCOUNTS ||--|| SUBSCRIPTIONS : has
BUYER_CUSTOMERS ||--o{ INVOICES : owes
INVOICES ||--o{ REMINDERS : triggers
INVOICES ||--o{ PAYMENTS : settled_by
BANK_IMPORTS ||--o{ BANK_TRANSACTIONS : contains
BANK_TRANSACTIONS ||--o| PAYMENTS : matched_to
ACCOUNTS {
uuid id PK
text company_name
text registration_no
text status
timestamp created_at
}
USERS {
uuid id PK
uuid account_id FK
text email
text role
timestamp created_at
}
BUYER_CUSTOMERS {
uuid id PK
uuid account_id FK
text name
text phone
int credit_terms_days
}
INVOICES {
uuid id PK
uuid account_id FK
uuid buyer_customer_id FK
text invoice_number
numeric amount
date issue_date
date due_date
text status
}
REMINDERS {
uuid id PK
uuid invoice_id FK
text channel
timestamp scheduled_at
timestamp sent_at
text status
}
BANK_IMPORTS {
uuid id PK
uuid account_id FK
text bank_name
text status
timestamp created_at
}
BANK_TRANSACTIONS {
uuid id PK
uuid import_id FK
date txn_date
numeric amount
text match_status
}
PAYMENTS {
uuid id PK
uuid invoice_id FK
uuid bank_transaction_id FK
numeric amount
date paid_at
}
SUBSCRIPTIONS {
uuid id PK
uuid account_id FK
text plan
text status
date current_period_end
}
account_id foreign key present on every top-level table — a buyer customer, invoice, or bank import that doesn't trace back to the requesting user's account is invisible by construction.UNIQUE(account_id, invoice_number) — invoice numbers are only unique within a tenant.CHECK (amount > 0) on invoices, payments, bank_transactions.CHECK (due_date >= issue_date) on invoices.buyer_customers.account_id must equal invoices.account_id for any linked row — enforce via a composite foreign key or an application-layer check on write.account_id = current_setting('app.current_account_id').| Endpoint | Auth / role | Purpose |
|---|---|---|
POST /api/auth/login | Public | Session-based login |
GET /api/account | Any role | Current tenant profile |
POST /api/users/invite | Owner only | Invite a finance-staff user |
GET/POST /api/customers | Owner, finance staff | List / create buyer customers |
GET/POST /api/invoices | Owner, finance staff | List / create invoices |
POST /api/invoices/import | Owner, finance staff | CSV import, returns row-level validation errors |
POST /api/invoices/:id/reminders/send-now | Owner, finance staff | Force-send a reminder outside schedule |
POST /api/bank-imports | Owner, finance staff | Upload a bank statement file |
POST /api/bank-imports/:id/transactions/:txnId/match | Owner, finance staff | Confirm or correct a suggested match |
GET /api/billing | Owner only | Current plan and invoice history |
POST /api/webhooks/payment-provider | Signed webhook, no user session | Billing provider payment events |
Request/response bodies follow the entities above directly (e.g. POST /api/invoices takes {buyer_customer_id, invoice_number, amount, issue_date, due_date} and returns the created invoice with its computed status) — no separate schema needed beyond the table definitions until implementation surfaces an exception.
| Route | Purpose |
|---|---|
/login | Session entry |
/onboarding | Company setup → import invoices → reminder schedule |
/dashboard | Ageing summary: total outstanding, overdue count, a simple bar by bucket |
/customers, /customers/:id | Buyer customer list and their invoice history |
/invoices, /invoices/:id | Filterable list (all / due soon / overdue / paid); detail shows reminder log, payment log, QR preview |
/invoices/import | CSV upload wizard with row-level error display |
/reconciliation | Upload statement, review unmatched transactions, confirm suggested matches |
/settings | Reminder schedule/templates, WhatsApp number, users |
/billing | Plan, payment method, invoices from us |
Primarily a desktop tool for an office finance workflow — optimise the dashboard, invoice table, and reconciliation screen for desktop first. Owner-level users will check the ageing dashboard from a phone occasionally, so that one screen needs a usable mobile layout (card list instead of a wide table); the CSV import and reconciliation screens can be desktop-only at MVP.
Minimal, professional, numbers-forward — this is a finance tool a skeptical accountant needs to trust at a glance. Tabular figures, a single calm accent color, and status conveyed by a small colored indicator (current/due soon/overdue) rather than heavy color-blocking the whole row.
Sized for one developer. The goal is a pilot customer live by week 6–8, not a finished platform.
| Phase | Objective | Key deliverables | Done when |
|---|---|---|---|
| 0 Week 1 |
Foundations | Auth, tenant (account) model, RLS policies, customer + invoice CRUD, empty dashboard | A test account can log in and manually create a customer and an invoice, scoped correctly to itself |
| 1 Weeks 2–3 |
Get invoices in and visible | CSV import with row-level validation, ageing dashboard with real buckets | A sample 50-row invoice CSV from a real distributor imports cleanly and the dashboard buckets it correctly |
| 2 Weeks 3–4 |
Automate the chase | Reminder scheduler, WhatsApp manual-tap link (or API if the Meta verification lands in time), email channel, DuitNow QR on every reminder (pending B.10) | A scheduled reminder fires at the right age threshold and the QR code resolves to a real, scannable payment |
| 3 Weeks 5–6 |
Close the loop | Bank statement CSV upload, amount/date matching logic, manual-match UI for low-confidence cases | A real bank statement from a pilot customer's bank matches at least 80% of its transactions automatically |
| 4 Weeks 7–8 |
Launch-ready | Billing/checkout (setup fee + subscription), settings screen, onboarding polish from phase-0 pilot feedback | First 1–2 paying pilot customers are live and have been billed successfully |
| P1 Post-pilot |
Scale what pilots proved | Accounting-software export/import, live bank feed if a viable API exists, reminder template customisation | Decided from real pilot usage data, not speculation |
| Stage | Technical founder | Commercial founder |
|---|---|---|
| Discovery | Confirm the DuitNow QR issuance question technically | Get 3 distributor finance managers to confirm the pain and the RM249–499/mo price point in conversation, before a line of code is load-bearing on it |
| Planning | Lock the CSV import schema against a real sample export, not an assumed one | Line up 2–3 pilot customers willing to hand over a real (anonymised, if needed) bank statement sample for Phase 3 |
| Development | Build Phases 0–4 in sequence; flag immediately if bank-statement parsing is taking materially longer than scoped | Keep the 2–3 pilot relationships warm; start the next 3 prospects in parallel so Phase 4 isn't launching into an empty pipeline |
| Testing | Fix issues surfaced by real pilot CSV/statement imports — these will not match the test fixtures exactly | Sit with the first pilot through their first import and first reminder send; collect what confused them, verbatim |
| Launch | Deploy, monitor reminder-delivery and bank-match failure rates | Convert the pilot(s) to paying, and start the next five-customer push through the same referral channels |