feat: Enhance invoice file backup procedure to include JSON snapshots
Playwright Tests / test (push) Canceled after 0s
Playwright Tests / test (push) Canceled after 0s
- Updated the stored procedure `fds__admin_backupAndClearInvoiceFile.sql` to back up not only the invoice file but also JSON snapshots of related data (invoices, items, details, service requests) to ensure consistency and traceability. - Modified the table structure of `fds__invoice_file_backups` to accommodate new JSON columns for storing these snapshots. - Added an ADR (0030) document to outline the decision and rationale behind these changes, emphasizing the importance of maintaining a consistent set of data alongside the backed-up file.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.Metrics;
|
using System.Diagnostics.Metrics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ rest of the Administration menu (`fds_sys > 4`), and reachable both from the
|
|||||||
menu (Administration → Spezialfunktionen) and by opening the URL
|
menu (Administration → Spezialfunktionen) and by opening the URL
|
||||||
`/specialties` directly.
|
`/specialties` directly.
|
||||||
|
|
||||||
|
The menu entry is deliberately hidden: it only appears when the Administration
|
||||||
|
dropdown is opened with **Shift** held (Shift+click, or Shift+Enter/Space on
|
||||||
|
the focused toggle); a plain open hides it again
|
||||||
|
(`$fis.bindShiftOnlyMenuItems` in `fis_main_menu.js`, driven by the item's
|
||||||
|
`lclass: 'shift-only'`). This keeps the maintenance actions out of casual
|
||||||
|
reach without adding another authorization level — the item is still part of
|
||||||
|
the menu array, so the `/specialties` deep link is unaffected.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
- **Module load** follows the standard contract (see
|
- **Module load** follows the standard contract (see
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ Access is restricted to users whose `fds_sys` module authorization is greater th
|
|||||||
does it push a single "Administration" dropdown entry into `$ocms.ocmsmenu` — containing this
|
does it push a single "Administration" dropdown entry into `$ocms.ocmsmenu` — containing this
|
||||||
module's "Systemüberwachung" (`init:admin`) item alongside `usr`'s "Benutzerverwaltung"
|
module's "Systemüberwachung" (`init:admin`) item alongside `usr`'s "Benutzerverwaltung"
|
||||||
(`init:usr`) item — and re-render `#mainmenu`. Users below the threshold never see the
|
(`init:usr`) item — and re-render `#mainmenu`. Users below the threshold never see the
|
||||||
dropdown and never fetch either module's script.
|
dropdown and never fetch either module's script. Within the dropdown, "Systemüberwachung"
|
||||||
|
(like "Spezialfunktionen") is a `shift-only` item: it is listed only when the dropdown is
|
||||||
|
opened with **Shift** held (`$fis.bindShiftOnlyMenuItems`); the `/admin` deep link is
|
||||||
|
unaffected. This is a convenience/accident guard, not an authorization level.
|
||||||
2. **Server-side gate** — every `/do/admin/*` endpoint resolves
|
2. **Server-side gate** — every `/do/admin/*` endpoint resolves
|
||||||
`fis_getModuleAuth('fds_sys', authuser)` and returns 401 unless it is > 4. The only
|
`fis_getModuleAuth('fds_sys', authuser)` and returns 401 unless it is > 4. The only
|
||||||
exception is `admin/auth`, which returns `{ manage: 0 }` for unauthorized users so the
|
exception is `admin/auth`, which returns `{ manage: 0 }` for unauthorized users so the
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
status: Accepted
|
||||||
|
date: 2026-09-17
|
||||||
|
applyTo:
|
||||||
|
- "Fuchs_Database/dbo/Stored Procedures/fds__admin_backupAndClearInvoiceFile.sql"
|
||||||
|
- "Fuchs_Database/dbo/Tables/fds__invoice_file_backups.sql"
|
||||||
|
supersededBy: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
# 0030 — Invoice-file backup carries a JSON snapshot of the invoice data
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR 0027 introduced `[dbo].[fds__invoice_file_backups]`: one row per
|
||||||
|
admin backup-and-clear action holding the invoice's `[file]` bytes before
|
||||||
|
they are cleared for regeneration (ADR 0025). The backup held only the
|
||||||
|
rendered PDF. The data it was rendered from — the `fds__invoices` row, its
|
||||||
|
`fds__invoice_items` rows, its `fds__invoice_details` row and its
|
||||||
|
`fds__invoice_servicerequests` link rows — lives on in
|
||||||
|
the live tables and keeps changing (payment status, modification stamps,
|
||||||
|
later edits). A backed-up file could therefore no longer be matched against
|
||||||
|
the data state it was produced from, which is exactly the question the
|
||||||
|
backup exists to answer when a regenerated document differs from the
|
||||||
|
original.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- Each backup row additionally stores four JSON snapshots, written in the
|
||||||
|
same statement and transaction as the file copy:
|
||||||
|
- `InvoiceJson` — the `fds__invoices` row (`FOR JSON PATH,
|
||||||
|
WITHOUT_ARRAY_WRAPPER`), **without** `[file]`, which is already stored as
|
||||||
|
bytes in the row and would otherwise be base64-duplicated;
|
||||||
|
- `InvoiceItemsJson` — all `fds__invoice_items` rows for the invoice as a
|
||||||
|
JSON array, ordered by `SortOrder`, `Position`, `Id`;
|
||||||
|
- `InvoiceDetailsJson` — the `fds__invoice_details` row, or `NULL` if the
|
||||||
|
invoice has none;
|
||||||
|
- `InvoiceServiceRequestsJson` — all `fds__invoice_servicerequests` rows
|
||||||
|
for the invoice as a JSON array, ordered by `SortOrder`, `Id`. Not part
|
||||||
|
of the rendered document, but part of the invoice's data set.
|
||||||
|
- The snapshots are JSON columns on the **same** backup row rather than
|
||||||
|
four mirror tables: one row is one consistent set (file + data), the
|
||||||
|
atomicity guarantee of ADR 0027 covers it without extra keys, and adding a
|
||||||
|
column to an invoice table does not require touching the backup schema.
|
||||||
|
The item/detail/service-request snapshots use `SELECT *`, so new columns appear
|
||||||
|
automatically; the invoice snapshot lists its columns explicitly only to
|
||||||
|
exclude `[file]` — a new `fds__invoices` column must be added to that
|
||||||
|
list.
|
||||||
|
- Restore is manual (`OPENJSON` in SSMS); no app-side reader is added. The
|
||||||
|
snapshots are forensic evidence, not a rollback mechanism.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Backup rows grow by the size of the invoice's data (small next to the
|
||||||
|
PDF). No retention policy is added — same standing as ADR 0027.
|
||||||
|
- `INCLUDE_NULL_VALUES` is used so a `NULL` in the snapshot is
|
||||||
|
distinguishable from a column that did not exist at backup time.
|
||||||
|
- Adding a column to `fds__invoices` requires updating the explicit column
|
||||||
|
list in the proc; `fds__invoice_items` / `fds__invoice_details` /
|
||||||
|
`fds__invoice_servicerequests` need no change.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Mirror tables** (`fds__invoice_backups`, `fds__invoice_items_backups`,
|
||||||
|
…) keyed by backup `uid`: typed and queryable, but four schemas that
|
||||||
|
drift with every invoice-table change and four inserts to keep atomic.
|
||||||
|
Rejected for maintenance cost; the snapshots are read rarely and by hand.
|
||||||
|
- **Snapshot only the invoice header**: the items are what the PDF body is
|
||||||
|
rendered from; leaving them out would not answer "what did this file
|
||||||
|
contain and why".
|
||||||
@@ -17,7 +17,13 @@
|
|||||||
whose fds_sys authorization exceeds 4. One auth check gates the whole dropdown; it (and
|
whose fds_sys authorization exceeds 4. One auth check gates the whole dropdown; it (and
|
||||||
therefore all module scripts) is only added after that check resolves, so lower-privileged
|
therefore all module scripts) is only added after that check resolves, so lower-privileged
|
||||||
users never see it or fetch any module's code. Called from fis_main_go.js once the DOM (and
|
users never see it or fetch any module's code. Called from fis_main_go.js once the DOM (and
|
||||||
the initial main menu) is ready. Returns a promise that settles once the menu is final. */
|
the initial main menu) is ready. Returns a promise that settles once the menu is final.
|
||||||
|
|
||||||
|
Systemüberwachung and Spezialfunktionen are hidden entries: they are only shown while the
|
||||||
|
Administration dropdown is opened with the Shift key held (Shift+click on the toggle). A plain
|
||||||
|
click hides them again, so the diagnostics/maintenance pages are never one accidental click
|
||||||
|
away. The items still exist in the menu array, so the /admin and /specialties deep links
|
||||||
|
(openModuleFromPath) keep working. */
|
||||||
$fis.addAdminMenuButtonsIfAuthorized = function () {
|
$fis.addAdminMenuButtonsIfAuthorized = function () {
|
||||||
if ($('#m_admin_grp').length > 0) { return Promise.resolve(); } // idempotent
|
if ($('#m_admin_grp').length > 0) { return Promise.resolve(); } // idempotent
|
||||||
return $fis.getAuth('fds_sys').then(function (auth) {
|
return $fis.getAuth('fds_sys').then(function (auth) {
|
||||||
@@ -27,18 +33,32 @@ $fis.addAdminMenuButtonsIfAuthorized = function () {
|
|||||||
itm: [
|
itm: [
|
||||||
{ lbl: $t.m_usr, id: 'm_usr', fnc: 'init:usr', ico: 'glyphicon glyphicon-user' },
|
{ lbl: $t.m_usr, id: 'm_usr', fnc: 'init:usr', ico: 'glyphicon glyphicon-user' },
|
||||||
{ lbl: $t.m_usrgrp, id: 'm_usrgrp', fnc: 'init:usrgrp', ico: 'glyphicon glyphicon-list' },
|
{ lbl: $t.m_usrgrp, id: 'm_usrgrp', fnc: 'init:usrgrp', ico: 'glyphicon glyphicon-list' },
|
||||||
{ lbl: $t.m_admin_status, id: 'm_admin', fnc: 'init:admin', ico: 'glyphicon glyphicon-dashboard' },
|
{ lbl: $t.m_admin_status, id: 'm_admin', fnc: 'init:admin', ico: 'glyphicon glyphicon-dashboard', lclass: 'shift-only' },
|
||||||
{ lbl: $t.m_specialties, id: 'm_specialties', fnc: 'init:specialties', ico: 'glyphicon glyphicon-wrench' }
|
{ lbl: $t.m_specialties, id: 'm_specialties', fnc: 'init:specialties', ico: 'glyphicon glyphicon-wrench', lclass: 'shift-only' }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
/* Re-render the main menu cleanly: drop the previously generated menu list
|
/* Re-render the main menu cleanly: drop the previously generated menu list
|
||||||
(never the .nav-right settings dropdown) and rebuild it from the array. */
|
(never the .nav-right settings dropdown) and rebuild it from the array. */
|
||||||
$('#mainmenu').children('ul:not(.nav-right)').remove();
|
$('#mainmenu').children('ul:not(.nav-right)').remove();
|
||||||
$('#mainmenu').ocmsmenu($ocms.ocmsmenu);
|
$('#mainmenu').ocmsmenu($ocms.ocmsmenu);
|
||||||
|
$fis.bindShiftOnlyMenuItems($('#m_admin_grp'));
|
||||||
}
|
}
|
||||||
}).catch(function () { /* auth lookup failed → leave the menu unchanged */ });
|
}).catch(function () { /* auth lookup failed → leave the menu unchanged */ });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Hides every `.shift-only` entry of a dropdown group and reveals them only for an opening
|
||||||
|
initiated with Shift held. Bound on mousedown (fires before Bootstrap's click toggle, so the
|
||||||
|
list is already in the right state when the dropdown becomes visible) and on keydown for
|
||||||
|
keyboard users (Shift+Enter / Shift+Space on the focused toggle). */
|
||||||
|
$fis.bindShiftOnlyMenuItems = function (group) {
|
||||||
|
let items = group.find('.shift-only').hide();
|
||||||
|
if (items.length === 0) { return; }
|
||||||
|
let apply = function (e) { items.toggle(e.shiftKey === true); };
|
||||||
|
group.children('a.dropdown-toggle')
|
||||||
|
.on('mousedown', apply)
|
||||||
|
.on('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { apply(e); } });
|
||||||
|
};
|
||||||
|
|
||||||
/* Deep link: the server renders the same SPA shell for every /{fn} URL (IntranetController.Index
|
/* Deep link: the server renders the same SPA shell for every /{fn} URL (IntranetController.Index
|
||||||
ignores fn), so opening e.g. /specialties directly would otherwise just show the dashboard.
|
ignores fn), so opening e.g. /specialties directly would otherwise just show the dashboard.
|
||||||
Opens the module named by the first path segment — but only one the user's menu actually
|
Opens the module named by the first path segment — but only one the user's menu actually
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/* "Specialties" — admin-only page (URL /specialties, menu Administration → Spezialfunktionen)
|
/* "Specialties" — admin-only page (URL /specialties, menu Administration → Spezialfunktionen;
|
||||||
|
the menu entry is only visible when the dropdown is opened with Shift held, see fis_main_menu.js)
|
||||||
that exposes the otherwise hidden maintenance actions as cards with small forms. Each card
|
that exposes the otherwise hidden maintenance actions as cards with small forms. Each card
|
||||||
calls its real action endpoint (which enforces the fds_sys > 4 gate itself); this module only
|
calls its real action endpoint (which enforces the fds_sys > 4 gate itself); this module only
|
||||||
renders and reports. Add a card per action in `cards`. */
|
renders and reports. Add a card per action in `cards`. */
|
||||||
|
|||||||
@@ -3248,7 +3248,13 @@ $fis.draft = {
|
|||||||
whose fds_sys authorization exceeds 4. One auth check gates the whole dropdown; it (and
|
whose fds_sys authorization exceeds 4. One auth check gates the whole dropdown; it (and
|
||||||
therefore all module scripts) is only added after that check resolves, so lower-privileged
|
therefore all module scripts) is only added after that check resolves, so lower-privileged
|
||||||
users never see it or fetch any module's code. Called from fis_main_go.js once the DOM (and
|
users never see it or fetch any module's code. Called from fis_main_go.js once the DOM (and
|
||||||
the initial main menu) is ready. Returns a promise that settles once the menu is final. */
|
the initial main menu) is ready. Returns a promise that settles once the menu is final.
|
||||||
|
|
||||||
|
Systemüberwachung and Spezialfunktionen are hidden entries: they are only shown while the
|
||||||
|
Administration dropdown is opened with the Shift key held (Shift+click on the toggle). A plain
|
||||||
|
click hides them again, so the diagnostics/maintenance pages are never one accidental click
|
||||||
|
away. The items still exist in the menu array, so the /admin and /specialties deep links
|
||||||
|
(openModuleFromPath) keep working. */
|
||||||
$fis.addAdminMenuButtonsIfAuthorized = function () {
|
$fis.addAdminMenuButtonsIfAuthorized = function () {
|
||||||
if ($('#m_admin_grp').length > 0) { return Promise.resolve(); } // idempotent
|
if ($('#m_admin_grp').length > 0) { return Promise.resolve(); } // idempotent
|
||||||
return $fis.getAuth('fds_sys').then(function (auth) {
|
return $fis.getAuth('fds_sys').then(function (auth) {
|
||||||
@@ -3258,18 +3264,32 @@ $fis.addAdminMenuButtonsIfAuthorized = function () {
|
|||||||
itm: [
|
itm: [
|
||||||
{ lbl: $t.m_usr, id: 'm_usr', fnc: 'init:usr', ico: 'glyphicon glyphicon-user' },
|
{ lbl: $t.m_usr, id: 'm_usr', fnc: 'init:usr', ico: 'glyphicon glyphicon-user' },
|
||||||
{ lbl: $t.m_usrgrp, id: 'm_usrgrp', fnc: 'init:usrgrp', ico: 'glyphicon glyphicon-list' },
|
{ lbl: $t.m_usrgrp, id: 'm_usrgrp', fnc: 'init:usrgrp', ico: 'glyphicon glyphicon-list' },
|
||||||
{ lbl: $t.m_admin_status, id: 'm_admin', fnc: 'init:admin', ico: 'glyphicon glyphicon-dashboard' },
|
{ lbl: $t.m_admin_status, id: 'm_admin', fnc: 'init:admin', ico: 'glyphicon glyphicon-dashboard', lclass: 'shift-only' },
|
||||||
{ lbl: $t.m_specialties, id: 'm_specialties', fnc: 'init:specialties', ico: 'glyphicon glyphicon-wrench' }
|
{ lbl: $t.m_specialties, id: 'm_specialties', fnc: 'init:specialties', ico: 'glyphicon glyphicon-wrench', lclass: 'shift-only' }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
/* Re-render the main menu cleanly: drop the previously generated menu list
|
/* Re-render the main menu cleanly: drop the previously generated menu list
|
||||||
(never the .nav-right settings dropdown) and rebuild it from the array. */
|
(never the .nav-right settings dropdown) and rebuild it from the array. */
|
||||||
$('#mainmenu').children('ul:not(.nav-right)').remove();
|
$('#mainmenu').children('ul:not(.nav-right)').remove();
|
||||||
$('#mainmenu').ocmsmenu($ocms.ocmsmenu);
|
$('#mainmenu').ocmsmenu($ocms.ocmsmenu);
|
||||||
|
$fis.bindShiftOnlyMenuItems($('#m_admin_grp'));
|
||||||
}
|
}
|
||||||
}).catch(function () { /* auth lookup failed → leave the menu unchanged */ });
|
}).catch(function () { /* auth lookup failed → leave the menu unchanged */ });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Hides every `.shift-only` entry of a dropdown group and reveals them only for an opening
|
||||||
|
initiated with Shift held. Bound on mousedown (fires before Bootstrap's click toggle, so the
|
||||||
|
list is already in the right state when the dropdown becomes visible) and on keydown for
|
||||||
|
keyboard users (Shift+Enter / Shift+Space on the focused toggle). */
|
||||||
|
$fis.bindShiftOnlyMenuItems = function (group) {
|
||||||
|
let items = group.find('.shift-only').hide();
|
||||||
|
if (items.length === 0) { return; }
|
||||||
|
let apply = function (e) { items.toggle(e.shiftKey === true); };
|
||||||
|
group.children('a.dropdown-toggle')
|
||||||
|
.on('mousedown', apply)
|
||||||
|
.on('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { apply(e); } });
|
||||||
|
};
|
||||||
|
|
||||||
/* Deep link: the server renders the same SPA shell for every /{fn} URL (IntranetController.Index
|
/* Deep link: the server renders the same SPA shell for every /{fn} URL (IntranetController.Index
|
||||||
ignores fn), so opening e.g. /specialties directly would otherwise just show the dashboard.
|
ignores fn), so opening e.g. /specialties directly would otherwise just show the dashboard.
|
||||||
Opens the module named by the first path segment — but only one the user's menu actually
|
Opens the module named by the first path segment — but only one the user's menu actually
|
||||||
|
|||||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -25,7 +25,8 @@ let $spt = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* "Specialties" — admin-only page (URL /specialties, menu Administration → Spezialfunktionen)
|
/* "Specialties" — admin-only page (URL /specialties, menu Administration → Spezialfunktionen;
|
||||||
|
the menu entry is only visible when the dropdown is opened with Shift held, see fis_main_menu.js)
|
||||||
that exposes the otherwise hidden maintenance actions as cards with small forms. Each card
|
that exposes the otherwise hidden maintenance actions as cards with small forms. Each card
|
||||||
calls its real action endpoint (which enforces the fds_sys > 4 gate itself); this module only
|
calls its real action endpoint (which enforces the fds_sys > 4 gate itself); this module only
|
||||||
renders and reports. Add a card per action in `cards`. */
|
renders and reports. Add a card per action in `cards`. */
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -8,7 +8,10 @@
|
|||||||
-- admin/backupclearinvoicefile action (fds_sys > 4 gate, IntranetController.Admin.cs),
|
-- admin/backupclearinvoicefile action (fds_sys > 4 gate, IntranetController.Admin.cs),
|
||||||
-- which logs through the normal ILogger/DB-log-sink pipeline (ADR 0024) instead.
|
-- which logs through the normal ILogger/DB-log-sink pipeline (ADR 0024) instead.
|
||||||
--
|
--
|
||||||
-- Copies the invoice's current [file] bytes into [dbo].[fds__invoice_file_backups],
|
-- Copies the invoice's current [file] bytes into [dbo].[fds__invoice_file_backups]
|
||||||
|
-- together with JSON snapshots of its fds__invoices / fds__invoice_items /
|
||||||
|
-- fds__invoice_details / fds__invoice_servicerequests rows (ADR 0030: file + data
|
||||||
|
-- as one consistent set),
|
||||||
-- then clears [file] so the document regenerates on next read (ADR 0025). Both steps
|
-- then clears [file] so the document regenerates on next read (ADR 0025). Both steps
|
||||||
-- run in one transaction. Only a finalised, non-external invoice qualifies — the same
|
-- run in one transaction. Only a finalised, non-external invoice qualifies — the same
|
||||||
-- precondition ADR 0025 puts on regeneration: clearing a draft's file would let the
|
-- precondition ADR 0025 puts on regeneration: clearing a draft's file would let the
|
||||||
@@ -37,11 +40,42 @@ BEGIN
|
|||||||
BEGIN TRY
|
BEGIN TRY
|
||||||
BEGIN TRANSACTION;
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__invoice_file_backups] ([InvoiceId], [DocumentName], [file], [BackedUpBy])
|
-- The backup row carries the file AND JSON snapshots of the data rows it was rendered from
|
||||||
SELECT [Id], [DocumentName], [file], @authuser
|
-- (ADR 0030), so a backup is a self-contained, consistent set. The invoice snapshot lists
|
||||||
FROM [dbo].[fds__invoices]
|
-- its columns explicitly to leave [file] out (it is already stored as bytes in [file] and
|
||||||
WHERE [Id] = @Id AND [file] IS NOT NULL
|
-- would otherwise be base64-duplicated into the JSON); items, details and service requests take
|
||||||
AND [DateFinalized] IS NOT NULL AND ISNULL([IsExternal], 0) = 0;
|
-- every column.
|
||||||
|
INSERT INTO [dbo].[fds__invoice_file_backups]
|
||||||
|
([InvoiceId], [DocumentName], [file], [InvoiceJson], [InvoiceItemsJson], [InvoiceDetailsJson],
|
||||||
|
[InvoiceServiceRequestsJson], [BackedUpBy])
|
||||||
|
SELECT i.[Id], i.[DocumentName], i.[file],
|
||||||
|
(SELECT i.[Id], i.[Version], i.[InvoiceId], i.[InvoiceType], i.[InvoiceTitle], i.[DocumentName],
|
||||||
|
i.[InvoiceBalance], i.[InvoiceBalance_net], i.[InvoiceVAT_net1], i.[InvoiceVAT_1],
|
||||||
|
i.[InvoiceVAT_net2], i.[InvoiceVAT_2], i.[PaymentTerm], i.[DueDate], i.[CustomerId],
|
||||||
|
i.[SendToAddress], i.[SendToEmail], i.[SendToEmailCc], i.[ProvisionPeriod], i.[ProvisionLocation],
|
||||||
|
i.[PaymentStatus], i.[IsPayed], i.[IsSent], i.[IsFinal], i.[IsCanceled], i.[Replaces_InvId],
|
||||||
|
i.[CustomValues], i.[SendToAddressJson], i.[DateSent], i.[UserSent], i.[DateFinalized], i.[UserFinalized],
|
||||||
|
i.[DateCancelled], i.[UserCancelled], i.[DateCreated], i.[UserCreated], i.[DateModified], i.[UserModified],
|
||||||
|
i.[ExternalId], i.[IsExternal], i.[file_guid]
|
||||||
|
FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER),
|
||||||
|
(SELECT it.*
|
||||||
|
FROM [dbo].[fds__invoice_items] it
|
||||||
|
WHERE it.[InvId] = i.[Id]
|
||||||
|
ORDER BY it.[SortOrder], it.[Position], it.[Id]
|
||||||
|
FOR JSON PATH, INCLUDE_NULL_VALUES),
|
||||||
|
(SELECT d.*
|
||||||
|
FROM [dbo].[fds__invoice_details] d
|
||||||
|
WHERE d.[InvId] = i.[Id]
|
||||||
|
FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER),
|
||||||
|
(SELECT sr.*
|
||||||
|
FROM [dbo].[fds__invoice_servicerequests] sr
|
||||||
|
WHERE sr.[InvId] = i.[Id]
|
||||||
|
ORDER BY sr.[SortOrder], sr.[Id]
|
||||||
|
FOR JSON PATH, INCLUDE_NULL_VALUES),
|
||||||
|
@authuser
|
||||||
|
FROM [dbo].[fds__invoices] i
|
||||||
|
WHERE i.[Id] = @Id AND i.[file] IS NOT NULL
|
||||||
|
AND i.[DateFinalized] IS NOT NULL AND ISNULL(i.[IsExternal], 0) = 0;
|
||||||
|
|
||||||
UPDATE [dbo].[fds__invoices]
|
UPDATE [dbo].[fds__invoices]
|
||||||
SET [file] = NULL
|
SET [file] = NULL
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ CREATE TABLE [dbo].[fds__invoice_file_backups] (
|
|||||||
[InvoiceId] VARCHAR (10) NOT NULL,
|
[InvoiceId] VARCHAR (10) NOT NULL,
|
||||||
[DocumentName] VARCHAR (50) NULL,
|
[DocumentName] VARCHAR (50) NULL,
|
||||||
[file] VARBINARY (MAX) NOT NULL,
|
[file] VARBINARY (MAX) NOT NULL,
|
||||||
|
-- JSON snapshots (FOR JSON PATH) of the invoice's data rows at backup time, so the file and
|
||||||
|
-- the data it was rendered from stay together as one consistent set (ADR 0030).
|
||||||
|
-- InvoiceJson: the fds__invoices row without [file]; InvoiceItemsJson: array of
|
||||||
|
-- fds__invoice_items rows; InvoiceDetailsJson: the fds__invoice_details row (NULL if none);
|
||||||
|
-- InvoiceServiceRequestsJson: array of fds__invoice_servicerequests rows.
|
||||||
|
[InvoiceJson] NVARCHAR (MAX) NULL,
|
||||||
|
[InvoiceItemsJson] NVARCHAR (MAX) NULL,
|
||||||
|
[InvoiceDetailsJson] NVARCHAR (MAX) NULL,
|
||||||
|
[InvoiceServiceRequestsJson] NVARCHAR (MAX) NULL,
|
||||||
[BackedUpAt] DATETIME CONSTRAINT [DF_fds__invoice_file_backups_BackedUpAt] DEFAULT (getutcdate()) NOT NULL,
|
[BackedUpAt] DATETIME CONSTRAINT [DF_fds__invoice_file_backups_BackedUpAt] DEFAULT (getutcdate()) NOT NULL,
|
||||||
[BackedUpBy] VARCHAR (25) NULL,
|
[BackedUpBy] VARCHAR (25) NULL,
|
||||||
CONSTRAINT [PK_fds__invoice_file_backups] PRIMARY KEY CLUSTERED ([uid] ASC)
|
CONSTRAINT [PK_fds__invoice_file_backups] PRIMARY KEY CLUSTERED ([uid] ASC)
|
||||||
|
|||||||
+1
-1
Submodule OCORE updated: 9f9b2d2220...b0a3ac5ba0
+1
-1
Submodule OCORE_web updated: 4eec682f03...729d7bfc9d
Reference in New Issue
Block a user