Skip to main content

Billing::Agreement

Purpose

A Service Agreement captures the negotiated commercial terms between Jod and an Org::Company before any invoicing can happen. It records what was agreed (platform fee rates, unit prices, discounts) and ties those terms to specific entitlement instruments (Gig, Placement, Workforce).

Agreements exist to decouple negotiation from invoicing: sales can negotiate and sign terms today, while invoices are generated later using the agreement's terms as input.

Model Context

ContextDetails
AggregateBilling::Agreement (root) + Billing::AgreementTerm (child)
LayerCommercial Documents
Upstream dependenciesBilling::Account (must exist), Billing::Entitlement (terms reference instruments)
Downstream dependentsBilling::Invoice (uses agreement terms to compute invoice line items)

State Machine

FromToTriggerNotes
(new)activeUC-1: Agreement createdDefault status on creation
activesupersededUC-3: New agreement replaces this oneOld agreement preserved for audit
activeterminatedUC-5: Admin terminatesNo further invoices allowed

Use Cases

IDUse CaseTriggerActor
UC-1Create an agreement with negotiated commercial terms for a companySales finalizes negotiated terms after signingAdmin (sales)
UC-2Edit an agreement before any invoice references itSales needs to correct a mistake before invoicingAdmin (sales)
UC-3Supersede an agreement when terms are renegotiatedCompany renegotiates terms (renewal, rate change)Admin (sales)
UC-4View agreement details and term history for a companyAdmin needs to review current or past termsAdmin
UC-5Terminate an agreement when a company relationship endsCompany relationship ends or agreement voidedAdmin
UC-6Use agreement terms to compute invoice line item pricingCompany wants to purchase creditsAdmin / System

UC-1: Create an agreement with negotiated commercial terms for a company

FieldDetails
ActorIdentities::Admin (sales/ops)
TriggerSales finalizes negotiated commercial terms with a company after signing

Preconditions:

  • Billing::Account exists for the Org::Company
  • Admin has the signed agreement document (PDF)

System Behaviour:

  1. Admin enters agreement details:
    • code — human-readable identifier (e.g. SG-GIG-2026-0001)
    • document_url — S3 path to signed PDF
    • effective_from — when the agreement takes effect. Frontend sends a bare civil date (YYYY-MM-DD); the server anchors it to midnight in the company's timezone via DateTimeUtils.parse_civil_date (see the datetime-and-timezones convention doc), then stores the resulting calendar day in a Postgres date column
    • effective_to — optional expiry (null = open-ended), stored as a date, inclusive of the entered date — an agreement "effective to 4 July" still covers invoices issued on 4 July. That inclusivity is expressed at comparison time (effective_to >= today, see UC-6) rather than by storing an end-of-day time
  2. Admin defines one or more terms, each specifying:
    • which Billing::Entitlement instrument it applies to
    • term_key — what is being negotiated (fee_rate, unit_price, discount_rate)
    • term_value + term_unit — the negotiated value (e.g. 2000 bps = 20% fee rate)
  3. System validates inputs (see Business Rules)
  4. Agreement + terms created in a single DB transaction
  5. Admin audit trail recorded (admin_created_by)

