Spaces:
Sleeping
Enterprise-Grade Contractor Invoicing System
Implementation Complete β
Overview
A fully-featured, enterprise-grade invoicing system with version tracking, audit logging, ticket linking, and partial payment support. Every invoice edit creates a new version using a copy-on-write strategy for complete audit trails.
π― Enterprise Features Implemented
β 1. Version Tracking (Copy-on-Write)
- Every edit creates a new version of the invoice
- Original invoice preserved forever (immutable audit trail)
- Version chain:
v1 β v2 β v3 β v4 (latest) - Only latest version has
is_latest_version = TRUE - All versions share same
invoice_number previous_version_idlinks to previous versionrevision_notesexplain what changed
Database Fields:
version INTEGER DEFAULT 1 NOT NULL
previous_version_id UUID REFERENCES contractor_invoices(id)
is_latest_version BOOLEAN DEFAULT TRUE NOT NULL
revision_notes TEXT
β 2. Audit Logging
- All changes logged to
audit_logstable - Tracks: WHO did WHAT, WHEN, and WHAT changed
- Changes stored as JSONB diff:
{"old": {...}, "new": {...}} - Integrates with existing audit system
β 3. Ticket Linking/Unlinking
- Automatic ticket sync: Adding line item β sets
ticket.contractor_invoice_id - Automatic unlinking: Removing line item β clears ticket invoice reference
- Prevents double-invoicing: Tickets can only be on ONE active invoice
- Fields updated:
ticket.contractor_invoice_idticket.is_invoicedticket.invoiced_at
β 4. Partial Payments
- Multiple payments supported: Record payments over time
- Automatic totals:
amount_due = total_amount - amount_paid(computed) - Auto status updates:
partially_paid: when0 < amount_paid < total_amountpaid: whenamount_paid >= total_amount
- Payment history preserved across versions
β 5. Flexible Line Items
Invoice can include:
- Tickets: Installation/repair work
- Vehicles: Vehicle rental charges
- Labor: Labor costs
- Materials: Material costs
- Other: Miscellaneous charges
Each line item: {id, type, ticket_id?, description, quantity, unit_price, total}
β 6. Automatic Totals
subtotal= Sum of all line itemstax_amount= (subtotal Γ tax_rate) / 100total_amount= subtotal + tax_amount - discount_amountamount_due= total_amount - amount_paid (computed column)- Recalculated on every version
β 7. Status Workflow
draft β sent β paid
β
partially_paid β paid
β
overdue / disputed / cancelled / on_hold
β 8. Invoice Numbering
Auto-generated format: INV-{CONTRACTOR}-{YEAR}-{SEQUENCE}
Example: INV-ACME-2025-00142
β 9. Soft Delete
- Invoice never truly deleted (sets
deleted_at) - All versions soft deleted together
- Tickets automatically unlinked
- Audit trail preserved
π Files Created
1. Migration
File: migrations/007_add_invoice_versioning.sql
- Adds versioning fields to
contractor_invoices - Creates indexes for version queries
- Includes comments for documentation
2. Model
File: src/app/models/contractor_invoice.py
- SQLAlchemy ORM model
- Relationships: contractor, client, project, created_by, previous_version
- Computed properties:
amount_due,is_fully_paid,is_partially_paid,is_overdue - Helper methods:
get_line_item_by_id(),get_ticket_ids() - Table constraints and indexes
3. Schemas
File: src/app/schemas/contractor_invoice.py
Key Schemas:
LineItemBase,TicketLineItemCreate,GenericLineItemCreateContractorInvoiceCreate,ContractorInvoiceUpdate,ContractorInvoiceResponseContractorInvoiceSummary(lightweight for lists)AddLineItemRequest,RemoveLineItemRequest,UpdateLineItemRequestRecordPaymentRequest,ChangeStatusRequestInvoiceVersionSummary,InvoiceVersionHistoryResponseInvoiceSearchFilters(with validation)
Validation:
- Date relationships (due_date >= issue_date, etc.)
- Positive amounts
- Tax rate 0-100%
- Line item totals auto-calculated
4. Service
File: src/app/services/contractor_invoice_service.py
Key Methods:
create_invoice()- Create draft with auto-generated invoice numberupdate_invoice()- Create new version with changesadd_line_item()- Add item, create version, link ticketremove_line_item()- Remove item, create version, unlink ticketrecord_payment()- Add payment, auto-update statuschange_status()- Change status, create versionget_invoice_by_id()- Get single invoiceget_version_history()- Get all versions of an invoicelist_invoices()- List with filtersgenerate_invoice_number()- Auto-generate unique numbers
Authorization:
platform_admin: Full accessproject_manager: Manage their contractor's invoicesdispatcher: Manage their contractor's invoices
5. API Endpoints
File: src/app/api/v1/contractor_invoices.py
Endpoints:
POST /contractor-invoices - Create invoice
GET /contractor-invoices/{id} - Get invoice
GET /contractor-invoices - List invoices (filters)
PUT /contractor-invoices/{id} - Update invoice
DELETE /contractor-invoices/{id} - Soft delete
POST /contractor-invoices/{id}/line-items - Add line item
DELETE /contractor-invoices/{id}/line-items/{item_id} - Remove line item
POST /contractor-invoices/{id}/payments - Record payment
PUT /contractor-invoices/{id}/status - Change status
GET /contractor-invoices/{id}/versions - Version history
GET /contractor-invoices/{id}/versions/{version} - Get specific version
6. Updated Files
src/app/models/ticket.py- Addedcontractor_invoicerelationshipsrc/app/api/v1/router.py- Registered invoice router
π§ Database Schema
Updated contractor_invoices Table
CREATE TABLE contractor_invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Parties
contractor_id UUID NOT NULL REFERENCES contractors(id),
client_id UUID NOT NULL REFERENCES clients(id),
project_id UUID REFERENCES projects(id),
-- Invoice Details
invoice_number TEXT NOT NULL UNIQUE,
invoice_title TEXT,
-- Billing Period
billing_period_start DATE NOT NULL,
billing_period_end DATE NOT NULL,
-- Amounts
subtotal DECIMAL(12, 2) NOT NULL,
tax_rate DECIMAL(5, 2) DEFAULT 0,
tax_amount DECIMAL(12, 2) DEFAULT 0,
discount_amount DECIMAL(12, 2) DEFAULT 0,
total_amount DECIMAL(12, 2) NOT NULL,
amount_paid DECIMAL(12, 2) DEFAULT 0,
amount_due DECIMAL(12, 2) GENERATED ALWAYS AS (total_amount - amount_paid) STORED,
currency TEXT DEFAULT 'KES',
-- Line Items (JSONB)
line_items JSONB DEFAULT '[]',
-- Status & Dates
status contractor_invoice_status DEFAULT 'draft',
issue_date DATE NOT NULL,
due_date DATE NOT NULL,
sent_date DATE,
paid_date DATE,
-- Payment Details
payment_method TEXT,
payment_reference TEXT,
payment_notes TEXT,
-- Metadata
notes TEXT,
terms_and_conditions TEXT,
additional_metadata JSONB DEFAULT '{}',
-- Audit
created_by_user_id UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE,
-- VERSIONING (NEW)
version INTEGER DEFAULT 1 NOT NULL,
previous_version_id UUID REFERENCES contractor_invoices(id),
is_latest_version BOOLEAN DEFAULT TRUE NOT NULL,
revision_notes TEXT
);
Indexes
CREATE INDEX idx_contractor_invoices_version ON contractor_invoices (id, version);
CREATE INDEX idx_contractor_invoices_latest ON contractor_invoices (is_latest_version)
WHERE is_latest_version = TRUE AND deleted_at IS NULL;
CREATE INDEX idx_contractor_invoices_previous_version ON contractor_invoices (previous_version_id)
WHERE previous_version_id IS NOT NULL;
π Line Items Format (JSONB)
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "ticket",
"ticket_id": "660e8400-e29b-41d4-a716-446655440001",
"description": "Installation at Site XYZ",
"quantity": 1,
"unit_price": 5000.00,
"total": 5000.00
},
{
"id": "770e8400-e29b-41d4-a716-446655440002",
"type": "vehicle",
"description": "Vehicle rental - 3 days",
"quantity": 3,
"unit_price": 1500.00,
"total": 4500.00
},
{
"id": "880e8400-e29b-41d4-a716-446655440003",
"type": "material",
"description": "Fiber optic cable - 100m",
"quantity": 100,
"unit_price": 50.00,
"total": 5000.00
}
]
π Version History Example
Scenario: Invoice Created β Line Item Added β Payment Recorded
Version 1: Initial Creation
{
"invoice_number": "INV-ACME-2025-00001",
"version": 1,
"total_amount": 10000.00,
"amount_paid": 0.00,
"status": "draft",
"line_items": [
{"id": "...", "type": "ticket", "total": 5000.00},
{"id": "...", "type": "vehicle", "total": 5000.00}
],
"is_latest_version": true,
"previous_version_id": null,
"revision_notes": "Initial version"
}
Version 2: Added Material Line Item
{
"invoice_number": "INV-ACME-2025-00001",
"version": 2,
"total_amount": 15000.00,
"amount_paid": 0.00,
"status": "draft",
"line_items": [
{"id": "...", "type": "ticket", "total": 5000.00},
{"id": "...", "type": "vehicle", "total": 5000.00},
{"id": "...", "type": "material", "total": 5000.00} // NEW
],
"is_latest_version": true,
"previous_version_id": "version-1-uuid",
"revision_notes": "Added material charges"
}
Version 3: Changed Status to Sent
{
"invoice_number": "INV-ACME-2025-00001",
"version": 3,
"total_amount": 15000.00,
"amount_paid": 0.00,
"status": "sent", // CHANGED
"sent_date": "2025-11-17", // SET
"line_items": [...],
"is_latest_version": true,
"previous_version_id": "version-2-uuid",
"revision_notes": "Sent invoice to client"
}
Version 4: Recorded Partial Payment
{
"invoice_number": "INV-ACME-2025-00001",
"version": 4,
"total_amount": 15000.00,
"amount_paid": 7500.00, // CHANGED
"status": "partially_paid", // AUTO-UPDATED
"sent_date": "2025-11-17",
"line_items": [...],
"payment_method": "mobile_money",
"payment_reference": "MPESA-XYZ123",
"is_latest_version": true,
"previous_version_id": "version-3-uuid",
"revision_notes": "Recorded payment of 7500.00 KES"
}
π§ͺ Usage Examples
Create Invoice
POST /contractor-invoices
{
"contractor_id": "uuid",
"client_id": "uuid",
"project_id": "uuid",
"billing_period_start": "2025-11-01",
"billing_period_end": "2025-11-30",
"issue_date": "2025-11-17",
"due_date": "2025-12-17",
"tax_rate": 16,
"line_items": [
{
"type": "ticket",
"ticket_id": "uuid",
"description": "Installation at Site A",
"quantity": 1,
"unit_price": 5000
}
]
}
Add Line Item
POST /contractor-invoices/{id}/line-items
{
"line_item": {
"type": "vehicle",
"description": "Vehicle rental - 2 days",
"quantity": 2,
"unit_price": 1500
},
"revision_notes": "Added vehicle rental charges"
}
Record Payment
POST /contractor-invoices/{id}/payments
{
"amount": 5000,
"payment_method": "mobile_money",
"payment_reference": "MPESA-ABC123",
"payment_notes": "Partial payment from client",
"revision_notes": "First installment received"
}
Get Version History
GET /contractor-invoices/{id}/versions
Response:
{
"invoice_number": "INV-ACME-2025-00001",
"versions": [
{
"id": "uuid-v1",
"version": 1,
"total_amount": 10000,
"status": "draft",
"revision_notes": "Initial version",
"created_at": "2025-11-17T10:00:00Z",
"is_latest_version": false
},
{
"id": "uuid-v2",
"version": 2,
"total_amount": 15000,
"status": "draft",
"revision_notes": "Added line item",
"created_at": "2025-11-17T11:00:00Z",
"is_latest_version": false
},
{
"id": "uuid-v3",
"version": 3,
"total_amount": 15000,
"status": "sent",
"revision_notes": "Sent to client",
"created_at": "2025-11-17T12:00:00Z",
"is_latest_version": true
}
],
"latest_version_id": "uuid-v3",
"total_versions": 3
}
π Authorization
| Role | Create | View | Edit | Delete | Payments |
|---|---|---|---|---|---|
| platform_admin | β All | β All | β All | β Yes | β Yes |
| project_manager | β Own contractor | β Own contractor | β Own contractor | β No | β Own contractor |
| dispatcher | β Own contractor | β Own contractor | β Own contractor | β No | β Own contractor |
| client_admin | β No | β Own client | β No | β No | β No |
| field_agent | β No | β No | β No | β No | β No |
β Testing Checklist
- Run migration:
007_add_invoice_versioning.sql - Create invoice with line items
- Verify ticket linking (ticket.contractor_invoice_id set)
- Add line item β verify new version created
- Remove line item β verify ticket unlinked
- Record partial payment β verify status =
partially_paid - Record full payment β verify status =
paid - Change status β verify new version created
- Get version history β verify all versions returned
- Soft delete β verify tickets unlinked
- Check audit_logs table β verify all changes logged
π Next Steps (Future Enhancements)
- PDF Generation: Generate invoice PDFs for client delivery
- Email Integration: Auto-send invoices via email
- Payment Gateway: Integrate M-Pesa for online payments
- Recurring Invoices: Auto-generate invoices monthly
- Invoice Templates: Customizable invoice layouts
- Multi-Currency: Support for USD, EUR, etc.
- Tax Compliance: Integrate with tax systems (KRA eTIMS)
- Payment Plans: Installment payment schedules
- Late Fees: Auto-calculate penalties for overdue invoices
- Client Portal: Let clients view/pay invoices online
π Related Documentation
- Schema:
docs/schema/schema.sql(lines 2518-2599) - Migration:
migrations/007_add_invoice_versioning.sql - Model:
src/app/models/contractor_invoice.py - Schemas:
src/app/schemas/contractor_invoice.py - Service:
src/app/services/contractor_invoice_service.py - API:
src/app/api/v1/contractor_invoices.py
Implementation Status: β COMPLETE - Enterprise-Grade Invoicing Ready for Production