Skip to content

Data Model Architecture

The foundation of this product. The data model must handle real-world contract complexity — tiered rates, role-based pricing, expense caps, cumulative tracking, and multi-tenant isolation.


Every entity below lives under an Organization tenant. All queries are scoped by organizationId.

Organization
├── id
├── name
├── slug (subdomain or URL identifier)
├── plan (free | starter | professional | enterprise)
├── maxUsers
├── maxVendors
├── settings (JSON — currency, thresholds, notification prefs)
├── createdAt
└── updatedAt
User
├── id
├── organizationId → Organization
├── email
├── name
├── role (admin | senior_auditor | auditor | viewer)
├── status (active | inactive)
├── lastLoginAt
├── createdAt
└── updatedAt

Vendor
├── id
├── organizationId → Organization
├── name
├── registrationNumber (tax/company registration)
├── contactPerson
├── email
├── phone
├── status (active | inactive | suspended)
├── industry (legal | consulting | audit | IT | other)
├── paymentTerms (net30 | net60 | net90 | other)
├── createdAt
└── updatedAt

A vendor can have multiple contracts (different engagements, different periods).

Contract
├── id
├── organizationId → Organization
├── vendorId → Vendor
├── contractReference (internal reference number)
├── title ("Legal Services 2026", "IT Support Q1-Q2")
├── type (fixed_fee | hourly | retainer | milestone)
├── status (draft | active | expired | terminated)
├── startDate
├── endDate
├── totalCeiling (NTE amount for entire contract)
├── monthlyCeiling (NTE per month, optional)
├── currency
├── autoRenew (boolean)
├── notes
├── createdAt
└── updatedAt

This is where most products fail. Rates are NOT simple — you need a rate card model that supports tiered rates, role-based pricing, and time-based escalations.

ContractRateCard
├── id
├── contractId → Contract
├── serviceCategory ("litigation", "advisory", "development")
├── roleLevel ("partner", "senior_associate", "junior", "paralegal")
├── approvedRate (decimal)
├── rateType (hourly | daily | fixed_per_deliverable)
├── maxHoursPerMonth (optional)
├── maxHoursTotal (optional, lifetime of contract)
├── billingIncrement (6min | 15min | 30min | 1hr)
├── effectiveFrom
├── effectiveTo (supports annual rate escalations)
└── notes
  • Different rates per seniority level
  • Different rates per service type
  • Rate changes over time (annual escalations)
  • Billing increment rules (law firms bill in 6-minute increments)
  • Hour caps per period and lifetime

Separate from service rates. Controls what expenses a vendor can claim and at what limits.

ContractExpenseRule
├── id
├── contractId → Contract
├── expenseCategory ("travel", "printing", "courier", "software")
├── allowed (boolean)
├── maxPerInstance (optional)
├── maxPerMonth (optional)
├── maxTotal (optional)
├── requiresPreApproval (boolean)
├── markupAllowed (boolean)
├── maxMarkupPercent (optional)
└── notes

A law firm contract might specify:

  • Travel expenses allowed up to R5,000/month, no markup
  • Printing at cost, max R500/month
  • All travel requires pre-approval
  • Software licenses not claimable

Multi-line item support is non-negotiable. Real invoices have dozens of line items.

Invoice
├── id
├── organizationId → Organization
├── vendorId → Vendor
├── contractId → Contract
├── invoiceNumber (vendor's reference)
├── invoiceDate
├── receivedDate
├── periodStart (billing period)
├── periodEnd
├── subtotal
├── tax
├── totalAmount
├── currency
├── status (pending | under_review | approved | rejected | queried | paid)
├── submittedBy
├── reviewedBy
├── reviewedAt
├── notes
├── attachments (JSON array — file references)
├── createdAt
└── updatedAt
InvoiceLineItem
├── id
├── invoiceId → Invoice
├── lineNumber
├── description
├── serviceCategory
├── roleLevel
├── quantity (hours, days, units)
├── rate
├── amount (quantity x rate)
├── itemType (service | expense | disbursement)
├── expenseCategory (if itemType = expense)
├── date (date service was rendered)
└── notes

Each validation check produces a finding. Findings are the audit trail of what the engine detected.

ValidationFinding
├── id
├── invoiceId → Invoice
├── lineItemId → InvoiceLineItem (nullable — some findings are invoice-level)
├── ruleType (see Rule Types below)
├── severity (low | medium | high | critical)
├── expectedValue
├── actualValue
├── variance (amount of discrepancy)
├── description (human-readable explanation)
├── status (open | acknowledged | resolved | waived)
├── resolvedBy
├── resolvedAt
├── resolution (explanation of how it was resolved)
└── createdAt
Rule TypeDescription
rate_mismatchBilled rate differs from approved rate card
hours_exceeded_monthlyHours exceed monthly cap for role/service
hours_exceeded_totalHours exceed lifetime cap for contract
monthly_ceiling_breachTotal billing exceeds monthly NTE
contract_ceiling_breachCumulative billing exceeds contract NTE
expense_exceededExpense exceeds category cap
unapproved_serviceService category not in contract
unapproved_expenseExpense category not allowed
duplicate_chargeMatches a previous invoice line item
duplicate_invoiceInvoice number already submitted
billing_increment_violationHours not billed in correct increments
expired_contractInvoice references an expired contract
markup_violationExpense markup exceeds allowed percentage
arithmetic_errorLine item math doesn’t add up
date_anomalyService dates outside billing period or on weekends
pre_approval_missingExpense requires pre-approval, none documented

Materialized/cached view for cumulative tracking. Powers both the validation engine and the dashboard.

ContractUsageSummary
├── contractId → Contract
├── period (month — e.g., "2026-03")
├── totalBilled
├── totalHoursByRole (JSON: {"partner": 45, "senior": 120})
├── totalExpenses
├── cumulativeBilledToDate
├── cumulativeHoursToDate
├── remainingCeiling
└── lastUpdated

Recalculate after every invoice is processed.


Immutable log of every action. Critical for compliance.

AuditLog
├── id
├── organizationId → Organization
├── entityType (invoice | contract | vendor | finding)
├── entityId
├── action (created | updated | approved | rejected | queried | resolved | waived)
├── performedBy → User
├── previousState (JSON snapshot)
├── newState (JSON snapshot)
├── reason (optional — required for waivers and rejections)
├── ipAddress
└── timestamp

Organization
├── Users (many)
├── Vendors (many)
│ └── Contracts (many)
│ ├── RateCards (many)
│ ├── ExpenseRules (many)
│ ├── UsageSummaries (many, per period)
│ └── Invoices (many)
│ ├── LineItems (many)
│ └── Findings (many)
└── AuditLog (many)