Extracting Line-Item Discounts and Rebates From an Invoice
9 min read · updated August 11, 2026
The word “discount” on an invoice refers to at least three unrelated objects. Two of them reduce the amount you owe and one of them does not, and every reconciliation failure involving a discount is a case of the third being treated as one of the first two.
Three levels, three meanings
The European invoicing standard EN 16931, which Peppol BIS Billing 3.0 implements, is unusually clear here because it gives each level its own business term. That vocabulary is worth borrowing even if you never touch a UBL document.
- The line allowance — BT-136, an allowance amount attached to one invoice line. It is already reflected in that line’s net amount, BT-131. A 10% trade discount on one product code lives here.
- The document allowance — BT-107, the sum of allowances at document level, applied after the line nets are summed. A settlement discount for volume across the whole order, or a goodwill credit, lives here.
- The separate credit note — not on this invoice at all. A rebate agreed after the fact arrives as its own document referencing the original, and it must be applied once, against that original, not folded into the invoice extraction.
The distinction is not cosmetic; it decides where the amount enters the arithmetic. The invoice total without tax, BT-109, is the sum of line nets minus document allowances plus document charges. A line allowance is already inside the first of those terms. Move it to the document level and you subtract it twice; move a document allowance into a line and the line net stops matching quantity times unit price.
An early-payment discount is not an allowance
Terms of the form “2/10 net 30” promise 2% off if the invoice is paid within ten days. That is a conditional future event. At the moment the invoice is extracted it has not happened, it may never happen, and the invoice total is the full amount. It is a payment term, not an allowance.
Models get this wrong in a specific and predictable way: asked for “discounts on this invoice”, a model reads the terms line, sees a percentage, and returns it as a discount object with an amount it computes itself. The amount is arithmetically correct and semantically wrong, and if a downstream system subtracts it, the payment is short by exactly 2% on every invoice from that supplier.
Keep them in different fields with different names and no shared type:
"allowances": [
{ "level": "line", "line_no": 3, "reason": "Trade discount 10%",
"amount": { "minor": 4200, "currency": "EUR" } },
{ "level": "document", "reason": "Volume rebate",
"amount": { "minor": 15000, "currency": "EUR" } }
],
"payment_terms": {
"raw": "2/10 net 30",
"net_days": 30,
"early_discount": { "percent": "2.0", "within_days": 10 }
}Nothing under payment_terms ever enters the total. Parsing that string reliably, including its European variants, is the subject of extracting payment terms into a structured field.
The discount that is already in the price
Article 226(8) of Council Directive 2006/112/EC requires an invoice to show, per rate, the taxable amount, the unit price exclusive of VAT, and any discounts or rebates not included in that unit price. The final clause is the interesting one: a discount that has been baked into the unit price does not have to appear anywhere.
This is extremely common on contract pricing. The line reads 120 units at 14.50, the list price is 16.00, and the 9.4% customer discount is invisible on the document. No extraction can recover it, because the information is not present. Any pipeline that reports “discount captured” as a coverage metric will look catastrophically bad on exactly the suppliers where pricing is best controlled, and the metric is measuring document formatting rather than commercial reality.
If you need realised discount against list, that is a join against your own price master on product code, not a field on the invoice. Extract the product code well and the question answers itself elsewhere.
Signs, parentheses and trailing minuses
Allowances are the field where sign conventions do the most damage, because a sign error here is a doubling of the error rather than a rounding of it: subtracting a negative adds.
- Accounting parentheses.
(1,234.56)is negative one thousand two hundred thirty-four point five six. A model transcribing it as a positive number, or dropping the parentheses as punctuation, flips the sign of the largest correction on the invoice. - Trailing minus. Systems descended from mainframe report writers print
1234.56-. Same value, different position, and a naive numeric parse either fails or silently returns the positive. - Positive magnitude in a negative slot. EN 16931 models an allowance as a positive amount inside an allowance structure, and it is subtracted by the calculation rather than by its sign. A PDF invoice usually prints it as a negative line. Pick one convention for your schema, state it in the field description — this is squarely designing a schema for variants you have not seen — and normalise on the way in. Mixing the two is how the same allowance ends up added on one supplier and subtracted on another.
Ask the model for the string as printed alongside the parsed value. A stored as_printed of "(420.00)" next to a parsed +42000 minor units is a bug you can find with a query; the same bug without the string is a number that looks fine.
How the double-count happens, and what stops it
The failure has a standard shape. The invoice shows a line at 1,740.00 with “less 10% = 174.00” printed underneath and a line net of 1,566.00. A model returns three things: the gross line, the discount as a document allowance, and the net line. A downstream total then computes 1,566.00 minus 174.00 and lands 174.00 low. The extraction is not hallucinating; it is reporting everything it can see, which is exactly the right behaviour for transcription and exactly the wrong behaviour for arithmetic.
Three assertions catch it, and all three are cheap:
- For every line: quantity times unit price minus line allowances plus line charges equals the line net, within one minor unit. A line whose allowance is also recorded at document level fails this immediately.
- The sum of line nets must equal the stated subtotal. If it does not, and the difference equals a discount amount you extracted, you know precisely which level that discount belongs at.
- No allowance amount may appear at two levels with the same value and the same reason. This is a duplicate check within one document and it catches the case above directly.
All three are instances of the same idea: the invoice states its own answer, so you never have to trust the extraction on totals. The full version of that argument, with the calculation chain and a tolerance that is not a floating-point epsilon, is in reconciling an invoice total against its own line items. Where a discount genuinely cannot be placed — a lump-sum adjustment with no reason code and no matching line — the honest output is the amount, the raw text, and a review flag, which is the trade-off discussed in designing schemas for edge cases.