962f1d9586
Stable backport combining #860 (never reached stable) and #900: - jest.config.js gains testTimeout: 120000 — stable still ran on Jest's 5s default for anything unpinned, while its migration chain (134 core migrations via backports) is nearly as long as beta's. - All 19 suite-level jest.setTimeout(30000/60000) pins raised to 120s; local pins override the config default (#860's rationale). - All 15 hook-ARGUMENT timeout pins on migration-booting beforeAll hooks raised to 120s (#900's rationale — the 3.97.0-beta.0 release PR failed on exactly this class on the beta side). Untouched: the three suites whose pinned hooks don't run migrations (webhookDelivery, imageProcessor.storage, storageBackend) and publicQuotes' 30s pin on the rate-limit lockout test. No test logic changed. Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>
80 lines
3.0 KiB
JavaScript
80 lines
3.0 KiB
JavaScript
/**
|
|
* Negative line items (Rabatt / manual discount lines) are accepted
|
|
* end-to-end as long as the resulting total stays ≥ 0. When the
|
|
* discount would drive the total negative, the service rejects with
|
|
* a clear, code-tagged error so the admin is steered to Storno for
|
|
* credit-note workflows.
|
|
*
|
|
* Touches the actual createInvoice / createQuote service paths so a
|
|
* future change to either computeTotals or the guard fires this test.
|
|
*/
|
|
|
|
const { bootCrmDb, seedMinimal } = require('./helpers/crmDb');
|
|
|
|
// Service-level CRM calls cold-require heavy modules (pdfService,
|
|
// nodemailer, etc.) on first use; the global 5 s per-test budget is
|
|
// too tight for that. Bump it for this file only.
|
|
jest.setTimeout(120000);
|
|
|
|
describe('discount line items (negative unit_price_minor)', () => {
|
|
let db;
|
|
let cleanup;
|
|
let adminId;
|
|
let customerId;
|
|
let invoiceService;
|
|
|
|
beforeAll(async () => {
|
|
({ db, cleanup } = await bootCrmDb());
|
|
({ adminId, customerId } = await seedMinimal(db));
|
|
invoiceService = require('../../src/services/invoiceService');
|
|
}, 120000);
|
|
|
|
afterAll(async () => {
|
|
if (cleanup) await cleanup();
|
|
});
|
|
|
|
// Quote-side coverage of the symmetric validator + guard is
|
|
// deliberately omitted: createQuote's init path takes ~30 s under
|
|
// this harness (something in pdfService / emailProcessor cold-
|
|
// require), which would push the suite well past CI's per-test
|
|
// budget. The shape of the guard is identical to the invoice one
|
|
// covered below; a future change to extract the slow init or to
|
|
// stub it for tests should re-enable a parallel quote test.
|
|
|
|
describe('invoices', () => {
|
|
it('accepts a negative-price line and computes the net correctly', async () => {
|
|
const { invoiceIds } = await invoiceService.createInvoice({
|
|
customerAccountId: customerId,
|
|
currency: 'CHF',
|
|
vatRate: 0,
|
|
lineItems: [
|
|
{ position: 1, quantity: 1, description: 'Photo service', unit_price_minor: 20000, discount_percent: 0 },
|
|
{ position: 2, quantity: 1, description: 'Treuerabatt', unit_price_minor: -5000, discount_percent: 0 },
|
|
],
|
|
}, adminId);
|
|
|
|
expect(Array.isArray(invoiceIds)).toBe(true);
|
|
expect(invoiceIds.length).toBe(1);
|
|
|
|
const row = await db('invoices').where({ id: invoiceIds[0] }).first();
|
|
expect(row.net_amount_minor).toBe(15000);
|
|
expect(row.total_amount_minor).toBe(15000);
|
|
});
|
|
|
|
it('rejects when the discount drives the total negative', async () => {
|
|
await expect(invoiceService.createInvoice({
|
|
customerAccountId: customerId,
|
|
currency: 'CHF',
|
|
vatRate: 0,
|
|
lineItems: [
|
|
{ position: 1, quantity: 1, description: 'Photo service', unit_price_minor: 10000, discount_percent: 0 },
|
|
{ position: 2, quantity: 1, description: 'Übergroßer Rabatt', unit_price_minor: -50000, discount_percent: 0 },
|
|
],
|
|
}, adminId)).rejects.toMatchObject({
|
|
code: 'INVOICE_TOTAL_NEGATIVE',
|
|
statusCode: 400,
|
|
});
|
|
});
|
|
});
|
|
});
|