foxygit / digitalskrivare_merge Log in
commit 0b9f4315a3dc38db36395cfb2fec87ae8cdd06d8
Author:     Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
AuthorDate: Wed Sep 2 09:07:11 2026 +0200
Commit:     Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
CommitDate: Wed Sep 2 09:07:11 2026 +0200

    Lägg till debugOutputDir för manuellt testande

    Test-only config: kopiera mergad PDF (försättsblad + remiss) till en
    mapp innan utskrift/städning, så resultatet kan granskas utan att en
    skrivare måste fungera. Tom i drift.

    WorkflowTests: happy path (generate→merge→debugkopia→print via fake
    IPrinter, 2 sidor) + felväg (utskriftsfel propageras, stage auditas).
    17/17 gröna.
---
 README.md                                |  1 +
 config/appsettings.json                  |  3 +-
 src/Remiss.Core/AppConfig.cs             | 10 ++++
 src/Remiss.Core/RemissWorkflow.cs        | 15 +++++
 tests/Remiss.Core.Tests/WorkflowTests.cs | 85 +++++++++++++++++++++++++++++
 tools/SumatraPDF-settings.txt            | 94 ++++++++++++++++++++++++++++++++
 6 files changed, 207 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index a08ecb8..f3b94ff 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,7 @@ tests/
 | `errorLogPath` | Tom = `%LOCALAPPDATA%\Remiss\logs\error.log` |
 | `printRetries` / `printRetryDelayMs` | Retry vid utskriftsfel |
 | `deleteSourceAfterPrint` | `false` i Fas 1 (behåll test-PDF:en). `true` i Fas 2. |
+| `debugOutputDir` | **Endast test.** Satt = kopiera mergad PDF hit (patientdata, städas ej). Tom i drift. |

 ## Säkerhet

diff --git a/config/appsettings.json b/config/appsettings.json
index 0845cfd..6c3a5a3 100644
--- a/config/appsettings.json
+++ b/config/appsettings.json
@@ -7,5 +7,6 @@
   "errorLogPath": "",
   "printRetries": 3,
   "printRetryDelayMs": 2000,
-  "deleteSourceAfterPrint": false
+  "deleteSourceAfterPrint": false,
+  "debugOutputDir": ""
 }
diff --git a/src/Remiss.Core/AppConfig.cs b/src/Remiss.Core/AppConfig.cs
index 5a9ff6d..1bf5776 100644
--- a/src/Remiss.Core/AppConfig.cs
+++ b/src/Remiss.Core/AppConfig.cs
@@ -19,6 +19,13 @@ public sealed class AppConfig
     public int PrintRetryDelayMs { get; set; } = 2000;
     public bool DeleteSourceAfterPrint { get; set; }

+    /// <summary>
+    /// ENDAST FÖR UTVECKLING/TEST. Om satt kopieras den mergade PDF:en (försättsblad
+    /// + remiss) hit innan utskrift och städning, så resultatet kan granskas.
+    /// Kopian innehåller patientdata och städas inte – lämna tom i drift.
+    /// </summary>
+    public string DebugOutputDir { get; set; } = "";
+
     [JsonIgnore]
     public string ConfigFilePath { get; private set; } = "";

@@ -58,6 +65,9 @@ public sealed class AppConfig

         if (string.IsNullOrWhiteSpace(ErrorLogPath))
             ErrorLogPath = Path.Combine(appRoot, "logs", "error.log");
+
+        if (!string.IsNullOrWhiteSpace(DebugOutputDir))
+            DebugOutputDir = ToAbsolute(DebugOutputDir, baseDir);
     }

     private static string ToAbsolute(string path, string baseDir) =>
diff --git a/src/Remiss.Core/RemissWorkflow.cs b/src/Remiss.Core/RemissWorkflow.cs
index fb3566b..240426e 100644
--- a/src/Remiss.Core/RemissWorkflow.cs
+++ b/src/Remiss.Core/RemissWorkflow.cs
@@ -37,6 +37,9 @@ public sealed class RemissWorkflow
             stage = "merge";
             PdfMerger.Merge(mergedPath, coverPath, sourcePdfPath);

