foxygit / digitalskrivare_merge Log in
commit 5ea7131636bc32b8b8da34b61f1e049c846d634c
Author:     Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
AuthorDate: Wed Sep 2 09:20:55 2026 +0200
Commit:     Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
CommitDate: Wed Sep 2 09:20:55 2026 +0200

    Tom targetPrinter = Windows standardskrivare

    SumatraPdfPrinter använder -print-to-default när targetPrinter är tom
    (inga dialoger), annars -print-to "<namn>". DefaultPrinter läser
    standardskrivarens namn ur registret för audit-loggen. appsettings
    default ändrad till "" + kommentarer. IPrinter.Print tar string?.

    Nya tester: blank printer-väg, utskriftsfel, DefaultPrinter. 19/19.

    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
 config/appsettings.json                        | 11 ++++++++-
 src/Remiss.Core/Abstractions/IPrinter.cs       |  3 ++-
 src/Remiss.Core/AppConfig.cs                   |  2 ++
 src/Remiss.Core/Printing/DefaultPrinter.cs     | 32 ++++++++++++++++++++++++++
 src/Remiss.Core/Printing/SumatraPdfPrinter.cs  | 24 ++++++++++++-------
 src/Remiss.Core/RemissWorkflow.cs              |  7 +++++-
 tests/Remiss.Core.Tests/DefaultPrinterTests.cs | 15 ++++++++++++
 tests/Remiss.Core.Tests/WorkflowTests.cs       | 23 ++++++++++++++++--
 8 files changed, 104 insertions(+), 13 deletions(-)

diff --git a/config/appsettings.json b/config/appsettings.json
index 6c3a5a3..838f58b 100644
--- a/config/appsettings.json
+++ b/config/appsettings.json
@@ -1,12 +1,21 @@
 {
-  "targetPrinter": "Microsoft Print to PDF",
+  // Namngiven skrivare, eller "" för Windows standardskrivare (inga dialoger).
+  "targetPrinter": "",
+
   "sumatraPdfPath": "tools/SumatraPDF.exe",
   "mottagningarPath": "mottagningar.json",
+
+  // Tomma = under %LOCALAPPDATA%\Remiss
   "secureTempRoot": "",
   "auditLogPath": "",
   "errorLogPath": "",
+
   "printRetries": 3,
   "printRetryDelayMs": 2000,
+
+  // false i Fas 1 (behåll test-PDF:en), true i Fas 2
   "deleteSourceAfterPrint": false,
+
+  // ENDAST TEST: kopiera mergad PDF hit innan utskrift/städning. Tom i drift.
   "debugOutputDir": ""
 }
diff --git a/src/Remiss.Core/Abstractions/IPrinter.cs b/src/Remiss.Core/Abstractions/IPrinter.cs
index 4158365..9dd1a74 100644
--- a/src/Remiss.Core/Abstractions/IPrinter.cs
+++ b/src/Remiss.Core/Abstractions/IPrinter.cs
@@ -6,5 +6,6 @@ namespace Remiss.Core.Abstractions;
 /// </summary>
 public interface IPrinter
 {
-    void Print(string pdfPath, string printerName);
+    /// <param name="printerName">Namngiven skrivare, eller tom/null för systemets standardskrivare.</param>
+    void Print(string pdfPath, string? printerName);
 }
