Note (2026-04-24): After this document was written, legal_entity was renamed to tenant and the old tenant was renamed to organization. Read references to these terms with the pre-rename meaning.

Installments & Fee Line Items Implementation Plan

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


Task 1: Schema — Add Installment type and line item category

Files:

Step 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"

Task 2: Validation — Write failing tests for check-amount-due

Files:

Step 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"

Task 3: Validation — Implement check-amount-due and update check-subtotal

Files:

Step 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):

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"

Task 4: FVR — Update error type from :prepayment to :amount-due

Files:

Step 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"

Task 5: Extraction prompt — Add installments, category, and updated amount-due

Files:

Step 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"

Task 6: Maesn — Add installment line items to booking proposal

Files:

Step 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"

Task 7: UI — Show fees and installments in payment summary

Files:

Step 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"

Task 8: GitHub issue for deferred account assignment

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).


Task 9: Run full test suite and lint

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"