-- ============================================= -- Author: Dr. Stefan Ott -- Create date: 2026-09-17 -- Description: App-facing counterpart to the manual [dbo].[_BackupAndClearInvoiceFile] maintenance -- tool (see that proc's header comment and Docs/Decisions/0025's "Alternatives -- considered"). That proc is explicitly manual-only and leaves no application log -- entry, so it is never called from here — this is a separate proc for the hidden -- admin/backupclearinvoicefile action (fds_sys > 4 gate, IntranetController.Admin.cs), -- 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] -- 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 -- 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 -- next read persist a draft render (fds__setInvoiceFile writes WHERE [file] IS NULL) -- that then blocks the real final PDF forever; an external (MFR) document cannot be -- regenerated at all. The calling service checks this too; this is defense in depth. -- An empty OUTPUT rowset means nothing was backed up/cleared (invoice missing, not -- finalised, external, or [file] already NULL) — rows-affected can't be used for this -- because of SET NOCOUNT ON (same reasoning as fds__setInvoiceFile, ADR 0025). -- ============================================= CREATE PROCEDURE [dbo].[fds__admin_backupAndClearInvoiceFile] @authuser varchar(25), @Id varchar(10) AS BEGIN SET NOCOUNT ON; -- The clear must never commit without its backup row: XACT_ABORT turns any statement-level -- error inside the transaction into a doomed transaction, and the CATCH rolls back and re-raises -- so the caller sees an error instead of an OUTPUT row (same pattern as the mfr__updt__* procs). SET XACT_ABORT ON; INSERT INTO [dbo].[fds__admin_activity] ([activity], [authuser], [info]) VALUES ('fds__admin_backupAndClearInvoiceFile', @authuser, (SELECT * FROM (VALUES(@Id)) AS z([id]) FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER)); BEGIN TRY BEGIN TRANSACTION; -- The backup row carries the file AND JSON snapshots of the data rows it was rendered from -- (ADR 0030), so a backup is a self-contained, consistent set. The invoice snapshot lists -- its columns explicitly to leave [file] out (it is already stored as bytes in [file] and -- would otherwise be base64-duplicated into the JSON); items, details and service requests take -- 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] SET [file] = NULL OUTPUT inserted.[Id] WHERE [Id] = @Id AND [file] IS NOT NULL AND [DateFinalized] IS NOT NULL AND ISNULL([IsExternal], 0) = 0; COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; INSERT INTO dbo.[fds__admin_dberrors] ([UserName], [ErrorNumber], [ErrorState], [ErrorSeverity], [ErrorLine], [ErrorProcedure], [ErrorMessage], [ErrorDateTime]) VALUES (SUSER_SNAME(), ERROR_NUMBER(), ERROR_STATE(), ERROR_SEVERITY(), ERROR_LINE(), ERROR_PROCEDURE(), ERROR_MESSAGE(), GETDATE()); THROW; END CATCH; END