A discount that goes negative is a refund. A discount larger than the order total is a payout. Both look like ordinary arithmetic until they reach the ledger.
The usual shape is familiar: a percentage rule, a fixed-amount rule, a stacking rule, and somewhere in the checkout handler a line that says if the result is below zero, use zero. Then the same guard appears in the cart preview. Then in the invoice generator. Then someone adds a new promo type and writes the multiplication without the guard, because the guard was never part of the type — it was part of whoever remembered.
We put the clamp in the discount type itself. Applying a discount to a Money amount cannot return less than zero and cannot exceed the order total, because the operation that produces the number owns both bounds. Call sites stop checking. New promo types inherit the invariant instead of re-deriving it.
After years on payment and lending systems, our take is that money bugs are rarely arithmetic bugs. They are placement bugs: the rule was correct, it just lived in the caller instead of the type, so the fourth caller never got it. We open-sourced the promocodes package we use for this at github.com/shipmindlabs/promocodes — the interesting part is not the promo engine, it is where the bounds live.
Where do your money invariants live right now — in the type, or in the handler that happened to remember them?