Skip to main content

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

ContextDetails
EntityBilling::LegalEntity (independent — not an aggregate root)
LayerCatalog & Pricing
Upstream dependenciesGeo::Country (the country this legal entity is registered in)
Downstream dependentsBilling::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

FromToTriggerNotes
(new)activeUC-1: Legal entity createdDefault status on creation
activeinactiveUC-3: Admin deactivatesNo new invoices or prices can reference this entity

Use Cases

IDUse CaseTriggerActor
UC-1Create a legal entity for a new jurisdictionJod expands into a new country/marketAdmin (finance)
UC-2Edit a legal entity's mutable fieldsAddress changes, Xero integration configuredAdmin (finance)
UC-3Deactivate a legal entity that is no longer operatingJod exits a market or restructures legal entitiesAdmin (finance)
UC-4Generate the next invoice number for a legal entityInvoice creation requires sequential numberingSystem
UC-5View all legal entitiesAdmin reviewing billing configurationAdmin

FieldDetails
ActorIdentities::Admin (finance)
TriggerJod expands into a new country/market

Preconditions:

  • Geo::Country exists for the target jurisdiction

System Behavior:

  1. 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 address
    • tax_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-)
  2. Admin optionally configures xero_organisation_id for accounting integration
  3. System initializes invoice_number_sequence to 0
  4. System creates the legal entity with status: active
  5. Admin audit trail recorded

Business Rules:

  • registration_number must be unique across all legal entities
  • invoice_number_prefix must be unique across all legal entities
  • default_currency must be a valid ISO 4217 currency code appropriate for the country
  • tax_regime must match the country of registration (e.g. sg_gst for Singapore, id_vat for Indonesia, kr_vat for 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

FieldDetails
ActorIdentities::Admin (finance)
TriggerRegistered address changes, Xero integration is configured or updated

Preconditions:

  • Legal entity exists

System Behavior:

  1. Admin modifies allowed fields: registered_address, xero_organisation_id
  2. System validates inputs
  3. 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 422 error
  • 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_address into 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

note

This use case is not implemented since Legal Entities are unlikely to be deactivated.

FieldDetails
ActorIdentities::Admin (finance)
TriggerJod exits a market, or legal entity is restructured/replaced

Preconditions:

  • Legal entity exists and is active

System Behavior:

  1. Admin deactivates the legal entity
  2. System sets status: inactive
  3. 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)?

note

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.

FieldDetails
ActorSystem
TriggerInvoice creation requires a sequential invoice number

Preconditions:

  • Legal entity exists and is active

System Behavior:

  1. System acquires a row-level lock on the legal entity record (SELECT ... FOR UPDATE)
  2. System increments invoice_number_sequence by 1
  3. System generates the full invoice number: {invoice_number_prefix}{zero-padded sequence}
    • Example: prefix SG-INV-, sequence 42 → SG-INV-000042
  4. 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_sequence incremented
  • Unique invoice number returned to caller

FieldDetails
ActorIdentities::Admin
TriggerAdmin reviewing billing configuration

Preconditions:

  • None

System Behavior:

  1. Admin navigates to the billing configuration page → Legal Entities tab
  2. 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:

  1. registration_number is unique across all legal entities
  2. invoice_number_prefix is unique across all legal entities
  3. Invoice number sequence always increases per entity — gaps are allowed, but the same number is never used twice
  4. Legal entities are never hard-deleted — only deactivated
  5. Core identity fields (legal_name, registration_number, country_id, tax_regime, default_currency, invoice_number_prefix) cannot be changed after creation
  6. An inactive legal entity cannot be referenced by new invoices or new product prices
  7. A legal entity's default_currency must match the standard currency for its country
  8. Admin audit trail (admin_created_by, admin_updated_by) is always populated

Model Interactions

Related ModelRelationshipInteraction
Geo::CountryLegalEntity belongs_to CountryThe country where this Jod entity is registered. This determines which market it sells in.
Billing::ProductPriceLegalEntity has_many ProductPricesEach 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::InvoiceInvoice belongs_to LegalEntityAppears 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::AdminLegalEntity tracks created_by / updated_byTracks which admin created or last changed the legal entity.

Schema Gaps

Items identified by comparing use cases against the current DBML schema (billing.dbml):

GapImpactSuggested Resolution
No status column on billing_legal_entitiesCannot distinguish active vs inactive entitiesAdd status string [not null, default: 'active', note: 'rails_enum(:active, :inactive)']
No unique index on invoice_number_prefixCould accidentally create duplicate prefixesAdd unique index
No unique constraint on country_idCould create duplicate legal entities for the same countryAdd unique index (or flag as open question if multiple entities per country is intentionally supported)