dodil/erp-method

modules/ar/module.yaml

# Invoicing (AR): the module source.
#
# Version 2 (2026-10-18, milestone M1): the spec the ar-core PACKAGE is converted from into a BRICK
# (erp-brick-ar). What changed from version 1, and why:
#   - receipts, allocation, settlement, collections, statements and credit holds moved to the PAYMENTS module
#     (modules/payments): one table of allocations, owned by the module that records money. Invoicing keeps
#     the documents (invoice, credit note, write-off) and reads settlement back. Story ids AR-05, AR-06,
#     AR-07, AR-08, AR-11 and AR-16 now live there as PAY- stories (see payments' provenance)
#   - tax is computed by the TAX module and frozen on the invoice; the rounding method is its setting
#   - invoices arrive from SOURCES through one use case (raise_draft): a sales order, a billing schedule, an
#     accepted delivery item, a contract adapter, or by hand. The quote no longer raises invoices directly:
#     an accepted quote becomes a sales order (sales-order SO-01), and the order asks invoicing (SO-21)
#   - billing schedules (one per sold line), metered drafts and the ledger mode (post | export | off) come from
#     a customer design run in a pilot engagement (2026-10-18), generalised here; its step keys are kept so a
#     process signed against that design is this reference with steps switched off (deviation_support below)
#
# Version 3 (2026-10-22, DRAFT for sign-off; version 2 below stays the signed one until it is signed):
#   - dated rows, no back-dating: "today" is the date in ar_settings.business_timezone; a billing profile,
#     an account-map row, a ledger-mode switch or a number format takes effect after today, never on or before
#     it. The only dates that may be today or earlier are accounting facts dated by their document (an
#     invoice's issue date is the day it is issued; its tax point is the supply date its source records).
#     An issued document is never edited; a correction is a credit note, a change is a new row
#   - owner-held controls (ar:settings:controls, a new OWNER actor) separated from day-to-day settings
#     (ar:settings:write); system actions are exact-grant machine permissions
#   - readiness gates on every transition with preconditions, answered as {gate, met, reason, fix}
#   - every AI finding is a suggestion a person accepts or dismisses, recorded (ar_ai_finding) (AR-32)
#   - an invoice line names the order line it bills (sales_order_id, so_line_no -> so_line), its unit is
#     product's unit_code, and its tax amounts are the tax module's, frozen at issue
#   - migration: the Odoo slice (account.move out_invoice / out_refund) read-only, reconciled to zero
#     unexplained and signed by the customer (AR-33, AR-34, workflow migration_run, section `migration:`)
#
# Version 4 (2026-10-22, the SAP full-scope pass, DRAFT for the one re-sign; extends erp-brick-ar as built, version 3
# unchanged otherwise):
#   - L2C leftovers folded in: the rename cancel_draft -> cancel_draft_invoice (and every ar transition named with its
#     noun, unique across the method); the build's rows supplier_name and supplier_address
#     (functional_currency is gl's, per company_code, read via GET /companies/{code}); ONE deposit rule with sales-order (a deposit may be deducted in PART, tracked per
#     deposit, never beyond it, each deduction pinned to the deposit's frozen tax code: tax TAX-47) - this CHANGES AR-15's
#     signed "deducted" (whole) to "deducted in whole or in part"; the "tax point in a filed return" gate now has its
#     callee (tax TAX-33); composition roles stated (the cashier holds ar:invoice:read)
#   - a down-payment REQUEST that is not a VAT invoice, settled into a deposit invoice when paid (also the pro forma)
#     (AR-35, AR-36); early-settlement discount with its VAT correction (AR-37, AR-38); statutory late-payment interest
#     and compensation (AR-39); intercompany billing to a company of the group (AR-40, AR-41); recurring entries by hand
#     (AR-42); e-invoicing by Peppol BIS Billing 3.0 (AR-43, AR-44); doubtful-debt provisions (AR-45); deferred revenue
#     released over the service period (AR-46); document splitting carried to the ledger (AR-47)
#   - dunning STAYS in payments (PAY-31, reminder levels): invoicing supplies the documents and the interest note; the
#     customer statement stays in payments (PAY-11) and invoicing links to it
#
# SIGNED OFF BY THE CUSTOMER (version 2): Seemo, DODIL (reference customer), 2026-10-18, after the M1 review
# page. Each process is signed on its own (signoff/ar/<process>.json, erp-compose signoff verify); the
# decisions settled on the review are under `decisions:` below.
#
# Four decisions an accountant checks first:
#   - the invoice keeps ONE immutable id for life; the invoice NUMBER is stamped at issue
#   - a stored lifecycle (a person's decision) is separate from a derived settlement (arithmetic)
#   - payments, credit notes and write-offs all reduce a balance through ONE allocation table (in payments)
#   - tax is per line with a per-rate summary, computed by the tax module with an HMRC-permitted rounding
#     method; subtotal + tax = total always holds
#
# APQC PCF v8.0: 10743 (9.2.2 Invoice customer): 10795 (9.2.2.2 Generate customer billing data), 10796
# (9.2.2.3 Transmit billing data to customers), 10797 (9.2.2.4 Post receivable entries), 10798 (9.2.2.5
# Resolve customer billing inquiries); 10808 (9.2.4.5 Process adjustments/write off balances).

# Version 8 (2026-10-25, DRAFT for sign-off, wave 4; a seam, no process change):
#   - POST /invoices/raise takes source_kind timesheet (timesheets' monthly run of a contract or manual project,
#     TS-13), with its own seam row
#
# Version 7 (2026-10-25, DRAFT for sign-off; the fixes the bricks and the end-to-end wiring found, and wave 3's routes):
#   - a raised line's net with a pricing unit: net = quantity_priced x unit_price / pricing_quantity x (1 -
#     discount_pct / 100), rounded once (shared.yaml money.rates.per_quantity: pricing_quantity is the count the
#     price is per, 100 in 2.4500 per 100); a given net_amount is the caller's exact net and is kept, never
#     recomputed
#   - GET /billed-lines: net_amount is positive for an invoice line and for a credit-note line alike; kind
#     credit_note says it reduces the basis
#   - deductions is the one name on POST /invoices/raise; deposit_deductions (and deposit_invoice_ids) are
#     deprecated and accepted until ar's next major
#   - the AR-34 migration seams name their routes and machine permissions: bp:migration:read, tax:migration:read,
#     product:migration:read, and payments' settlement read
#   - POST /invoices/raise takes source_kind expense (expenses' rebill, EXP-21) and pos (an on-account till sale,
#     POS on_account), each with its own seam row
#   - use cases (MCP tools) of the shared families name the module noun (tool_names)
#
id: ar
name: Invoicing
short: Invoicing
version: 8
maturity: tested           # ar-core implements version 1's invoice, credit and posting stories on a bucket (pattern §14); the brick is not built; version 3 is a draft for sign-off
summary: >-
  Send invoices for what you sold (from an order, a repeating schedule, delivered work, or by hand),
  correct them with credit notes, and keep the books in step, without re-typing anything the sales side
  already captured.

decisions:                            # settled by the reference customer on the M1 review, 2026-10-18
  - decided: "receipts, allocations, collections and credit holds move to the payments module; invoicing keeps the documents and reads settlement back (AR-10, AR-30)"
    by: "Seemo, DODIL (reference customer)"
    date: "2026-10-18"
  - decided: "a system-raised draft never issues itself by default; auto_issue_sources is a per-source setting, empty by default (AR-23)"
    by: "Seemo, DODIL (reference customer)"
    date: "2026-10-18"
  - decided: "the pilot's chase and record_payment steps stay drawn in the invoice process (chase by hand; the receipt recorded in payments)"
    by: "Seemo, DODIL (reference customer)"
    date: "2026-10-18"
  - decided: "the ledger mode defaults to post when the ledger (gl) is installed, else a monthly export to the accountant (AR-21, AR-24)"
    by: "Seemo, DODIL (reference customer)"
    date: "2026-10-18"
  - proposed: "v4: one deposit rule with sales-order: a final invoice may deduct a deposit in whole or in part, tracked per deposit, never beyond what remains; the deduction line is pinned to the deposit's frozen tax code"
    status: "for the SAP full-scope re-sign (2026-10-22): L2C leftover 'deposits'"
  - proposed: "v4: deposit_mode defaults to invoice (a deposit is a VAT invoice when asked); request (a payment request, the VAT invoice when paid) is the setting"
    status: "for the SAP full-scope re-sign (2026-10-22)"
  - proposed: "v4: an early-settlement discount taken is corrected by a credit note raised by the system at the invoice's rates (settlement_discount_vat credit_note); the no-credit-note wording is the setting"
    status: "for the SAP full-scope re-sign (2026-10-22)"
  - proposed: "v4: statutory late-payment interest is suggested, never raised automatically (late_interest_mode suggest, business customers only)"
    status: "for the SAP full-scope re-sign (2026-10-22)"

facets:
  domain: finance
  industries: []           # cross-industry; overlays would name real ones
  customer_words:
    - "send invoices"
    - "invoice the customer"
    - "monthly invoices that raise themselves"
    - "stop re-typing orders into the accounts"
    - "credit note"
    - "50% up front"
    - "pro forma"
    - "early payment discount"
    - "late payment interest"
    - "intercompany recharge"
    - "e-invoicing"
    - "Peppol"
    - "bad debt provision"
    - "deferred revenue"
  replaces: [spreadsheets, Xero, QuickBooks, Sage, a Word template and a folder, Odoo Invoicing]
  pillars: [sql]
  first_module_fit: high
  depends_on:
    - module: business-partner
      why: an invoice is addressed to a party; sales and invoicing must mean the same customer
      required: true
    - module: tax
      why: the tax on every line and the per-rate summary
      required: recommended
    - module: payments
      why: what has been paid, allocated and is outstanding
      required: recommended
    - module: sales-order
      why: invoicing what an order has due (SO-21) and its repeating lines (SO-20)
      required: optional
    - module: product
      why: line descriptions, units and billing type copied from the product
      required: optional
    - module: gl
      why: posting invoices, credits and write-offs to the books
      required: optional
    - module: crm
      why: showing a rep that a customer is overdue (AR-12)
      required: optional
  feeds: [gl, payments]
  owns_master_data: []
  references_master_data: [business_partner, product, tax_code]

actors:
  - id: biller
    name: Whoever sends the invoices (office, bookkeeper)
    does: reviews drafts, issues, sends, raises credit notes, exports to the accountant
    sees: every invoice
    permissions: [ar:invoice:read, ar:invoice:write, ar:invoice:issue, ar:invoice:send, ar:credit_note:write, ar:writeoff:write, ar:schedule:read, ar:export_batch:run, ar:schedule:write, ar:provision:write]
  - id: collector
    name: Whoever chases money and records it
    does: chases overdue invoices by hand, records what was received (in payments)
    sees: every open invoice and its history
    permissions: [ar:invoice:read, ar:invoice:collect]
  - id: approver
    name: Owner / finance manager
    does: approves credit notes, voids and write-offs, sets terms, customers' billing profiles, the account map and the document layout
    sees: everything
    permissions: [ar:invoice:read, ar:invoice:void, ar:credit_note:approve, ar:writeoff:approve, ar:customer:write, ar:settings:write, ar:migration:read, ar:provision:approve]
  - id: owner
    name: The owner (or whoever the owner names)
    does: holds the controls on the people who invoice and approve (time zone, numbering, ledger mode, auto-issue, void policy, write-off threshold, second approver) and signs the migration reconciliation
    sees: everything
    permissions: [ar:invoice:read, ar:settings:controls, ar:migration:read, ar:migration:sign]
    note: "the controls are never held by whoever they control: a biller or approver holding ar:settings:controls is a configuration error the access test refuses"
  - id: sales
    name: Sales (read-only)
    does: checks whether a customer is overdue before quoting them again
    sees: invoice status and balance for a customer; never lines or margin
    permissions: [ar:status:read]
  - id: customer
    name: The customer
    external: true
    does: receives the invoice and pays it
    sees: their own invoices, by email or the portal
    permissions: []
  - id: system
    name: The system
    does: raises drafts from schedules and sources, checks a draft before issue, posts to the ledger, plans, stops and finishes schedules, numbers issued documents, derives what is due, loads and reconciles a migration
    sees: everything, through its own service account
    permissions: [ar:invoice:read, ar:invoice:raise, ar:invoice:check, ar:invoice:post, ar:schedule:plan, ar:schedule:stop, ar:schedule:finish, ar:number_series:claim, ar:migration:load, ar:migration:reconcile, ar:invoice:settle_discount, ar:payment_request:settle, ar:revenue:release, ar:einvoice:deliver, ar:intercompany:hand_over]
    note: "each is an exact grant to one service client, never reached by a wildcard (permissions.machine)"

