feat(crm): Mahngebühr on a separate Mahnung document; invoice stays immutable
Corrected dunning model (Mara): a Mahnung is a reminder LETTER showing the new total (original + Mahngebühr), NOT a separate invoice and NOT a mutation of the issued invoice. - The invoice PDF no longer shows the fee (buildInvoiceRenderContext reports lateFeeAmountMinor 0) and is NEVER re-rendered by a reminder — it stays immutable (§14/§11). - applyReminder now: tracks the fee as dunning state on the row (gross late_fee_amount_minor + new late_fee_vat_minor for the VAT portion, migration 144), renders a separate MAHNUNG PDF (pdfService 'mahnung' kind — reuses the invoice layout: same lines + Mahngebühr row + new total, 'Mahnung' title, no QR), stored under storage/business-docs/mahnung/, and attaches BOTH the unchanged original invoice + the Mahnung to the reminder email. - Fee resolvers split into net + VAT-rate (toggle + org-rate gated); a gross wrapper feeds the payment-check preview. en + de PDF title. Outstanding/collections still read late_fee_amount_minor (now dunning state). P3 (tax-report/Banana booking of the Mahngebühr VAT) stays Treuhänder-gated. Syntax + 17/17 workflow/invoice tests green. NOTE: the Mahnung PDF render path isn't unit-tested (PDF rendering is flaky in the test env) — eyeball on the dev box: fire a level-2 reminder, confirm the Mahnung PDF shows the new total and the original invoice PDF is unchanged.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Migration 144: track the VAT portion of the Mahngebühr separately.
|
||||
*
|
||||
* The dunning rework keeps the fee on the invoice ROW as dunning state (gross
|
||||
* in late_fee_amount_minor) but renders it on a separate Mahnung document, NOT
|
||||
* on the immutable invoice. `late_fee_vat_minor` records the VAT component
|
||||
* (0 when VAT-exempt — DE/AT, or the org has no VAT) so the Mahnung can show
|
||||
* the breakdown and the tax report can later book the Mahngebühr VAT (CH).
|
||||
*/
|
||||
exports.up = async function (knex) {
|
||||
if (!(await knex.schema.hasTable('invoices'))) return;
|
||||
if (!(await knex.schema.hasColumn('invoices', 'late_fee_vat_minor'))) {
|
||||
await knex.schema.alterTable('invoices', (t) => {
|
||||
t.bigInteger('late_fee_vat_minor').notNullable().defaultTo(0);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function (knex) {
|
||||
if (!(await knex.schema.hasTable('invoices'))) return;
|
||||
if (await knex.schema.hasColumn('invoices', 'late_fee_vat_minor')) {
|
||||
await knex.schema.alterTable('invoices', (t) => t.dropColumn('late_fee_vat_minor'));
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user