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 Design

Date: 2026-03-12 Status: Approved

Problem

Utility invoices (and similar recurring-service invoices) bundle multiple financial components into a single payment:

  1. Service charges for the current period (the invoice total)
  2. Deductions for prior installments already paid (prepayments — handled today)
  3. Fees outside the service total (e.g. late fees, dunning charges)
  4. New installments for future billing periods (advance payments)

The current system only models (1) and (2). Fee line items like Mahngebühr are extracted as line items but break the validation formula because they sit outside the subtotal→total path. Installments are not captured at all.

The "Zu zahlender Betrag" (actual payment amount) = total - prepayments + fees + installments, but amount-due currently only validates against total - prepayments.

Example: EVN Jahresrechnung

Strom inkl. USt (total):            2.000,23€   ← services rendered
Bezahlte Teilbeträge (prepayments):  -450,00€   ← prior installments paid
Restforderung:                       1.550,23€
Mahngebühr (fee line item):             6,50€   ← outside total
Erster neuer Teilbetrag:               544,00€   ← advance for next period
Zu zahlender Betrag:                 2.100,73€   ← actual bank transfer

Design

1. Schema Changes

New Installment type (top-level on InvoiceData)

(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]]])   ;; ISO 8601

;; On InvoiceData:
[:installments [:maybe [:vector Installment]]]

Installments are separate from line items because:

New :category field on LineItem

[:category {:optional true} [:enum :service :fee]]

Optional field: existing data without a category is implicitly :service.

amount-due semantics

No schema change. amount-due represents the actual payment amount (Zu zahlender Betrag), not just total - prepayments.

2. Extraction Prompt Changes

Installments (new section after prepayments)

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

Updated amount-due definition

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

Line item category field

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

3. Validation Changes

check-prepayments → renamed to check-amount-due

New formula:

amount-due ≈ total - Σ(prepayments) + Σ(fee line items) + Σ(installments)

Uses existing document-level tolerance (0.6%, floor 0.10).

Updated check-subtotal

Filter to only :service line items:

subtotal ≈ Σ(line items where category ∈ {:service, nil})

No changes to check-total or check-line-items

Fee line items don't affect the total formula. Individual line item math is unaffected by category.

4. FVR Changes

Renamed error type: :prepayment:amount-due

**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 corrections
- If the document's amount-due is wrong: confirmed=true

Correction paths: prepayments, installments, amount-due, line item category.

Subtotal error update

Note that only :service line items contribute to subtotal. FVR can correct miscategorized line items.

5. Maesn Integration Changes

Installments as positive line items

installment-items  (mapv (fn [{:keys [description amount]}]
                           {:description      (or (sanitize-booking-text description)
                                                  "Teilbetrag")
                            :totalGrossAmount amount})
                     installments)
api-line-items     (-> gross-line-items
                       (into prepayment-items)
                       (into installment-items))

No accountNumber on installment line items — accountant assigns the correct prepayment asset account in DATEV. See [GH issue TBD] for deferred account assignment decision.

No changes to effective-amount or isPaymentOrder

amount-due is now the Zu zahlender Betrag, so existing logic works as-is.

6. UI Changes

Payment summary rendering order

Subtotal → Discount → Shipping → Packaging → Surcharges → Tax → Total
→ Prepayments (negative, green)
→ Fee line items (positive, with description)
→ Installments (positive, with description and due date)
→ Amount Due

Fee line items appear in the payment summary (between Total and Amount Due) in addition to the line items table, so the user understands why amount-due ≠ total - prepayments.

Amount Due row shown when any of: prepayments, fee items, installments exist, or amount-due ≠ total.

7. Deferred: Account Assignment for Installments

Create a GitHub issue to evaluate account assignment strategies post-rollout:

  1. Per-legal-entity config
  2. Account picker in ERP UI
  3. Keep manual assignment in DATEV (current)