Order Line Item Amounts

1. Overview

OrderLineItem carries five monetary columns: gross, discount, price, net and fee. This page defines each one, states the identity that binds them, and records where the values come from.

The model separates two different questions that are easy to conflate:

  • What did the customer owe us? — answered by gross, discount and net.

  • What did it cost us to collect it? — answered by fee.

A line item is one participant’s entry into one event, or one membership. Quantity is always one, so the line amount and the unit amount are the same number.

2. The amount model

Column Meaning Source

gross

List price before any discount

The product’s catalogue price at the time of sale

discount

Reduction granted on this line

gross less the amount actually charged

price

Amount charged after discount

What the customer was asked to pay for this line

net

Revenue recognised for this line

Equal to price

fee

Cost of collecting the payment

The payment processor’s charge, VAT inclusive

2.1. The identity

net = gross - discount

price holds the same value as net. The two are kept separate because price is the charged amount as stated on the order, while net is the revenue figure financial reporting sums. They coincide today and nothing should assume they must.

fee is not part of this identity. It is an expense incurred against the line, not a reduction of what the customer owed. Revenue is net; the cost of collection is fee; the contribution after payment costs is net - fee. Subtracting fee from net when storing the value would make the line’s revenue depend on how the customer chose to pay, which is not a property of the sale.

2.2. A worked example

A high-school entry with a catalogue price of R200, sold to a school at a bulk rate of R30, paid by card where the processor charges 4.2% plus VAT on the amount transacted:

Column Value Derivation

gross

200.00

Catalogue price

discount

170.00

200.00 - 30.00

price

30.00

Charged to the school

net

30.00

gross - discount

fee

1.45

30.00 x 4.2% x 1.15, rounded

Revenue for the line is R30.00. Payment cost is R1.45. Contribution is R28.55. The R170.00 discount is visible as forgone revenue without having to reconstruct it from the catalogue.

3. Fee attribution

The payment processor charges per order, not per line. A single charge covers every participant on that order.

The stored fee is therefore an apportionment: the order’s total processor charge, divided across its lines in proportion to price. Where every line on an order carries the same price the result is an even split; where prices differ, a line that contributed more of the transacted amount absorbs more of the fee. Rounding is absorbed on the last line so the apportioned amounts sum exactly to the charge.

Two properties follow, and both matter when reading a report:

  • A line’s fee is an allocation, not an observed charge. Only the order-level total is a fact.

  • An order paid by a method that carries no per-transaction charge has no fee to apportion, and its lines carry zero rather than a NULL.

3.1. Fees are VAT inclusive

fee is stored VAT inclusive, because that is the amount that leaves the bank account. A processor invoice states its line amounts exclusive of VAT and adds VAT as a single figure at the foot, so reconciling a stored fee against an invoice line requires multiplying the invoice figure by 1.15.

A processor invoice also carries charges that are not per-transaction — payout or withdrawal fees, and adjustments for refunds. These have no order to attach to and are not represented in fee. An invoice total will therefore exceed the sum of stored fees for that period by exactly those items.

4. Relationship to the Recon entity

Recon — the payment-processor settlement import described in Payment Reconciliation Design — also has columns named gross, fee and nett. They do not mean the same thing as the columns on OrderLineItem.

OrderLineItem Recon

Question answered

What did the customer owe, and what did collection cost?

What did the processor actually settle into the account?

gross

List price before discount

Amount transacted

Net column

net = gross - discount

nett = gross - fee - fee_tax

Role of fee

An expense recorded alongside revenue

A deduction that produces the settled amount

The two are reconciled against each other, not merged. A settlement line’s nett is what the bank received; the matching order lines' net is the revenue those entries represent. The difference between them is the payment cost, which appears as fee on one side and as fee plus fee_tax on the other.

Read a column name in this codebase only together with the entity it sits on.

5. Where the values are written

Two code paths create order line items, and they populate the model to different depths.

5.1. Membership orders

The membership order update path receives the charged and pre-discount amounts from the storefront and populates the full sales picture. The storefront’s line subtotal becomes gross; its line total becomes price and net; the difference becomes discount. A line reduced to zero against a non-zero catalogue price records the whole catalogue price as the discount, so that a comped membership still shows what was given away.

5.2. Event registration orders

Event participants are attached to an order at registration, which happens before payment. The order is created with the number DRAFT and the participants are linked to it; only when the buyer proceeds to pay is a storefront order created and the order given its real number.

The line item factory sets price and net when it is given a price or a product to price from, and leaves them at the created default otherwise. It never sets gross, discount or fee on any path.

6. Limitations of the current design

These are properties of the system as it stands. A reader reasoning about the data needs all of them.

6.1. The fee column is never written

No code path assigns fee. The column exists on the entity, the DTO and the criteria filter, and is readable through the API, but nothing populates it. Payment costs are consequently absent from every figure the system derives, and must be obtained from the processor’s own records.

6.2. Event registration records no amounts

Where the registration path attaches a participant without a resolved product or price, the line is created with price at zero and gross, discount, net and fee left NULL — even though the price is determinable at that moment from the participant’s event category, which carries a product reference.

An order in this state has no monetary value anywhere in the database. Its status records whether it was paid; how much it was paid for does not survive. Where such orders dominate an event, financial reporting for that event cannot be answered from the system at all.

6.3. The net fallback subtracts the discount twice

Two REST resources fill in a missing net when mapping a line item to its DTO, computing it as price - discount. Because price already holds the post-discount amount, a line with a stored discount and no stored net yields a value one discount too low.

The defect is latent rather than active: the only path that writes discount also writes net, so no stored row currently satisfies both conditions. It will produce wrong totals for the first line item that carries a discount without a net — including any line populated by a back-fill that sets gross and discount but not net. The order total is summed from these DTO values, so the error propagates from the line to the order.

6.4. Net and price cannot diverge

Both writing paths assign the same value to price and net. Nothing enforces the identity net = gross - discount at the persistence layer, so a row can be written that violates it, and nothing detects that it has been.

7. Reading amounts safely

Until the columns above are populated across all order types, a report that needs monetary values should:

  • Treat a NULL gross as "not recorded", never as zero. A zero gross asserts the item was free.

  • Treat price of zero as meaningful only when gross is also recorded — a zero price against a recorded gross is a full discount; a zero price against a NULL gross is an unpopulated row.

  • Derive discount as gross - net rather than reading the column, where gross and net are both present, since that expression holds by definition and the column may be NULL.

  • Take payment costs from the processor’s settlement records rather than from fee.