Finance App — Technical Documentation
Application: finance Project: DjangoPlay Type: Django application Primary domain: Financial identity, invoicing, taxation, payments, credit notes, and scheduled billing Status: Active implementatio...
On this page ▾
- 1. Overview
- 2. Architectural Goals
- 3. Scope
- Included
- Currently outside the domain
- 4. Application Dependencies
- Primary dependencies
- 5. Application Structure
- 6. Domain Model
- 6.1 FinanceConfiguration
- 6.2 FinanceEntityMapping
- 6.3 Address
- 6.4 Contact
- 6.5 TaxProfile
- 6.6 TaxRate
- 6.7 DocumentSequence
- 6.8 Invoice
- 6.9 LineItem
- 6.10 PaymentMethod
- 6.11 Payment
- 6.12 CreditNote
- 6.13 BillingSchedule
- 7. Invoice Lifecycle
- States
- Transition graph
- Editable states
- Terminal states
- 8. Invoice Immutability
- 9. Invoice Creation Workflow
- 10. Invoice Numbering
- Format
- Prefixes
- 11. Totals Calculation
- Line subtotal
- Invoice subtotal
- Invoice tax total
- Invoice grand total
- Outstanding balance
- 12. Tax Architecture
- Components
- 13. Tax Policy Resolver
- Unmapped countries
- 14. India GST
- Interstate supply
- Intrastate supply
- 15. Tax Rate Lookup
- 16. Tax Snapshots
- 17. Tax Exemption
- 18. Identity and Compliance Layer
- Correct pattern
- Avoid
- 19. Payment Workflow
- 20. Credit Note Workflow
- 21. Billing Schedule Workflow
- 22. Billing Frequency
- 23. REST API
- 24. Invoice API
- List invoices
- Retrieve invoice
- Create invoice
- Finalize
- Cancel
- Void
- Record payment
- Issue credit note
- Add line item
- Remove line item
- 25. Address API
- List
- Retrieve
- Create
- 26. Contact API
- List
- Retrieve
- Create
- 27. Tax Profile API
- List
- Retrieve
- Create
- 28. Reference Data APIs
- Payment methods
- Tax rates
- 29. API Mutation Rule
- 30. Permissions
- 31. API Error Handling
- 32. DTO Layer
- 33. Exception Hierarchy
- 34. Audit and History
- 35. Database Integrity
- Invoice line items
- Billing schedules
- Credit notes
- Tax rates
- Payments
- Document sequences
- 36. Database Tables
- 37. Migration State
- 38. Django Admin
- 39. Management Commands
- Generate scheduled invoices
- Seed finance tax rates
- 40. Development Data Generation
- 41. Finance Data Generation Flow
- 42. Configuration Rules
- Default country
- Scheduled invoice issuer
- 43. Service Ownership Rules
- Invoice creation
- Invoice editing
- Invoice state changes
- Payments
- Credit notes
- Scheduled invoices
- Identity
- Numbering
- Tax
- 44. Rules for Future Development
- Rule 1 — Do not put business rules in models
- Rule 2 — Do not mutate invoice status directly
- Rule 3 — Do not edit finalized invoices
- Rule 4 — Do not calculate tax inside views
- Rule 5 — Do not query TaxRate from arbitrary application code
- Rule 6 — Do not create financial document numbers manually
- Rule 7 — Do not bypass identity workflows
- Rule 8 — Prefer database configuration for operational defaults
- Rule 9 — Keep calculators pure
- Rule 10 — Add abstractions only when a real requirement exists
- 45. Adding a New Country Tax Policy
- Step 1
- Step 2
- Step 3
- Step 4
- Step 5
- 46. Adding a New Financial Document Type
- 47. Transaction Boundaries
- 48. Concurrency Considerations
- 49. Operational Flow
- 50. Scheduled Billing Flow
- 51. Financial Integrity Model
- 52. Current Limitations
- Payment allocation
- Refunds
- Debit notes
- Multi-country tax
- Financial accounting
- 53. Recommended Testing Strategy
- State machine tests
- Tax tests
- Invoice tests
- Payment tests
- Credit note tests
- Billing tests
- Numbering tests
- 54. Architecture Summary
- 55. Core Architectural Principle
1. Overview
The finance app is the consolidated financial domain of DjangoPlay.
It provides:
- Financial identity data for business entities
- Structured billing addresses and contacts
- Tax profiles and tax identifiers
- Configurable tax rates
- Country-specific tax-policy resolution
- Invoice creation and lifecycle management
- Invoice line-item calculation
- Payment recording and outstanding-balance management
- Credit notes for post-finalization adjustments
- Recurring billing schedules
- Financial document numbering
- Finance configuration
- REST API access
- Django Admin management
- Scheduled invoice generation
- Development/test data generation through
devtools
The app replaces the former fincore and invoices applications.
The current architecture intentionally keeps the finance domain inside the Django project. It is structured so that the domain can be extracted later if a genuine second consumer requires a standalone package.
2. Architectural Goals
The finance architecture follows these principles:
- Models are persistence-focused.
- Business rules live in services and workflows.
- Invoice lifecycle transitions have one state-machine implementation.
- Financial mutations occur through explicit workflows.
- Tax calculation is policy-driven rather than hardcoded into invoices.
- Tax rates are database configuration rather than source-code constants.
- Finalized invoices are immutable.
- Credit notes are used for post-finalization reductions.
- Totals are calculated deterministically.
- Financial document numbering is centralized and concurrency-safe.
- Audit/history behavior follows the application's common model conventions.
- The API layer delegates mutations to workflows rather than calling model
.save()directly. - Configuration that may legitimately change operationally is database-backed.
- DTOs are introduced only at actual service boundaries.
3. Scope
Included
- Finance configuration
- Entity-to-finance mapping
- Addresses
- Contacts
- Tax profiles
- Tax rates
- Payment methods
- Invoices
- Invoice line items
- Payments
- Credit notes
- Billing schedules
- Document sequences
- Tax policy resolution
- India GST calculation
- No-tax fallback policy
- Invoice state management
- Invoice totals
- API endpoints
- Admin management
- Scheduled invoice generation
- Finance data-generation services
Currently outside the domain
The following are intentionally not implemented as part of the current finance app:
- Double-entry accounting ledger
- General ledger
- Journal entries
- Accounts payable
- Accounts receivable subledger
- Bank reconciliation
- Payment-provider integrations
- Payment allocation across multiple invoices
- Refund workflow
- Debit-note workflow
- Financial reporting warehouse
- Financial analytics platform
- Event sourcing
- CQRS
- Event bus
- AI/ML financial processing
- Multi-tenant runtime isolation
- Plugin marketplace
These should only be introduced when there is a concrete product requirement.
4. Application Dependencies
The finance application participates in the following dependency chain:
authx / users / teamcentral
|
v
core / audit
|
v
policyengine
|
v
locations
|
v
entities
|
v
financePrimary dependencies
core
Provides common model infrastructure such as:
TimeStampedModelAuditFieldsModelActiveManager- soft-delete behavior
- audit lifecycle integration
audit
Tracks financial model lifecycle events and historical changes.
locations
Provides geographic reference data used by finance:
- country
- region
- subregion
- city
entities
Provides the business entities that participate in financial transactions.
Examples:
- invoice issuer
- invoice recipient
- billing customer
- finance entity mapping
policyengine
Provides application-level permission infrastructure.
Finance adds a local FinanceActionPermission because custom money-related API actions require explicit permission handling.
5. Application Structure
finance/
├── __init__.py
├── apps.py
├── constants.py
├── urls.py
│
├── models/
│ ├── __init__.py
│ ├── configuration.py
│ ├── entity_mapping.py
│ ├── address.py
│ ├── contact.py
│ ├── tax_profile.py
│ ├── tax_rate.py
│ ├── document_sequence.py
│ ├── invoice.py
│ ├── line_item.py
│ ├── payment.py
│ ├── payment_method.py
│ ├── billing_schedule.py
│ └── credit_note.py
│
├── services/
│ ├── __init__.py
│ ├── dto.py
│ ├── exceptions.py
│ ├── config.py
│ ├── compliance.py
│ ├── numbering.py
│ ├── state_machine.py
│ │
│ ├── calculators/
│ │ ├── __init__.py
│ │ ├── totals.py
│ │ └── tax.py
│ │
│ ├── tax/
│ │ ├── __init__.py
│ │ ├── resolver.py
│ │ ├── rate_lookup.py
│ │ └── policies/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── india_gst.py
│ │ └── no_tax.py
│ │
│ └── workflows/
│ ├── __init__.py
│ ├── identity_workflow.py
│ ├── invoice_workflow.py
│ ├── payment_workflow.py
│ ├── credit_note_workflow.py
│ └── billing_schedule_workflow.py
│
├── api/
│ ├── __init__.py
│ ├── serializers.py
│ ├── views.py
│ ├── urls.py
│ ├── permissions.py
│ └── exceptions.py
│
├── admin/
│ ├── __init__.py
│ ├── configuration.py
│ ├── address.py
│ ├── contact.py
│ ├── tax_profile.py
│ ├── tax_rate.py
│ ├── payment_method.py
│ ├── invoice.py
│ ├── line_item.py
│ ├── payment.py
│ ├── billing_schedule.py
│ ├── credit_note.py
│ ├── _permissions.py
│ └── _workflow_action_views.py
│
├── forms/
│ ├── __init__.py
│ ├── configuration.py
│ ├── address.py
│ ├── contact.py
│ ├── tax_profile.py
│ ├── tax_rate.py
│ └── payment_method.py
│
├── management/
│ └── commands/
│ ├── __init__.py
│ ├── generate_scheduled_invoices.py
│ └── seed_finance_tax_rates.py
│
└── migrations/
├── 0001_initial.py
├── 0002_initial.py
├── 0003_initial.py
└── __init__.py6. Domain Model
6.1 FinanceConfiguration
Table: finance_configuration
Singleton configuration for finance-wide operational defaults.
Fields:
| Field | Purpose |
|---|---|
default_country |
Default country used for tax-policy resolution |
default_issuing_entity |
Issuer used for system-generated scheduled invoices |
The singleton is enforced through pk=1.
Configuration is cached for five minutes.
Use:
FinanceConfiguration.load()Do not use:
FinanceConfiguration.objects.first()inside domain services.
The configuration is intended to be changed through Django Admin rather than hardcoded into application logic.
6.2 FinanceEntityMapping
Table: finance_entity_mapping
Provides a finance-specific mapping layer between financial resources and entity-like objects.
Fields:
entity_uuidentity_typeentity_idcontent_type
It is used by:
- addresses
- contacts
- tax profiles
This allows finance identity data to be associated with an entity without coupling every finance identity model directly to one concrete entity implementation.
6.3 Address
Table: finance_address
Represents a structured financial address.
Fields include:
entity_mappingaddress_typestreet_addresscitypostal_codecountryregionsubregionis_default
Address validation belongs to:
finance.services.complianceAddress creation/update belongs to:
finance.services.workflows.identity_workflow6.4 Contact
Table: finance_contact
Represents a financial/business contact.
Fields include:
entity_mappingnameemailphone_numberrolecountryis_primary
Validation is performed through the compliance service.
6.5 TaxProfile
Table: finance_tax_profile
Stores entity-level tax identity and exemption information.
Fields include:
entity_mappingtax_identifiertax_identifier_typeis_tax_exempttax_exemption_reasontax_exemption_documentcountryregion
A TaxProfile describes who the entity is for tax purposes.
It does not define the tax rate.
6.6 TaxRate
Table: finance_tax_rate
Stores effective-dated tax-rate configuration.
Fields:
country_coderegion_codehsn_sac_coderate_typerate_percenteffective_fromeffective_to
The distinction is important:
TaxProfile
= entity tax identity
TaxRate
= tax configuration/reference dataTax rates are managed independently so that one rate can apply to many entities and invoices.
6.7 DocumentSequence
Table: finance_document_sequence
Stores the current sequence for financial document numbering.
Unique key:
issuer + document_type + periodSupported document types:
INVOICECREDIT_NOTEDEBIT_NOTE
DEBIT_NOTE is currently reserved for future workflow support.
6.8 Invoice
Table: finance_invoice
The primary financial document.
Core relationships:
Invoice
├── issuer -> entities.Entity
├── recipient -> entities.Entity
├── billing_address -> finance.Address
├── billing_country -> locations.CustomCountry
├── billing_region -> locations.CustomRegion
├── line_items -> finance.LineItem
├── payments -> finance.Payment
└── credit_notes -> finance.CreditNoteImportant fields:
invoice_numberdescriptionissuerrecipientbilling_addressbilling_countrybilling_regionissue_datedue_datestatuspayment_termscurrencysubtotal_amounttax_total_amounttotal_amounttax_exemption_statustax_policy_nametax_breakdownissuer_gstinrecipient_gstin
Invoice totals are stored for efficient access, but are produced by the workflow/calculator layer rather than model save() logic.
6.9 LineItem
Table: finance_line_item
Represents an invoice line.
Fields:
invoicedescriptionhsn_sac_codequantityunit_pricediscountsubtotal_amounttax_total_amounttotal_amounttax_breakdown
Database constraints enforce:
- quantity > 0
- unit price >= 0
- discount >= 0
Line items are created/removed through invoice workflows.
6.10 PaymentMethod
Table: finance_payment_method
Reference data describing accepted payment methods.
Examples depend on the configured choices in finance.constants.
The API exposes this resource as read-only.
Operational management is performed through Admin.
6.11 Payment
Table: finance_payment
Records a payment against an invoice.
Fields:
invoiceamountpayment_datepayment_methodpayment_referencestatus
Payment creation is handled by:
finance.services.workflows.payment_workflow.record_payment()The workflow prevents payment amounts from exceeding the outstanding invoice balance.
6.12 CreditNote
Table: finance_credit_note
Represents a financial adjustment against a finalized invoice.
Fields:
invoicecredit_note_numberamountreasonissued_at
A credit note is the sanctioned mechanism for reducing a finalized invoice.
It cannot exceed the invoice's total credited amount.
6.13 BillingSchedule
Table: finance_billing_schedule
Represents recurring billing.
Fields:
customerdescriptionfrequencystart_dateend_datenext_billing_dateamountstatus
Supported frequencies are defined by finance.constants.
The schedule stores the customer, not the issuer.
The issuer is resolved from:
FinanceConfiguration.default_issuing_entityThis prevents the system from guessing who should issue a scheduled invoice.
7. Invoice Lifecycle
The invoice state machine is defined exclusively in:
finance.services.state_machineStates
DRAFT
PENDING
SENT
PARTIALLY_PAID
PAID
OVERDUE
CANCELLED
VOID
REFUNDEDTransition graph
DRAFT
├──> PENDING
└──> CANCELLED
PENDING
├──> DRAFT
├──> SENT
└──> CANCELLED
SENT
├──> PARTIALLY_PAID
├──> PAID
├──> OVERDUE
└──> VOID
PARTIALLY_PAID
├──> PAID
├──> OVERDUE
└──> VOID
OVERDUE
├──> PARTIALLY_PAID
├──> PAID
└──> VOID
PAID
└──> REFUNDED
CANCELLED
└── terminal
VOID
└── terminal
REFUNDED
└── terminalEditable states
Only:
DRAFT
PENDINGare editable.
Terminal states
CANCELLED
VOID
REFUNDEDNo other code should directly assign:
invoice.status = ...Use the state machine/workflow instead.
8. Invoice Immutability
Once an invoice reaches SENT, direct invoice editing is prohibited.
This protects the historical integrity of a finalized financial document.
For example:
DRAFT/PENDING
|
| edit line items
v
SENT
|
| adjustment required
v
CreditNoteThe system does not support editing a finalized invoice to change its amount.
Credit notes provide the controlled adjustment mechanism.
9. Invoice Creation Workflow
The canonical invoice creation path is:
API / scheduled billing
|
v
InvoiceCreateInput
|
v
invoice_workflow.create_invoice()
|
+--> validate input
|
+--> load issuer
|
+--> load recipient
|
+--> load billing address
|
+--> generate document number
|
+--> create invoice
|
+--> calculate line subtotals
|
+--> resolve tax policy
|
+--> calculate line taxes
|
+--> create line items
|
+--> calculate invoice totals
|
v
InvoiceAn invoice must contain at least one line item.
10. Invoice Numbering
Document numbering is centralized in:
finance.services.numbering.generate_document_number()Format
{PREFIX}-{ISSUER_ID}-{YEAR}-{SEQUENCE:06d}Examples:
INV-7963-2026-000123
CN-7963-2026-000045Prefixes
| Document | Prefix |
|---|---|
| Invoice | INV |
| Credit Note | CN |
| Debit Note | DN |
The sequence is scoped by:
issuer
document_type
yearThe sequence row is locked using:
select_for_update()inside a database transaction.
This prevents concurrent requests from receiving the same document number.
11. Totals Calculation
Totals are calculated by pure functions in:
finance.services.calculators.totalsLine subtotal
(quantity × unit_price) − discountThe result cannot be negative.
Invoice subtotal
sum(line_item.subtotal_amount)Invoice tax total
sum(line_item.tax_total_amount)Invoice grand total
subtotal + tax_totalOutstanding balance
invoice_total − completed_paymentsThe minimum outstanding balance is zero.
12. Tax Architecture
Tax processing is deliberately separated into:
Resolver
|
v
Policy
|
v
Rate Lookup
|
v
TaxRateComponents
services/tax/resolver.py
services/tax/rate_lookup.py
services/tax/policies/base.py
services/tax/policies/india_gst.py
services/tax/policies/no_tax.py13. Tax Policy Resolver
The policy registry currently contains:
POLICY_REGISTRY = {
"IN": IndiaGSTPolicy,
}The resolver determines the policy from the country code.
The default country is obtained from:
FinanceConfiguration.default_countryIt is not hardcoded as the operational default.
Unmapped countries
The current configuration has:
FALLBACK_TO_NO_TAX = TrueTherefore an unmapped country resolves to NoTaxPolicy.
This is useful while only a subset of countries has implemented tax policies.
For a production environment with multiple actively billed countries, consider disabling the fallback so missing tax support becomes an explicit configuration error rather than a zero-tax result.
14. India GST
The current implemented country policy is:
IndiaGSTPolicyGST is determined from:
- supplier country
- supplier region
- place-of-supply region
- HSN/SAC code
- tax exemption status
- effective tax rate
Interstate supply
If supplier and place-of-supply regions differ:
IGSTis calculated.
Intrastate supply
If the supply is within the same state:
CGST + SGSTis calculated.
The total configured GST rate is split equally between CGST and SGST.
Example:
GST rate = 18%
CGST = 9%
SGST = 9%Interstate:
IGST = 18%15. Tax Rate Lookup
The rate lookup service is the only service that queries TaxRate for GST calculation.
Lookup precedence:
1. HSN/SAC-specific effective rate
2. General country effective rate
3. TaxRateNotConfiguredAn effective rate must satisfy:
effective_from <= calculation_date
AND
effective_to is NULL
OR
effective_to >= calculation_dateIf no applicable rate exists, the service raises:
TaxRateNotConfiguredThe system does not silently invent a tax rate.
16. Tax Snapshots
Invoices and line items store tax calculation results as JSON snapshots.
Example conceptual structure:
[
{
"label": "CGST",
"rate_percent": "9.00",
"amount": "90.00"
},
{
"label": "SGST",
"rate_percent": "9.00",
"amount": "90.00"
}
]The invoice also stores:
tax_policy_name
tax_breakdownThe purpose is historical reproducibility.
If tax configuration changes later, an already finalized invoice retains the tax calculation that was applied to it.
17. Tax Exemption
If the invoice is tax-exempt, the tax policy returns:
total_tax = 0and an empty tax-line breakdown.
The invoice's exemption status is preserved as part of the invoice data.
18. Identity and Compliance Layer
Validation is centralized in:
finance.services.complianceThe main validation functions cover:
validate_address()
validate_contact()
validate_tax_profile()The corresponding mutations are performed by:
finance.services.workflows.identity_workflowCorrect pattern
DTO
|
v
compliance validation
|
v
identity workflow
|
v
model persistenceAvoid
Address.objects.create(...)
TaxProfile.objects.create(...)
Contact.objects.create(...)from application/business code.
The workflow guarantees validation and audit-user stamping are consistently applied.
19. Payment Workflow
Payments are created through:
payment_workflow.record_payment()The workflow:
- Rejects payments for
DRAFTandPENDINGinvoices. - Rejects terminal invoices.
- Requires a positive payment amount.
- Calculates completed payments.
- Calculates outstanding balance.
- Rejects overpayment.
- Loads an active payment method.
- Creates the payment.
- Transitions the invoice:
- to
PARTIALLY_PAIDwhen balance remains - to
PAIDwhen fully paid
- to
Conceptually:
Invoice total
|
v
Outstanding balance
|
+---- payment < balance ----> PARTIALLY_PAID
|
+---- payment = balance ----> PAID
|
+---- payment > balance ----> REJECT20. Credit Note Workflow
Credit notes are created through:
credit_note_workflow.issue_credit_note()Eligible invoice states:
SENT
PARTIALLY_PAID
PAID
OVERDUEA credit note:
- must be positive
- must reference an eligible invoice
- cannot make cumulative credits exceed the invoice total
- receives a centralized document number
- is created transactionally
Conceptually:
Finalized Invoice
|
v
Credit Note
|
v
Reduced economic exposureThe invoice itself is not rewritten.
21. Billing Schedule Workflow
Scheduled billing is implemented by:
billing_schedule_workflow.generate_due_invoices()The process is:
ACTIVE BillingSchedule
|
| next_billing_date <= today
v
resolve configured issuer
|
v
load customer's default address
|
v
create invoice through normal invoice workflow
|
v
advance next_billing_date
|
v
mark COMPLETED when end_date is passedThe generated invoice uses the same:
- document numbering
- tax calculation
- totals calculation
- audit behavior
- invoice workflow
as a manually created invoice.
This avoids having a second invoice-generation implementation.
22. Billing Frequency
The schedule frequency is defined by:
finance.constants.BILLING_FREQUENCY_CHOICESCalendar arithmetic prefers dateutil.relativedelta.
This means monthly schedules use calendar-month arithmetic rather than blindly adding 30 days.
For example:
2026-01-31 + 1 monthuses calendar-aware handling instead of assuming:
2026-03-02from a fixed 30-day increment.
23. REST API
The finance API is registered through:
finance/api/urls.pyand included through the project's finance URL configuration.
Current API resources:
/invoices/
/addresses/
/contacts/
/tax-profiles/
/payment-methods/
/tax-rates/The exact external prefix is determined by the project's root URL configuration.
24. Invoice API
List invoices
GET /invoices/Retrieve invoice
GET /invoices/{id}/Create invoice
POST /invoices/Creation delegates to:
invoice_workflow.create_invoice()Finalize
POST /invoices/{id}/finalize/Cancel
POST /invoices/{id}/cancel/Void
POST /invoices/{id}/void/Record payment
POST /invoices/{id}/record-payment/Issue credit note
POST /invoices/{id}/issue-credit-note/Add line item
POST /invoices/{id}/line-items/Remove line item
DELETE /invoices/{id}/line-items/{line_item_id}/There is intentionally no generic:
PUT /invoices/{id}/
PATCH /invoices/{id}/Invoice mutations are exposed as explicit domain actions.
25. Address API
List
GET /addresses/Retrieve
GET /addresses/{id}/Create
POST /addresses/Creation delegates to:
identity_workflow.create_address()26. Contact API
List
GET /contacts/Retrieve
GET /contacts/{id}/Create
POST /contacts/Creation delegates to:
identity_workflow.create_contact()27. Tax Profile API
List
GET /tax-profiles/Retrieve
GET /tax-profiles/{id}/Create
POST /tax-profiles/Creation delegates to:
identity_workflow.create_tax_profile()28. Reference Data APIs
Payment methods
GET /payment-methods/
GET /payment-methods/{id}/Tax rates
GET /tax-rates/
GET /tax-rates/{id}/These APIs are read-only.
Management is performed through Django Admin.
29. API Mutation Rule
The finance API follows a strict rule:
HTTP request
|
v
DRF serializer
|
v
DTO
|
v
workflow
|
v
model persistenceNot:
HTTP request
|
v
ModelSerializer.save()
|
v
modelThis guarantees that API callers cannot bypass:
- state-machine rules
- tax calculation
- compliance validation
- numbering
- payment validation
- credit-note rules
- audit stamping
30. Permissions
Finance uses the shared policy-engine permission system.
Invoice custom actions use:
FinanceActionPermissionThis exists because custom DRF actions such as:
finalize
cancel
void
record_payment
issue_credit_note
add_line_item
remove_line_itemare not necessarily covered by a generic CRUD action map.
Financial mutations therefore require explicit permission handling.
31. API Error Handling
Finance domain exceptions are handled through:
finance.api.exceptions.handle_finance_errorsThe API returns a consistent structure:
{
"code": "error_code",
"message": "Human-readable message",
"details": {}
}This keeps domain errors separate from raw Django/DRF implementation exceptions.
32. DTO Layer
DTOs are defined in:
finance.services.dtoCurrent DTO categories include:
TaxContext
TaxLineBreakdown
TaxBreakdown
AddressInput
TaxProfileInput
ContactInput
LineItemInput
InvoiceCreateInput
PaymentInput
CreditNoteInputDTOs are frozen dataclasses.
They are used where a real service boundary exists.
The project deliberately does not maintain one DTO for every database model.
33. Exception Hierarchy
Finance-specific domain errors are centralized in:
finance.services.exceptionsExamples of domain failures include:
- invalid state transitions
- invalid invoice status for payment
- invalid invoice status for credit note
- overpayment
- invalid amounts
- missing tax configuration
- missing tax policy
- missing finance configuration
- missing issuing entity
The objective is to expose domain failures explicitly rather than relying on generic database errors.
34. Audit and History
Finance models use the common application lifecycle infrastructure.
Most financial models provide:
objects
all_objectswhere:
objects
= active records
all_objects
= all records, including soft-deleted recordsHistorical tracking is provided through:
HistoricalRecords()This is especially important for financial records because historical changes need to remain inspectable.
Soft deletion should be used consistently with the application's TimeStampedModel behavior.
35. Database Integrity
The finance schema uses database constraints in addition to service-level validation.
Examples include:
Invoice line items
quantity > 0
unit_price >= 0
discount >= 0Billing schedules
amount >= 0Credit notes
amount > 0Tax rates
Active tax-rate uniqueness is constrained across:
country
region
HSN/SAC
effective_from
rate_typePayments
Active payment references are unique per invoice when a reference is supplied.
Document sequences
A sequence is unique per:
issuer
document_type
period36. Database Tables
The current finance domain uses these primary tables:
finance_configuration
finance_entity_mapping
finance_address
finance_contact
finance_tax_profile
finance_tax_rate
finance_document_sequence
finance_invoice
finance_line_item
finance_payment_method
finance_payment
finance_credit_note
finance_billing_scheduleHistorical tables are generated by django-simple-history.
37. Migration State
The current app contains:
0001_initial.py
0002_initial.py
0003_initial.pyThese migrations represent the current finance schema.
No separate historical migration chain from fincore or invoices should be introduced into the active application architecture.
38. Django Admin
Finance provides administrative interfaces for:
- Finance Configuration
- Addresses
- Contacts
- Tax Profiles
- Tax Rates
- Payment Methods
- Invoices
- Line Items
- Payments
- Billing Schedules
- Credit Notes
Configuration/reference records that intentionally belong in Admin include:
FinanceConfiguration
TaxRate
PaymentMethodThis allows operational changes without changing application source code.
39. Management Commands
Generate scheduled invoices
python manage.py generate_scheduled_invoicesThis executes the scheduled billing workflow.
Seed finance tax rates
python manage.py seed_finance_tax_ratesThis command is designed to seed the baseline India tax configuration idempotently.
It is intended to prevent a fresh environment from reaching invoice creation without the required general India GST rate.
40. Development Data Generation
Finance development/test data generation is intentionally implemented under:
devtools/services/finance/The current services include:
context.py
reference_data_service.py
identity_generation_service.py
billing_schedule_service.py
invoice_generation_service.py
generation_service.pyThe generators use the real finance workflows wherever financial business behavior is involved.
This is important because generated data should exercise the same:
- validation
- numbering
- tax
- totals
- state transitions
- audit behavior
as production-created data.
41. Finance Data Generation Flow
Conceptually:
Reference Data
|
v
Entities
|
v
Finance Identity
(Address / Contact / TaxProfile)
|
v
Billing Schedules
|
v
Invoices
|
v
Payments / Credit NotesThe orchestration context allows multiple generation services to share:
- random-number state
- caches
- generated objects
- execution context
42. Configuration Rules
Default country
Do not hardcode:
"IN"as the operational default in service code.
Use:
FinanceConfiguration.default_countryScheduled invoice issuer
Do not infer the issuer from a billing schedule.
Use:
FinanceConfiguration.default_issuing_entityIf scheduled billing requires an issuer and none is configured, the workflow should fail explicitly.
43. Service Ownership Rules
Invoice creation
invoice_workflow.create_invoice()Invoice editing
invoice_workflow.add_line_item()
invoice_workflow.remove_line_item()Invoice state changes
invoice_workflow.finalize_invoice()
invoice_workflow.cancel_invoice()
invoice_workflow.void_invoice()
invoice_workflow.mark_overdue()Payments
payment_workflow.record_payment()Credit notes
credit_note_workflow.issue_credit_note()Scheduled invoices
billing_schedule_workflow.generate_due_invoices()Identity
identity_workflow.create_address()
identity_workflow.update_address()
identity_workflow.create_contact()
identity_workflow.update_contact()
identity_workflow.create_tax_profile()
identity_workflow.update_tax_profile()Numbering
numbering.generate_document_number()Tax
tax.resolver
tax.rate_lookup
tax.policies.*44. Rules for Future Development
When extending the finance application:
Rule 1 — Do not put business rules in models
Avoid:
def save(...):
# business workflowBusiness rules belong in services/workflows.
Rule 2 — Do not mutate invoice status directly
Avoid:
invoice.status = "PAID"
invoice.save()Use the state machine/workflow.
Rule 3 — Do not edit finalized invoices
If a finalized invoice needs a reduction:
CreditNoteshould be used.
Rule 4 — Do not calculate tax inside views
Views should call workflows.
Tax calculation belongs under:
services/tax/
services/calculators/tax.pyRule 5 — Do not query TaxRate from arbitrary application code
Use:
services.tax.rate_lookupso rate-selection logic remains centralized.
Rule 6 — Do not create financial document numbers manually
Use:
services.numberingRule 7 — Do not bypass identity workflows
Address, contact, and tax-profile mutations should pass through:
identity_workflowRule 8 — Prefer database configuration for operational defaults
Examples:
default country
default issuing entity
tax rates
payment methodsshould not become hardcoded deployment assumptions.
Rule 9 — Keep calculators pure
Calculation functions should avoid:
- database access
- caching
- side effects
- model mutation
This makes them deterministic and easy to test.
Rule 10 — Add abstractions only when a real requirement exists
Do not introduce:
- repository layers
- generic interfaces
- event buses
- package adapters
- CQRS
- generic DTO hierarchies
unless a real use case requires them.
45. Adding a New Country Tax Policy
To add a new country's tax support:
Step 1
Create a policy under:
finance/services/tax/policies/For example:
vat_eu.pyStep 2
Implement the TaxPolicy interface.
Step 3
Add the country to:
finance/services/tax/resolver.pyExample:
POLICY_REGISTRY = {
"IN": IndiaGSTPolicy,
"DE": GermanyVATPolicy,
}Step 4
Implement the required rate lookup/configuration model behavior.
Step 5
Add appropriate tests.
The invoice workflow should not need country-specific branching.
46. Adding a New Financial Document Type
Document numbering already supports:
DEBIT_NOTEthrough:
DOCUMENT_PREFIXESHowever, adding a document type should include:
- Domain model if required
- Workflow
- State/lifecycle rules if applicable
- API exposure if required
- Admin support
- Audit/history support
- Validation
- Tests
- Numbering integration
Do not implement a new document type by copying invoice logic into another view.
47. Transaction Boundaries
Financial mutations are transactional.
The following workflows use database transactions:
create_invoice
add_line_item
remove_line_item
finalize_invoice
cancel_invoice
void_invoice
record_payment
issue_credit_note
create_address
update_address
create_tax_profile
update_tax_profile
create_contact
update_contact
generate_due_invoicesThis ensures a partially completed financial operation does not leave the database in an inconsistent state.
48. Concurrency Considerations
Document numbering is explicitly concurrency-safe.
The sequence row is locked using:
select_for_update()inside an atomic transaction.
This protects against:
Request A -> sequence 123
Request B -> sequence 123and ensures distinct sequence numbers are issued.
Other high-contention financial workflows should preserve transactional integrity when future concurrency-sensitive features are introduced.
49. Operational Flow
A normal manually created invoice follows:
Entity
|
+--> Address
|
+--> Tax Profile
|
v
Invoice Creation
|
+--> Document Number
|
+--> Line Items
|
+--> Tax Policy
|
+--> Tax Rate
|
+--> Tax Breakdown
|
+--> Totals
|
v
DRAFT
|
v
PENDING
|
v
SENT
|
+-------------------+
| |
v v
PARTIALLY_PAID PAID
| |
v v
PAID REFUNDEDCancellation/void paths are controlled by the state machine.
50. Scheduled Billing Flow
Scheduler
|
v
generate_scheduled_invoices
|
v
generate_due_invoices()
|
v
Find ACTIVE schedules
|
v
next_billing_date <= as_of
|
v
Resolve default issuing entity
|
v
Resolve customer default address
|
v
invoice_workflow.create_invoice()
|
+--> numbering
+--> tax
+--> totals
+--> audit
|
v
Advance next_billing_date
|
v
Complete schedule when end_date reached51. Financial Integrity Model
The finance architecture deliberately separates three concerns:
Identity
|
+--> Address
+--> Contact
+--> TaxProfile
Reference Configuration
|
+--> FinanceConfiguration
+--> TaxRate
+--> PaymentMethod
Financial Transactions
|
+--> Invoice
+--> LineItem
+--> Payment
+--> CreditNote
+--> BillingScheduleThis prevents tax configuration, entity identity, and transaction state from becoming one coupled model structure.
52. Current Limitations
The current implementation has several deliberate limitations.
Payment allocation
Payments currently belong directly to one invoice.
A separate allocation model is not implemented.
Refunds
REFUNDED exists as an invoice state, but a dedicated refund workflow/payment-refund model is not currently implemented.
Debit notes
Numbering support exists, but debit-note business workflow/model behavior is not yet implemented.
Multi-country tax
The resolver is extensible, but India GST is the only fully implemented country policy.
Financial accounting
The application is an operational finance/invoicing layer, not a full accounting ledger.
53. Recommended Testing Strategy
Tests should be organized around domain behavior rather than only model coverage.
State machine tests
Verify:
- every legal transition
- every illegal transition
- editable states
- terminal states
Tax tests
Verify:
- India interstate GST
- India intrastate GST
- HSN/SAC-specific rate
- general fallback rate
- effective dates
- exemption
- missing rate
- unmapped country
Invoice tests
Verify:
- invoice requires line items
- line subtotal
- tax
- grand total
- line-item addition
- line-item removal
- cannot remove final line
- finalized invoice cannot be edited
Payment tests
Verify:
- positive amount
- valid invoice states
- outstanding balance
- partial payment
- full payment
- overpayment rejection
Credit note tests
Verify:
- eligible states
- positive amount
- cumulative credit limit
- numbering
Billing tests
Verify:
- due schedules
- future schedules
- end dates
- calendar-based frequency advancement
- configured issuer
- missing issuer
- missing customer address
Numbering tests
Verify:
- prefixes
- yearly reset
- issuer isolation
- document-type isolation
- concurrent generation
54. Architecture Summary
The finance application can be summarized as:
+------------------+
| API |
| DRF ViewSets |
+--------+---------+
|
v
+------------------+
| Serializers |
| DTOs |
+--------+---------+
|
v
+-----------------------------+
| Workflows |
|-----------------------------|
| Identity |
| Invoice |
| Payment |
| Credit Note |
| Billing Schedule |
+------+-----------------------+
|
+--------------+--------------+
| | |
v v v
+-------------+ +------------+ +------------+
| State | | Calculators| | Tax Engine |
| Machine | | Totals/Tax | | Policies |
+-------------+ +------------+ +------------+
| | |
+--------------+--------------+
|
v
+--------------+
| Django ORM |
| Finance |
| Models |
+------+-------+
|
v
+--------------+
| PostgreSQL |
+--------------+55. Core Architectural Principle
The finance application should always preserve this boundary:
Models
= persistence
Services
= domain logic
Workflows
= state-changing business operations
Calculators
= deterministic financial mathematics
Tax Policies
= jurisdiction-specific tax behavior
API
= transport and authorization
Admin
= operational configuration and administrationThe most important implementation rule is:
Financial business operations must enter through an explicit workflow rather than through direct model mutation.
This is the central design constraint that keeps invoice lifecycle, tax calculation, numbering, payments, credit notes, compliance, and audit behavior consistent across API, Admin, scheduled jobs, and development data generation.