stories:
  # ── person ────────────────────────────────────────────────────────────────────────────────
  - id: AR-01
    kind: person
    actor: biller
    story: As the person who invoices, I want an invoice drafted from what was sold (an order, an accepted item, a schedule) in one step, so that nothing is re-typed and the customer is billed what they agreed.
    given: a confirmed sales order with invoiceable lines
    when: the order asks for its invoice (SO-21)
    then: 'a draft exists with those lines, keyed invoice_id = "so:<sales_order_id>:<request_no>", each line carrying the order line it bills (sales_order_id, so_line_no -> so_line) and that line''s unit_code; tax computed by the tax module at this invoice''s tax point; asking again returns the same invoice'
    screen: Invoice
    path: { process: ar.invoice, choices: {} }
    reference: "partial: ar-core raises from an accepted quote (keyed quote:<quote_id>); the generic source is not built"
  - id: AR-02
    kind: person
    actor: biller
    story: As the biller, I want to raise an invoice by hand for something with no order, so that ad-hoc work can still be billed.
    given: a customer, and a line with a description, quantity, price and tax category
    when: I save it
    then: line totals, the per-rate tax summary and the total are computed by the system, never typed, and the invoice is a draft
    screen: Invoice
    reference: implemented (ar-core)
  - id: AR-03
    kind: person
    actor: biller
    story: As the biller, I want issuing an invoice to fix its number, its dates and its contents, so that what the customer received cannot silently change.
    given: a draft with at least one line, usage filled in, and a customer with a bill-to address and terms
    when: I issue it
    then: 'stamped with the next number from the series, an issue date of today (business_timezone, never typed), a tax point (the supply date its source records, else the issue date) and a due date from the terms; the tax is frozen (tax TAX-11); the bill-to details in force today are copied onto it; every later edit is refused with "credit it instead"; when a gate fails, issue is refused and the screen shows each gate as {gate, met, reason, fix} (the tax content check TAX-12 among them)'
    screen: Invoice
    reference: implemented (ar-core), without the content check or the readiness answer
    customer_varies: the number format, whether it restarts each year
    note: >-
      "unique", not "gap-free-sequential": on this platform the number comes from a high-water-mark row plus
      MAX(number), with the row's primary key as the uniqueness guard and a retry on conflict, claimed with a
      plain INSERT (never an upsert); the number is re-read after issue (platform_constraints).
  - id: AR-04
    kind: person
    actor: biller
    story: As the biller, I want to correct an issued invoice with a credit note rather than an edit, so that the audit trail and the tax records survive.
    given: an issued invoice, whether paid or not
    when: I credit it in full or in part and the approver approves
    then: the credit note has its own number, a credit date of today, lines and per-rate tax (from the tax module, at the original invoice's rates for a correction), is allocated against the invoice through payments, reduces its balance, and the original stays readable and unchanged
    screen: Credit note
    reference: implemented (ar-core)
  - id: AR-09
    kind: person
    actor: approver
    story: As the owner, I want to write off a remainder that will never be paid, so that it stops appearing on the chase list and the books show the loss.
    given: an invoice with 12.40 outstanding, below the write-off threshold of 25.00
    when: a write-off is proposed with a reason and I approve it
    then: the write-off is allocated against the invoice through payments like a receipt, the balance clears, and the loss posts as bad debt where the ledger is installed
    screen: Invoice
    reference: implemented (ar-core)
  - id: AR-12
    kind: person
    actor: sales
    story: As a sales rep, I want to see whether a customer is overdue before I quote them again, so that I am not discounting for someone who has not paid.
    given: a customer with an invoice 40 days past due
    when: I open them in the sales system
    then: I see the overdue amount, the oldest age, and whether they are on credit hold, read-only, through invoicing's narrow status route, never lines
    screen: Customer billing
    reference: implemented (ar-core)
  - id: AR-13
    kind: person
    actor: biller
    story: As the biller, I want to send the invoice and know it went, so that "did you get it?" has an answer.
    given: an issued invoice
    when: I press Send
    then: the document is produced from the frozen invoice and handed to the mail capability; the send is recorded with the date and address; re-sending records a second send, never a second invoice
    screen: Invoice
    reference: implemented (ar-core)
    customer_varies: the delivery route (an email attachment, a portal, Peppol later) and the document layout
  - id: AR-14
    kind: person
    actor: biller
    story: As the biller, I want to cancel a draft that should never have existed, and void an issued invoice only where it is allowed, so that the numbering stays explainable.
    given: a draft, and an issued invoice with no allocations
    when: I cancel the draft, and the approver voids the issued one with a reason
    then: 'the draft leaves no number behind; the void keeps its number, reverses its posting on today''s date and drops out of every balance; void is allowed only as the owner''s void_policy says (default never_sent: never sent, no allocations, tax point not in a filed VAT return, answered by tax''s GET /vat-returns/tax-point-status, v4); otherwise a full credit note is the only path and the readiness answer says so'
    screen: Invoice
    reference: implemented (ar-core), without the reversing journal
  - id: AR-15
    kind: person
    actor: biller
    story: As the biller, I want a deposit invoiced before the work and deducted from the final invoice, so that "50% up front" is a real invoice.
    given: an order asking for a 50% deposit of 5,000.00 net (SO-09)
    when: the deposit is issued and later the final invoice raised
    then: 'the deposit is a normal invoice with its own tax; the final invoice shows the deposit deducted, so only the balance is due. v6: a part invoice may deduct too, not only the final one. v4 (one rule with sales-order, L2C leftover): a deduction may be the whole deposit or a PART of it (sales-order names the amount per deposit); what remains of each deposit = its net less the deductions on issued invoices and less credits against it, re-derived, never stored; a deduction above what remains is refused (gate deposit_remaining); each deduction line is pinned to the deposit''s frozen tax code and rate (tax TAX-47), so it carries the deposit''s category and tax accepts it; two finals issued at once cannot both take the same remainder (a claim row per deduction, keyed [deposit_invoice_id, final invoice_id])'
    screen: Invoice
    reference: not built
  - id: AR-17
    kind: person
    actor: biller
    story: As the biller, I want the list of invoices with their state and what is outstanding, so that I can find one without searching for it.
    given: invoices in every state
    when: I open the list
    then: I can filter by state, customer, currency, source and date, page through it, and each row shows the number, the customer, the total and the outstanding (from payments)
    screen: Invoices
    reference: "partial: ar-core's list has no pagination or source filter (known_gaps)"
  - id: AR-19
    kind: person
    actor: biller
    story: As the biller, I want a usage-billed period to arrive as a draft with the usage line left for me, so that metered billing is never estimated.
    given: a metered schedule and its period due
    when: the draft is raised
    then: it has the usage line with quantity empty; issue is refused until it is filled; a zero month may be cancelled and leaves no number
    screen: Invoice
    reference: not built
  - id: AR-23
    kind: person
    actor: biller
    story: As the biller, I want drafts raised by the system to wait for my review, so that nothing reaches a customer unchecked.
    given: drafts raised from schedules and orders overnight
    when: I open the drafts
    then: each shows its source and the AI pre-issue check (when on); none is issued automatically unless the setting says so for that source
    screen: Invoices
    reference: not built
  - id: AR-24
    kind: person
    actor: biller
    story: As the biller, I want issued invoices exported for our accountant each month, so that the books stay where they are until we move them.
    given: the ledger mode export, and 40 invoices and 2 credit notes issued in October
    when: I run the October export
    then: one file (CSV, and the accountant's import format where configured) with every document once, a batch record, and each document marked with the batch; running October again exports nothing new
    screen: Invoices
    reference: not built
  - id: AR-25
    kind: person
    actor: approver
    story: As the finance manager, I want each customer's bill-to details and terms kept with invoicing, so that the party master holds identity and invoicing holds how to bill.
    given: a customer whose party has no address or invoice email
    when: I set their bill-to name, address, invoice email, terms and currency, from tomorrow
    then: a new dated profile row (valid_from after today; a first profile may start today); drafts issued on or after valid_from use it; at issue it is copied onto the invoice, so a later change never alters an issued invoice; a valid_from of today (for a customer who already has one) or earlier is refused
    screen: Customer billing
    reference: not built

  - id: AR-31
    kind: person
    actor: owner
    story: As the owner, I want the controls on how my people invoice kept to me and dated, so that nobody loosens a rule for themselves and no change rewrites yesterday.
    given: auto_issue_sources empty and void_policy never_sent
    when: I set auto_issue_sources to schedule from tomorrow, and the approver tries to change void_policy
    then: 'a new dated control row, valid_from tomorrow; drafts raised today still wait; the approver''s change is refused (ar:settings:controls); a control dated today or earlier is refused'
    screen: Settings
    reference: not built
  - id: AR-32
    kind: person
    actor: biller
    story: As the biller, I want to accept or dismiss each AI finding on a draft, so that the check helps me and never decides for me.
    given: a draft with the AI pre-issue check on, and a finding "total 2.1x the last 6 months median"
    when: I dismiss it with a reason, or accept it and edit the line
    then: 'the finding is recorded (ar_ai_finding: step, model, the evidence, decided_by, decided_at, decision, reason); nothing in the draft changed without my action; issue never waits on a finding'
    screen: Invoice
    reference: not built
  - id: AR-33
    kind: person
    actor: owner
    story: As the owner moving off Odoo, I want a reconciliation of every count, total and open balance against Odoo that I sign, so that I know nothing was lost before I switch.
    given: a migration run loaded from Odoo and reconciled
    when: I open it, read the differences and their explanations, and sign
    then: every check is equal or explained; sign is refused while any check is neither (readiness {gate, met, reason, fix}); the signature records who, when and the run's checksums; a later re-run needs a new signature
    screen: Migration
    reference: not built

  # ── system ────────────────────────────────────────────────────────────────────────────────
  - id: AR-10
    kind: system
    actor: system
    trigger: on read (derived)
    story: The invoice shows what is still outstanding and how old it is whenever anyone looks, from payments' settlement, so that "overdue" is never stale.
    given: an invoice of 1,000.00 with a 400.00 receipt and a 100.00 credit note allocated
    when: anyone opens it
    then: 500.00 outstanding, part paid, its age; computed by payments (PAY-30); invoicing stores no paid flag
    reference: implemented (ar-core, in-module); moves to payments
  - id: AR-18
    kind: system
    actor: system
    trigger: on the billing day of each schedule period
    story: The system raises the invoice for each schedule period that falls due, so that nobody bills monthly lines by hand.
    given: a schedule of 12 monthly periods from 2026-10-01, in advance
    when: the daily pass runs on 2026-11-01, twice
    then: 'one draft for period 2, keyed invoice_id = "rec:<schedule_id>:2"; the second pass creates nothing (a plain-INSERT claim; the loser gets 23505 and moves on); which period is due, how many are billed and what remains are derived, never counted'
    reference: not built
    customer_varies: billing day (start anniversary or calendar month), in advance or in arrears, proration
  - id: AR-26
    kind: system
    actor: system
    trigger: when the last period of a schedule is raised
    story: The system finishes a schedule once every period is billed.
    given: a schedule of 12 periods with 12 raised
    when: the pass runs
    then: the schedule is finished; nothing more is raised
    reference: not built

  - id: AR-34
    kind: system
    actor: system
    trigger: when a migration run is loaded (migration.odoo below)
    story: The system carries Odoo's customer invoices and credit notes over as history, read-only, so that statements, VAT records and open balances continue without re-keying.
    given: an Odoo database with 1,200 posted out_invoice and 40 out_refund moves, mapped partners and tax codes
    when: the load runs twice
    then: 'one invoice per move keyed invoice_id = "odoo:<db>:<move id>", issued with Odoo''s number, dates, lines, tax and totals exactly as Odoo stored them (never recomputed, never re-posted to the ledger, never re-sent, never raised again by a schedule); credit notes keyed the same way and linked to the invoice they reverse; the second run changes nothing'
    reference: not built

  # ── integration ───────────────────────────────────────────────────────────────────────────
  - id: AR-20
    kind: integration
    actor: system
    trigger: when a source asks for an invoice (a sales order SO-21, a deposit SO-09, an adapter)
    story: Sources ask invoicing for a draft through one use case, raise_draft(source_kind, source_id, bp_id, currency, lines), so that every way of selling bills the same way.
    given: a sales order's request
    when: it calls raise_draft
    then: one draft keyed on (source_kind, source_id); a second call returns it; lines keep their source keys so "billed vs sold" per source line is one GROUP BY (billed_per_source)
    owner: ar
    reference: "partial: ar-core has the quote path only"
  - id: AR-21
    kind: integration
    actor: system
    trigger: when an invoice is issued or voided, or a credit note or write-off approved, and the ledger mode is post
    story: Invoicing posts the accounting entries to the ledger, so that the books agree with the invoices without re-keying.
    given: an issued invoice of 1,000.00 plus 200.00 VAT, with the account map configured
    when: it posts
    then: a balanced journal (receivable 1,200.00 / revenue 1,000.00 / VAT by code 200.00) through the ledger's route, journal_id derived from the invoice id so re-posting changes nothing; a closed period is refused and named
    owner: gl
    reference: implemented (ar-core), except the void's reversing journal
  - id: AR-22
    kind: integration
    actor: biller
    trigger: when a customer is invoiced for the first time
    story: The customer comes from the shared record rather than being typed again.
    given: business-partner is installed
    when: an invoice is raised for a company that already exists in sales
    then: it uses the same bp_id (derived from the bp_key, business-partner v0.3.1), and no second customer row is created
    owner: business-partner
    reference: implemented (ar-core), with the bp_key/bp_id column fault in known_gaps
  - id: AR-27
    kind: integration
    actor: system
    trigger: when a source confirms a line that repeats (a recurring or metered order line SO-20, a contract line through an adapter)
    story: A repeating line becomes one billing schedule, so that each line can end on its own date.
    given: 'plan_billing(source_kind, schedule_id = "<source_id>|<line_id>", bp_id, starts_on, periods, unit_price, discount_pct, currency, billing_kind, billing_plan)'
    when: it is called twice
    then: one schedule per line, active, starts_on as the source line records it (an accounting fact of the order); a period whose billing day is already past is raised in the next daily pass, issued today, covering its own period; the second call changes nothing
    owner: ar
    reference: not built
  - id: AR-28
    kind: integration
    actor: system
    trigger: when the source of a schedule ends early (an order cancelled, a contract terminated or superseded)
    story: Billing stops after the period in progress; issued invoices are untouched.
    given: an active schedule with 5 of 12 periods raised
    when: stop_billing(source_id, reason) is called
    then: the schedule is stopped with the reason, dated today; period 6 is never raised; the 5 issued invoices are unchanged (an earlier end the customer is owed is a credit note against them, never a back-dated stop)
    owner: ar
    reference: not built
  - id: AR-29
    kind: integration
    actor: system
    trigger: when a delivery item is accepted (a projects brick, a delivery adapter)
    story: An accepted one-off item is billed on acceptance.
    given: an accepted item worth 35,000.00
    when: raise_for_acceptance(item) is called
    then: 'one draft keyed "acc:<item_id>"; a second call returns it'
    owner: ar
    reference: not built
  - id: AR-30
    kind: integration
    actor: system
    trigger: when a credit note is approved or a write-off approved
    story: Credit notes and write-offs reduce a balance through payments' allocation, so that there is one mechanism behind every balance.
    given: an approved credit note of 100.00 against an invoice
    when: it is allocated
    then: payments records an allocation (source_kind credit_note) and the invoice's outstanding falls by 100.00; invoicing never writes pay_ tables
    owner: payments
    reference: "implemented in ar-core as its own ar_allocations; moves to payments"

  # ── v4: SAP full scope ────────────────────────────────────────────────────────────────────
  - id: AR-35
    kind: person
    actor: biller
    story: As the biller, I want to ask a customer for a down payment (or send a pro forma) without issuing a VAT invoice, so that VAT falls due only when they pay.
    given: 'deposit_mode request; a confirmed order of 10,000.00 net asking for 50% (SO-09), customer bp_id 7311'
    when: sales-order raises the deposit, or I raise a pro forma for an order or a quote by hand, and issue it
    then: 'a payment request keyed "dpr:<sales_order_id>:<request_no>" (or a uuid by hand) with its own number from the DPR- series, the amount 6,000.00 (5,000.00 net and the VAT it will carry, computed by tax for display), titled "Payment request - this is not a VAT invoice" (a pro forma is the same document, titled "Pro forma invoice - this is not a VAT invoice"); it posts nothing to the ledger, is not in the receivable and not in any VAT return; it shows on the customer''s open items as requested, never as owed; cancelling it is allowed while nothing is received'
    screen: Invoice
    reference: not built
  - id: AR-36
    kind: integration
    actor: system
    trigger: when payments allocates a receipt to a payment request (PAY-03 names the request)
    story: A paid payment request becomes the deposit invoice dated by the money, so that the VAT tax point is the day the payment was received.
    given: 'request DPR-2026-0004 for 6,000.00; a receipt of 6,000.00 received 2026-11-20 naming it; today 2026-11-21'
    when: payments calls POST /payment-requests/{request_id}/settle with {payment_id, received_on, amount}
    then: 'a deposit invoice keyed "dep:<request_id>:<n>" for the amount received (net and VAT split at the request''s rates, tax point 2026-11-20 - a document-dated fact - issue date today), issued and numbered from the INV series, frozen by tax, posted in post mode; payments then allocates the receipt to that invoice, so it is paid; a part payment issues a deposit invoice for the part and the request stays open for the rest; the same payment settles once (keyed on payment_id)'
    owner: ar
    reference: not built
  - id: AR-37
    kind: person
    actor: approver
    story: As the finance manager, I want payment terms with an early-settlement discount printed on the invoice the way HMRC requires, so that a customer paying early pays less and the VAT is right.
    given: 'terms "2/10 net 30": 2% if paid within 10 days, else net 30 days'
    when: an invoice of 1,000.00 net plus 200.00 VAT (1,200.00) is issued on 2026-11-02 under those terms
    then: 'the invoice shows the discount rate (VAT Notice 700 para 16.3.1 lists it), the date it ends (2026-11-12) and the discounted amount 1,176.00; with settlement_discount_vat invoice_wording it also prints HMRC''s statement that no credit note will be issued and the customer may only recover the VAT actually paid; the terms row is frozen once an issued invoice used it'
    screen: Settings
    reference: not built
  - id: AR-38
    kind: integration
    actor: system
    trigger: when payments allocates a receipt within the discount window that is short by the discount (PAY-16)
    story: Invoicing raises the settlement-discount credit note at the invoice's own rates, so that the VAT corrected is exactly the VAT on the discount.
    given: 'the invoice above; a receipt of 1,176.00 received 2026-11-10'
    when: payments calls POST /invoices/{invoice_id}/settlement-discount with {payment_id, received_on, discount_amount 24.00}
    then: 'a credit note kind settlement_discount for 24.00 gross (20.00 net, 4.00 VAT, the lines pinned to the invoice''s frozen codes pro rata per code: tax TAX-47), approved by the rule (the terms set the amount; no person is needed, its approved_by is "terms"), numbered CN-, credit date today, posted, and allocated by payments, so the invoice is paid; refused (gate within_discount_window) when received_on is after 2026-11-12, and (gate discount_matches_terms) when the amount is not the terms'' percentage of the gross outstanding at the receipt, both exact; a second call for the same payment returns the same credit note; under invoice_wording no credit note is raised: payments records the discount as an allocation (source_kind settlement_discount) and tax reads it for the return'
    owner: ar
    reference: not built
  - id: AR-39
    kind: person
    actor: biller
    story: As the biller, I want the statutory interest and compensation on a business customer's late payment worked out, so that we can claim what the law gives us when we decide to.
    given: 'late_interest_mode suggest; a business customer''s invoice of 12,000.00 due 2026-06-30 and paid 2026-09-28 (90 days late); the reference rate on 2026-06-30 was the base rate 4.25%, so the statutory rate for July-December is 12.25%'
    when: I open the customer and accept the suggested interest note
    then: 'interest = 12,000.00 x 12.25% x 90 / 365 = 362.47 (simple interest, daily, from the day after the due date to the receipt date read from payments), plus fixed compensation 100.00 (a debt of 10,000.00 or more; 40.00 under 1,000.00, 70.00 from 1,000.00), raised as a draft invoice kind interest with tax code OS (statutory interest is outside the scope of VAT), naming the invoice and the days; the reference rates are dated rows (ar_interest_rate, from 30 June and 31 December); consumers are never charged (the treatment from tax says business or consumer); with mode off nothing is suggested; the draft is reviewed and issued like any other'
    screen: Invoice
    reference: not built
  - id: AR-40
    kind: person
    actor: biller
    story: As the biller of one company in a group, I want to invoice another company of the group, so that intercompany recharges are real invoices both sides can reconcile.
    given: 'company UK01 and company UK02 in one installation; UK02 is a business partner flagged as intercompany partner of company code UK02; a management recharge of 8,000.00 net'
    when: I raise and issue the invoice from UK01 to UK02
    then: 'the invoice carries company_code UK01 and trading_partner UK02, numbered from UK01''s own series; it posts to the intercompany receivable and revenue purposes (ic_receivable, ic_revenue) with trading_partner on every line, so the ledger''s intercompany reconciliation matches it to UK02''s payable; VAT follows tax (one VAT group computes none: the treatment vat_group); on issue it is handed to the partner company''s purchasing as a bill (AR-41)'
    screen: Invoice
    reference: not built
  - id: AR-41
    kind: integration
    actor: system
    trigger: when an intercompany invoice issues and the partner company is in this installation
    story: An issued intercompany invoice arrives as a bill in the partner company, so that the two sides never differ by typing.
    given: the issued invoice from UK01 to UK02
    when: invoicing calls purchasing's POST /bills/intercompany for company UK02
    then: 'one supplier bill keyed "ic:<invoice_id>" with the same lines, amounts and tax, in UK02; calling twice adds nothing; a credit note against the invoice is handed over the same way; where the partner company is elsewhere, the invoice is sent like any other (AR-13 or AR-43)'
    owner: ar
    reference: not built
  - id: AR-42
    kind: person
    actor: biller
    story: As the biller, I want to set up a recurring invoice by hand for something with no order (rent, a retainer), so that it raises itself like an order's repeating line.
    given: 'a customer renting a desk at 450.00 a month from 2026-12-01 for 12 months, no order'
    when: I create a recurring invoice with the line, the start and the number of periods
    then: 'a schedule keyed "man:<uuid>|1" with source_kind manual on the same engine as AR-18 (raised each period as a draft, reviewed unless the owner''s auto_issue_sources names manual, finished after period 12, stopped with a reason dated today); starts_on is today or later (a manual schedule has no source document to date it earlier); editing the price is a new schedule from a later period, never an edit of issued invoices'
    screen: Billing
    reference: not built
  - id: AR-43
    kind: person
    actor: biller
    story: As the biller, I want to send an invoice as a Peppol e-invoice to a customer who receives them, so that it lands in their accounts system without retyping and we are ready for the UK mandate.
    given: 'customer 7311 with einvoice channel peppol and participant id 0088:5060012349998 (GLN) on their billing profile; an access-point provider capability composed'
    when: I send an issued invoice
    then: 'the invoice is rendered as UBL 2.1 to Peppol BIS Billing 3.0 (CustomizationID urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0) from the frozen invoice (lines, unit_code UN/ECE Rec 20, VAT category per tax code, the totals) and validated against the EN 16931 and Peppol BIS 3 rules before it leaves; a failed rule refuses the send with {gate: einvoice_valid, met: false, reason: the rule id and text, fix: the field}; a valid one goes to the access point and the send is recorded with the message id; the PDF is still produced for our records; a credit note is a UBL CreditNote the same way'
    screen: Invoice
    reference: not built
  - id: AR-44
    kind: system
    actor: system
    trigger: when the access point reports delivery or the buyer's response
    story: The system records what happened to an e-invoice, so that "did they get it?" is answered by the network, not by email.
    given: the e-invoice sent above
    when: the access point reports delivered, then a Peppol Invoice Response with status RE (rejected) and reason "PO number missing"
    then: 'the send row records each status with its time (delivered; AB acknowledged, IP in process, UQ under query, CA conditionally accepted, RE rejected, AP accepted, PD paid); a rejection shows on the invoice for the biller with the reason (the invoice is not changed: a correction is a credit note and a new invoice); a duplicate notification adds nothing'
    reference: not built
  - id: AR-45
    kind: person
    actor: approver
    story: As the finance manager, I want a provision for doubtful debts proposed at each month end from the aging and the disputes, so that the books show what we really expect to collect.
    given: 'provision rates by aging bucket (0-30: 0%, 31-60: 2%, 61-90: 10%, 90+: 50%) and a specific 100% on invoices in dispute; payments'' aging at 2026-11-30'
    when: the biller proposes the November provision and I approve it
    then: 'a provision run keyed "prov:<company_code>:2026-11" lists each bucket and each specific invoice with the rate, the outstanding EXCLUDING VAT (VAT is recovered by bad debt relief, not provided), and the provision; approval posts the movement since last month (debit bad-debt expense, credit the allowance for doubtful debts) dated the period end, journal id derived from the run; the proposer may not approve it when second_approver is on; rates are dated rows; a written-off invoice leaves the provision and its loss is the write-off''s'
    screen: Provisions
    reference: not built
  - id: AR-46
    kind: system
    actor: system
    trigger: monthly, when the ledger period closes (or by the composition's clock at month end)
    story: The system releases revenue invoiced in advance over the service period, so that an annual subscription is 1/12 revenue each month.
    given: 'an invoice issued 2026-10-01 for an annual service 2026-10-01 to 2027-09-30 at 12,000.00 net, posted to deferred revenue (the line carries service_from / service_to)'
    when: the release runs for October, twice
    then: '12,000.00 x 31/365 = 1,019.18 moves from deferred_revenue to revenue for October (days in the period over days in the service), keyed [invoice_id, line_no, period_id] so the second run adds nothing; the last period takes the remainder so the line releases exactly 12,000.00; a credit note against the line reverses what is not yet released first; lines without a service period post straight to revenue as before'
    reference: not built
  - id: AR-47
    kind: integration
    actor: system
    trigger: when an invoice or credit note posts and the ledger splits documents by cost centre
    story: Each invoice line carries the cost centre it belongs to, so that the ledger can split the receivable and VAT lines and balance each segment.
    given: 'an invoice with a consulting line of 6,000.00 (cost centre CC-CONS) and a training line of 4,000.00 (CC-TRAIN), VAT 2,000.00'
    when: it posts
    then: 'the revenue lines carry their cost_centre; the receivable and VAT lines are sent unsplit with the document''s split basis (the revenue lines), and gl splits them 60/40 (receivable 7,200.00 / 4,800.00, VAT 1,200.00 / 800.00) when its document splitting is on; invoicing never decides a segment and never splits itself; a line with no cost centre takes the customer''s billing profile default, else the gate cost_centre on issue where gl requires one'
    owner: gl
    reference: not built

processes:
  - key: invoice
    pcf_id: 10743
    pcf_name: Invoice customer
    reference_names: ["Order to Cash", "Customer Contract to Revenue"]
    brick: ar
  - key: writeoff
    pcf_id: 10808
    pcf_name: Process adjustments/write off balances
    reference_names: ["Collections"]
    brick: ar

workflows:
  - entity: schedule
    states: [active, finished, stopped]
    terminal: [finished, stopped]
    transitions:
      - { key: plan_billing, name: Create a schedule for the sold line, from: none, to: active, by: system, gate: "a source confirmed a recurring, instalment or usage line", effect: "one schedule per line, keyed <source_id>|<line_id>" }
      - { key: plan_manual_schedule, name: "Set up a recurring invoice by hand (v4)", from: none, to: active, by: biller, gate: "a customer with a billing profile; starts_on today or later", effect: "a schedule keyed man:<uuid>|1, source_kind manual, on the same engine (AR-42)" }
      - { key: finish_schedule, name: Finish the schedule, from: active, to: finished, by: system, gate: "every period raised" }
      - { key: stop_billing, name: Stop billing after the current period, from: active, to: stopped, by: system, gate: "the source ended early (AR-28); the stop is dated today, never earlier", effect: "issued invoices are untouched; money owed back is a credit note" }
    note: "which period is due, how many are billed and what remains are DERIVED from starts_on, periods and the invoices raised; nothing increments a counter"
    readiness:
      - { transition: plan_billing, gate: source_line_repeats, reason: "the line is not recurring, instalment or usage", fix: "bill it once through raise_draft" }
      - { transition: plan_billing, gate: customer_profile, reason: "the customer has no billing profile", fix: "Customer billing: set bill-to and terms (AR-25)" }
      - { transition: stop_billing, gate: not_back_dated, reason: "a stop cannot be dated before today", fix: "stop today and credit the periods already issued (AR-04)" }
      - { transition: plan_manual_schedule, gate: starts_not_past, reason: "a recurring invoice set up by hand starts today or later", fix: "pick today or a later start" }
  - entity: invoice
    states: [draft, issued, void, cancelled]
    terminal: [issued, void, cancelled]
    transitions:
      - { key: raise_due, name: "Raise this period's draft", from: none, to: draft, by: system, gate: "a schedule period is due", effect: "keyed rec:schedule:period, so a second run makes nothing", timer: "the billing day of the period" }
      - { key: raise_from_order, name: Raise the draft the order asked for, from: none, to: draft, by: system, gate: "a source called raise_draft (an order, a deposit, an adapter)", effect: "keyed on the source; lines keep their source keys" }
      - { key: raise_for_acceptance, name: Raise the draft for the accepted item, from: none, to: draft, by: system, gate: "an item was accepted (AR-29)", effect: "keyed acc:item" }
      - { key: draft_invoice, name: Raise an invoice by hand, from: none, to: draft, by: biller, gate: "work with no order (AR-02)", effect: "totals and tax computed, never typed" }
      - { key: raise_interest_draft, name: "Raise the late-payment interest note (v4)", from: none, to: draft, by: biller, gate: "late_interest_mode is not off; the customer is a business; an invoice paid late or still overdue", effect: "a draft kind interest at the statutory rate plus fixed compensation, code OS (AR-39)" }
      - { key: settle_payment_request_invoice, name: "Raise the deposit invoice a paid request became (v4)", from: none, to: draft, by: system, gate: "payments settled a payment request (AR-36)", effect: "keyed dep:<request_id>:<n>, tax point = received_on; issued at once (it is the money already received)" }
      - { key: ai_check_draft, name: "AI checks the draft before issue (suggest)", from: draft, to: draft, by: system, gate: "the pre-issue check is on", effect: "unusual amounts, a missing PO, an odd tax code or a duplicate of last month listed for the biller" }
      - { key: review_draft, name: Review the draft, from: draft, to: draft, by: biller, effect: "drafts are not issued automatically unless the setting says so for the source" }
      - { key: fill_usage, name: "Enter the period's usage", from: draft, to: draft, by: biller, gate: "the draft has a usage line" }
      - { key: accept_invoice_findings, name: Accept or dismiss each AI finding, from: draft, to: draft, by: biller, gate: "the draft has findings (AR-32)", effect: "each decision recorded in ar_ai_finding; nothing changes without the biller" }
      - { key: cancel_draft_invoice, name: Cancel the draft, from: draft, to: cancelled, by: biller, effect: "no number used; a zero-usage period ends here" }
      - { key: issue, name: Issue the invoice, from: draft, to: issued, by: biller, gate: "at least one line; usage filled in; the customer has a billing profile in force today; the tax content check passes; the tax point is not in a locked or filed VAT return (tax GET /vat-returns/tax-point-status) or a closed period; v4: each deposit deduction within what remains of its deposit; a cost centre on every revenue line where the ledger requires one", effect: "numbered from the company's series; issue date today; due date from the terms; tax and bill-to frozen; revenue for a service period to deferred revenue (AR-46)" }
      - { key: post_to_ledger, name: Post to the ledger, from: issued, to: issued, by: system, gate: "the ledger mode is post", effect: "a balanced journal keyed on the invoice" }
      - { key: send, name: Send it (a person presses Send), from: issued, to: issued, by: biller, effect: "the send recorded; delivery tracked by the mail capability, or by the e-invoice access point (v4)" }
      - { key: deliver_einvoice, name: "Deliver as a Peppol e-invoice (v4)", from: issued, to: issued, by: system, gate: "the customer's channel is peppol with a participant id; the UBL passes EN 16931 and Peppol BIS 3 validation", effect: "UBL 2.1 to the access point, the message id on the send (AR-43)" }
      - { key: record_einvoice_status, name: "Record the e-invoice's delivery and the buyer's response (v4)", from: issued, to: issued, by: system, effect: "each status with its time on the send row (AR-44)" }
      - { key: hand_to_partner_company, name: "Hand the intercompany invoice to the partner company (v4)", from: issued, to: issued, by: system, gate: "trading_partner names a company in this installation", effect: "a bill keyed ic:<invoice_id> in that company's purchasing (AR-41)" }
      - { key: release_deferred_revenue, name: "Release revenue for the period (v4)", from: issued, to: issued, by: system, gate: "a line with a service period not fully released", effect: "deferred to revenue for the days in the period, keyed [invoice_id, line_no, period_id] (AR-46)", timer: "the month end" }
      - { key: receive_invoice, name: Receive the invoice, from: issued, to: issued, by: customer }
      - { key: chase, name: Chase by hand, from: issued, to: issued, by: collector, gate: "overdue and outstanding above zero, and reminders are by hand", timer: "the due date passes unpaid" }
      - { key: pay, name: Pay, from: issued, to: issued, by: customer }
      - { key: record_payment, name: Record the receipt and what it settles, from: issued, to: issued, by: collector, effect: "in payments: the receipt and its allocations; outstanding is derived" }
      - { key: export, name: Include in the monthly export to the accountant, from: issued, to: issued, by: biller, gate: "the ledger mode is export", effect: "in one export batch, once" }
      - { key: void_invoice, name: Void the invoice, from: issued, to: void, by: approver, requires: reason, approval: true, gate: "no allocations, and the owner's void_policy allows it (default never_sent)", effect: "drops out of every balance; reversing journal dated today in post mode" }
    note: >-
      SETTLEMENT is derived, never stored: outstanding = total - SUM(allocations of payments, credit notes and
      write-offs), computed by payments; open, part paid, paid and written off follow from it, so there is no
      "paid" state to set. For the screens the brick answers each document's readiness: every gate of its next
      transitions as {gate, met, reason, fix} (fix a link to the screen that clears it), so no screen re-derives
      a rule. A migrated invoice (source_kind odoo) is issued history: post_to_ledger, send and raise never run
      on it; credit notes and allocations do.
    readiness:
      - { transition: issue, gate: has_lines, reason: "the invoice has no lines", fix: "add a line" }
      - { transition: issue, gate: usage_filled, reason: "a usage line has no quantity", fix: "enter the period's usage (AR-19)" }
      - { transition: issue, gate: billing_profile, reason: "the customer has no bill-to address, invoice email or terms in force today", fix: "Customer billing (AR-25)" }
      - { transition: issue, gate: tax_content, reason: "the tax module's content check failed: its missing[] named", fix: "the field it names, or the tax code on the line" }
      - { transition: issue, gate: tax_point_open, reason: "the tax point falls in a filed VAT return or a closed ledger period", fix: "issue with a tax point of today, or ask the accountant to reopen" }
      - { transition: issue, gate: number_series, reason: "no series for this document type and year", fix: "Settings: number format" }
      - { transition: post_to_ledger, gate: account_map, reason: "no account in force today for a purpose or tax code on the invoice", fix: "Settings: account map" }
      - { transition: void_invoice, gate: no_allocations, reason: "a receipt, credit or write-off is allocated to it", fix: "credit it instead (AR-04)" }
      - { transition: void_invoice, gate: void_policy, reason: "the owner's void_policy does not allow voiding this invoice (it was sent, or its VAT return is filed)", fix: "credit it instead (AR-04)" }
      - { transition: export, gate: not_exported, reason: "already in batch <period>:<format>", fix: "none needed: it is exported once" }
      - { transition: issue, gate: deposit_remaining, reason: "a deduction takes more than what remains of deposit <invoice_no>", fix: "deduct at most what remains" }
      - { transition: issue, gate: cost_centre, reason: "the ledger splits by cost centre and a revenue line has none", fix: "the line's cost centre, or the customer's default on Customer billing" }
      - { transition: [issue, deliver_einvoice], gate: einvoice_valid, reason: "the e-invoice fails rule <rule id>: <text>", fix: "the field the rule names" }
      - { transition: deliver_einvoice, gate: participant_id, reason: "the customer's billing profile has no Peppol participant id", fix: "Customer billing: e-invoice channel" }
      - { transition: raise_interest_draft, gate: business_customer, reason: "statutory interest applies only between businesses", fix: "none: a consumer is never charged" }
      - { transition: raise_interest_draft, gate: interest_mode_on, reason: "late_interest_mode is off", fix: "Settings (the owner's control)" }
  - entity: credit_note
    states: [draft, approved, cancelled]
    terminal: [approved, cancelled]
    transitions:
      - { key: draft_credit, name: Draft a credit note, from: none, to: draft, by: biller, gate: "an issued invoice to correct, or a goodwill credit on the customer" }
      - { key: approve_credit, name: Approve the credit note, from: draft, to: approved, by: approver, approval: true, gate: "lines and tax present; it does not exceed what remains creditable on the invoice it names; the approver is not its author where second_approver is on", effect: "numbered; credit date today; tax frozen; posted in post mode" }
      - { key: allocate_credit, name: Allocate the credit, from: approved, to: approved, by: biller, effect: "in payments: against one or more invoices, or left on the customer" }
      - { key: cancel_credit, name: Cancel the draft credit, from: draft, to: cancelled, by: biller }
      - { key: raise_settlement_discount, name: "Credit the early-settlement discount taken (v4)", from: none, to: approved, by: system, gate: "received within the discount window; the amount is the terms' percentage of the gross outstanding at the receipt; settlement_discount_vat is credit_note", effect: "numbered, at the invoice's frozen rates pro rata (tax TAX-47), approved by the terms, posted, allocated by payments (AR-38)" }
    readiness:
      - { transition: approve_credit, gate: has_lines, reason: "the credit note has no lines", fix: "add a line, or credit the invoice in full" }
      - { transition: approve_credit, gate: within_invoice, reason: "it exceeds the invoice total less what is already credited", fix: "lower the amount, or credit the customer as goodwill (no invoice)" }
      - { transition: approve_credit, gate: second_person, reason: "you drafted it, and the owner asks for a second person", fix: "ask another approver" }
      - { transition: raise_settlement_discount, gate: within_discount_window, reason: "received on <received_on>, after the discount ended on <date>", fix: "none: the short payment stays outstanding" }
      - { transition: raise_settlement_discount, gate: discount_matches_terms, reason: "<amount> is not <pct>% of the gross outstanding <gross>", fix: "allocate the receipt; the difference stays outstanding or goes to tolerance" }
  - entity: writeoff
    states: [proposed, approved, rejected]
    terminal: [approved, rejected]
    transitions:
      - { key: propose_writeoff, name: Propose a write-off, from: none, to: proposed, by: biller, requires: reason }
      - { key: approve_writeoff, name: Approve the write-off, from: proposed, to: approved, by: approver, approval: true, gate: "below the threshold, or the approver accepts it above", effect: "allocated in payments; bad debt posted in post mode" }
      - { key: reject_writeoff, name: Reject the write-off, from: proposed, to: rejected, by: approver, requires: reason }
    readiness:
      - { transition: approve_writeoff, gate: outstanding, reason: "the amount exceeds what is outstanding on the invoice", fix: "write off the outstanding amount" }
      - { transition: approve_writeoff, gate: threshold, reason: "above the owner's write-off threshold: an explicit confirmation is asked", fix: "confirm, with the reason" }
      - { transition: approve_writeoff, gate: second_person, reason: "you proposed it, and the owner asks for a second person", fix: "ask another approver" }
  - entity: migration_run
    states: [loaded, reconciled, signed, discarded]
    terminal: [signed, discarded]
    transitions:
      - { key: load_invoices_from_odoo, name: Load the Odoo history, from: none, to: loaded, by: system, gate: "the customer's doors are decided (migration.odoo.door); business-partner and tax have mapped every partner and tax the moves use", effect: "documents written read-only, keyed odoo:<db>:<move id>; a re-run changes nothing (AR-34)" }
      - { key: reconcile_invoice_migration, name: Reconcile against Odoo, from: loaded, to: reconciled, by: system, effect: "every migration.odoo.reconcile check computed on both sides, with the difference" }
      - { key: explain_invoice_difference, name: Explain each difference, from: reconciled, to: reconciled, by: approver, requires: reason, gate: "a check is not equal and may be explained", effect: "the explanation recorded beside the check" }
      - { key: reload_invoice_migration, name: Load again after a fix, from: [loaded, reconciled], to: loaded, by: system, gate: "not signed", effect: "the same keys; explanations on unchanged checks kept" }
      - { key: sign_invoice_reconciliation, name: Sign the reconciliation, from: reconciled, to: signed, by: owner, approval: true, gate: "every check equal, or explained where explained is allowed", effect: "who, when and the run's checksums recorded; invoicing takes writes for these customers from now" }
      - { key: discard_invoice_migration, name: Discard the run, from: [loaded, reconciled], to: discarded, by: owner, requires: reason, effect: "the run's documents are removed; nothing was ever issued from them" }
    readiness:
      - { transition: load_invoices_from_odoo, gate: doors_decided, reason: "a migration door is undecided", fix: "Migration: answer it" }
      - { transition: load_invoices_from_odoo, gate: partners_mapped, reason: "Odoo partners used by the moves have no bp_id", fix: "run the business-partner migration first" }
      - { transition: load_invoices_from_odoo, gate: taxes_mapped, reason: "Odoo taxes used by the moves have no tax_code", fix: "run the tax migration first" }
      - { transition: sign_invoice_reconciliation, gate: zero_unexplained, reason: "checks neither equal nor explained: named", fix: "explain each, or fix and reload" }
      - { transition: sign_invoice_reconciliation, gate: must_equal, reason: "a check that must be equal differs", fix: "fix the mapping and reload; it cannot be explained away" }

  - entity: payment_request
    states: [draft, issued, settled, cancelled]
    terminal: [settled, cancelled]
    transitions:
      - { key: raise_payment_request, name: "Ask for a down payment, or raise a pro forma (v4)", from: none, to: draft, by: biller, gate: "deposit_mode request, or a pro forma asked; the amount within the order total less deposits already asked (sales-order SO-09)", effect: "keyed dpr:<sales_order_id>:<request_no> from an order, a uuid by hand; amounts computed by tax for display only (AR-35)" }
      - { key: issue_payment_request, name: Issue the payment request, from: draft, to: issued, by: biller, gate: "a billing profile in force today", effect: "numbered DPR-; titled not a VAT invoice; nothing posted, nothing in a VAT return" }
      - { key: settle_payment_request, name: "Settle the request from the receipt that paid it", from: issued, to: [issued, settled], by: system, gate: "payments names a receipt and its amount, at most what remains of the request", effect: "a deposit invoice for the amount received, tax point the received date (AR-36); settled when nothing remains" }
      - { key: cancel_payment_request, name: Cancel the payment request, from: [draft, issued], to: cancelled, by: biller, requires: reason, gate: "nothing received against it" }
    readiness:
      - { transition: settle_payment_request, gate: within_request, reason: "the receipt is more than what remains of the request", fix: "settle what remains; the rest stays unallocated on the customer" }
      - { transition: cancel_payment_request, gate: nothing_received, reason: "a receipt already settled part of it", fix: "credit the deposit invoice it became (AR-04)" }
  - entity: provision_run
    states: [proposed, approved, rejected]
    terminal: [approved, rejected]
    transitions:
      - { key: propose_doubtful_provision, name: "Propose the month's doubtful-debt provision (v4)", from: none, to: proposed, by: biller, gate: "the month has ended; payments' aging as at its last day", effect: "a run keyed prov:<company_code>:<period_id>: per bucket and per disputed invoice, net of VAT (AR-45)" }
      - { key: approve_doubtful_provision, name: Approve and post the provision, from: proposed, to: approved, by: approver, approval: true, gate: "the approver is not the proposer where second_approver; the ledger period is open", effect: "the movement since the last run posted, dated the period end" }
      - { key: reject_doubtful_provision, name: Reject the provision, from: proposed, to: rejected, by: approver, requires: reason }
    readiness:
      - { transition: approve_doubtful_provision, gate: second_person, reason: "you proposed it, and the owner asks for a second person", fix: "ask another approver" }
      - { transition: approve_doubtful_provision, gate: period_open, reason: "the ledger period is closed", fix: "ask the accountant, or post in the next period" }
entities:
  - id: invoice
    meaning: a demand for payment, addressed to a customer
    natural_key: 'invoice_id: immutable for life, derived from its source ("so:<sales_order_id>:<request_no>", "rec:<schedule_id>:<period>", "acc:<item_id>", "<adapter>:<source>") and a uuid otherwise (shared.yaml keys.invoice_id). The invoice NUMBER is stamped at issue, never the key'
    key_attributes: [bp_id, lifecycle, "kind (regular | deposit | final | interest, v4)", "source_kind (sales_order | schedule | acceptance | manual | contract | payment_request | odoo | expense | pos | timesheet)", "company_code (shared.yaml keys.company_code, v4: the issuing company; one company by default)", "trading_partner (the partner's company_code on an intercompany invoice, v4)", "settlement_discount_pct (percent), settlement_discount_until (frozen from the terms at issue, v4)", "einvoice_message_id (v4)", invoice_no, currency, fx_rate, issue_date, tax_point_date, due_date, terms_id, customer_po_ref, bill_to_json, subtotal, tax_total, total, total_functional, source_id, export_batch_id, migration_run_id]
    relationships: ["-> business_partner", "1..n invoice_line", "1..n invoice_tax", "0..n send"]
    master_data: module-owned
    reference_table: ar_invoices
  - id: invoice_line
    meaning: one billed item, with where it came from
    natural_key: [invoice_id, line_no]
    key_attributes: [product_id, description, unit_code (product's, shared.yaml keys.unit_code), quantity, unit_price (NUMERIC 19,4), discount_pct (percent), tax_code, net_amount, tax_amount, sales_order_id, so_line_no, service_from, service_to, schedule_id, item_id, deducts_invoice_id, "pinned_from (the earlier document whose frozen tax this line takes: a deposit deduction, v4)", "cost_centre (shared.yaml keys.cost_centre, v4)", "interest_on_invoice_id, interest_days, interest_rate_pct (percent) (an interest line, v4)", odoo_line_id]
    relationships: ["-> so_line (sales_order_id, so_line_no), when invoiced from an order", "-> product", "-> tax_code"]
    master_data: module-owned
    reference_table: ar_invoice_lines
    note: >-
      deducts_invoice_id names the deposit a final invoice's negative line uses up (AR-15). tax_code and
      tax_amount are the tax module's compute() result, frozen at issue; invoicing never derives a rate.
      service_from / service_to are the period a schedule line covers. "Billed vs sold" per order line is
      SUM(quantity) GROUP BY (sales_order_id, so_line_no) over issued invoices less approved credit notes.
  - id: invoice_tax
    meaning: the tax summary the document must print, one row per code (frozen copy of tax compute())
    natural_key: [invoice_id, tax_code]
    key_attributes: [rate_pct, net_amount, tax_amount, rounding_method]
    master_data: module-owned
    reference_table: ar_invoice_tax
  - id: credit_note
    meaning: a tax document reducing what a customer owes
    natural_key: credit_id (immutable); credit_no stamped at approval
    key_attributes: [bp_id, invoice_id (nullable), credit_date, currency, reason, status, subtotal, tax_total, total, "source_kind (manual | settlement_discount | odoo, v4)", "payment_id (the receipt a settlement discount was taken on, v4)", company_code]
    relationships: ["1..n credit_note_line", "1..n credit_note_tax"]
    master_data: module-owned
    reference_table: [ar_credit_notes, ar_credit_note_lines, ar_credit_note_tax]
  - id: writeoff
    meaning: a balance the business accepts it will not collect
    natural_key: writeoff_id
    key_attributes: [invoice_id, amount, reason, status, approved_by, approved_at]
    master_data: module-owned
    reference_table: ar_writeoffs
  - id: schedule
    meaning: one sold line billed over time
    natural_key: 'schedule_id = "<source_id>|<line_id>" (an order line, or a contract line through an adapter)'
    key_attributes: [bp_id, source_kind, source_id, line_id, product_id, description, billing_kind, billing_plan, starts_on, periods, unit_price (NUMERIC 19,4), discount_pct (percent), currency, in_advance, status, stopped_at, stop_reason]
    relationships: ["-> business_partner", "0..n invoice (source_kind schedule)"]
    master_data: module-owned
    reference_table: ar_schedule
  - id: customer
    meaning: how a party is billed (bill-to details, terms, currency), as dated rows
    natural_key: [bp_id, valid_from]
    key_attributes: [bill_to_name, bill_to_address, invoice_email, terms_id, currency, "einvoice_channel (email | peppol, v4)", "peppol_participant_id (scheme:id, e.g. 0088:<GLN> or 9932:<GB VAT>, v4)", "default_cost_centre (v4)", "intercompany_company_code (the group company this party is, v4)", set_by, set_at]
    relationships: ["-> business_partner"]
    master_data: module-owned
    reference_table: ar_customer
    note: "the party master holds identity (legal name, country, tax id); invoicing holds how to bill; the tax treatment is the tax module's (tax_party). The profile in force on a date is the row with the latest valid_from on or before it; a new row starts after today (a customer's first may start today); rows are never edited"
  - id: send
    meaning: a record that the document went out
    natural_key: [invoice_id, sent_at]
    key_attributes: [channel (email | portal | peppol), address, format (pdf | ubl), result, "message_id, statuses_json (each e-invoice status and its time, v4)"]
    master_data: module-owned
    reference_table: ar_sends
  - id: export_batch
    meaning: one export of issued documents to an outside ledger
    natural_key: 'batch_id = "<period>:<format>"'
    key_attributes: [period, format, document_count, created_at, file_key]
    master_data: module-owned
    reference_table: ar_export_batch
  - id: number_series
    meaning: the counter behind invoice and credit-note numbers (STATE, not a setting)
    natural_key: 'series_id (e.g. "INV-2026", "CN-2026", "DPR-2026"; with more than one company "INV-UK01-2026", v4)'
    key_attributes: [high_water_mark, updated_at]
    master_data: module-owned (state)
    reference_table: ar_number_series
  - id: terms
    meaning: when payment is due
    natural_key: terms_id
    key_attributes: [days, basis (invoice_date | end_of_month), description, early_discount_pct (percent), "early_discount_days (v4)", active]
    master_data: module-owned (configuration)
    reference_table: ar_terms
    note: "a terms row an issued invoice used is never edited (the invoice carries its due date anyway); different terms are a new terms_id, the old one deactivated"
  - id: settings
    meaning: how this business invoices, as dated rows
    natural_key: [key, valid_from]
    key_attributes: [value, set_by, set_at]
    note: "the value in force today is the row with the latest valid_from on or before today (business_timezone); a new row starts after today, except the first row of a key; controls (marked ar:settings:controls) need that permission"
    master_data: module-owned (configuration)
    reference_table: ar_settings
  - id: account_map
    meaning: which ledger account each posting hits
    natural_key: [purpose, tax_code, valid_from]
    key_attributes: [account_id]
    master_data: module-owned (configuration); the accounts themselves belong to the ledger
    reference_table: ar_account_map
    note: "purpose is receivable | revenue | tax | bad_debt | fx_gain | fx_loss | deposit; v4 adds ic_receivable, ic_revenue, deferred_revenue, settlement_discount, interest_income, doubtful_debt_expense, doubtful_debt_allowance. A posting uses the row in force on the document's own date (issue, credit or write-off date); a new row starts after today"
  - id: payment_request
    meaning: "a down-payment request or a pro forma: asks for money, is not a VAT invoice (v4)"
    natural_key: 'request_id: "dpr:<sales_order_id>:<request_no>" from an order, else a uuid; request_no stamped at issue from the DPR- series'
    key_attributes: [bp_id, company_code, lifecycle, kind (down_payment | pro_forma), sales_order_id, currency, net_amount, tax_amount_display, total, issue_date, due_date, deposit_invoice_ids, cancel_reason]
    master_data: module-owned
    reference_table: ar_payment_request
    note: "posts nothing and is in no VAT return; what remains = total less the deposit invoices it became, re-derived"
  - id: deposit_claim
    meaning: "one final invoice's deduction from one deposit: the claim that stops two finals taking the same remainder (v4)"
    natural_key: [deposit_invoice_id, final_invoice_id]
    key_attributes: [net_amount, tax_code, claimed_at, released_at]
    master_data: module-owned (state)
    reference_table: ar_deposit_claim
    note: "written with a plain INSERT when the final is issued, then the remainder is re-derived in a second transaction and the claim released (released_at) if it went over; cancelled drafts never claim"
  - id: interest_rate
    meaning: "the statutory late-payment reference rate and margin from a date (v4)"
    natural_key: valid_from
    key_attributes: [reference_rate_pct (percent), margin_pct (percent, 8 by the 1998 Act), source]
    master_data: module-owned (configuration)
    reference_table: ar_interest_rate
    note: "rows from 1 January and 1 July, carrying the base rate of 31 December and 30 June; dated, never edited"
  - id: provision_run
    meaning: "one month's doubtful-debt provision and what it posted (v4)"
    natural_key: 'run_id = "prov:<company_code>:<period_id>"'
    key_attributes: [lifecycle, as_at, lines_json (bucket or invoice_id, outstanding_net, rate_pct, provision), total, previous_total, movement, proposed_by, approved_by, journal_id]
    master_data: module-owned
    reference_table: ar_provision_run
  - id: provision_rate
    meaning: "the provision percentage per aging bucket from a date (v4)"
    natural_key: [bucket, valid_from]
    key_attributes: [rate_pct (percent), set_by]
    master_data: module-owned (configuration)
    reference_table: ar_provision_rate
  - id: revenue_release
    meaning: "the revenue released from deferred for one invoice line and period (v4)"
    natural_key: [invoice_id, line_no, period_id]
    key_attributes: [amount, days, released_at, journal_id]
    master_data: module-owned
    reference_table: ar_revenue_release
  - id: ai_finding
    meaning: one AI suggestion on a document and the person's decision on it (consent and audit)
    natural_key: [invoice_id, step, run_no]
    key_attributes: [model, inputs_digest, finding, evidence, decision (open | accepted | dismissed), reason, decided_by, decided_at]
    master_data: module-owned
    reference_table: ar_ai_finding
    note: "written by the AI step as open; only a person moves it to accepted or dismissed; a finding never edits the draft"
  - id: migration_run
    meaning: one load of another system's invoices, its reconciliation and the customer's signature
    natural_key: 'run_id = "odoo:<db>:<cutover_date>"'
    key_attributes: [source (odoo), source_db, cutover_date, doors_json, status, loaded_at, checksums_json, signed_by, signed_at]
    relationships: ["1..n migration_check"]
    master_data: module-owned (state)
    reference_table: ar_migration_run
  - id: migration_check
    meaning: one reconciliation check of a run, both sides and the difference
    natural_key: [run_id, check_key, slice]
    key_attributes: [odoo_value, ours_value, difference, must (equal | explained), explanation, explained_by]
    master_data: module-owned (state)
    reference_table: ar_migration_check
    note: "slice is the grouping a check runs over (e.g. year|currency, bp_id|currency, tax_code|vat_period)"

master_data:
  owns: []
  references:
    - entity: business_partner
      owner: business-partner
      how: ar_invoices.bp_id = business_partner.bp_id
      if_absent: "invoicing keeps its own customer list: the exact duplication this module exists to avoid"
    - entity: product
      owner: product
      how: ar_invoice_lines.product_id, fields copied at line time
    - entity: tax_code
      owner: tax
      how: ar_invoice_lines.tax_code, ar_invoice_tax, frozen at issue
    - entity: company
      owner: gl
      how: "ar_invoices.company_code and trading_partner (shared.yaml keys.company_code, v4)"
      if_absent: "one company; company_code is the installation's default and no intercompany invoice exists"
  warning: >-
    Invoicing is where a duplicated customer list finally costs money: the statement will not match the sales
    view and nobody can say which is right. Install business-partner first.

jobs:
  - story: AR-18
    trigger: daily
    runs: raise due drafts for every active schedule
    where_options: [the composition's clock worker calling the brick's raise_due route (--reserved 1 --max-replicas 1), external scheduler]
    idempotent: 'keyed invoice_id = "rec:<schedule_id>:<period>", a plain-INSERT claim'
  - story: AR-26
    trigger: in the same pass as AR-18
    runs: finish schedules whose every period is raised
    where_options: [the same worker]
    idempotent: "finished is set once"
  - story: AR-46
    trigger: monthly, at the month end (the composition's clock worker) or when gl closes the period
    runs: release_deferred_revenue for every line with a service period
    where_options: [the composition's clock worker calling the brick's release route (--reserved 1 --max-replicas 1), gl's close calling it]
    idempotent: "keyed [invoice_id, line_no, period_id]"
  - story: AR-44
    trigger: when the access point calls back
    runs: record_einvoice_status
    where_options: [in the provider's callback request]
    idempotent: "keyed on (message_id, status): a repeat adds nothing"
  - story: AR-10
    trigger: on read
    runs: settlement, from payments
    where_options: [derived on read]
    idempotent: "nothing stored"

seams:
  - story: AR-20
    with: sales-order
    owner: ar
    rule: "a source calls raise_draft; it never writes ar_ tables; it reads back what was billed through billed_per_source(source_kind, source_id)"
  - story: AR-27
    with: sales-order
    owner: ar
    rule: "plan_billing / stop_billing are invoicing's use cases; a contract adapter in the composition calls the same ones with source_kind contract"
  - story: AR-21
    with: gl
    owner: gl
    rule: >-
      invoicing calls POST /journal-entry/journals (permission gl:journal:post) with a balanced journal; the ids
      are BIGINTs invoicing derives itself (int.from_bytes(blake2b(<invoice_id>, digest_size=7))), so re-posting
      changes the ledger once. In export mode nothing is posted and AR-24 runs instead.
  - story: AR-22
    with: business-partner
    owner: business-partner
    rule: "invoicing reads partners and calls promote (role customer); it never writes them"
  - story: AR-12
    with: crm
    owner: ar
    rule: "sales reads status and balance through invoicing's narrow status route, never ar_ tables and never a view (DataK3 rejects CREATE VIEW)"
  - story: AR-30
    with: payments
    owner: payments
    rule: "invoicing calls payments' allocate(source_kind credit_note | writeoff, ...); payments owns every allocation"
  - story: AR-29
    with: sales-order
    owner: ar
    rule: "a projects or delivery brick calls raise_for_acceptance; it never writes ar_ tables"
  - story: AR-34
    with: business-partner
    owner: business-partner
    direction: outbound
    route: "GET /migration-map/{db}/{model}?ids="
    permission: bp:migration:read           # v7: machine, held by ar's migration client
    request: "path db, model res.partner; query ids=<comma-separated Odoo ids, at most 500>"
    request_schema:
      - { field: db, type: string, required: true, in: path }
      - { field: model, type: string, required: true, in: path, values: [res.partner] }
      - { field: ids, type: array, required: true, in: query, items: [integer] }
    response: "200 [{odoo_model, odoo_id, ours: bp_id as a string}]; an id not migrated is omitted"
    rule: "business-partner's own migration maps each Odoo res.partner to a bp_id first; invoicing reads that map and never creates a partner"
    status: "v7: route and permission named (business-partner v3 BP-02)"
  - story: AR-34
    with: tax
    owner: tax
    direction: outbound
    route: "GET /migration-map/{db}/{model}?ids="
    permission: tax:migration:read          # v7: machine, held by ar's migration client
    request: "path db, model account.tax | account.fiscal.position; query ids=<comma-separated Odoo ids>"
    request_schema:
      - { field: db, type: string, required: true, in: path }
      - { field: model, type: string, required: true, in: path, values: [account.tax, account.fiscal.position] }
      - { field: ids, type: array, required: true, in: query, items: [integer] }
    response: "200 [{odoo_model, odoo_id, ours}]; an id not migrated is omitted"
    rule: "tax's migration maps each Odoo account.tax to a tax_code; invoicing copies Odoo's tax amounts as frozen history and never asks tax to recompute them"
    status: "v7: route and permission named (tax TAX-15)"
  - story: AR-34
    with: product
    owner: product
    direction: outbound
    route: "GET /migration-map/{db}/{model}?ids="
    permission: product:migration:read      # v7: machine, held by ar's migration client
    request: "path db, model product.product | uom.uom; query ids=<comma-separated Odoo ids, at most 500>"
    request_schema:
      - { field: db, type: string, required: true, in: path }
      - { field: model, type: string, required: true, in: path, values: [product.product, uom.uom] }
      - { field: ids, type: array, required: true, in: query, items: [integer] }
    response: "200 [{odoo_model, odoo_id, ours: product_id | unit_code}]; an id not migrated is omitted"
    rule: "an invoice line's product and unit resolve through product's map; a line whose product was not migrated keeps no product_id and its description"
    status: "v7: the outbound side of product's PRD-33 row for ar"
  - story: AR-34
    with: payments
    owner: payments
    direction: outbound
    route: GET /settlement
    permission: pay:receipt:read            # payments has no migration read route: the open-balance check reads its settlement of the migrated invoice ids
    request: "query invoice_ids=<comma-separated, at most 200>, as_at (the cut-over date)"
    response: "200 {as_at, invoices: [{invoice_id, currency, total, outstanding, state, age_days}], unknown: [ids]}"
    rule: "payments migrates Odoo's receipts and reconciliations as allocations keyed on the same invoice_id; the open-balance check reads payments' settlement and cannot pass before it has run"
    status: "v7: route and permission named (payments PAY-40)"
  - story: AR-34
    with: gl
    owner: gl
    rule: "migrated documents are never posted: the ledger's own migration carries Odoo's journal entries, so posting them again would book revenue twice"
  # ── v4 ──
  - story: AR-14
    with: tax
    owner: tax
    route: "GET /vat-returns/tax-point-status?date="
    permission: tax:tax_point:read          # held by ar's service client
    request: "query date=YYYY-MM-DD (the document's tax point)"
    response: '200 {date, period_key | null, status: none | open | draft | locked | submitted, filed: bool, gate: {gate: tax_point_open, met, reason, fix}}'
    rule: "issue's gate tax_point_open and void_policy never_sent read this answer (tax TAX-33); a non-200 answers the gate as unknown, met false, so nothing issues into a period nobody can vouch for"
    status: aligned 2026-10-22 with tax TAX-33
  - story: AR-15
    with: tax
    owner: tax
    route: "POST /documents/compute"
    permission: tax:document_tax:compute
    request: '{document_ref, direction (sale | purchase; default sale, so every v2 caller is unchanged), tax_point (YYYY-MM-DD), bp_id, currency, treatment_code (optional), prices_include_tax (optional), fx_rate (optional), ship_to_country (optional, v4: ISO 3166; goods lines are taxed where the ship-to receives them, services by the customer), self_billed (optional, v4: a purchase-direction invoice we issue on the supplier''s behalf under a self-billing agreement), lines: [{line_no, description, category_code (the product''s tax_category), supply (goods | services), amount, quantity, unit_price, pinned_from (optional; an earlier frozen document_ref), tax_code (with pinned_from)}]}'
    response: '{document_ref, status, lines: [{line_no, tax_code, rate_pct, net, vat, gross, vat_reclaimable, vat_non_reclaimable (purchase)}], summary: [{tax_code, rate_pct, net, vat, vat_due, gross}], totals: {net, vat, vat_due, gross, method, vat_reclaimable, vat_non_reclaimable}, gates: [{gate, met, reason, fix}], version}; a pinned line answers the earlier document''s frozen code and rate, or the gates pinned_code / pinned_within_original'
    rule: ar sends document_ref invoice_id; a deposit deduction, a settlement-discount credit and a correcting credit note send pinned lines (TAX-47); invoicing never applies a rate
    status: built; aligned 2026-10-22 with tax TAX-20
  - story: AR-36
    with: payments
    owner: ar
    route: "POST /payment-requests/{request_id}/settle"
    permission: ar:payment_request:settle   # machine, held by payments' service client
    request: "{payment_id, received_on: YYYY-MM-DD, amount, currency}"
    response: "200 {request_id, deposit_invoice_id, invoice_no, amount, remaining, status: issued | settled}; 200 the same for the same payment_id; 422 {gate: within_request}"
    rule: "payments then allocates the receipt to deposit_invoice_id through its own allocate; invoicing never writes pay_ tables"
    status: aligned 2026-10-22 with payments PAY-36
  - story: AR-36
    with: payments
    owner: ar
    route: "GET /payment-requests/{request_id}"
    permission: ar:invoice:read   # machine, held by payments' service client
    request: "{}"
    response: "200 {request_id, bp_id, currency, remaining, status, ...}; 404 unknown request"
    rule: "payments reads the request's customer and currency BEFORE it calls settle, so a receipt of another customer is refused while nothing is settled (the settle call keys on payment_id and never sees the payer)"
    status: added 2026-10-23 by the payments v4 audit (M6); needs sign-off
  - story: AR-38
    with: payments
    owner: ar
    route: "POST /invoices/{invoice_id}/settlement-discount"
    permission: ar:invoice:settle_discount  # machine, held by payments' service client
    request: "{payment_id, received_on: YYYY-MM-DD, discount_amount}"
    response: "201 {credit_id, credit_no, net, tax, total, approved_by: terms}; 200 the same for the same payment_id; 422 {gate: within_discount_window | discount_matches_terms, reason, fix}"
    rule: "invoicing then calls payments' POST /allocations/from-source (source_kind credit_note) as for any credit (AR-30)"
    status: aligned 2026-10-22 with payments PAY-16
  - story: AR-39
    with: payments
    owner: payments
    route: "GET /allocations?invoice_ids=&from="
    permission: pay:receipt:read            # ar's service client holds it
    request: query invoice_ids=<comma-separated, at most 200>, from=YYYY-MM-DD (optional)
    response: '200 {allocations: [{source_kind, source_id, invoice_id, seq, amount, currency, allocated_on, received_on, reverses_seq}]}'
    rule: "late days run from the day after the due date to each receipt's received_on (the money, not the allocation); still-open amounts run to today"
    status: aligned 2026-10-22 with payments PAY-46
  - story: AR-45
    with: payments
    owner: payments
    route: "GET /collections/aging?as_at="
    permission: pay:receipt:read
    request: query as_at=YYYY-MM-DD, company_code (optional; the default company)
    response: '200 {as_at, buckets: [{bucket, invoices: [{invoice_id, bp_id, outstanding, days_past_due, disputed}]}]}'
    rule: "the provision is computed on outstanding net of VAT (outstanding x net / gross of the frozen invoice)"
    status: aligned 2026-10-22 with payments PAY-40
  - story: AR-45
    with: gl
    owner: gl
    route: "POST /journal-entry/journals"
    permission: gl:journal:post
    request: '{journal_id: BIGINT as a string (optional; = derived_id(source_document, event), checked), source_module: "ar", source_document: invoice_id | credit_id | writeoff_id, event: issue | void | approve | provision | release, journal_date: YYYY-MM-DD, currency: ISO 4217, fx_rate: string (optional; the document''s frozen rate), memo, company_code (optional; the default company), ledger_id (optional; absent = every ledger of the company), lines: [{account_id: BIGINT as string | purpose (+ tax_code | bank_account_id | partner company | asset class), debit: "0.00" (optional), credit: "0.00" (optional), memo, bp_id (optional), cost_centre (optional), profit_centre (optional; derived when absent), partner_company (optional; derived from bp_id), movement_type (optional), against_document (optional; the open item this line clears), ic_reference (optional), asset_id (optional)}]}'
    response: '201 {journal_id, entry_no, period_id, status: posted, replay: false, lines: n}; 200 the same with replay: true for the same body; 409 {gate: period_open | journal_conflict, met: false, reason, fix}; 422 {gate: balanced | purposes_mapped | fx_rate | bounds, reason (the difference or the purpose named), fix}'
    rule: 'the account map gives each purpose an account; posted once per run; this call: {source_module: "ar", source_document: "prov:<company_code>:<period_id>", event: provision, journal_date: the period end, lines: [{purpose: doubtful_debt_expense, debit}, {purpose: doubtful_debt_allowance, credit}] (the movement; a release reverses the sides)}'
    status: aligned 2026-10-22 with gl GL-30
  - story: AR-46
    with: gl
    owner: gl
    route: "POST /journal-entry/journals"
    permission: gl:journal:post
    request: '{journal_id: BIGINT as a string (optional; = derived_id(source_document, event), checked), source_module: "ar", source_document: invoice_id | credit_id | writeoff_id, event: issue | void | approve | provision | release, journal_date: YYYY-MM-DD, currency: ISO 4217, fx_rate: string (optional; the document''s frozen rate), memo, company_code (optional; the default company), ledger_id (optional; absent = every ledger of the company), lines: [{account_id: BIGINT as string | purpose (+ tax_code | bank_account_id | partner company | asset class), debit: "0.00" (optional), credit: "0.00" (optional), memo, bp_id (optional), cost_centre (optional), profit_centre (optional; derived when absent), partner_company (optional; derived from bp_id), movement_type (optional), against_document (optional; the open item this line clears), ic_reference (optional), asset_id (optional)}]}'
    response: '201 {journal_id, entry_no, period_id, status: posted, replay: false, lines: n}; 200 the same with replay: true for the same body; 409 {gate: period_open | journal_conflict, met: false, reason, fix}; 422 {gate: balanced | purposes_mapped | fx_rate | bounds, reason (the difference or the purpose named), fix}'
    rule: 'issue posts a service-period line to deferred_revenue; each release moves its days'' share; this call: {source_module: "ar", source_document: "rel:<invoice_id>:<line_no>:<period_id>", event: release, journal_date: the period end, lines: [{purpose: deferred_revenue, debit}, {purpose: revenue, credit, cost_centre}]}'
    status: aligned 2026-10-22 with gl GL-30
  - story: AR-40
    with: gl
    owner: gl
    route: "POST /journal-entry/journals"
    permission: gl:journal:post
    request: '{journal_id: BIGINT as a string (optional; = derived_id(source_document, event), checked), source_module: "ar", source_document: invoice_id | credit_id | writeoff_id, event: issue | void | approve | provision | release, journal_date: YYYY-MM-DD, currency: ISO 4217, fx_rate: string (optional; the document''s frozen rate), memo, company_code (optional; the default company), ledger_id (optional; absent = every ledger of the company), lines: [{account_id: BIGINT as string | purpose (+ tax_code | bank_account_id | partner company | asset class), debit: "0.00" (optional), credit: "0.00" (optional), memo, bp_id (optional), cost_centre (optional), profit_centre (optional; derived when absent), partner_company (optional; derived from bp_id), movement_type (optional), against_document (optional; the open item this line clears), ic_reference (optional), asset_id (optional)}]}'
    response: '201 {journal_id, entry_no, period_id, status: posted, replay: false, lines: n}; 200 the same with replay: true for the same body; 409 {gate: period_open | journal_conflict, met: false, reason, fix}; 422 {gate: balanced | purposes_mapped | fx_rate | bounds, reason (the difference or the purpose named), fix}'
    rule: 'gl''s journal line names the partner company partner_company (the value of ar''s trading_partner); gl''s intercompany match (GL-52) reads ar''s GET /intercompany/open; this call: {source_module: "ar", source_document: invoice_id, event: issue, company_code, lines: [{purpose: ic_receivable, debit, bp_id, partner_company (the invoice''s trading_partner)}, {purpose: ic_revenue, credit, partner_company}, {purpose: tax, tax_code, credit}]}'
    status: aligned 2026-10-22 with gl GL-30
  - story: AR-40
    with: gl
    owner: ar
    route: "GET /intercompany/open?company_code=&trading_partner=&as_at="
    permission: ar:invoice:read             # gl's service client holds it
    request: "query company_code, trading_partner, as_at"
    response: "200 {company_code, trading_partner, as_at, documents: [{invoice_id | credit_id, number, currency, total, outstanding}]}"
    rule: "the receivable side of gl's intercompany reconciliation; outstanding from payments' settlement"
    status: aligned 2026-10-22 with gl GL-52
  - story: AR-41
    with: purchasing
    owner: purchasing
    route: "POST /bills/intercompany"
    permission: purchasing:bill:write       # ar's service client holds it
    request: '{company_code: the receiving (partner) company, bill_id: "ic:<invoice_id>", supplier_company_code, supplier_invoice_no, issue_date, tax_point_date, due_date, currency, lines: [{line_no, description, quantity, unit_code, unit_price, net_amount, tax_code, tax_amount}], totals}'
    response: '201 {bill_id, status: recorded}; 200 the same for the same bill_id; 409 {gate: company_known, reason, fix}'
    rule: 'purchasing''s record_intercompany_bill (PUR-64): the bill is recorded with our frozen tax as the supplier''s (purchase direction codes by tax''s mapping of the sale code); invoicing never writes pur_ tables'
    status: aligned 2026-10-22 with purchasing PUR-64
  - story: AR-47
    with: gl
    owner: gl
    route: "POST /journal-entry/journals"
    permission: gl:journal:post
    request: '{journal_id: BIGINT as a string (optional; = derived_id(source_document, event), checked), source_module: "ar", source_document: invoice_id | credit_id | writeoff_id, event: issue | void | approve | provision | release, journal_date: YYYY-MM-DD, currency: ISO 4217, fx_rate: string (optional; the document''s frozen rate), memo, company_code (optional; the default company), ledger_id (optional; absent = every ledger of the company), lines: [{account_id: BIGINT as string | purpose (+ tax_code | bank_account_id | partner company | asset class), debit: "0.00" (optional), credit: "0.00" (optional), memo, bp_id (optional), cost_centre (optional), profit_centre (optional; derived when absent), partner_company (optional; derived from bp_id), movement_type (optional), against_document (optional; the open item this line clears), ic_reference (optional), asset_id (optional)}]}'
    response: '201 {journal_id, entry_no, period_id, status: posted, replay: false, lines: n}; 200 the same with replay: true for the same body; 409 {gate: period_open | journal_conflict, met: false, reason, fix}; 422 {gate: balanced | purposes_mapped | fx_rate | bounds, reason (the difference or the purpose named), fix}'
    rule: "gl's document splitting (its owner control document_splitting: gl splits receivable and tax lines by the revenue lines' cost centres, GL-22); invoicing never splits"
    status: aligned 2026-10-22 with gl GL-30
  - story: AR-43
    with: ar          # the platform capability einvoice.send (not a module)
    owner: ar
    rule: "ar sends an e-invoice through the platform capability einvoice.send (provider = the einvoice_provider setting), never to an access point directly; the delivery status comes back on POST /einvoices/{message_id}/status. OPEN (2026-10-23, seam-gap PR): erp-platform capabilities.yaml has no einvoice.send entry yet, so the capability request and response are specified only here"
    status: "added 2026-10-23, seam-gap PR"
  - story: AR-43
    with: ar
    owner: ar
    route: "POST /einvoices/{message_id}/status"
    permission: ar:einvoice:deliver         # machine, held by the access-point provider's client
    request: "{message_id, status: delivered | AB | IP | UQ | CA | RE | AP | PD, reason, at}"
    response: "200 {invoice_id, statuses}"
    rule: "the access point is a provider capability composed beside the brick (like payments' PAY-43): ar hands it the validated UBL and it calls back here; it never writes ar_ tables"
  - story: AR-03
    with: payments
    owner: payments
    route: "GET /bank-accounts/default?company_code=&currency="
    permission: pay:receipt:read            # ar's service client holds it
    request: "query company_code, currency"
    response: '200 {bank_account_id, name, account_name, sort_code, account_number, iban, bic}; 404 {gate: receiving_account, reason, fix}'
    rule: "the remittance details printed on the invoice and carried in the UBL PaymentMeans are payments' receiving account for the company and currency (PAY-27, PAY-47)"
    status: aligned 2026-10-22 with payments PAY-47
  - story: AR-09
    with: tax
    owner: ar
    route: "GET /writeoffs?status=approved&approved_from="
    permission: ar:invoice:read             # tax's service client holds it
    request: "query status, approved_from"
    response: "200 {writeoffs: [{writeoff_id, invoice_id, bp_id, amount, currency, approved_at, invoice_issue_date, tax_point_date, due_date, total}]}"
    rule: "tax's bad debt relief reads approved write-offs (tax TAX-34); tax never reads ar_ tables"
    status: aligned 2026-10-22 with tax TAX-34
  - story: AR-21
    with: gl
    owner: gl
    route: GET /companies/{company_code}
    permission: gl:company:read
    request: path company_code
    response: '200 {company_code, name, country, functional_currency, chart_id, bp_id, financial_year_end_month, ledgers: [ledger_id]}; 404 when unknown or not visible'
    rule: 'the functional currency an invoice''s total_functional is in (shared.yaml conventions.functional_currency): gl''s, per company_code; ar keeps no row of its own'
    status: aligned 2026-10-22 with gl GL-61
  - story: AR-20
    with: sales-order
    owner: ar
    route: POST /invoices/raise
    permission: ar:invoice:raise
    request: '{source_kind: sales_order, source_id: so:{sales_order_id}:{request_no}, bp_id (the payer), currency, kind (regular | deposit | final), customer_po_ref, issue_hint (optional, v4: cash = issued at once when the owner control auto_issue_sources contains sales_order:cash, else a draft), lines: [{description, quantity, unit_code (optional: copied from the product when left out), unit_price, discount_pct, tax_category (optional: copied from the product when left out), product_id, sales_order_id, so_line_no, pricing_unit_code (optional, v6), pricing_quantity (optional, v6; v7: the count unit_price is per, a whole number 1 to 100000, default 1), quantity_priced (v7: the line''s quantity in pricing_unit_code, exact; required with pricing_unit_code), net_amount (optional, v6)}], sold_to_bp_id, ship_to_bp_id, bill_to_bp_id (optional business-partner ids, v6: sales-order SO-40 partner functions), deductions (a final invoice or a part invoice, v6; never a deposit): [{deposit_invoice_id, net_amount, tax_category}] (v7: the one name; deposit_deductions [{invoice_id, amount}] and deposit_invoice_ids are deprecated and accepted until ar''s next major, each read as deductions)}'
    request_schema:
      - { field: source_kind, type: string, required: true, values: [sales_order, contract, expense, pos, timesheet] }
      - { field: source_id, type: string, required: true }
      - { field: bp_id, type: bp_id, required: true }
      - { field: currency, type: currency, required: true }
      - { field: kind, type: string, required: false, values: [regular, deposit, final] }
      - { field: customer_po