Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f81265e
ap: refuse the approve of a pending bill carrying a paid amount (PR 7…
futurebuildai Oct 11, 2026
224d884
ap: key the void no reversal arm on a live journal entry, not the tot…
futurebuildai Oct 11, 2026
66c68cf
ap: read the currency.default setting for an omitted currency (PR 89 …
futurebuildai Oct 11, 2026
6df4a2e
api: the write off fragment answers the code, not an invented blocker…
futurebuildai Oct 11, 2026
d05a587
ap: two cosmetics from the PR 78 reviews (r7 P3-2, r8 P3-3)
futurebuildai Oct 11, 2026
6b1e955
ap: the currency setting test puts the seeded row back
futurebuildai Oct 11, 2026
ed8346a
docs: the ap curl example dates as placeholders
futurebuildai Oct 11, 2026
55a5245
Merge remote-tracking branch 'origin/refactor/v1' into refactor/ap-fo…
futurebuildai Oct 11, 2026
692efdb
ap: a reversal counts against a bill's credit only while it is posted…
futurebuildai Oct 11, 2026
da56fc6
ap: the void's linked arm is keyed on the ledger, never a 500 (PR 99 …
futurebuildai Oct 11, 2026
5832e3d
ap: a payment's entry posts in the bills' currency (PR 99 r2 P2-2)
futurebuildai Oct 11, 2026
e51700f
ap: a double posted bill does not void with its duplicate credit live…
futurebuildai Oct 11, 2026
57d030e
ap: three review round 1 follow ups (PR 99 r1 P3-1, P3-2, P3-3)
futurebuildai Oct 11, 2026
831f422
ap: straighten the void's doc comment and order the ledger reads
futurebuildai Oct 11, 2026
6ea9b72
contract: the void and payment rules of the PR 99 review fixes
futurebuildai Oct 11, 2026
61b4deb
Merge remote-tracking branch 'origin/refactor/v1' into refactor/ap-fo…
futurebuildai Oct 11, 2026
6e9ba51
Merge remote-tracking branch 'origin/refactor/v1' into refactor/ap-fo…
futurebuildai Oct 11, 2026
321f289
ap: liveness is the net of the whole reversal chain (review r4 P1-1)
futurebuildai Oct 11, 2026
440b980
ap: the approve refuses a bill that already holds a live entry (revie…
futurebuildai Oct 11, 2026
54cb126
gl: a lost reversal insert race maps to ErrAlreadyReversed (review r4…
futurebuildai Oct 11, 2026
be9293d
gl: a reversal posts in the entry's own currency (review r4 P3-3)
futurebuildai Oct 11, 2026
9e2f171
ap: pin the void of a linked entry and reversal both void (review r3 …
futurebuildai Oct 11, 2026
3d73c55
docs and contract: the chain liveness rule, the approve refusal, the …
futurebuildai Oct 11, 2026
d76b9c1
Merge remote-tracking branch 'origin/refactor/v1' into refactor/ap-fo…
futurebuildai Oct 11, 2026
c1585e8
ap: the chain liveness nets per currency, and the GL reverse route ke…
futurebuildai Oct 11, 2026
2ad596e
ap: a GL void that lands mid void maps to the same 409, never a 500
futurebuildai Oct 11, 2026
8ea80a1
ap: the liveness refusals name the net amount, never a credit alone
futurebuildai Oct 11, 2026
ecd4eb8
ap: the reinstated entry test asserts the ledger ties after every act
futurebuildai Oct 11, 2026
6d8d098
gl: the reversal race map is keyed on the one reversal index's name
futurebuildai Oct 11, 2026
9f42561
ap: gofmt the cross currency reproducer's comment alignment
futurebuildai Oct 11, 2026
7a24cbe
contract: regenerate schema.d.ts from the round 4 fragment wording
futurebuildai Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 49 additions & 12 deletions core/api/fragments/ap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,18 +174,52 @@ paths:
posting failure fails the act (a closed period is a 409 blocker
period_closed; a line with no account is a 409 blocker
line_needs_account; a required chart of accounts code that is missing
is a 409 blocker account_missing naming it). To voided: from pending
(nothing posted) or
approved (the entry is reversed whole; a bill that prices to zero
posted nothing and voids with nothing to reverse); a reason is
is a 409 blocker account_missing naming it; a pending bill that
already carries a paid amount, a legacy or hand edited row no payment
of its own could write, is a 409 blocker has_paid_amount naming the
cents, and the row is left for the operator to mend by hand; a
pending bill that already holds a live journal entry, a legacy row
whose posting ran before the status moved with its entry linked by
hand or not at all, is a 409 blocker entry_not_linked naming the live
entry by its number and id, nothing written). To
voided: from pending or
approved; a bill's credit is live exactly when the POSTED entries of
its VENDOR_INVOICE entry and that entry's whole reversal chain leave
a net amount on Accounts Payable (a reversal of a reversal puts the
credit back, whatever depth the chain has reached; the net is summed
per currency, never across two, as the trial balance sums it, so a
chain holding two currencies whose whole sums cancel still reads
live in the currency that holds the amount, however the chain
arose; the net may stand as a credit or as a debit, and the
refusals name a net amount standing on Accounts Payable, never a
credit alone), and the void
decides on that net, never on one step of the chain: a bill whose
entry holds no live credit (one that prices to zero, a legacy bill
whose posting was swallowed, or an entry the GL routes have already
undone, linked or not) voids with nothing to reverse, a linked live
entry with no reversal row is reversed whole in the entry's own
currency, and a linked live entry whose one reversal row is already
taken (the row is not posted, or the chain was reinstated behind it,
so no new reversal can post) is refused with the
reversal_not_posted
blocker naming the shape; a
reason is
required, a bill with money applied is refused with the has_payments
blocker, an approved or partial bill that owes but has no linked
journal entry (a legacy bill whose posting was swallowed, or one
holding the removed sync's one leg credit) with the entry_not_linked
blocker, a bill that holds a live journal entry it is not linked to
(a legacy bill whose posting was swallowed, one holding the removed
sync's one leg credit, one whose total was hand edited after its
approve, or one posted twice, the duplicate named) with the
entry_not_linked
blocker, and a bill whose linked entry does not balance with the
entry_unbalanced blocker; nothing is written on either refusal. No
route links an entry to a bill yet: such a bill stays approved and
payable until a later repair item (a stated limit).
entry_unbalanced blocker; nothing is written on any refusal, and a
reversal insert that loses a race with a concurrent GL reverse on the
same entry answers the same reversal_not_posted 409, never a 500, and
so does a GL void of the linked entry that commits while the void
runs (the entry is no longer POSTED, there is nothing left to
reverse, nothing is written and the retry voids with nothing to
reverse). No
route links an entry to a bill yet: such a bill keeps its status
until a later repair item (a stated limit).
parameters:
- name: id
in: path
Expand Down Expand Up @@ -282,8 +316,11 @@ paths:
is a 409 blocker invoice_not_approved; a bill of another vendor is a
400 naming invoice_ids[i]); a repeated id in invoice_ids is a 400
naming its index (appears twice); more than the named bills still owe
is a 409 blocker exceeds_open. The entry posts through gl.PostEntry inside
the act's transaction (DR Accounts Payable, CR Cash), and a posting
is a 409 blocker exceeds_open. The named bills must share one
currency: a payment spanning two is a 409 blocker currency_mismatch
naming the bills that differ. The entry posts through gl.PostEntry
inside the act's transaction in the bills' currency
(DR Accounts Payable, CR Cash), and a posting
failure fails the act (a closed period is a 409 blocker period_closed;
a missing chart of accounts code is a 409 blocker account_missing
naming it).
Expand Down
7 changes: 5 additions & 2 deletions core/api/fragments/gl.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,11 @@ paths:
dated today and linked by reverses_entry_id. The optional body reason
is appended to the memo; an undecodable or absent body is ignored.
Only a POSTED entry that has not already been reversed and has at
least 2 lines can be reversed. Every refusal, including an unknown
id, is a 400.
least 2 lines can be reversed. The reversal posts in the original
entry's own currency (the entry priced its legs into that currency,
so a reversal in the currency.default setting's code would leave the
control account holding both halves of one undo in different
currencies). Every refusal, including an unknown id, is a 400.
parameters:
- name: id
in: path
Expand Down
7 changes: 4 additions & 3 deletions core/api/fragments/invoice.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,10 @@ paths:
description: 'Roles admin, owner, finance; a reason is required. Writes an amount of an unpaid or partial invoice
off as bad debt: DR 5040 / CR 1020, a WRITE_OFF application and a WRITE_OFF subledger row. A write off
that takes the open amount to zero closes the invoice as written_off, else it stays partial. The precondition
is the invoice''s revision. Refused with 409 exceeds_open_amount, invoice_not_open (invalid_state_transition)
or period_closed. Writes invoice.written_off when it closes the invoice, else invoice.partial, and customer.updated.
Reversing the application (POST /ar/applications/{id}/reverse) reopens the invoice.'
is the invoice''s revision. Refused with 409 exceeds_open_amount, invalid_state_transition (the invoice is
neither unpaid nor partial) or period_closed. Writes invoice.written_off when it closes the invoice, else
invoice.partial, and customer.updated. Reversing the application (POST /ar/applications/{id}/reverse) reopens
the invoice.'
parameters:
- name: id
in: path
Expand Down
8 changes: 4 additions & 4 deletions core/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1317,7 +1317,7 @@ paths:
tags: [ap]
summary: Approve or void a vendor invoice
description: >-
Role guard admin, owner or finance, held to the caller's branch wall. Replaces the old /approve route. The body names the target status and carries the revision (or If-Match); a write with neither is 428, a mismatch 409 stale_revision. To approved: only from pending; the entry posts through gl.PostEntry inside the transition's transaction (a debit leg per line on the line's account, the tax spread across the line accounts pro rata, the credit to Accounts Payable), and a posting failure fails the act (a closed period is a 409 blocker period_closed; a line with no account is a 409 blocker line_needs_account; a required chart of accounts code that is missing is a 409 blocker account_missing naming it). To voided: from pending (nothing posted) or approved (the entry is reversed whole; a bill that prices to zero posted nothing and voids with nothing to reverse); a reason is required, a bill with money applied is refused with the has_payments blocker, an approved or partial bill that owes but has no linked journal entry (a legacy bill whose posting was swallowed, or one holding the removed sync's one leg credit) with the entry_not_linked blocker, and a bill whose linked entry does not balance with the entry_unbalanced blocker; nothing is written on either refusal. No route links an entry to a bill yet: such a bill stays approved and payable until a later repair item (a stated limit).
Role guard admin, owner or finance, held to the caller's branch wall. Replaces the old /approve route. The body names the target status and carries the revision (or If-Match); a write with neither is 428, a mismatch 409 stale_revision. To approved: only from pending; the entry posts through gl.PostEntry inside the transition's transaction (a debit leg per line on the line's account, the tax spread across the line accounts pro rata, the credit to Accounts Payable), and a posting failure fails the act (a closed period is a 409 blocker period_closed; a line with no account is a 409 blocker line_needs_account; a required chart of accounts code that is missing is a 409 blocker account_missing naming it; a pending bill that already carries a paid amount, a legacy or hand edited row no payment of its own could write, is a 409 blocker has_paid_amount naming the cents, and the row is left for the operator to mend by hand; a pending bill that already holds a live journal entry, a legacy row whose posting ran before the status moved with its entry linked by hand or not at all, is a 409 blocker entry_not_linked naming the live entry by its number and id, nothing written). To voided: from pending or approved; a bill's credit is live exactly when the POSTED entries of its VENDOR_INVOICE entry and that entry's whole reversal chain leave a net amount on Accounts Payable (a reversal of a reversal puts the credit back, whatever depth the chain has reached; the net is summed per currency, never across two, as the trial balance sums it, so a chain holding two currencies whose whole sums cancel still reads live in the currency that holds the amount, however the chain arose; the net may stand as a credit or as a debit, and the refusals name a net amount standing on Accounts Payable, never a credit alone), and the void decides on that net, never on one step of the chain: a bill whose entry holds no live credit (one that prices to zero, a legacy bill whose posting was swallowed, or an entry the GL routes have already undone, linked or not) voids with nothing to reverse, a linked live entry with no reversal row is reversed whole in the entry's own currency, and a linked live entry whose one reversal row is already taken (the row is not posted, or the chain was reinstated behind it, so no new reversal can post) is refused with the reversal_not_posted blocker naming the shape; a reason is required, a bill with money applied is refused with the has_payments blocker, a bill that holds a live journal entry it is not linked to (a legacy bill whose posting was swallowed, one holding the removed sync's one leg credit, one whose total was hand edited after its approve, or one posted twice, the duplicate named) with the entry_not_linked blocker, and a bill whose linked entry does not balance with the entry_unbalanced blocker; nothing is written on any refusal, and a reversal insert that loses a race with a concurrent GL reverse on the same entry answers the same reversal_not_posted 409, never a 500, and so does a GL void of the linked entry that commits while the void runs (the entry is no longer POSTED, there is nothing left to reverse, nothing is written and the retry voids with nothing to reverse). No route links an entry to a bill yet: such a bill keeps its status until a later repair item (a stated limit).
parameters:
- name: id
in: path
Expand Down Expand Up @@ -1400,7 +1400,7 @@ paths:
tags: [ap]
summary: Pay a vendor
description: >-
Role guard admin, owner or finance. The request's money is int64 amount_cents (a decimal amount is a 400 naming it; the old float dollars amount is gone); the rest of the shape keeps its today form (uppercase methods) until the payment routes' own conversion. The payment locks the named bills in id order and applies only to bills in approved or partial (a pending, paid or voided bill is a 409 blocker invoice_not_approved; a bill of another vendor is a 400 naming invoice_ids[i]); a repeated id in invoice_ids is a 400 naming its index (appears twice); more than the named bills still owe is a 409 blocker exceeds_open. The entry posts through gl.PostEntry inside the act's transaction (DR Accounts Payable, CR Cash), and a posting failure fails the act (a closed period is a 409 blocker period_closed; a missing chart of accounts code is a 409 blocker account_missing naming it).
Role guard admin, owner or finance. The request's money is int64 amount_cents (a decimal amount is a 400 naming it; the old float dollars amount is gone); the rest of the shape keeps its today form (uppercase methods) until the payment routes' own conversion. The payment locks the named bills in id order and applies only to bills in approved or partial (a pending, paid or voided bill is a 409 blocker invoice_not_approved; a bill of another vendor is a 400 naming invoice_ids[i]); a repeated id in invoice_ids is a 400 naming its index (appears twice); more than the named bills still owe is a 409 blocker exceeds_open. The named bills must share one currency: a payment spanning two is a 409 blocker currency_mismatch naming the bills that differ. The entry posts through gl.PostEntry inside the act's transaction in the bills' currency (DR Accounts Payable, CR Cash), and a posting failure fails the act (a closed period is a 409 blocker period_closed; a missing chart of accounts code is a 409 blocker account_missing naming it).
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
Expand Down Expand Up @@ -5465,7 +5465,7 @@ paths:
tags: [gl]
summary: Reverse a posted journal entry
description: >-
Role guard admin or owner. Books a posted, net-zero reversing entry dated today and linked by reverses_entry_id. The optional body reason is appended to the memo; an undecodable or absent body is ignored. Only a POSTED entry that has not already been reversed and has at least 2 lines can be reversed. Every refusal, including an unknown id, is a 400.
Role guard admin or owner. Books a posted, net-zero reversing entry dated today and linked by reverses_entry_id. The optional body reason is appended to the memo; an undecodable or absent body is ignored. Only a POSTED entry that has not already been reversed and has at least 2 lines can be reversed. The reversal posts in the original entry's own currency (the entry priced its legs into that currency, so a reversal in the currency.default setting's code would leave the control account holding both halves of one undo in different currencies). Every refusal, including an unknown id, is a 400.
parameters:
- name: id
in: path
Expand Down Expand Up @@ -6857,7 +6857,7 @@ paths:
tags:
- invoice
summary: Write off part or all of an invoice's open amount
description: 'Roles admin, owner, finance; a reason is required. Writes an amount of an unpaid or partial invoice off as bad debt: DR 5040 / CR 1020, a WRITE_OFF application and a WRITE_OFF subledger row. A write off that takes the open amount to zero closes the invoice as written_off, else it stays partial. The precondition is the invoice''s revision. Refused with 409 exceeds_open_amount, invoice_not_open (invalid_state_transition) or period_closed. Writes invoice.written_off when it closes the invoice, else invoice.partial, and customer.updated. Reversing the application (POST /ar/applications/{id}/reverse) reopens the invoice.'
description: 'Roles admin, owner, finance; a reason is required. Writes an amount of an unpaid or partial invoice off as bad debt: DR 5040 / CR 1020, a WRITE_OFF application and a WRITE_OFF subledger row. A write off that takes the open amount to zero closes the invoice as written_off, else it stays partial. The precondition is the invoice''s revision. Refused with 409 exceeds_open_amount, invalid_state_transition (the invoice is neither unpaid nor partial) or period_closed. Writes invoice.written_off when it closes the invoice, else invoice.partial, and customer.updated. Reversing the application (POST /ar/applications/{id}/reverse) reopens the invoice.'
parameters:
- name: id
in: path
Expand Down
Loading
Loading