AIRA — WhatsApp AI Agent
The practical guide to AIRA, the ASPLUS WhatsApp AI Agent: what it is, how an owner uses it, and how to test it. Companion build plan:
docs/ai-agent-v1-plan.md. Sister channel (same capabilities over ChatGPT/Claude):docs/ai-connect-guide.md.Status: LIVE on production, Pro / Lifetime tier only (gated per company — see §A6). Persona is fixed as AIRA. Onboarding is the shared ASPLUS number + OTP model (Model A). Feature flag
AI_AGENT_ENABLED; the inbound webhook is never gated by the flag.
1. What it is (in one paragraph)
AIRA lets a company owner run their accounting by messaging one shared official ASPLUS WhatsApp number. The owner writes in Bahasa Melayu or English; AIRA answers questions (sales, invoices, outstanding, reports…) and can record entries (money in/out, invoice/bill payment, draft invoice/quotation). It also reads a receipt photo and drafts the expense, transcribes a voice note, and can send a report PDF back to the chat. Every action that changes the books is confirmed by the owner first. AIRA reuses the existing ASPLUS accounting logic — it never posts its own numbers.
Scope: owner-only; identity-based tenancy (the sender's verified number picks the company); text + voice + receipt-image; read + confirmed writes. Not included: customer/supplier chatbots, broadcast/automation, auto-posting without confirmation.
PART A — User Guide (for the business owner)
A1. One-time setup (verify your number)
- In ASPLUS, open AI (top navbar) → AIRA.
- Verify your WhatsApp number. Enter the personal WhatsApp number you'll message from → AIRA sends a one-time code to that number → enter it back in ASPLUS. This proves you control the number; only verified numbers can reach your business data. (There is no "connect your own WhatsApp API" step — you use the shared ASPLUS number.)
- Once verified, tap Chat with AIRA (opens WhatsApp to the official number) and say hello. That's it.
Multi-company: verify the same number and AIRA routes to whichever company the number is authorized for. If a number is authorized for more than one company, AIRA asks which company before acting on writes.
A2. What you can ask (examples)
Message the AIRA number like you'd text a staff member:
| You say | AIRA does |
|---|---|
| "Berapa jualan bulan ni?" | Sales summary for this month |
| "Untung rugi bulan lepas" | Profit & loss last month |
| "Invois mana belum bayar?" | Outstanding invoices + total |
| "Status invoice INV00012" | That invoice's status + amounts |
| "Cari customer Ahmad" | Customer search |
| "Baki bank sekarang" | Cash & bank balance |
| "Balance sheet" / "Trial balance" | Statement summaries |
| "Hantar P&L bulan ni PDF" | Sends a report PDF to the chat |
| (send a receipt photo) | Reads it, drafts a Money-Out → asks you to confirm |
| (send a voice note) | Transcribes it, then acts on the request |
| "Rekod duit masuk RM500 jualan tunai" | Proposes a Money-In → confirm |
| "Bayar invoice INV00012" | Proposes a payment → confirm |
| "Buat invoice untuk Ahmad, 2 x Widget" | Proposes a draft invoice → confirm |
A3. How confirmation keeps you safe
Anything that creates or changes a record never happens silently. AIRA replies with a summary and two buttons — Confirm ✅ / Cancel ❌ (or reply ya / tidak). Nothing is saved until you confirm, and confirming twice never duplicates. This cannot be switched off.
A4. Prices are never invented
For an invoice/quotation the price comes from your product list (the Item's saved price) and SST from your tax settings. If AIRA can't match a product or customer, it asks you instead of guessing.
A5. AI Credits (per company, channel-weighted)
AIRA runs over WhatsApp, which carries a Meta per-message cost the in-app tools don't, so AIRA turns are weighted:
- Text / voice message → 2 AI Credits (per answered turn).
- Receipt / bill photo → 3 AI Credits (OCR + draft).
Tapping Confirm/Cancel and failed/unreadable replies are free. Credits come from that company's own wallet and are not shared between your companies — each has its own balance, monthly allocation (by its own plan) and top-ups. If a company runs out, AIRA tells you to top up. (The web AI Assistant and web OCR stay at 1 credit; AI Connect reads are free — those channels have no Meta cost.)
Config:
ai_agent.credits_per_message(envAI_AGENT_CREDITS_PER_MESSAGE, =2) andai_agent.credits_per_receipt(envAI_AGENT_CREDITS_PER_RECEIPT, =3).
A6. Who can use it (Pro, per company)
AIRA is available on Pro / Lifetime plans, checked against the active company's own subscription. A company on a free/expired plan is declined even if another company in the same account is Pro — entitlement follows the company, not the account.
A7. Good to know (current limits)
- Invoices/quotations are created as drafts — you issue them in ASPLUS (that's when e-Invoice/MyInvois submission happens).
- Owner-only. Anyone texting the number who isn't a verified owner gets a polite "not authorized" reply and sees no data.
- Voice notes and receipt/bill photos are processed; other file types get a safe decline. Large media is rejected (audio ≈ 1 MB, image ≈ 5 MB caps).
- A per-sender rate limit (default 20/min) protects the number and your credits from a runaway sender.
PART B — Testing Guide (QA / internal)
B1. Environment & flags
- Feature flag
AI_AGENT_ENABLED(reveal to users). Shared-number modeAI_AGENT_SHARED_NUMBER=true(owners OTP-verify against the shared channel provisioned byphp artisan ai-agent:provision-shared). Safety beltAI_AGENT_LIVE_MODE(false = no real WhatsApp send; the LLM still runs). - Webhook:
AI_AGENT_WA_VERIFY_TOKEN,AI_AGENT_WA_APP_SECRET(AI_AGENT_WA_VERIFY_SIGNATURE=truein prod). OTP template:AI_AGENT_OTP_TEMPLATE,AI_AGENT_OTP_TEMPLATE_LANG(e.g.en_US),AI_AGENT_OTP_TEMPLATE_APP(the{{2}}value, e.g.AIRA, when the approved template has a second body variable — else Meta rejects with #132000). - Model:
AI_AGENT_OPENAI_API_KEY(orOPENAI_API_KEY),AI_AGENT_MODEL(defaultgpt-4o-mini). Receipt OCR needs the OCR/GCP config. - Queue: inbound dispatches to the
ai-agentqueue (prod = database driver + RunCloud worker). Sync driver runs inline.
B2. Prepare a test tenant
Use a Pro staging company with products, a customer, some invoices and a
supplier bill. Verify an owner number (OTP) against the shared channel. To
exercise the brain in dry-run, POST a signed sample Meta payload to
/webhooks/ai-agent/whatsapp with the shared channel's phone_number_id and an
authorized from; inspect ai_agent_messages, ai_agent_pending_actions,
ai_agent_usage_logs, ai_credit_transactions, and storage/logs/laravel.log.
B3. Read scenarios (tenant-scoped, must tie out to the reports)
- [ ] Sales / expense / P&L for a period → matches the Reporting screens.
- [ ] Outstanding invoices + total AR → matches the AR list.
- [ ]
get_balance_sheet(Assets = Liabilities + Equity),get_trial_balance(Debit = Credit),get_cash_flow→ tie out. - [ ] find invoice / bill / contact / transaction → rows for this company only.
- [ ] "hantar … PDF" → a report PDF is delivered to the chat.
B4. Write + confirmation scenarios
- [ ] "Record money in RM500 …" → confirmation + a
pendingrow; ya →Transaction+ balancedJournalEntry; ya again → no duplicate. - [ ] "Bayar invoice X" → confirm →
Payment; invoice flips Paid when settled. - [ ] "Buat invoice untuk [customer]: 2 x [product]" → draft preview (prices from master, SST line, total) → confirm → draft, no JE.
- [ ] Quotation → status new, no tax, correct total.
- [ ] "Bayar bill [number]" → confirm →
PurchasePayment+ JE; bill flips Paid. - [ ] Send a receipt photo → drafted Money-Out preview → confirm → recorded; the scan debits 1 credit (refunded if discarded).
- [ ] Reply tidak / Cancel → nothing saved.
B5. Guardrails (must all fail safely)
- [ ] Unauthorized sender → safe "not authorized" reply, no data.
- [ ] Non-Pro / expired company → upgrade nudge; nothing runs, nothing charged.
- [ ] Unknown product / customer → AIRA asks; never invents a price.
- [ ] Overpay or already-paid bill/invoice → refused.
- [ ] Capability off in Settings → those tools disappear (e.g. Payments off →
record_bill_paymentblocked). - [ ] Out of credits (this company) → asks to top up; failed turns not charged.
- [ ] Rate limit exceeded → brief "slow down" reply; not run, not charged.
B6. Run the automated suite (locally / CI)
composer install --ignore-platform-req=ext-gd
php artisan test tests/Unit/AiAgent tests/Feature/AiAgent
Covers the parser, tenant isolation, unauthorized sender, confirmation idempotency, webhook HMAC (fail-closed), tool registry + permission filtering, orchestrator loop, permission enforcement, SST maths, sales-document creation, overpayment/already-paid, multi-company routing and the per-company credit wallet.
B7. Troubleshooting
| Symptom | Likely cause / check |
|---|---|
| No reply | ai-agent queue worker not running; or LIVE_MODE=false (dry-run — check logs) |
| "not authorized" for the owner | Number not verified (OTP), or format mismatch |
| OTP never arrives | Template name/lang wrong (#132001) or {{2}} param missing (#132000 → set AI_AGENT_OTP_TEMPLATE_APP=AIRA); cold number needs an Authentication template |
| Webhook 403 | AI_AGENT_WA_APP_SECRET missing/wrong (fails closed) |
| Upgrade nudge for a paid account | Active company is the free/expired one — entitlement is per-company |
| Balance shows 0 unexpectedly | Per-company wallet — that company's own balance (siblings don't share) |
PART C — Reference
C1. Flow
WhatsApp inbound → /webhooks/ai-agent/whatsapp (HMAC verify, 200 fast)
→ ProcessAgentInboundJob (dedupe by wamid)
→ resolve shared channel → authorize sender (verified number) → pin company
→ unknown number: drop · unknown sender: safe reply · owner: continue
→ entitlement gate (active company's own Pro sub) → rate limit → credit pre-check
→ media: voice → transcribe · image → receipt OCR (drafts a Money-Out)
→ AgentOrchestrator (LLM + tools)
read tool → answer (or report PDF)
write tool → pending action + Confirm/Cancel → (on Confirm) execute once
→ reply + persist + debit 1 credit (this company) + usage log
C2. Tools (22)
Read (16): get_sales_summary, get_expense_summary, get_profit_and_loss, get_cash_bank_balance, get_outstanding_invoices, find_invoice, get_invoice_status, find_contact, find_transaction, get_balance_sheet, get_trial_balance, get_cash_flow, get_outstanding_bills, find_bill, get_bill_status, send_report_pdf. Write (6, all confirmed): create_money_in, create_money_out, record_invoice_payment, create_invoice (draft), create_quotation, record_bill_payment.
C3. Permission groups (Settings)
view_business_data · reports · create_transactions · sales_documents ·
payments. Effective permission = what the owner is granted ∩ what the company
enabled in Settings.
C4. Data model
whatsapp_channels (shared channel), ai_agent_authorized_users (verified
numbers + OTP state), ai_agent_conversations, ai_agent_messages (wamid-deduped),
ai_agent_pending_actions (confirm state machine, idempotency key),
ai_agent_usage_logs (per-company telemetry), ai_agent_settings. Credits flow
through ai_credit_transactions (per-company wallet on companies.ai_credits).
C5. Config / env
config/ai_agent.php — feature flag, Meta base/version, App-level webhook verify
token + secret, live_mode, shared_number, OTP template settings, owner_limit
(+ device add-on), per-minute rate limit, model provider/name/key, fixed persona
= AIRA, official_number, history + tool-iteration limits, entitled_tiers
(pro/lifetime), credits_per_message, media caps (voice/image), and the tool
registry.
C6. Security properties
Identity-based tenancy via AgentTenantContext (explicit team/company scoping — no
web session, no Auth::login); the company is pinned server-side from the verified
sender, never chosen by the model. X-Hub-Signature-256 HMAC verify (fail-closed);
wamid dedupe + confirm-once idempotency; unknown-sender safe reply; mandatory
confirmation for all writes; per-company entitlement + credit gates; encrypted
channel credentials.
C7. Known limits / deferred
- Invoices/quotations are drafts — issuing (JE + e-Invoice/MyInvois) stays a web action.
- One authorized role (
owner); staff/supplier/shareholder + step-up PIN are schema-ready but not enabled. - Admin monitor at
/admin/ai-agent(read-only usage feed). - The same accounting capabilities are also reachable over MCP from ChatGPT/Claude
— see
docs/ai-connect-guide.md.