diff --git a/src/Remiss.Core/AppConfig.cs b/src/Remiss.Core/AppConfig.cs
index 1bf5776..19795b2 100644
--- a/src/Remiss.Core/AppConfig.cs
+++ b/src/Remiss.Core/AppConfig.cs
@@ -9,7 +9,9 @@ namespace Remiss.Core;
 /// </summary>
 public sealed class AppConfig
 {
+    /// <summary>Namngiven målskrivare. Tom = Windows standardskrivare (utan dialoger).</summary>
     public string TargetPrinter { get; set; } = "";
+
     public string SumatraPdfPath { get; set; } = "tools/SumatraPDF.exe";
     public string MottagningarPath { get; set; } = "mottagningar.json";
     public string SecureTempRoot { get; set; } = "";
diff --git a/src/Remiss.Core/Printing/DefaultPrinter.cs b/src/Remiss.Core/Printing/DefaultPrinter.cs
new file mode 100644
index 0000000..a638f57
--- /dev/null
+++ b/src/Remiss.Core/Printing/DefaultPrinter.cs
@@ -0,0 +1,32 @@
+using Microsoft.Win32;
+
+namespace Remiss.Core.Printing;
+
+/// <summary>
+/// Läser namnet på Windows standardskrivare. Används bara för audit-loggen –
+/// själva utskriften låter SumatraPDF välja default via <c>-print-to-default</c>.
+/// </summary>
+public static class DefaultPrinter
+{
+    public static string? TryGetName()
+    {
+        try
+        {
+            using var key = Registry.CurrentUser.OpenSubKey(
+                @"Software\Microsoft\Windows NT\CurrentVersion\Windows");
+
+            if (key?.GetValue("Device") is string device && device.Length > 0)
+            {
+                // Formatet är "Skrivarnamn,winspool,Ne00:"
+                var comma = device.IndexOf(',');
+                return comma > 0 ? device[..comma] : device;
+            }
+        }
+        catch
+        {
+            // ignore – audit får då "(systemets standardskrivare)"
+        }
+
+        return null;
+    }
+}
diff --git a/src/Remiss.Core/Printing/SumatraPdfPrinter.cs b/src/Remiss.Core/Printing/SumatraPdfPrinter.cs
index 1bc4ff0..7aea623 100644
--- a/src/Remiss.Core/Printing/SumatraPdfPrinter.cs
+++ b/src/Remiss.Core/Printing/SumatraPdfPrinter.cs
@@ -20,16 +20,15 @@ public sealed class SumatraPdfPrinter : IPrinter
         _retryDelayMs = Math.Max(0, retryDelayMs);
     }

