Billing::LegalEntity
Purpose
A Legal Entity represents one of Jod's registered companies in a specific country — for example, StaffAny Pte Ltd in Singapore, or PT StaffAny in Indonesia. It stores the company's legal name, registered address, tax rules, default currency, and invoice number series.
Legal entities serve two roles: (1) they are linked to product prices (Billing::ProductPrice) so that each price carries the correct currency, tax rules, and seller details for that country; (2) they appear on invoices as the seller, providing the legal name, address, and a sequential invoice number (e.g., SG-INV-000001) needed for tax compliance.
Legal entities are set up when Jod enters a new market and rarely change after that. Key identity fields (legal name, tax rules, currency) cannot be edited after creation — this ensures that invoices already issued under this entity remain accurate.
Today, the invoice numbering sequence on this model is reserved for future use. Finance allocates invoice reference numbers manually (via Xero) and sales enters them on the Jod invoice. Once the Gig domain is implemented and Jod interfaces with Xero programmatically, this model's invoice_number_prefix and invoice_number_sequence will become the source of truth for new invoice numbers.
Model Context
| Context | Details |
|---|---|
| Entity | Billing::LegalEntity (independent — not an aggregate root) |
| Layer | Catalog & Pricing |
| Upstream dependencies | Geo::Country (the country this legal entity is registered in) |
| Downstream dependents | Billing::ProductPrice (the Jod entity that sells products in each country, linked through product prices), Billing::Invoice (seller name and address on invoices, invoice number generation, tax rules) |
State Machine
| From | To | Trigger | Notes |
|---|---|---|---|
| (new) | active | UC-1: Legal entity created | Default status on creation |
active | inactive | UC-3: Admin deactivates | No new invoices or prices can reference this entity |
Use Cases
| ID | Use Case | Trigger | Actor |
|---|---|---|---|
| UC-1 | Create a legal entity for a new jurisdiction | Jod expands into a new country/market | Admin (finance) |
| UC-2 | Edit a legal entity's mutable fields | Address changes, Xero integration configured | Admin (finance) |
| UC-3 | Deactivate a legal entity that is no longer operating | Jod exits a market or restructures legal entities | Admin (finance) |
| UC-4 | Generate the next invoice number for a legal entity | Invoice creation requires sequential numbering | System |
| UC-5 | View all legal entities | Admin reviewing billing configuration | Admin |
UC-1: Create a legal entity for a new jurisdiction
| Field | Details |
|---|---|
| Actor | Identities::Admin (finance) |
| Trigger | Jod expands into a new country/market |
Preconditions:
Geo::Countryexists for the target jurisdiction
System Behavior:
- Admin enters legal entity details:
legal_name— official registered company name (e.g., "StaffAny Pte Ltd")registration_number— official company registration (e.g., UEN for Singapore)registered_address— official registered office addresstax_regime— applicable tax system (sg_gst,id_vat,kr_vat)default_currency— primary currency for this jurisdiction (e.g.,SGD)invoice_number_prefix— prefix for invoice numbering (e.g.,SG-INV-)
- Admin optionally configures
xero_organisation_idfor accounting integration - System initializes
invoice_number_sequenceto 0 - System creates the legal entity with
status: active - Admin audit trail recorded
Business Rules:
registration_numbermust be unique across all legal entitiesinvoice_number_prefixmust be unique across all legal entitiesdefault_currencymust be a valid ISO 4217 currency code appropriate for the countrytax_regimemust match the country of registration (e.g.sg_gstfor Singapore,id_vatfor Indonesia,kr_vatfor Korea)- Creating a legal entity is rare — it only happens when Jod starts operating in a new country
- The legal entity is immediately active — no draft state
Postconditions:
- Legal entity exists and is active
- Invoice number sequence initialized to 0
- Product prices can now be created referencing this legal entity
UC-2: Edit a legal entity's mutable fields
| Field | Details |
|---|---|
| Actor | Identities::Admin (finance) |
| Trigger | Registered address changes, Xero integration is configured or updated |
Preconditions:
- Legal entity exists
System Behavior:
- Admin modifies allowed fields:
registered_address,xero_organisation_id - System validates inputs
- Admin audit trail recorded
Business Rules:
- Mutable fields:
registered_address,xero_organisation_id - Immutable fields:
legal_name,registration_number,country_id,tax_regime,default_currency,invoice_number_prefix— these are part of the entity's legal identity and are already used by existing invoices and price records — changing them would make those records inaccurate - The API rejects any attempt to modify an immutable field with a descriptive
422error - If an immutable field genuinely needs to change (e.g., company rebranding), create a new legal entity and deactivate the old one
- Address changes only affect future invoices — invoices already created keep the address that was used at the time
Postconditions:
- Legal entity reflects updated values
- Audit trail shows who changed what and when
Open Questions:
- Should the system copy
registered_addressinto the invoice when it is created (like it does for bill-to fields), or should it always read the current address from the legal entity? Recommendation: copy it into the invoice, so historical invoices always show the address that was correct at the time
UC-3: Deactivate a legal entity that is no longer operating
This use case is not implemented since Legal Entities are unlikely to be deactivated.
| Field | Details |
|---|---|
| Actor | Identities::Admin (finance) |
| Trigger | Jod exits a market, or legal entity is restructured/replaced |
Preconditions:
- Legal entity exists and is
active
System Behavior:
- Admin deactivates the legal entity
- System sets
status: inactive - Admin audit trail recorded
Business Rules:
- Existing invoices are NOT affected — they reference the legal entity historically
- No new invoices can be created with this legal entity as the seller
- No new product prices can be created referencing this legal entity
- If there are still active product prices linked to this entity, the system warns the admin — but the warning does not block deactivation
- Deactivation is irreversible — there is no reactivation endpoint. To re-enter a market, create a new legal entity
Postconditions:
- Legal entity status =
inactive - Invoice number sequence is preserved (for audit — the numbers were consumed)
Open Questions:
- Should deactivation be blocked if there are active (non-expired, non-discarded) product prices referencing this entity? Or just warn?
- Should deactivation be reversible (allow reactivation)?
UC-4: Generate the next invoice number for a legal entity
This use case is not invoked by the current manual flow. Today, finance allocates invoice numbers in Xero and sales enters them on the Jod invoice (see billing-invoice.md UC-1). UC-4 will be invoked once the Gig domain is implemented and Jod begins generating invoice numbers programmatically and pushing them to Xero. The invoice_number_prefix and invoice_number_sequence columns are retained for that future use.
| Field | Details |
|---|---|
| Actor | System |
| Trigger | Invoice creation requires a sequential invoice number |
Preconditions:
- Legal entity exists and is
active
System Behavior:
- System acquires a row-level lock on the legal entity record (
SELECT ... FOR UPDATE) - System increments
invoice_number_sequenceby 1 - System generates the full invoice number:
{invoice_number_prefix}{zero-padded sequence}- Example: prefix
SG-INV-, sequence 42 →SG-INV-000042
- Example: prefix
- System returns the generated invoice number
Business Rules:
- Invoice number generation uses a database lock to prevent two invoices from getting the same number at the same time
- Sequence numbers always increase. Gaps are allowed (voided invoices still use up a number), but duplicates are never allowed
- This is standard accounting practice: once a number is assigned, it cannot be reused even if the invoice is voided
- The zero-padding width should be consistent (6 digits recommended, supporting up to 999,999 invoices per entity)
- An inactive legal entity cannot generate new invoice numbers
Postconditions:
invoice_number_sequenceincremented- Unique invoice number returned to caller
UC-5: View all legal entities
| Field | Details |
|---|---|
| Actor | Identities::Admin |
| Trigger | Admin reviewing billing configuration |
Preconditions:
- None
System Behavior:
- Admin navigates to the billing configuration page → Legal Entities tab
- System displays all legal entities with: legal name, registration number, country, tax regime, default currency, invoice number prefix, current sequence number, status
Business Rules:
- Inactive legal entities remain visible (audit requirement)
- Filterable by status and country
Postconditions:
- Read-only operation — no data changes
Invariants
Rules that must always hold, regardless of use case:
registration_numberis unique across all legal entitiesinvoice_number_prefixis unique across all legal entities- Invoice number sequence always increases per entity — gaps are allowed, but the same number is never used twice
- Legal entities are never hard-deleted — only deactivated
- Core identity fields (
legal_name,registration_number,country_id,tax_regime,default_currency,invoice_number_prefix) cannot be changed after creation - An inactive legal entity cannot be referenced by new invoices or new product prices
- A legal entity's
default_currencymust match the standard currency for its country - Admin audit trail (
admin_created_by,admin_updated_by) is always populated
Model Interactions
| Related Model | Relationship | Interaction |
|---|---|---|
Geo::Country | LegalEntity belongs_to Country | The country where this Jod entity is registered. This determines which market it sells in. |
Billing::ProductPrice | LegalEntity has_many ProductPrices | Each product price is linked to a legal entity as the seller for that country. The price uses the legal entity's tax rules and currency. |
Billing::Invoice | Invoice belongs_to LegalEntity | Appears on invoices as the seller. Provides the legal name and address for the invoice header, and generates sequential invoice numbers (e.g., SG-INV-000042). |
Identities::Admin | LegalEntity tracks created_by / updated_by | Tracks which admin created or last changed the legal entity. |
Schema Gaps
Items identified by comparing use cases against the current DBML schema (billing.dbml):
| Gap | Impact | Suggested Resolution |
|---|---|---|
No status column on billing_legal_entities | Cannot distinguish active vs inactive entities | Add status string [not null, default: 'active', note: 'rails_enum(:active, :inactive)'] |
No unique index on invoice_number_prefix | Could accidentally create duplicate prefixes | Add unique index |
No unique constraint on country_id | Could create duplicate legal entities for the same country | Add unique index (or flag as open question if multiple entities per country is intentionally supported) |