feat(crm): allow negative line items for manual discount/Rabatt rows
Drops the isInt({ min: 0 }) constraint on lineItems.*.unitPriceMinor
in both the adminInvoices and adminQuotes POST/PUT validators so
admins can add Treuerabatt / Frühbucherrabatt rows as standalone
negative-priced lines (matches standard DE/CH invoice practice).
A service-layer guard rejects saves whose computed total goes below
zero (INVOICE_TOTAL_NEGATIVE / QUOTE_TOTAL_NEGATIVE, both 400) so a
mis-typed discount can't accidentally mint a credit-balance invoice
that would masquerade as a regular row in dashboards. Credit notes
still belong in the Storno path (createStorno), which is unchanged.
Quote-side integration coverage is omitted for now — createQuote's
cold-require path takes ~30s under the test harness; the invoice
test exercises the same validator + guard shape.
This commit is contained in:
@@ -696,6 +696,18 @@ async function createInvoice(payload, adminId, trx = db) {
|
||||
const shippingMinor = ensureInt(payload.shippingAmountMinor);
|
||||
const totalMinor = netMinor + vatMinor + shippingMinor;
|
||||
|
||||
// Negative line items (Rabatt) are allowed, but the resulting
|
||||
// invoice total must not go below zero. Credit notes belong in
|
||||
// the Storno path (createStorno), which mints a separate
|
||||
// kind='storno' record with cancels_invoice_id set.
|
||||
if (totalMinor < 0) {
|
||||
throw new AppError(
|
||||
'Invoice total cannot be negative. To issue a credit note, cancel the original invoice with Storno.',
|
||||
400,
|
||||
'INVOICE_TOTAL_NEGATIVE',
|
||||
);
|
||||
}
|
||||
|
||||
const bank = await businessProfileService.resolveBankAccountForCurrency(currency, payload.businessBankAccountId);
|
||||
|
||||
// Snapshot the selected payment-term template (net days / Skonto /
|
||||
|
||||
Reference in New Issue
Block a user