-    public void Print(string pdfPath, string printerName)
+    /// <param name="printerName">
+    /// Namngiven skrivare, eller tom/null för Windows standardskrivare.
+    /// </param>
+    public void Print(string pdfPath, string? printerName)
     {
         if (!File.Exists(_exePath))
             throw new FileNotFoundException(
                 $"SumatraPDF hittades inte: {_exePath}. Lägg binären i tools/ (se tools/README.md).", _exePath);

-        if (string.IsNullOrWhiteSpace(printerName))
-            throw new InvalidOperationException(
-                "Ingen målskrivare konfigurerad (targetPrinter i appsettings.json).");
-
         Exception? last = null;
         for (var attempt = 1; attempt <= _retries; attempt++)
         {
@@ -49,7 +48,7 @@ public sealed class SumatraPdfPrinter : IPrinter
         throw new InvalidOperationException($"Utskrift misslyckades efter {_retries} försök.", last);
     }

-    private void RunOnce(string pdfPath, string printerName)
+    private void RunOnce(string pdfPath, string? printerName)
     {
         var psi = new ProcessStartInfo
         {
@@ -59,8 +58,17 @@ public sealed class SumatraPdfPrinter : IPrinter
             RedirectStandardOutput = true,
             RedirectStandardError = true,
         };
-        psi.ArgumentList.Add("-print-to");
-        psi.ArgumentList.Add(printerName);
+
+        if (string.IsNullOrWhiteSpace(printerName))
+        {
+            psi.ArgumentList.Add("-print-to-default");
+        }
+        else
+        {
+            psi.ArgumentList.Add("-print-to");
+            psi.ArgumentList.Add(printerName);
+        }
+
         psi.ArgumentList.Add("-silent");
         psi.ArgumentList.Add(pdfPath);

diff --git a/src/Remiss.Core/RemissWorkflow.cs b/src/Remiss.Core/RemissWorkflow.cs
index 240426e..3dae6de 100644
--- a/src/Remiss.Core/RemissWorkflow.cs
+++ b/src/Remiss.Core/RemissWorkflow.cs
@@ -2,6 +2,7 @@ using Remiss.Core.Abstractions;
 using Remiss.Core.Audit;
 using Remiss.Core.Models;
 using Remiss.Core.Pdf;
+using Remiss.Core.Printing;
 using Remiss.Core.Security;

 namespace Remiss.Core;
@@ -41,13 +42,17 @@ public sealed class RemissWorkflow
             DumpForDebug(mergedPath);

             stage = "print";
+            var printerName = string.IsNullOrWhiteSpace(_config.TargetPrinter)
+                ? DefaultPrinter.TryGetName() ?? "(systemets standardskrivare)"
+                : _config.TargetPrinter;
             _printer.Print(mergedPath, _config.TargetPrinter);

             stage = "source-cleanup";
             if (_config.DeleteSourceAfterPrint)
                 SecureFile.Shred(sourcePdfPath);

-            _audit.Write("remiss.sent", mottagning.Namn, mottagning.HsaId, success: true, stage: "done");
+            _audit.Write("remiss.sent", mottagning.Namn, mottagning.HsaId,
+                success: true, stage: "done", detail: $"printer={printerName}");
         }
         catch (Exception ex)
         {
diff --git a/tests/Remiss.Core.Tests/DefaultPrinterTests.cs b/tests/Remiss.Core.Tests/DefaultPrinterTests.cs
new file mode 100644
index 0000000..a1a5a90
--- /dev/null
+++ b/tests/Remiss.Core.Tests/DefaultPrinterTests.cs
@@ -0,0 +1,15 @@
+using Remiss.Core.Printing;
+using Xunit;
+
+namespace Remiss.Core.Tests;
+
+public sealed class DefaultPrinterTests
+{
+    [Fact]
+    public void TryGetName_neverThrows_andReturnsNullOrANonEmptyName()
+    {
+        var name = DefaultPrinter.TryGetName();
+        Assert.True(name is null || name.Length > 0);
+        Assert.DoesNotContain(",", name ?? "");   // registervärdets ",winspool,..." ska vara bortkapat
+    }
+}
diff --git a/tests/Remiss.Core.Tests/WorkflowTests.cs b/tests/Remiss.Core.Tests/WorkflowTests.cs
index 34b5cf8..9ec7ee7 100644
--- a/tests/Remiss.Core.Tests/WorkflowTests.cs
+++ b/tests/Remiss.Core.Tests/WorkflowTests.cs
@@ -23,7 +23,7 @@ public sealed class WorkflowTests : IDisposable
     {
         public string? PrintedFile { get; private set; }
         public string? Printer { get; private set; }
-        public void Print(string pdfPath, string printerName)
+        public void Print(string pdfPath, string? printerName)
         {
             Assert.True(File.Exists(pdfPath));       // mergad fil finns när utskrift anropas
             PrintedFile = pdfPath;
@@ -58,6 +58,25 @@ public sealed class WorkflowTests : IDisposable
         Assert.Contains("remiss.sent", auditText);
     }

+    [Fact]
+    public void Run_withBlankTargetPrinter_passesBlankThroughAndAuditsAPrinter()
+    {
+        var source = Path.Combine(_dir, "remiss.pdf");
+        CoverPageGenerator.Generate(source, new CoverInfo("Body", "hsa-body", DateTime.Now));
+
+        var cfg = new AppConfig { TargetPrinter = "" };
+        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.True(string.IsNullOrEmpty(printer.Printer));   // SumatraPDF väljer -print-to-default
+        Assert.Contains("printer=", File.ReadAllText(Path.Combine(_dir, "audit.jsonl")));
+    }
+
     [Fact]
     public void Run_propagatesPrinterFailureAndAuditsTheStage()
     {
@@ -80,6 +99,6 @@ public sealed class WorkflowTests : IDisposable

     private sealed class ThrowingPrinter : IPrinter
     {
-        public void Print(string pdfPath, string printerName) => throw new InvalidOperationException("no printer");
+        public void Print(string pdfPath, string? printerName) => throw new InvalidOperationException("no printer");
     }
 }