Business Rules:

  • At least one AgreementTerm is required
  • Each (agreement, entitlement, term_key) combination must be unique
  • term_value must be a positive integer
  • term_key must be one of: fee_rate, discount_rate, unit_price
  • term_unit must be one of: bps, cents, credits
  • fee_rate and discount_rate terms are always assumed to be bps — the system rejects any other term_unit paired with these two term keys. unit_price has no such pairing check today (it isn't read by invoicing at all — see UC-6); broader agreement-term validation is a deliberate follow-up, out of scope for now.
  • Terms capture negotiated commercial values only — not tax (tax is determined by Billing::LegalEntity + Billing::ProductPrice at invoice time)

Postconditions:

  • Agreement is persisted and visible on the billing account detail page
  • No entitlements are granted — that only happens via invoice posting
  • The agreement is available for use when creating future invoices

UC-2: Edit an agreement before any invoice references it

FieldDetails
ActorIdentities::Admin
TriggerSales needs to correct a mistake or update terms before any invoice has been created using this agreement

Preconditions:

  • Agreement exists
  • No invoice has been created referencing this agreement

System Behaviour:

  1. Admin modifies agreement fields (code, dates, document_url) and/or terms
  2. System verifies no invoices reference this agreement
  3. Agreement + terms updated in a single DB transaction
  4. Admin audit trail recorded (admin_updated_by)

Business Rules:

  • If any Billing::Invoice references this agreement, the edit is rejected — admin must create a new agreement instead (see UC-3)
  • All creation-time validations still apply

Postconditions:

  • Agreement reflects updated values
  • Audit trail shows who changed what and when

Open Questions:

  • Should we track edit history (versioning) or is the admin audit trail sufficient?
  • Do we allow adding/removing terms on edit, or only changing values?

UC-3: Supersede an agreement when terms are renegotiated

FieldDetails
ActorIdentities::Admin
TriggerCompany renegotiates terms (annual renewal, rate change, contract amendment)

Preconditions:

  • At least one existing agreement for this billing account

System Behaviour:

  1. Admin creates a new agreement with updated terms (same flow as UC-1)
  2. System marks the previous agreement's status as superseded
  3. The new agreement becomes the one used for future invoices

Business Rules:

  • Invoices already issued under the old agreement are not affected — they snapshot terms at issuance time
  • The old agreement remains visible and auditable (never deleted)
  • effective_from of the new agreement should be >= today (no retroactive superseding)

Postconditions:

  • Old agreement status = superseded, retains all historical data
  • New agreement is the active one for future invoice creation

Open Questions:

  • Should the system auto-detect the "active" agreement based on effective_from/effective_to date ranges, or should there be an explicit status field (active, superseded, terminated)?
  • Can a company have multiple simultaneously active agreements for different entitlement types (e.g. one agreement for Gig terms, another for Placement terms)? Or is it always one agreement per account?
  • Should superseding require the admin to explicitly link the new agreement to the one it replaces (supersedes_agreement_id)?

UC-4: View agreement details and term history for a company

FieldDetails
ActorIdentities::Admin
TriggerAdmin needs to review current or past terms for a company

Preconditions:

  • Billing::Account exists

System Behaviour:

  1. Admin navigates to Billing Account detail page → Agreements tab
  2. System displays all agreements for the account, ordered by effective_from descending
  3. Each agreement shows:
    • Code, effective dates, document link
    • All terms with formatted values (e.g. "20%", "$5.00", "1000 credits")

Business Rules:

  • Superseded and historical agreements remain visible (audit requirement)
  • Admin can open the signed agreement PDF via document_url

Postconditions:

  • Read-only operation — no data changes

UC-5: Terminate an agreement when a company relationship ends

FieldDetails
ActorIdentities::Admin
TriggerCompany relationship ends, or agreement needs to be voided

Preconditions:

  • Agreement exists and is active

System Behaviour:

  1. Admin terminates the agreement, providing a reason
  2. System marks agreement status as terminated
  3. No further invoices can reference this agreement

Business Rules:

  • Existing invoices are not affected
  • If this was the only active agreement, the company cannot be invoiced until a new agreement is created
  • Reason for termination should be recorded (metadata or notes field)

Postconditions:

  • Agreement status = terminated
  • Company's billing account still exists and retains entitlement balances

UC-6: Use agreement terms to compute invoice line item pricing

FieldDetails
ActorIdentities::Admin (or system, for automated renewals)
TriggerCompany wants to purchase credits

Preconditions:

  • An agreement is optional. When the admin selects one, it must belong to the billing account and be currently within its effective_from/effective_to window.

System Behaviour:

  1. If the admin selected an agreement, system uses it to resolve terms. If not (or the account has no agreement at all), pricing falls back entirely to the catalog.
  2. Only two term keys are read by invoicing today — fee_rate and discount_rate, both always in bps. A unit_price term is not read (invoice lines always use the catalog Billing::ProductPrice.unit_price_cents); broader agreement-term validation (including unit_price) is a deliberate follow-up, out of scope for now. Terms are looked up by (billing_entitlement_id, term_key) — generic across entitlements, not placement/gig-specific:
    • fee_rate term → the gig platform fee line item calculation. Falls back to Billing::ProductPrice.platform_fee_rate_bps (the list rate) when no agreement, or no matching term, exists.
    • discount_rate term → discount_cents = discount_rate_bps / 10_000 × the discounted line's amount_cents, landing on whichever line is Jod's own revenue: the :platform_fee line for gig (principal is the wage value owed to the buyer/company, never discounted), the :principal line for placement (no :platform_fee line exists). The discounted line's tax_cents is computed on the post-discount amount. There is no catalog fallback — no matching term (or no agreement) means discount_cents: 0.
  3. Invoice lines snapshot the resolved agreement terms at creation time

Business Rules:

  • Invoice may reference a billing_agreement_id for audit trail and pricing, but it is not required — omitting it (or having no agreement at all on the account) simply uses catalog pricing with no discount
  • When provided, the agreement must belong to the billing account and be currently effective, or invoice creation is rejected with a 422
  • Snapshotted terms on the invoice are immutable — even if the agreement is later superseded

Model Interaction:

  • Billing::Invoice → belongs_to Billing::Agreement (optional)
  • Billing::InvoiceLine snapshots the resolved platform_fee_rate_bps, discount_cents, and discount_rate_bps from the matching agreement terms (unit_price_cents always comes from the catalog)

Invariants

Rules that must always hold, regardless of use case:

  1. An agreement must belong to exactly one Billing::Account
  2. An agreement must have at least one AgreementTerm
  3. Each (agreement, entitlement, term_key) combination is unique (enforced by DB index idx_agreement_terms_unique_per_instrument)
  4. Agreements are never deleted — only superseded or terminated
  5. Agreement terms capture negotiated values only — tax is determined elsewhere
  6. Admin audit trail (admin_created_by, admin_updated_by) is always populated

Model Interactions

Related ModelRelationshipInteraction
Billing::AccountAgreement belongs_to AccountAccount must exist before agreement. One account can have many agreements (historical).
Billing::AgreementTermAgreement has_many AgreementTermsCreated together via nested attributes. Terms define the commercial values for each entitlement instrument.
Billing::EntitlementAgreementTerm belongs_to EntitlementEach term is scoped to a specific instrument (gig, placement, workforce).
Billing::InvoiceInvoice references AgreementActive agreement's terms inform invoice pricing. Invoice snapshots terms at creation.
Identities::AdminAgreement tracks created_by / updated_byAudit trail for who created or last modified the agreement.

Schema Gaps

Items identified during this specification that need resolution before related features can be built:

GapImpactSuggested Resolution
No status column on billing_agreementsCannot distinguish active vs superseded vs terminatedAdd status enum column with default active
No logic to determine "active" agreement for an accountInvoice creation (UC-6) needs to know which agreement to useInterim: Billing::Invoices::TeamCreateValidator now checks the referenced agreement's effective_from/effective_to window against "today" resolved in the billing account's company timezone (Time.current.in_time_zone(company_timezone).to_date) at invoice creation — not a raw Time.current comparison, since effective_from/effective_to are date columns with no instant to compare directly. Still open: no status column, so a terminated agreement inside its date window would still validate — the status column above is the durable fix.
No update endpointAdmin cannot correct mistakes (UC-2)Add TeamUpdateManager with invoice-reference guard
No supersedes_agreement_id columnCannot trace agreement lineageOptional — could rely on effective_from ordering instead