Claims & Expense Reimbursement (CLM)
Module code CLM · Release R2 (mileage from FFM in R3) · Packaging Plan tier (Professional+) · Requirements
FR-CLM-*,BR-CLM-*,RULE-CLM-*in the PRD and BRD · Related Payroll, Field Force, claim-to-reimburse
1. Overview and scope
CLM lets employees claim business expenses and get paid, under a policy that finance controls. One pipeline handles every reimbursement: bills, per-diem, and mileage generated from field travel.
In scope: expense categories; an Expense Policy (limits per category, per grade, per city tier, per day, per month; receipt thresholds; per-diem and mileage rates); multi-bill claims with attachments; GST fields; duplicate-bill detection; line-level verification with verified amounts; finance approval; payout via payroll (non-taxable reimbursement component) or a separate bank file; advances against claims and their settlement; a claims ledger; finance dashboard.
Out of scope: corporate card feeds, OCR (later add-on), travel booking.
2. Learned from the reference product
Source: Spryple analysis, §9, §16 and §21.6.
- [Observed] A claim is one or more bills (reimbursement type, bill number, bill date, amount, file) plus a description; the bill image is required with a 2 MB limit; IDs look like
CL_GEN_<company>_<n>(A/M/E: Claims; §21.6). - [Observed] Two-stage control: the manager verifies (and can reject individual bills and enter a verified amount), then finance approves and marks Paid (A: Claims).
- [Observed] Finance dashboard shows counts by status, a settlement ratio, a previous vs current scorecard and paid amount by type (A: Claims).
- [Observed] CRM travel records follow a separate path: Pending → Verified → Approved → Claimed, with an editable "KM Revised" (M/E: CRM; §21.7).
- [Observed] Status names differ by role (Submitted/In Progress vs Pending/Verified) (§9).
- [Inferred] There are no policy limits, no duplicate detection and no payroll payout; claims are "marked Paid" manually (§9; §16 gap CLM → PAY).
3. Our design decisions
| ID | Decision [Proposed] | Reason |
|---|---|---|
| D-CLM-01 | One status model for every role: Draft, Submitted, Verified, Approved, Scheduled, Paid, Rejected, SentBack. | Removes role-specific vocabularies. |
| D-CLM-02 | An effective-dated Expense Policy assigned by applicability (grade, location, department). Limits are evaluated per line at submit and shown live. | Finance control before money moves. |
| D-CLM-03 | Mileage claims are generated from FFM daily travel into the same pipeline; the travel approval path is merged. | One reimbursement path. |
| D-CLM-04 | Payout modes: PAYROLL (approved lines become REIMBURSEMENT pay inputs) or BANK_FILE (weekly payout batch). | Most SMEs pay with salary; field-heavy ones pay weekly. |
| D-CLM-05 | Duplicate detection by a hash of vendor GSTIN or name, bill number, date and amount across the tenant, plus perceptual hash of the image. | A common fraud pattern. |
| D-CLM-06 | Advances and claims share a ledger per employee; approved claims settle open advances first. | Correct balances. |
4. Domain model
clm_category.kind: BILL, PER_DIEM, MILEAGE. clm_claim.source: MANUAL, FFM_TRAVEL. clm_line.policy_result: OK, OVER_LIMIT_CAPPED, OVER_LIMIT_FLAGGED, RECEIPT_MISSING, DUPLICATE_SUSPECT.
5. State machines
6. Key business logic
6.1 Claim limits
checkLine(line, v, employee): # v = expense policy version for employee on expense_date
rule = v.categories[line.category] for employee's grade and city tier of the expense location
if line.category.kind == MILEAGE: line.amount = line.quantity_km * rule.rate_per_km[vehicle]
if line.category.kind == PER_DIEM: line.amount = days * rule.per_diem[city_tier]
if rule.receipt_required_above is set and line.amount > rule.receipt_required_above
and no attachment: result RECEIPT_MISSING (blocks submit)
caps = [rule.per_bill, rule.per_day - spent(employee, category, expense_date),
rule.per_month - spent(employee, category, month), rule.per_claim]
allowed = min(non-null caps)
if line.amount > allowed:
if v.over_limit_action == CAP: line.claimable = allowed ; result OVER_LIMIT_CAPPED
if v.over_limit_action == FLAG: line.claimable = line.amount ; result OVER_LIMIT_FLAGGED
if v.over_limit_action == BLOCK: reject the line
if expense_date < today - v.submit_within_days: block "too old"
if duplicateHash(line) exists in another non-rejected claim: result DUPLICATE_SUSPECT
# spent() counts Submitted, Verified, Approved, Scheduled and Paid lines (not Rejected)
6.2 Payout
on claim Approved:
net = approved_amount - open advance balance (settle advance first, post ledger entries)
if payout_mode == PAYROLL: create PAY input REIMBURSEMENT for the next open period ; status Scheduled
if payout_mode == BANK_FILE: add to the open payout batch ; status Scheduled
on pay.run.locked containing the input, or batch bank file confirmed: status Paid ; emit clm.claim.paid
7. Configuration
| Setting | Type | Default | Description |
|---|---|---|---|
categories | array of object | Travel, Food, Lodging, Fuel, Mileage, Phone, Misc | Category rules by grade and city tier |
categories[].per_bill | number | null | Cap per bill |
categories[].per_day | number | null | Cap per day |
categories[].per_month | number | null | Cap per month |
categories[].receipt_required_above | number | 0 | Amount above which a bill image is needed |
mileage.rate_per_km | object | two-wheeler 4, four-wheeler 10 | Example rates in rupees, set by the tenant |
per_diem | object | by city tier | Daily allowance |
over_limit_action | enum | FLAG | CAP, FLAG, BLOCK |
submit_within_days | integer | 60 | Age limit for expenses |
payout_mode | enum | PAYROLL | PAYROLL or BANK_FILE |
attachment.max_bytes | integer | 5242880 | 5 MB (reference uses 2 MB) |
duplicate_check | boolean | true | Duplicate detection |
verification_required | boolean | true | Manager verification step |
8. Screens by persona
| Persona | Web | Mobile |
|---|---|---|
| Employee | New claim (lines, live policy result), drafts, status tabs, advance request, ledger | Capture bill photo, offline draft, submit, status |
| Line manager | Verification queue with per-line verify, edit amount and reject, history | Verify and reject lines |
| Payroll manager / finance | Approval queue, payout batches, advances, finance dashboard (settlement ratio, scorecard, paid by type) | Approvals |
| Finance admin | Categories, expense policy editor, city tiers | — |
9. Events
| Event | Consumers |
|---|---|
clm.claim.submitted | NTF (workflow) |
clm.claim.verified | NTF |
clm.claim.approved | PAY (input when payroll mode), NTF |
clm.claim.rejected, clm.claim.sent_back | NTF |
clm.claim.paid | NTF, RPT |
clm.advance.approved | PAY or payout batch |
clm.duplicate.suspected | NTF (finance) |
Consumed: ffm.travel.verified (generate a mileage claim), pay.run.locked and pay.run.reversed (payout status), ext.fnf.initiated (settle open claims and advances in F&F).
10. API surface
| Resource | Main operations |
|---|---|
/v1/clm/categories, /v1/clm/policies | CRUD, versions |
/v1/clm/claims | POST, GET, PATCH draft, POST /:id/submit, POST /:id/withdraw |
/v1/clm/claims/:id/lines | CRUD in Draft; POST /:lineId/verify |
/v1/clm/claims/check | POST dry policy check |
/v1/clm/advances | POST, GET |
/v1/clm/payout-batches | POST, POST /:id/bank-file, POST /:id/confirm |
/v1/clm/ledger-entries | GET |
Detailed in API endpoints.
11. Permissions
| Permission | Scopes | Default roles |
|---|---|---|
clm.claim.create | SELF | employee |
clm.claim.verify | TEAM | line_manager, sales_manager |
clm.claim.approve | ORG, LOCATION | payroll_manager |
clm.payout.manage | ORG | payroll_manager |
clm.policy.manage | ORG | finance_admin |
clm.advance.approve | ORG | payroll_manager |
clm.report.export | TEAM, ORG | line_manager, payroll_manager |
12. Reports
| Report | Contents | Reference |
|---|---|---|
| Claims register | Applied on, employee, claim no, bill count, requested, verified, approved, status, actioned by | [Observed] A: Claims |
| Finance dashboard | Status counts, settlement ratio, scorecard, paid by type | [Observed] A: Claims |
| Policy exceptions | Over-limit, missing receipts, duplicates | [Proposed] |
| Spend by category, department and month | Trend | [Proposed] |
| GST input register | Lines with vendor GSTIN and GST amount | [Proposed] |
| Advance ageing | Open advances by age | [Proposed] |
13. Release and priority
| Capability | Release | MoSCoW |
|---|---|---|
| Categories, multi-bill claims, verify, approve, payout via payroll | R2 | Must |
| Expense policy with limits and receipt thresholds | R2 | Must |
| Duplicate detection | R2 | Should |
| Bank-file payout batches | R2 | Should |
| Advances and ledger | R2 | Should |
| Mileage claims from FFM travel | R3 | Must (with FFM) |
| GST capture | R2 | Should |
| OCR of bills | R4 | Could |
14. Open questions
- Is mileage reimbursement taxable above a threshold for the tenant's employees? Treatment must be configurable per component; verify.
- Do customers need a department-head step above an amount by default? The template includes it as a condition.
- Should reimbursements paid through payroll appear on the payslip or in a separate statement? The default is a separate payslip section.