Rechnungen: Überblick über fällige/überfällige Rechnungen und Verträge
This commit is contained in:
parent
5efe37ecc0
commit
3a2864ee76
5 changed files with 125 additions and 4 deletions
|
|
@ -1,10 +1,13 @@
|
|||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { buildApp } from '../src/server.js';
|
||||
import { run } from '../src/core/db.js';
|
||||
import { runOnce } from '../../worker/src/jobs.js';
|
||||
import { call, code, login, makeUser } from './helpers.js';
|
||||
|
||||
let app: FastifyInstance;
|
||||
beforeAll(async () => { app = await buildApp(); await app.ready(); });
|
||||
const drain = async () => { for (let i = 0; i < 30; i++) { await run("UPDATE jobs SET run_at = UTC_TIMESTAMP(3) WHERE status IN ('scheduled','retrying')"); if (!(await runOnce(() => undefined))) break; } };
|
||||
async function staff(email: string, role: string) {
|
||||
await makeUser({ email, kind: 'staff', staffRole: role }); const { client } = await login(app, email);
|
||||
const s = (await call(app, client, 'POST', '/auth/mfa/setup')).json(); await call(app, client, 'POST', '/auth/mfa/confirm', { code: code(s.secret) }); return client;
|
||||
|
|
@ -130,3 +133,48 @@ describe('Rechnungsposition aus Produkt mit Laufzeit-Staffel', () => {
|
|||
expect((await call(app, acc, 'GET', `/invoices/suggest-from-product/${prod.json().id}?termMonths=6`)).statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mahnwesen: überfällige Rechnungen und fällige Verträge', () => {
|
||||
it('Überfällige offene Rechnungen und Verträge ohne aktuelle Rechnung werden gefunden; bezahlte/aktuelle/inaktive nicht', async () => {
|
||||
const admin = await staff('rem-admin@example.com', 'superadmin'); const acc = await staff('rem-acc@example.com', 'accounting'); const support = await staff('rem-support@example.com', 'support');
|
||||
await call(app, admin, 'PUT', '/admin/company-settings', COMPANY);
|
||||
const A = await customer(admin, 'Kunde A', 'rem-a@example.com'); const B = await customer(admin, 'Kunde B', 'rem-b@example.com'); const C = await customer(admin, 'Kunde C', 'rem-c@example.com');
|
||||
const t19 = (await call(app, admin, 'GET', '/admin/tax-rates')).json().find((t: any) => t.rateBp === 1900).id;
|
||||
const prod = (await call(app, admin, 'POST', '/admin/products', {
|
||||
sku: 'HOST-REM', category: 'hosting', orderableByCustomer: true, requiresApproval: false, customerActions: [], status: 'active',
|
||||
version: { name: 'Hosting Monatlich', taxRateId: t19, setupCents: 0, recurringCents: 999, billingInterval: 'monthly', termMonths: 0, renewal: 'auto', renewalTermMonths: 1, noticeDays: 30, provisioning: {}, termPrices: [] },
|
||||
})).json();
|
||||
|
||||
// A: Vertrag seit 40 Tagen, nie in Rechnung gestellt -> fällig
|
||||
const orderA = (await call(app, admin, 'POST', '/orders', { orgId: A.org, items: [{ productId: prod.id }] })).json(); await drain();
|
||||
const contractA = (await call(app, admin, 'GET', `/orders/${orderA.id}`)).json().items[0].contractId;
|
||||
await run('UPDATE contracts SET started_at = DATE_SUB(UTC_TIMESTAMP(3), INTERVAL 40 DAY) WHERE id = ?', [contractA]);
|
||||
|
||||
// B: Vertrag seit 40 Tagen, aber vor 5 Tagen bereits in Rechnung gestellt -> nicht fällig
|
||||
const orderB = (await call(app, admin, 'POST', '/orders', { orgId: B.org, items: [{ productId: prod.id }] })).json(); await drain();
|
||||
const contractB = (await call(app, admin, 'GET', `/orders/${orderB.id}`)).json().items[0].contractId;
|
||||
await run('UPDATE contracts SET started_at = DATE_SUB(UTC_TIMESTAMP(3), INTERVAL 40 DAY) WHERE id = ?', [contractB]);
|
||||
const invB = (await call(app, acc, 'POST', '/invoices', { orgId: B.org })).json();
|
||||
await call(app, acc, 'PUT', `/invoices/${invB.id}/items`, { items: [{ description: 'x', quantity: 1, unitPriceNetCents: 999, taxBp: 1900, contractId: contractB }] });
|
||||
await call(app, acc, 'POST', `/invoices/${invB.id}/issue`);
|
||||
await run('UPDATE invoices SET issue_date = DATE_SUB(UTC_TIMESTAMP(3), INTERVAL 5 DAY) WHERE id = ?', [invB.id]);
|
||||
|
||||
// C: Vertrag erst seit 2 Tagen -> nicht fällig
|
||||
const orderC = (await call(app, admin, 'POST', '/orders', { orgId: C.org, items: [{ productId: prod.id }] })).json(); await drain();
|
||||
|
||||
// überfällige Rechnung bei A: ausgestellt, Zahlungsziel überschritten, offen
|
||||
const invA = (await call(app, acc, 'POST', '/invoices', { orgId: A.org })).json();
|
||||
await call(app, acc, 'PUT', `/invoices/${invA.id}/items`, { items: [{ description: 'überfällig', quantity: 1, unitPriceNetCents: 5000, taxBp: 1900 }] });
|
||||
await call(app, acc, 'POST', `/invoices/${invA.id}/issue`, { dueDate: new Date(Date.now() - 3 * 86400000).toISOString().slice(0, 10) });
|
||||
|
||||
const r = (await call(app, acc, 'GET', '/invoices/reminders')).json();
|
||||
expect(r.overdueInvoices.map((i: any) => i.id)).toContain(invA.id);
|
||||
expect(r.overdueInvoices.map((i: any) => i.id)).not.toContain(invB.id);
|
||||
const dueOrgs = r.dueContracts.map((c: any) => c.orgId);
|
||||
expect(dueOrgs).toContain(A.org); expect(dueOrgs).not.toContain(B.org); expect(dueOrgs).not.toContain(C.org);
|
||||
const entryA = r.dueContracts.find((c: any) => c.orgId === A.org); expect(entryA.productName).toBe('Hosting Monatlich'); expect(entryA.lastInvoicedAt).toBeNull();
|
||||
|
||||
expect((await call(app, support, 'GET', '/invoices/reminders')).statusCode).toBe(200); // Support liest mit
|
||||
expect((await call(app, A.client, 'GET', '/invoices/reminders')).statusCode).toBe(403); // Kunden nicht
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue