Files
app/src/__tests__/item-discount-schema.test.ts
BOHA 719d1f2659 feat(documents): per-item Sleva (% discount) on offers + invoices
Adds an optional per-line-item percentage discount ("Sleva") to offers and
issued invoices, on both the detail-page editor and the PDF.

- DB: discount Decimal(5,2) DEFAULT 0 on quotation_items + invoice_items
  (migration 20260613195833_add_item_discount).
- Totals/VAT: new shared lineNet() (qty × price × (1 − discount/100)); offers'
  NET total and invoices' subtotal AND VAT now compute on the discounted net
  (every getBase/enrichQuotation/stats path). Backward compatible: discount 0
  is identical to before.
- Schemas: discount = numberInRange(0,100), default 0; persisted on
  create/update (+ offer duplicate).
- Detail editors: editable "Sleva %" column — offers via DocumentItemsEditor's
  new opt-in showDiscount prop (issued orders unaffected); invoices in their own
  editor. Spinners hidden + 7rem column so "100" is fully visible. Read-only
  invoice view shows the column when any line has a discount.
- PDFs (offers + invoices): conditional "Sleva"/"Discount" column rendered only
  when an item has a discount; line/total/VAT always use the discounted net.

Tests: +16 (money, totals incl. VAT-on-discounted-net, schema range, PDF
column present/absent). Full suite 665 pass, tsc -b clean, lint 0. Editor
verified live via Playwright. Design spec in docs/superpowers/specs/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 20:46:44 +02:00

56 lines
1.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from "vitest";
import { CreateQuotationSchema } from "../schemas/offers.schema";
import { CreateInvoiceSchema } from "../schemas/invoices.schema";
/**
* The per-item Sleva is a percentage: the offer- and invoice-item schemas must
* accept 0100, default to 0 when absent, and reject out-of-range values.
*/
describe("offer item discount schema", () => {
it("accepts a 0100 discount", () => {
const r = CreateQuotationSchema.safeParse({
items: [{ description: "X", quantity: 1, unit_price: 100, discount: 15 }],
});
expect(r.success).toBe(true);
expect(r.success && r.data.items?.[0].discount).toBe(15);
});
it("defaults discount to 0 when absent", () => {
const r = CreateQuotationSchema.safeParse({
items: [{ description: "X", quantity: 1, unit_price: 100 }],
});
expect(r.success && r.data.items?.[0].discount).toBe(0);
});
it("rejects a discount above 100", () => {
const r = CreateQuotationSchema.safeParse({
items: [
{ description: "X", quantity: 1, unit_price: 100, discount: 150 },
],
});
expect(r.success).toBe(false);
});
});
describe("invoice item discount schema", () => {
it("accepts a 0100 discount and defaults to 0", () => {
const ok = CreateInvoiceSchema.safeParse({
items: [{ description: "X", quantity: 1, unit_price: 100, discount: 10 }],
});
expect(ok.success && ok.data.items?.[0].discount).toBe(10);
const def = CreateInvoiceSchema.safeParse({
items: [{ description: "X", quantity: 1, unit_price: 100 }],
});
expect(def.success && def.data.items?.[0].discount).toBe(0);
});
it("rejects a negative discount", () => {
const r = CreateInvoiceSchema.safeParse({
items: [{ description: "X", quantity: 1, unit_price: 100, discount: -5 }],
});
expect(r.success).toBe(false);
});
});