+            stage = "debug-copy";
+            DumpForDebug(mergedPath);
+
             stage = "print";
             _printer.Print(mergedPath, _config.TargetPrinter);

@@ -54,4 +57,16 @@ public sealed class RemissWorkflow
             throw;
         }
     }
+
+    // ENDAST TEST: kopiera ut mergad PDF (patientdata, städas inte) om debugOutputDir är satt.
+    private void DumpForDebug(string mergedPath)
+    {
+        if (string.IsNullOrWhiteSpace(_config.DebugOutputDir))
+            return;
+
+        Directory.CreateDirectory(_config.DebugOutputDir);
+        var target = Path.Combine(_config.DebugOutputDir, $"remiss-debug-{DateTime.Now:yyyyMMdd-HHmmss}.pdf");
+        File.Copy(mergedPath, target, overwrite: true);
+        _audit.Write("remiss.debugcopy", null, null, success: true, stage: "debug-copy", detail: "test-only");
+    }
 }
diff --git a/tests/Remiss.Core.Tests/WorkflowTests.cs b/tests/Remiss.Core.Tests/WorkflowTests.cs
new file mode 100644
index 0000000..34b5cf8
--- /dev/null
+++ b/tests/Remiss.Core.Tests/WorkflowTests.cs
@@ -0,0 +1,85 @@
+using Remiss.Core;
+using Remiss.Core.Abstractions;
+using Remiss.Core.Audit;
+using Remiss.Core.Models;
+using Remiss.Core.Pdf;
+using Remiss.Core.Security;
+using Xunit;
+
+namespace Remiss.Core.Tests;
+
+public sealed class WorkflowTests : IDisposable
+{
+    private readonly string _dir = Path.Combine(Path.GetTempPath(), "remiss-wf-" + Guid.NewGuid().ToString("N"));
+
+    public WorkflowTests() => Directory.CreateDirectory(_dir);
+
+    public void Dispose()
+    {
+        try { Directory.Delete(_dir, recursive: true); } catch { /* ignore */ }
+    }
+
+    private sealed class FakePrinter : IPrinter
+    {
+        public string? PrintedFile { get; private set; }
+        public string? Printer { get; private set; }
+        public void Print(string pdfPath, string printerName)
+        {
+            Assert.True(File.Exists(pdfPath));       // mergad fil finns när utskrift anropas
+            PrintedFile = pdfPath;
+            Printer = printerName;
+        }
+    }
+
+    [Fact]
+    public void Run_generatesMergesPrintsAndWritesDebugCopy()
+    {
+        var source = Path.Combine(_dir, "remiss.pdf");
+        CoverPageGenerator.Generate(source, new CoverInfo("Body", "hsa-body", DateTime.Now));
+
+        var debugDir = Path.Combine(_dir, "debug");
+        var cfg = new AppConfig { TargetPrinter = "TestPrinter", DebugOutputDir = debugDir };
+        var audit = new AuditLogger(Path.Combine(_dir, "audit.jsonl"), Path.Combine(_dir, "error.log"));
+        var printer = new FakePrinter();
+
+        using (var temp = new SecureTempStore(Path.Combine(_dir, "tmp")))
+        {
+            new RemissWorkflow(cfg, printer, audit).Run(source, new Mottagning("Hud SU", "hsa-1"), temp);
+        }
+
+        Assert.Equal("TestPrinter", printer.Printer);
+        var debugCopies = Directory.GetFiles(debugDir, "remiss-debug-*.pdf");
+        Assert.Single(debugCopies);
+
+        using var merged = PdfSharp.Pdf.IO.PdfReader.Open(debugCopies[0], PdfSharp.Pdf.IO.PdfDocumentOpenMode.Import);
+        Assert.Equal(2, merged.PageCount); // försättsblad + "remiss"
+
+        var auditText = File.ReadAllText(Path.Combine(_dir, "audit.jsonl"));
+        Assert.Contains("remiss.sent", auditText);
+    }
+
+    [Fact]
+    public void Run_propagatesPrinterFailureAndAuditsTheStage()
+    {
+        var source = Path.Combine(_dir, "remiss.pdf");
+        CoverPageGenerator.Generate(source, new CoverInfo("Body", "hsa-body", DateTime.Now));
+
+        var cfg = new AppConfig { TargetPrinter = "X" };
+        var audit = new AuditLogger(Path.Combine(_dir, "audit.jsonl"), Path.Combine(_dir, "error.log"));
+
+        var boom = new ThrowingPrinter();
+        using var temp = new SecureTempStore(Path.Combine(_dir, "tmp"));
+
+        Assert.Throws<InvalidOperationException>(() =>
+            new RemissWorkflow(cfg, boom, audit).Run(source, new Mottagning("Hud SU", "hsa-1"), temp));
+
+        var auditText = File.ReadAllText(Path.Combine(_dir, "audit.jsonl"));
+        Assert.Contains("remiss.failed", auditText);
+        Assert.Contains("\"stage\":\"print\"", auditText);
+    }
+
+    private sealed class ThrowingPrinter : IPrinter
+    {
+        public void Print(string pdfPath, string printerName) => throw new InvalidOperationException("no printer");
+    }
+}
diff --git a/tools/SumatraPDF-settings.txt b/tools/SumatraPDF-settings.txt
new file mode 100644
index 0000000..4cc5ee6
--- /dev/null
+++ b/tools/SumatraPDF-settings.txt
@@ -0,0 +1,94 @@
+# For documentation, see https://www.sumatrapdfreader.org/settings/settings3-5-1.html
+Theme = Light
+FixedPageUI [
+	TextColor = #000000
+	BackgroundColor = #ffffff
+	SelectionColor = #f5fc0c
+	WindowMargin = 2 4 2 4
+	PageSpacing = 4 4
+	InvertColors = false
+	HideScrollbars = false
+]
+ComicBookUI [
+	WindowMargin = 0 0 0 0
+	PageSpacing = 4 4
+	CbxMangaMode = false
+]
+ChmUI [
+	UseFixedPageUI = false
+]
+
+SelectionHandlers [
+]
+ExternalViewers [
+]
+
+ZoomLevels = 8.33 12.5 18 25 33.33 50 66.67 75 100 125 150 200 300 400 600 800 1000 1200 1600 2000 2400 3200 4800 6400
+ZoomIncrement = 0
+
+PrinterDefaults [
+	PrintScale = shrink
+]
+ForwardSearch [
+	HighlightOffset = 0
+	HighlightWidth = 15
+	HighlightColor = #6581ff
+	HighlightPermanent = false
+]
+Annotations [
+	HighlightColor = #ffff00
+	UnderlineColor = #00ff00
+	SquigglyColor = #ff00ff
+	StrikeOutColor = #ff0000
+	FreeTextColor =
+	FreeTextSize = 12
+	FreeTextBorderWidth = 1
+	TextIconColor =
+	TextIconType =
+	DefaultAuthor =
+]
+
+RememberOpenedFiles = true
+RememberStatePerDocument = true
+RestoreSession = true
+UiLanguage = sv
+EnableTeXEnhancements = false
+DefaultDisplayMode = automatic
+DefaultZoom = fit page
+Shortcuts [
+]
+EscToExit = false
+ReuseInstance = false
+ReloadModifiedDocuments = true
+
+MainWindowBackground = #80fff200
+FullPathInTitle = false
+ShowMenubar = true
+ShowToolbar = true
+ShowFavorites = false
+ShowToc = true
+NoHomeTab = false
+TocDy = 0
+SidebarDx = 0
+ToolbarSize = 18
+TabWidth = 300
+TreeFontSize = 0
+TreeFontWeightOffset = 0
+TreeFontName = automatic
+SmoothScroll = false
+ShowStartPage = true
+CheckForUpdates = true
+WindowState = 1
+WindowPos = 0 0 0 0
+UseTabs = true
+UseSysColors = false
+CustomScreenDPI = 0
+
+FileStates [
+]
+SessionData [
+]
+TimeOfLastUpdateCheck = 0 0
+OpenCountWeek = 817
+
+# Settings below are not recognized by the current version