Note (2026-04-24): After this document was written,
legal_entitywas renamed totenantand the oldtenantwas renamed toorganization. Read references to these terms with the pre-rename meaning.
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Add support for future installment payments and fee line item categorization to correctly model utility invoices where the payment amount includes charges beyond the service total.
Architecture: New Installment schema type + :category enum on line items. Validation formula expanded. Installments become positive Maesn line items without account assignment. Fee items shown in UI payment summary.
Tech Stack: Clojure, Malli schemas, HTMX/Hiccup UI, Maesn REST API
Design doc: docs/plans/2026-03-12-installments-and-fee-line-items-design.md
Files:
src/com/getorcha/schema/invoice/structured_data.cljStep 1: Add Installment schema definition
After the Prepayment definition (line 13), add:
(def Installment
"Future installment payment bundled with this invoice.
Represents an advance payment toward a future billing period."
[:map
[:description :string]
[:amount number?]
[:due-date [:maybe :string]]])
Step 2: Add :category to LineItem schema
In the LineItem definition (lines 73-94), add after :amount:
[:category {:optional true} [:enum :service :fee]]
Step 3: Add :installments to InvoiceData
After :prepayments (line 232), add:
[:installments [:maybe [:vector Installment]]]
Step 4: Verify schema compiles
Run: clj-kondo --lint src/com/getorcha/schema/invoice/structured_data.clj
Step 5: Commit
git add src/com/getorcha/schema/invoice/structured_data.clj
git commit -m "feat: add Installment schema and line item category enum"
Files:
test/com/getorcha/workers/ingestion/validation_test.cljStep 1: Update existing prepayment tests to expect :amount-due key
In test-prepayment-verification (lines 317-341), change all (conj fm-path :details :prepayment :status) references to (conj fm-path :details :amount-due :status).
Step 2: Add test for fee line items in amount-due calculation
(testing "amount-due includes fee line items"
(let [result (validation/validate
(merge base-invoice
{:subtotal 1000.0 :tax-amount 190.0 :total 1190.0
:prepayments [{:description "Deposit" :amount 500.0 :date nil :invoice-reference nil}]
:amount-due 696.5
:line-items [{:description "Service" :quantity 1 :unit-price 1000.0 :amount 1000.0}
{:description "Late fee" :quantity 1 :unit-price 6.5 :amount 6.5
:category :fee}]}))]
(is (= "pass" (get-in result (conj fm-path :status))))
(is (= "pass" (get-in result (conj fm-path :details :amount-due :status))))))
Expected: amount-due (696.5) = total (1190) - prepayments (500) + fees (6.5) = 696.5
Step 3: Add test for installments in amount-due calculation
(testing "amount-due includes installments"
(let [result (validation/validate
(merge base-invoice
{:subtotal 1000.0 :tax-amount 190.0 :total 1190.0
:installments [{:description "Installment" :amount 544.0 :due-date nil}]
:amount-due 1734.0
:line-items [{:description "Service" :quantity 1 :unit-price 1000.0 :amount 1000.0}]}))]
(is (= "pass" (get-in result (conj fm-path :status))))
(is (= "pass" (get-in result (conj fm-path :details :amount-due :status))))))
Expected: amount-due (1734) = total (1190) + installments (544) = 1734
Step 4: Add test for full EVN-style invoice (prepayments + fees + installments)
(testing "EVN-style: prepayments + fee + installment"
(let [result (validation/validate
(merge base-invoice
{:subtotal 1666.86
:tax-amount 333.37
:total 2000.23
:prepayments [{:description "Prior installments" :amount 450.0
:date nil :invoice-reference nil}]
:installments [{:description "New installment" :amount 544.0 :due-date nil}]
:amount-due 2100.73
:line-items [{:description "Energy" :quantity 1 :unit-price 1666.86 :amount 1666.86}
{:description "Late fee" :quantity 1 :unit-price 6.5 :amount 6.5
:category :fee}]}))]
(is (= "pass" (get-in result (conj fm-path :status))))
(is (= "pass" (get-in result (conj fm-path :details :amount-due :status))))))
Expected: amount-due (2100.73) = total (2000.23) - prepayments (450) + fees (6.5) + installments (544) = 2100.73
Step 5: Add test for subtotal excluding fee line items
(testing "subtotal check excludes fee line items"
(let [result (validation/validate
(merge base-invoice
{:subtotal 1000.0
:tax-amount 190.0
:total 1190.0
:amount-due 1196.5
:line-items [{:description "Service" :quantity 1 :unit-price 1000.0 :amount 1000.0}
{:description "Late fee" :quantity 1 :unit-price 6.5 :amount 6.5
:category :fee}]}))]
(is (= "pass" (get-in result (conj fm-path :status))))
(is (= "pass" (get-in result (conj fm-path :details :subtotal :status))))))
Step 6: Run tests to verify they fail
Run: clj -X:test:silent :nses '[com.getorcha.workers.ingestion.validation-test]' 2>&1 | grep -A 5 -E "(FAIL in|ERROR in)"
Expected: failures on the new tests (:amount-due key not found, fee/installment logic missing).
Step 7: Commit failing tests
git add test/com/getorcha/workers/ingestion/validation_test.clj
git commit -m "test: add failing tests for amount-due with fees and installments"
Files:
src/com/getorcha/workers/ingestion/validation.cljStep 1: Rename check-prepayments to check-amount-due and expand formula
Replace the function at lines 292-308 with:
(defn ^:private check-amount-due
"Verify amount-due = total - sum(prepayments) + sum(fee line items) + sum(installments).
Returns sub-check map or nil if amount-due is not present."
[{:keys [total prepayments installments amount-due line-items]}]
(when (and amount-due total)
(let [prepay-sum (reduce + 0.0 (map #(double (:amount %)) prepayments))
installment-sum (reduce + 0.0 (map #(double (:amount %)) installments))
fee-sum (reduce + 0.0 (->> line-items
(filter #(= :fee (:category %)))
(keep :amount)
(map double)))
expected-due (+ (- (double total) prepay-sum) fee-sum installment-sum)
diff (abs (- (double amount-due) expected-due))]
(if (< diff (tolerance-for amount-due :document))
{:status "pass"}
{:status "error"
:message (format "Amount due (%.2f) doesn't match expected %.2f (total %.2f - prepayments %.2f + fees %.2f + installments %.2f)"
(double amount-due) expected-due (double total) prepay-sum fee-sum installment-sum)
:details {:amount-due (double amount-due)
:total (double total)
:prepay-sum prepay-sum
:fee-sum fee-sum
:installment-sum installment-sum
:expected-due expected-due}}))))
Step 2: Update check-subtotal to filter by category
In check-subtotal (line 163), change:
[line-items subtotal gross?]
The function body computes amounts from all line items. Filter to only service items. Replace:
(let [amounts (keep :amount line-items)
With:
(let [service-items (filter #(not= :fee (:category %)) line-items)
amounts (keep :amount service-items)
And update the surcharges sum to also filter:
line-item-surcharges (reduce + 0.0 (map #(sum-surcharges (:surcharges %)) service-items))
Step 3: Update check-financial-math to use new function name and details key
In check-financial-math (lines 330-384):
(check-prepayments data) to (check-amount-due data)pre-result references and the details key from :prepayment to :amount-due:
pre-result (conj pre-result)
stays the same, but in the details map (around line 380):
pre-result (assoc :prepayment pre-result)
becomes:
pre-result (assoc :amount-due pre-result)
Step 4: Run tests
Run: clj -X:test:silent :nses '[com.getorcha.workers.ingestion.validation-test]' 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: all tests pass.
Step 5: Lint
Run: clj-kondo --lint src/com/getorcha/workers/ingestion/validation.clj
Step 6: Commit
git add src/com/getorcha/workers/ingestion/validation.clj
git commit -m "feat: expand validation to check-amount-due with fees and installments"
Files:
src/com/getorcha/workers/ingestion/post_process.cljStep 1: Update extract-failed-checks
At line 1743, change destructuring:
{:keys [line-items subtotal tax total prepayment]} details
to:
{:keys [line-items subtotal tax total amount-due]} details
At lines 1769-1770, change:
(and prepayment (= "error" (:status prepayment)))
(assoc :prepayment prepayment)
to:
(and amount-due (= "error" (:status amount-due)))
(assoc :amount-due amount-due)
Step 2: Update fvr-check-instructions
At lines 1879-1883, change:
(:prepayment failed-checks)
(conj "**Prepayment error**:
Verify amount-due = total - sum(prepayments).
- If we extracted wrong prepayment amounts: provide corrections
- If the document's amount-due is wrong: confirmed=true")
to:
(:amount-due failed-checks)
(conj "**Amount-due error**:
Verify amount-due = total - sum(prepayments) + sum(fee line items) + sum(installments).
- If we extracted wrong prepayment/installment amounts: provide corrections
- If fee line items were miscategorized (should be service or vice versa): provide line-items corrections with category
- If the document's amount-due is wrong: confirmed=true")
Step 3: Update fvr-output-schema
At lines 1935-1936, change:
(:prepayment failed-checks)
(assoc :prepayment base-entry)
to:
(:amount-due failed-checks)
(assoc :amount-due base-entry)
Step 4: Add installments to correction-field-paths
At line 1501, after the "prepayments" entry, add:
"installments" [:installments]
Step 5: Lint
Run: clj-kondo --lint src/com/getorcha/workers/ingestion/post_process.clj
Step 6: Commit
git add src/com/getorcha/workers/ingestion/post_process.clj
git commit -m "feat: update FVR to handle amount-due errors with fees and installments"
Files:
src/com/getorcha/workers/ingestion/extraction.cljStep 1: Update prepayments/amount-due documentation in prompt
At lines 125-135, replace the prepayment/amount-due block with:
- prepayments = array of prior payments/deposits already made (extract each separately)
- installments = array of NEW installment payments for FUTURE billing periods bundled into this invoice.
These are advance payments toward future service/consumption, not charges for services on this invoice.
Only extract installments that are included in the total payment amount — not future scheduled
payments shown for informational purposes.
CRITICAL: Do NOT confuse with prepayments (which are PRIOR payments being deducted).
- prepayments = money already paid in the past, subtracted from total
- installments = money being collected now for future periods, added to payment amount
Common in utility bills, subscription services, and recurring service contracts where the
settlement invoice bundles advance payments for the next period.
- amount-due = the actual amount to be paid / transferred.
This is the final payment amount on the invoice.
It includes: total - prepayments + fees + installments.
If there are no prepayments, fees, or installments, amount-due equals total.
CRITICAL for invoices with prepayments/deposits:
- total = gross invoice amount BEFORE deducting prepayments
- prepayments = extract EACH prepayment as a separate entry with description, date, and amount
- amount-due = final payment amount (may include fees and installments beyond the service total)
Look for prior payment references, deposits, advance payments already credited.
Look for the main invoice totals row, not the "remaining payment" or "balance due" row.
Step 2: Add category to line item schema in prompt
In the JSON line item schema (around line 480), add after "amount":
"category": "service" | "fee", // default "service". Use "fee" ONLY for charges
// that appear AFTER the invoice total — late fees,
// dunning charges, reminder fees. Everything else
// is "service".
Step 3: Add installments to JSON output schema
After the "amount-due" line (around line 447), add:
"installments": [
{
"description": string,
"amount": number,
"due-date": "YYYY-MM-DD" | null
}
] | null,
Step 4: Update financial-page-location note
At lines 139-140, update to include installments:
For invoice-level financial data (subtotal, total, tax, discount, prepayments, installments, amount-due),
record financial-page-location as [start-page, end-page] covering where this data appears.
Step 5: Lint
Run: clj-kondo --lint src/com/getorcha/workers/ingestion/extraction.clj
Step 6: Commit
git add src/com/getorcha/workers/ingestion/extraction.clj
git commit -m "feat: add installments, category, and updated amount-due to extraction prompt"
Files:
src/com/getorcha/integrations/maesn.cljStep 1: Add installments to destructuring
At line 312, add installments to the destructured keys (alongside prepayments).
Step 2: Add installment-items construction
After the prepayment-items block (line 352), add:
;; Installments as positive line items (no account — assigned in DATEV)
installment-items (mapv (fn [{:keys [description amount]}]
{:description (or (sanitize-booking-text description)
"Teilbetrag")
:totalGrossAmount amount})
installments)
Step 3: Update the into chain
Change line 353 from:
api-line-items (into gross-line-items prepayment-items)
to:
api-line-items (-> gross-line-items
(into prepayment-items)
(into installment-items))
Step 4: Lint
Run: clj-kondo --lint src/com/getorcha/integrations/maesn.clj
Step 5: Commit
git add src/com/getorcha/integrations/maesn.clj
git commit -m "feat: include installments as positive line items in Maesn booking proposal"
Files:
src/com/getorcha/erp/ui/components.cljStep 1: Add installments and line-items to payment-summary destructuring
At line 764, add installments and line-items to the :keys vector.
Step 2: Compute fee items from line items
Inside the function body, after the destructuring, add a let binding:
(let [fee-items (filter #(= :fee (:category %)) line-items)]
(Wrap the existing rendering body in this let.)
Step 3: Add fee items rendering after prepayments
After the prepayments block (line 862), add:
;; --- FEE LINE ITEMS ---
(when (seq fee-items)
(for [{:keys [description amount]} fee-items]
^{:key (str "fee-" description)}
[:div.value-row.fee
[:span.value-label description]
[:span.value-amount (format-currency amount currency)]]))
Step 4: Add installments rendering after fee items
;; --- INSTALLMENTS ---
(when (seq installments)
(for [{:keys [description amount due-date]} installments]
^{:key (str "installment-" description)}
[:div.value-row.installment
[:span.value-label (str description
(when due-date (str " (due " due-date ")")))]
[:span.value-amount (format-currency amount currency)]]))
Step 5: Update Amount Due visibility condition
At line 864, change:
(when (or (seq prepayments) (and amount-due (not= amount-due total)))
to:
(when (or (seq prepayments) (seq fee-items) (seq installments)
(and amount-due (not= amount-due total)))
Step 6: Lint
Run: clj-kondo --lint src/com/getorcha/erp/ui/components.clj
Step 7: Commit
git add src/com/getorcha/erp/ui/components.clj
git commit -m "feat: show fee line items and installments in payment summary"
Step 1: Create the issue
gh issue create \
--title "Determine account assignment strategy for installment line items" \
--label "enhancement" \
--body "Installment line items (advance payments for future billing periods) are currently sent to Maesn without an accountNumber. The accountant assigns the correct prepayment asset account manually in DATEV.
Options to evaluate:
1. **Per-legal-entity config** — e.g. \`{:advance-payment-account \"1518\"}\` in org settings
2. **Account picker in ERP UI** — installments get an account field like regular line items
3. **Keep manual** — accountant assigns in DATEV (current behavior)
Depends on customer feedback after rollout of installment support.
Context: docs/plans/2026-03-12-installments-and-fee-line-items-design.md"
Step 2: Commit is not needed (issue is on GitHub, not in code).
Step 1: Lint entire project
Run: clj-kondo --lint src test dev
Fix any issues found.
Step 2: Run all tests
Run: clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Ran .* tests)"
All tests should pass.
Step 3: Final commit if any lint fixes were needed
git add <fixed-files>
git commit -m "fix: lint issues from installments feature"