Ver código fonte

Created GUI for licensing server

Kenric Nugteren 1 ano atrás
pai
commit
c0ddfa0c93

+ 8 - 0
prs.licensing/App.xaml

@@ -0,0 +1,8 @@
+<Application x:Class="PRSLicensing.App"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:local="clr-namespace:PRSLicensing">
+    <Application.Resources>
+         
+    </Application.Resources>
+</Application>

+ 124 - 0
prs.licensing/App.xaml.cs

@@ -0,0 +1,124 @@
+using InABox.Configuration;
+using InABox.Core;
+using InABox.Wpf;
+using PRSServices;
+using Syncfusion.Licensing;
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Security.Principal;
+using System.ServiceProcess;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace PRSLicensing;
+
+/// <summary>
+/// Interaction logic for App.xaml
+/// </summary>
+public partial class App : Application
+{
+    static App()
+    {
+        PRSServiceInstaller.Assembly = typeof(App).Assembly;
+    }
+
+    private AdminStartup CheckAdmin()
+    {
+        if (!IsRunAsAdmin())
+        {
+            var proc = new ProcessStartInfo();
+            proc.UseShellExecute = true;
+            proc.WorkingDirectory = Environment.CurrentDirectory;
+            proc.FileName =
+                Path.ChangeExtension(Assembly.GetEntryAssembly().Location, "exe"); // Required to change to exe because .NET produces a .dll
+
+            proc.Verb = "runas";
+
+            try
+            {
+                Process.Start(proc);
+                Shutdown(0);
+                return AdminStartup.Done;
+            }
+            catch (Exception ex)
+            {
+                MessageWindow.ShowMessage("This program must be run as an administrator! \n\n" + ex, "Administrator required");
+            }
+
+            return AdminStartup.Cannot;
+        }
+
+        return AdminStartup.Already;
+    }
+
+    private static bool IsRunAsAdmin()
+    {
+        try
+        {
+            var id = WindowsIdentity.GetCurrent();
+            var principal = new WindowsPrincipal(id);
+            return principal.IsInRole(WindowsBuiltInRole.Administrator);
+        }
+        catch (Exception)
+        {
+            return false;
+        }
+    }
+
+    protected override void OnStartup(StartupEventArgs e)
+    {
+        SyncfusionLicenseProvider.RegisterLicense(CoreUtils.SyncfusionLicense(SyncfusionVersion.v23_2));
+
+        if (Environment.UserInteractive)
+        {
+            var args = Environment.GetCommandLineArgs();
+            if (args.Length <= 1)
+            {
+                if (CheckAdmin() != AdminStartup.Cannot)
+                {
+                    var f = new Console();
+                    f.ShowDialog();
+                    Shutdown(0);
+                }
+                else
+                {
+                    MessageWindow.ShowMessage("This program must be run as Admin!", "Administrator required");
+                    Shutdown(0);
+                }
+            }
+            else if (args[1].StartsWith("/service="))
+            {
+                var c = new LicensingConsole(args[1].Split('=').Last(), args[1].Split('=').Last(), false);
+                c.ShowDialog();
+                Shutdown(0);
+            }
+            else
+            {
+                Shutdown(0);
+            }
+        }
+        else
+        {
+            var args = Environment.GetCommandLineArgs();
+
+            var serviceName = "";
+            if (args.Length > 1 && args[1].StartsWith("/service=")) serviceName = args[1].Split('=').Last();
+
+            ServiceBase.Run(new PRSLicensingService(serviceName));
+            Shutdown(0);
+        }
+    }
+
+    private enum AdminStartup
+    {
+        Already,
+        Done,
+        Cannot
+    }
+}

+ 10 - 0
prs.licensing/AssemblyInfo.cs

@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+    ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+                                     //(used if a resource is not found in the page,
+                                     // or application resource dictionaries)
+    ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+                                              //(used if a resource is not found in the page,
+                                              // app, or any theme specific resource dictionaries)
+)]

+ 27 - 0
prs.licensing/Engine/LicensingConfiguration.cs

@@ -0,0 +1,27 @@
+using InABox.Configuration;
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PRSLicensing;
+
+public class LicensingConfiguration : BaseObject, ILocalConfigurationSettings
+{
+    public string ServiceName { get; set; } = "PRSLicensing";
+
+    [NullEditor]
+    public string Properties { get; set; } = "";
+
+    public string GetServiceName()
+    {
+        return GetServiceName(ServiceName);
+    }
+
+    public static string GetServiceName(string serviceName)
+    {
+        return "PRSLicensing_" + CoreUtils.SanitiseFileName(serviceName);
+    }
+}

+ 102 - 0
prs.licensing/Engine/LicensingEngine.cs

@@ -0,0 +1,102 @@
+using Comal.Classes;
+using Comal.Stores;
+using InABox.Clients;
+using InABox.Configuration;
+using InABox.Core;
+using InABox.Rpc;
+using InABox.Wpf.Reports;
+using PRS.Shared;
+using PRSServer;
+using PRSServices;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace PRSLicensing;
+
+public class LicensingEngine : Engine<LicensingEngineProperties>
+{
+    public override void Run()
+    {
+        Logger.Send(LogType.Information, "", "Starting..");
+
+        if (string.IsNullOrWhiteSpace(Properties.Server))
+        {
+            Logger.Send(LogType.Error, "", "Server is blank!");
+            return;
+        }
+
+        var transport = new RpcClientPipeTransport(DatabaseServerProperties.GetPipeName(Properties.Server, true));
+        ClientFactory.SetClientType(typeof(RpcClient<>), Platform.WebEngine, Version, transport);
+        CheckConnection();
+        
+        Logger.Send(LogType.Information, "", "Registering Classes");
+
+        StoreUtils.RegisterClasses();
+        CoreUtils.RegisterClasses();
+        ComalUtils.RegisterClasses();
+        PRSSharedUtils.RegisterClasses();
+        ReportUtils.RegisterClasses();
+        ConfigurationUtils.RegisterClasses();
+
+        Logger.Send(LogType.Information, "", "Starting Listener on port " + Properties.ListenPort);
+    }
+
+    private void CheckConnection()
+    {
+        // Wait for server connection
+        while (!Client.Ping())
+        {
+            Logger.Send(LogType.Error, "", "Database server unavailable. Trying again in 30 seconds...");
+            Task.Delay(30_000).Wait();
+            Logger.Send(LogType.Information, "", "Retrying connection...");
+        }
+
+        ClientFactory.SetBypass();
+    }
+
+    public override void Stop()
+    {
+        Logger.Send(LogType.Information, "", "Stopping");
+    }
+}
+
+public class LicensingEngineProperties : ServerProperties
+{
+
+    [ComboLookupEditor(typeof(LicensingDatabaseServerLookupGenerator))]
+    [EditorSequence(1)]
+    public string Server { get; set; }
+
+    [IntegerEditor]
+    [EditorSequence(2)]
+    public int ListenPort { get; set; }
+
+    public override ServerType Type()
+    {
+        return ServerType.Other;
+    }
+}
+
+public class LicensingDatabaseServerLookupGenerator : LookupGenerator<LicensingEngineProperties>
+{
+    public LicensingDatabaseServerLookupGenerator(LicensingEngineProperties[] items) : base(items)
+    {
+    }
+
+    protected override void DoGenerateLookups()
+    {
+        var config = new LocalConfiguration<ServerSettings>(CoreUtils.GetCommonAppData("PRSServer"), "");
+        var servers = config.LoadAll();
+        foreach (var server in servers.Select(x => x.Value.CreateServer(x.Key)))
+        {
+            if (server.Type == ServerType.Database)
+            {
+                AddValue(server.Key, server.Name);
+            }
+        }
+    }
+}

+ 12 - 0
prs.licensing/Engine/LicensingInstaller.cs

@@ -0,0 +1,12 @@
+using PRSServices;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PRSLicensing;
+
+[RunInstaller(true)]
+public class LicensingInstaller : PRSInstaller { }

+ 57 - 0
prs.licensing/Engine/PRSLicensingService.cs

@@ -0,0 +1,57 @@
+using InABox.Configuration;
+using InABox.Core;
+using PRSServer;
+using PRSServices;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PRSLicensing;
+
+internal class PRSLicensingService : PRSService
+{
+    public PRSLicensingService(string serviceName) : base(serviceName)
+    {
+    }
+
+    protected override ServerSettings GetSettings(string serviceName)
+    {
+        var configuration = GetConfiguration().Load();
+        return new ServerSettings
+        {
+            Type = ServerType.Other,
+            Properties = configuration.Properties
+        };
+    }
+    
+    public static IConfiguration<LicensingConfiguration> GetConfiguration()
+    {
+        return new LocalConfiguration<LicensingConfiguration>(CoreUtils.GetCommonAppData(), "");
+    }
+
+
+    protected override Server CreateServer(ServerSettings settings, string serviceName)
+    {
+        var result = new Server();
+        result.Key = serviceName;
+        result.Type = ServerType.Other;
+        //result.Sequence = Sequence == 0 ? DateTime.Now.Ticks : Sequence;
+
+        var properties = Serialization.Deserialize<LicensingEngineProperties>(settings.Properties);
+        if (properties != null)
+        {
+            result.Properties = properties;
+            result.Name = result.Properties.Name;
+        }
+        result.CommitChanges();
+
+        return result;
+    }
+
+    protected override IEngine? CreateEngine(ServerSettings settings)
+    {
+        return new LicensingEngine();
+    }
+}

+ 3 - 0
prs.licensing/FodyWeavers.xml

@@ -0,0 +1,3 @@
+<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
+  <PropertyChanged />
+</Weavers>

+ 74 - 0
prs.licensing/FodyWeavers.xsd

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
+  <xs:element name="Weavers">
+    <xs:complexType>
+      <xs:all>
+        <xs:element name="PropertyChanged" minOccurs="0" maxOccurs="1">
+          <xs:complexType>
+            <xs:attribute name="InjectOnPropertyNameChanged" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to control if the On_PropertyName_Changed feature is enabled.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="TriggerDependentProperties" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to control if the Dependent properties feature is enabled.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="EnableIsChangedProperty" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to control if the IsChanged property feature is enabled.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="EventInvokerNames" type="xs:string">
+              <xs:annotation>
+                <xs:documentation>Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="CheckForEquality" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="CheckForEqualityUsingBaseEquals" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to control if equality checks should use the Equals method resolved from the base class.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="UseStaticEqualsFromBase" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to control if equality checks should use the static Equals method resolved from the base class.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="SuppressWarnings" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to turn off build warnings from this weaver.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="SuppressOnPropertyNameChangedWarning" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to turn off build warnings about mismatched On_PropertyName_Changed methods.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+          </xs:complexType>
+        </xs:element>
+      </xs:all>
+      <xs:attribute name="VerifyAssembly" type="xs:boolean">
+        <xs:annotation>
+          <xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
+        </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="VerifyIgnoreCodes" type="xs:string">
+        <xs:annotation>
+          <xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
+        </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="GenerateXsd" type="xs:boolean">
+        <xs:annotation>
+          <xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
+        </xs:annotation>
+      </xs:attribute>
+    </xs:complexType>
+  </xs:element>
+</xs:schema>

+ 96 - 0
prs.licensing/GUI/Console.xaml

@@ -0,0 +1,96 @@
+<Window x:Class="PRSLicensing.Console"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:PRSLicensing"
+        xmlns:console="clr-namespace:InABox.Wpf.Console;assembly=InABox.Wpf"
+        xmlns:dg="clr-namespace:InABox.DynamicGrid;assembly=InABox.Wpf"
+        mc:Ignorable="d"
+        Title="PRS Licensing Engine" Height="600" Width="1200"
+        x:Name="Window"
+        Loaded="Window_Loaded">
+    <dg:DynamicSplitPanel View="Combined" AllowableViews="Combined" Anchor="Master" AnchorWidth="300"
+                          Margin="5,5,5,0"
+                          DataContext="{Binding ElementName=Window}">
+        <dg:DynamicSplitPanel.Header>
+            <Border BorderBrush="Gray" BorderThickness="0.75" Background="WhiteSmoke" Padding="0" Margin="0,0,0,0">
+                <Label Content="Service Manager" HorizontalContentAlignment="Center"
+                       VerticalContentAlignment="Center" />
+            </Border>
+        </dg:DynamicSplitPanel.Header>
+        <dg:DynamicSplitPanel.Master>
+            <Border Padding="0,0,5,0" Margin="0,5,0,5" BorderThickness="0,0,1,0" BorderBrush="LightGray">
+                <Grid>
+                    <Grid.RowDefinitions>
+                        <RowDefinition Height="Auto"/>
+                        <RowDefinition Height="Auto"/>
+                        <RowDefinition Height="Auto"/>
+                        <RowDefinition Height="*"/>
+                        <RowDefinition Height="Auto"/>
+                    </Grid.RowDefinitions>
+                    <TextBox x:Name="ServiceName"
+                             IsEnabled="False"
+                             Grid.Row="0"
+                             Padding="5"/>
+                    <Button x:Name="InstallButton"
+                            Click="InstallButton_Click"
+                            Margin="0,5,0,0"
+                            Padding="5"
+                            Grid.Row="1">
+                        <Button.Style>
+                            <Style TargetType="Button">
+                                <Setter Property="Content" Value="Install"/>
+                                <Style.Triggers>
+                                    <DataTrigger Binding="{Binding IsInstalled}" Value="True">
+                                        <Setter Property="Content" Value="Uninstall"/>
+                                    </DataTrigger>
+                                </Style.Triggers>
+                            </Style>
+                        </Button.Style>
+                    </Button>
+                    <Button x:Name="StartButton"
+                            Click="StartButton_Click"
+                            Margin="0,5,0,0"
+                            Padding="5"
+                            Grid.Row="2">
+                        <Button.Style>
+                            <Style TargetType="Button">
+                                <Setter Property="Content" Value="Start"/>
+                                <Setter Property="IsEnabled" Value="False"/>
+                                <Style.Triggers>
+                                    <DataTrigger Binding="{Binding IsRunning}" Value="True">
+                                        <Setter Property="Content" Value="Stop"/>
+                                    </DataTrigger>
+                                    <DataTrigger Binding="{Binding IsInstalled}" Value="True">
+                                        <Setter Property="IsEnabled" Value="True"/>
+                                    </DataTrigger>
+                                </Style.Triggers>
+                            </Style>
+                        </Button.Style>
+                        
+                    </Button>
+                    <Button x:Name="EditButton"
+                            Content="Edit"
+                            Click="EditButton_Click"
+                            Margin="0,5,0,0"
+                            IsEnabled="{Binding IsNotRunning}"
+                            Padding="5"
+                            Grid.Row="4"/>
+                </Grid>
+            </Border>
+        </dg:DynamicSplitPanel.Master>
+        <dg:DynamicSplitPanel.DetailHeader>
+            <Border BorderBrush="Gray" BorderThickness="0.75" Background="WhiteSmoke">
+                <Label Content="Console" HorizontalContentAlignment="Center"
+                       VerticalContentAlignment="Center" />
+            </Border>
+        </dg:DynamicSplitPanel.DetailHeader>
+        <dg:DynamicSplitPanel.Detail>
+            <console:ConsoleControl x:Name="ConsoleControl"
+                                    Enabled="{Binding IsRunning}"
+                                    Margin="0,5,0,0"
+                                    AllowLoadLogButton="False"/>
+        </dg:DynamicSplitPanel.Detail>
+    </dg:DynamicSplitPanel>
+</Window>

+ 309 - 0
prs.licensing/GUI/Console.xaml.cs

@@ -0,0 +1,309 @@
+using H.Pipes;
+using InABox.Configuration;
+using InABox.Core;
+using InABox.DynamicGrid;
+using InABox.Wpf.Editors;
+using InABox.WPF;
+using PRSServices;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Timers;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace PRSLicensing;
+
+/// <summary>
+/// Interaction logic for MainWindow.xaml
+/// </summary>
+public partial class Console : Window, INotifyPropertyChanged
+{
+    private LicensingConfiguration Settings { get; set; }
+
+    private Timer timer;
+
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    private bool _isRunning = false;
+    public bool IsRunning
+    {
+        get => _isRunning;
+        set
+        {
+            _isRunning = value;
+            OnPropertyChanged();
+            OnPropertyChanged(nameof(IsNotRunning));
+        }
+    }
+    public bool IsNotRunning => !_isRunning;
+
+    private bool _isInstalled = false;
+    public bool IsInstalled
+    {
+        get => _isInstalled;
+        set
+        {
+            _isInstalled = value;
+            OnPropertyChanged();
+        }
+    }
+
+    private PipeClient<string>? _client;
+    private Timer? RefreshTimer;
+
+    public Console()
+    {
+        InitializeComponent();
+        LoadSettings();
+
+        Progress.DisplayImage = PRSLicensing.Resources.splash_small.AsBitmapImage();
+
+        timer = new Timer(2000);
+        timer.Elapsed += Timer_Elapsed;
+        timer.AutoReset = true;
+        timer.Start();
+    }
+
+    private void Window_Loaded(object sender, RoutedEventArgs e)
+    {
+        RefreshStatus();
+    }
+
+    private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
+    {
+        RefreshStatus();
+    }
+
+    protected override void OnClosing(CancelEventArgs e)
+    {
+        base.OnClosing(e);
+
+        _client?.DisposeAsync().AsTask().Wait();
+        _client = null;
+        RefreshTimer?.Stop();
+    }
+
+    private void RefreshStatus()
+    {
+        IsRunning = PRSServiceInstaller.IsRunning(Settings.GetServiceName());
+        IsInstalled = PRSServiceInstaller.IsInstalled(Settings.GetServiceName());
+
+        if(_client is null)
+        {
+            if (IsRunning)
+            {
+                CreateClient();
+            }
+        }
+        else if(_client.PipeName != GetPipeName())
+        {
+            _client.DisposeAsync().AsTask().Wait();
+            _client = null;
+            CreateClient();
+        }
+        
+    }
+
+    private bool _creatingClient = false;
+
+    private void CreateClient()
+    {
+        if (_creatingClient) return;
+
+        _creatingClient = true;
+
+        var client = new PipeClient<string>(GetPipeName(), ".");
+        client.MessageReceived += (o, args) =>
+        {
+            Dispatcher.BeginInvoke(() =>
+            {
+                ConsoleControl.LoadLogEntry(args.Message ?? "");
+            });
+        };
+        client.Connected += (o, args) =>
+        {
+            Dispatcher.BeginInvoke(() =>
+            {
+                ConsoleControl.Enabled = true;
+            });
+        };
+        client.Disconnected += (o, args) =>
+        {
+            Dispatcher.BeginInvoke(() =>
+            {
+                ConsoleControl.Enabled = false;
+            });
+
+            if (RefreshTimer == null)
+            {
+                RefreshTimer = new Timer(1000);
+                RefreshTimer.Elapsed += RefreshTimer_Elapsed;
+            }
+            RefreshTimer.Start();
+        };
+        client.ExceptionOccurred += (o, args) =>
+        {
+
+        };
+        if (!client.IsConnecting)
+        {
+            client.ConnectAsync();
+        }
+        _client = client;
+
+        _creatingClient = false;
+    }
+    private void RefreshTimer_Elapsed(object? sender, ElapsedEventArgs e)
+    {
+        if (_client is null) return;
+
+        if (!_client.IsConnected)
+        {
+            if (!_client.IsConnecting)
+            {
+                _client.ConnectAsync();
+            }
+        }
+        else
+        {
+            RefreshTimer?.Stop();
+        }
+    }
+
+    private string GetPipeName()
+    {
+        return Settings.GetServiceName();
+    }
+
+    private void LoadSettings()
+    {
+        Settings = PRSLicensingService.GetConfiguration().Load();
+        ServiceName.Text = Settings.ServiceName;
+    }
+
+    private void SaveSettings()
+    {
+        PRSLicensingService.GetConfiguration().Save(Settings);
+    }
+
+    private void Install()
+    {
+        var username = GetProperties().Username;
+        string? password = null;
+        if (!string.IsNullOrWhiteSpace(username))
+        {
+            var passwordEditor = new PasswordDialog(string.Format("Enter password for {0}", username));
+            if(passwordEditor.ShowDialog() == true)
+            {
+                password = passwordEditor.Password;
+            }
+            else
+            {
+                password = null;
+            }
+        }
+        else
+        {
+            username = null;
+        }
+        PRSServiceInstaller.InstallService(
+            Settings.GetServiceName(),
+            "PRS Licensing Service",
+            Settings.ServiceName,
+            username,
+            password);
+    }
+
+    private void InstallButton_Click(object sender, RoutedEventArgs e)
+    {
+        if (PRSServiceInstaller.IsInstalled(Settings.GetServiceName()))
+        {
+            Progress.ShowModal("Uninstalling Service", (progress) =>
+            {
+                PRSServiceInstaller.UninstallService(Settings.GetServiceName());
+            });
+        }
+        else
+        {
+            Progress.ShowModal("Installing Service", (progress) =>
+            {
+                Install();
+            });
+        }
+        RefreshStatus();
+    }
+
+    private void StartButton_Click(object sender, RoutedEventArgs e)
+    {
+        if (PRSServiceInstaller.IsRunning(Settings.GetServiceName()))
+        {
+            Progress.ShowModal("Stopping Service", (progress) =>
+            {
+                PRSServiceInstaller.StopService(Settings.GetServiceName());
+            });
+        }
+        else
+        {
+            Progress.ShowModal("Starting Service", (progress) =>
+            {
+                PRSServiceInstaller.StartService(Settings.GetServiceName());
+            });
+        }
+        RefreshStatus();
+    }
+
+    private LicensingEngineProperties GetProperties()
+    {
+        var properties = Serialization.Deserialize<LicensingEngineProperties>(Settings.Properties) ?? new LicensingEngineProperties();
+        properties.Name = Settings.ServiceName;
+        return properties;
+    }
+
+    private void EditButton_Click(object sender, RoutedEventArgs e)
+    {
+        var grid = new DynamicItemsListGrid<LicensingEngineProperties>();
+        Settings.CommitChanges();
+        var properties = GetProperties();
+        if(grid.EditItems(new LicensingEngineProperties[] { properties }))
+        {
+            Settings.Properties = Serialization.Serialize(properties);
+            Settings.ServiceName = properties.Name;
+
+            if (Settings.IsChanged())
+            {
+                if(Settings.HasOriginalValue(x => x.ServiceName) || properties.HasOriginalValue(x => x.Username))
+                {
+                    var oldService = LicensingConfiguration.GetServiceName(Settings.GetOriginalValue(x => x.ServiceName));
+                    if (PRSServiceInstaller.IsInstalled(oldService))
+                    {
+                        Progress.ShowModal("Modifying Service", (progress) =>
+                        {
+                            PRSServiceInstaller.UninstallService(oldService);
+                            Install();
+                        });
+                    }
+                }
+                SaveSettings();
+                ServiceName.Text = Settings.ServiceName;
+                RefreshStatus();
+            }
+        }
+    }
+
+    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
+}

+ 117 - 0
prs.licensing/GUI/ServerConsole.cs

@@ -0,0 +1,117 @@
+using H.Pipes;
+using InABox.Wpf.Console;
+using PRSServices;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Timers;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Threading;
+
+namespace PRSLicensing;
+
+public class LicensingConsole : InABox.Wpf.Console.Console
+{
+    private PipeClient<string>? _client;
+
+    private PRSService? _service;
+
+    private readonly bool _monitoronly = true;
+
+    public string ServiceName { get; private set; }
+
+    private Timer? RefreshTimer;
+
+    public LicensingConsole(string servicename, string description, bool monitoronly = true): base(description)
+    {
+        ServiceName = servicename;
+        _monitoronly = monitoronly;
+    }
+
+    protected override void OnLoaded()
+    {
+        base.OnLoaded();
+
+        _client = new PipeClient<string>(ServiceName, ".");
+        _client.MessageReceived += (o, args) =>
+        {
+            Dispatcher.BeginInvoke(() =>
+            {
+                ConsoleControl.LoadLogEntry(args.Message ?? "");
+            });
+        };
+        _client.Connected += (o, args) =>
+        {
+            Dispatcher.BeginInvoke(() => { ConsoleControl.Enabled = true; });
+        };
+        _client.Disconnected += (o, args) =>
+        {
+            Dispatcher.BeginInvoke(() => { ConsoleControl.Enabled = false; });
+
+            if (RefreshTimer == null)
+            {
+                RefreshTimer = new Timer(1000);
+                RefreshTimer.Elapsed += RefreshTimer_Elapsed;
+            }
+            RefreshTimer.Start();
+        };
+        _client.ExceptionOccurred += (o, args) =>
+        {
+
+        };
+        if (!_client.IsConnecting)
+        {
+            _client.ConnectAsync();
+        }
+
+        if (!_monitoronly)
+        {
+            var timer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 3) };
+            timer.Tick += (o, args) =>
+            {
+                timer.IsEnabled = false;
+                _service = new PRSLicensingService(ServiceName);
+                _service.Run(ServiceName);
+            };
+            timer.IsEnabled = true;
+        }
+    }
+
+    private void RefreshTimer_Elapsed(object? sender, ElapsedEventArgs e)
+    {
+        if (_client is null) return;
+
+        if (!_client.IsConnected)
+        {
+            if (!_client.IsConnecting)
+            {
+                _client.ConnectAsync();
+            }
+        }
+        else
+        {
+            RefreshTimer?.Stop();
+        }
+    }
+
+    protected override void OnClosing()
+    {
+        base.OnClosing();
+
+        if (_monitoronly)
+        {
+            _client?.DisposeAsync().AsTask().Wait();
+            RefreshTimer?.Stop();
+        }
+        else
+            _service?.Halt();
+    }
+
+    protected override string GetLogDirectory()
+    {
+        return LicensingEngine.GetPath(ServiceName);
+    }
+}

+ 49 - 0
prs.licensing/PRSLicensing.csproj

@@ -0,0 +1,49 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>net6.0-windows</TargetFramework>
+    <Nullable>enable</Nullable>
+    <UseWPF>true</UseWPF>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <None Remove="Resources\splash-small.png" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="PropertyChanged.Fody" Version="3.4.1" />
+    <PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\inabox\InABox.Client.RPC\InABox.Client.RPC.csproj" />
+    <ProjectReference Include="..\..\inabox\inabox.wpf\InABox.Wpf.csproj" />
+    <ProjectReference Include="..\prs.services\PRSServices.csproj" />
+    <ProjectReference Include="..\prs.shared\PRS.Shared.csproj" />
+  </ItemGroup>
+    <Import Project="..\prs.stores\PRSStores.projitems" Label="Shared" />
+
+  <ItemGroup>
+    <Resource Include="Resources\splash-small.png">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Resource>
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Update="Engine\PRSLicensingService.cs" />
+    <Compile Update="Resources.Designer.cs">
+      <DependentUpon>Resources.resx</DependentUpon>
+      <DesignTime>True</DesignTime>
+      <AutoGen>True</AutoGen>
+    </Compile>
+  </ItemGroup>
+
+  <ItemGroup>
+    <EmbeddedResource Update="Resources.resx">
+      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+      <Generator>ResXFileCodeGenerator</Generator>
+    </EmbeddedResource>
+  </ItemGroup>
+
+</Project>

+ 11 - 0
prs.licensing/Properties/launchSettings.json

@@ -0,0 +1,11 @@
+{
+  "profiles": {
+    "PRSLicensing": {
+      "commandName": "Project",
+      "commandLineArgs": "/service=PRSLicensing_PRSLicensing"
+    },
+    "GUI": {
+      "commandName": "Project"
+    }
+  }
+}

+ 73 - 0
prs.licensing/Resources.Designer.cs

@@ -0,0 +1,73 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.42000
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace PRSLicensing {
+    using System;
+    
+    
+    /// <summary>
+    ///   A strongly-typed resource class, for looking up localized strings, etc.
+    /// </summary>
+    // This class was auto-generated by the StronglyTypedResourceBuilder
+    // class via a tool like ResGen or Visual Studio.
+    // To add or remove a member, edit your .ResX file then rerun ResGen
+    // with the /str option, or rebuild your VS project.
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    internal class Resources {
+        
+        private static global::System.Resources.ResourceManager resourceMan;
+        
+        private static global::System.Globalization.CultureInfo resourceCulture;
+        
+        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+        internal Resources() {
+        }
+        
+        /// <summary>
+        ///   Returns the cached ResourceManager instance used by this class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Resources.ResourceManager ResourceManager {
+            get {
+                if (object.ReferenceEquals(resourceMan, null)) {
+                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PRSLicensing.Resources", typeof(Resources).Assembly);
+                    resourceMan = temp;
+                }
+                return resourceMan;
+            }
+        }
+        
+        /// <summary>
+        ///   Overrides the current thread's CurrentUICulture property for all
+        ///   resource lookups using this strongly typed resource class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Globalization.CultureInfo Culture {
+            get {
+                return resourceCulture;
+            }
+            set {
+                resourceCulture = value;
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized resource of type System.Drawing.Bitmap.
+        /// </summary>
+        internal static System.Drawing.Bitmap splash_small {
+            get {
+                object obj = ResourceManager.GetObject("splash_small", resourceCulture);
+                return ((System.Drawing.Bitmap)(obj));
+            }
+        }
+    }
+}

+ 124 - 0
prs.licensing/Resources.resx

@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
+  <data name="splash_small" type="System.Resources.ResXFileRef, System.Windows.Forms">
+    <value>Resources\splash-small.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
+  </data>
+</root>

BIN
prs.licensing/Resources/splash-small.png


+ 46 - 0
prs.licensing/Templates/LicenseSummary.txt

@@ -0,0 +1,46 @@
+@using Comal.Classes
+@using GenHTTP.Api.Protocol
+@using GenHTTP.Modules.IO;
+@using InABox.Core
+@{
+var summary = new LicenseSummary {
+    LicenseFees = new() {
+        { typeof(CoreLicense).EntityName(),                 7.99 },
+        { typeof(DigitalFormsLicense).EntityName(),         3.99 },
+        { typeof(SchedulingControlLicense).EntityName(),    1.99 },
+        { typeof(TimeManagementLicense).EntityName(),       2.99 },
+        { typeof(AccountsPayableLicense).EntityName(),      1.99 },
+        { typeof(GPSTrackerLicense).EntityName(),           2.99 },
+        { typeof(LogisticsLicense).EntityName(),            4.99 },
+        { typeof(ScheduleEngineLicense).EntityName(),       2.99 },
+        { typeof(QuotesManagementLicense).EntityName(),     4.99 },
+        { typeof(LeaveManagementLicense).EntityName(),      2.99 },
+        { typeof(TaskManagementLicense).EntityName(),       1.99 },
+        { typeof(WarehouseLicense).EntityName(),            5.99 },
+        { typeof(ProjectManagementLicense).EntityName(),    4.99 },
+        { typeof(ManufacturingLicense).EntityName(),        4.99 },
+        { typeof(ProductManagementLicense).EntityName(),    2.99 },
+        { typeof(EquipmentLicense).EntityName(),            2.99 },
+        { typeof(HumanResourcesLicense).EntityName(),       2.99 },
+        { typeof(AccountsReceivableLicense).EntityName(),   1.99 }
+    },
+    TimeDiscounts = new() {
+        { 1,    0.00 },
+        { 3,    5.00 },
+        { 6,    5.00 },
+        { 12,   10.00 }
+    },
+    UserDiscounts = new() {
+        { 1,    00.31 },
+        { 6,    08.63 },
+        { 11,   16.94 },
+        { 21,   25.25 },
+        { 51,   33.57 }
+    }
+};
+
+Model.Respond()
+    .Status(ResponseStatus.OK)
+    .Content(Serialization.Serialize(summary));
+return;
+}

+ 170 - 0
prs.licensing/Templates/RenewLicense.txt

@@ -0,0 +1,170 @@
+@using Comal.Classes
+@using GenHTTP.Api.Protocol
+@using GenHTTP.Modules.IO
+@using InABox.Clients
+@using InABox.Core
+@using PRSServer
+@using System.Text
+@{
+var renewalPeriods = new List<Tuple<int, Func<DateTime, DateTime>>> {
+    new(1, x => x.AddDays(-7)),
+    new(3, x => x.AddDays(-14)),
+    new(6, x => x.AddMonths(-1))
+};
+
+
+var request = Serialization.Deserialize<LicenseRenewal>(Model.Request.Content);
+if(request == null){
+    Model.Respond().Status(ResponseStatus.BadRequest);
+    return;
+}
+
+Logger.Send(LogType.Information, "", $"Request for license renewal from {request.Company.CompanyName}");
+
+var customerID = request.OldLicense.CustomerID;
+if(customerID == Guid.Empty){
+    customerID = CreateNewCustomer().ID;
+    request.OldLicense.CustomerID = customerID;
+}
+
+Logger.Send(LogType.Information, "", "Generating new license");
+
+var newLicense = GenerateLicense();
+var newLicenseData = LicenseUtils.EncryptLicense(newLicense);
+if(newLicenseData == null){
+    Logger.Send(LogType.Error, "", "Encryption of new license failed!");
+    Model.Respond().Status(ResponseStatus.InternalServerError);
+    return;
+}
+
+Logger.Send(LogType.Information, "", "Generating customer document");
+var customerDocument = CreateNewCustomerDocument(customerID, $"{request.Company.CompanyName} - PRS License - {DateTime.Now:dd MMM yyyy}.txt", newLicenseData);
+Logger.Send(LogType.Information, "", "Creating invoice");
+CreateInvoice(customerID, request);
+
+Model.Respond()
+    .Status(ResponseStatus.OK)
+    .Content(Serialization.Serialize(new License { Data = newLicenseData } )); // Send just the encrypted data.
+return;
+
+int GetMonthDifference(DateTime date1, DateTime date2){
+    var months = (date2.Year - date1.Year) * 12 + (date2.Month - date1.Month);
+    
+    if(date2.Day >= date1.Day){
+        return months;
+    }
+    return months - 1;
+}
+LicenseData GenerateLicense(){
+    var renewalPeriodInMonths = GetMonthDifference(request.DateRenewed, request.NewExpiry);
+    var renewalAvailable = renewalPeriods
+        .Where(x => renewalPeriodInMonths >= x.Item1)
+        .MaxBy(x => x.Item1)
+        .Item2(request.NewExpiry);
+    
+    var newLicense = LicenseUtils.RenewLicense(request.OldLicense, request.DateRenewed, request.NewExpiry, renewalAvailable);
+    
+    return newLicense;
+}
+
+string NewCustomerCode(){
+    var code = request.Company.CompanyName.ToUpper();
+    var codes = new Client<Customer>().Query(
+        new Filter<Customer>(x => x.Code).BeginsWith(code),
+        new Columns<Customer>(x => x.Code))?.Rows.Select(x => x.Get<Customer, string>(x => x.Code)).ToList();
+    
+    var tryCode = code;
+    var i = 0;
+    while(codes.Contains(tryCode)){
+        tryCode = $"{code}{i}";
+        ++i;
+    }
+    
+    return tryCode;
+}
+Customer CreateNewCustomer(){
+    Logger.Send(LogType.Information, "", "Creating new customer");
+    
+    var customer = new Customer {
+        Code = NewCustomerCode(),
+        Name = $"{request.Company.CompanyName} - {request.Company.ABN}",
+        Delivery = request.Company.DeliveryAddress,
+        Email = request.Company.Email,
+        Postal = request.Company.PostalAddress
+    };
+    new Client<Customer>().Save(customer, "Created by License Renewal");
+    return customer;
+}
+CustomerDocument CreateNewCustomerDocument(Guid customerID, string fileName, string data){
+    var document = new Document {
+        FileName = fileName,
+        Private = true,
+        TimeStamp = DateTime.Now,
+        Data = Encoding.UTF8.GetBytes(data)
+    };
+    document.CRC = CoreUtils.CalculateCRC(document.Data);
+    
+    new Client<Document>().Save(document, "");
+    
+    var documentType = new Client<DocumentType>()
+        .Query(new Filter<DocumentType>(x => x.Code).IsEqualTo("LICENSE"), new Columns<DocumentType>(x => x.ID))
+        .Rows.FirstOrDefault()?.Get<DocumentType, Guid>(x => x.ID) ?? Guid.Empty;
+    if(documentType == Guid.Empty){
+        Logger.Send(LogType.Error, "", "Document Type 'LICENSE' doesn't exist");
+    }
+    
+    var customerDocument = new CustomerDocument();
+    customerDocument.Type.ID = documentType;
+    customerDocument.EntityLink.ID = customerID;
+    customerDocument.DocumentLink.ID = document.ID;
+    
+    new Client<CustomerDocument>().Save(customerDocument, "Created by License Renewal");
+    return customerDocument;
+}
+void CreateInvoice(Guid customerID, LicenseRenewal renewal){
+    var invoiceLines = new List<InvoiceLine>();
+    var notes = new List<string>();
+    foreach(var item in renewal.LicenseTracking){
+        var invoiceLine = new InvoiceLine {
+            Description = $"{item.License} - {item.Users} Users @ ${item.Rate:F2} per user",
+            ExTax = item.ExGST
+        };
+        invoiceLines.Add(invoiceLine);
+        notes.Add(invoiceLine.Description);
+    }
+    var discountLine = new InvoiceLine {
+        Description = $"${renewal.Discount:F2} discount",
+        ExTax = -renewal.Discount
+    };
+    invoiceLines.Add(discountLine);
+     notes.Add(discountLine.Description);
+    
+    var invoice = new Invoice {
+        Date = DateTime.Today,
+        Description = $"License Renewal - Stripe Transaction No. '{renewal.TransactionID}'"
+    };
+    invoice.CustomerLink.ID = customerID;
+    
+    new Client<Invoice>().Save(invoice, "Created by License Renewal");
+    
+    foreach(var line in invoiceLines){
+        line.InvoiceLink.ID = invoice.ID;
+    }
+    new Client<InvoiceLine>().Save(invoiceLines, "");
+    
+    var receipt = new Receipt {
+        Date = DateTime.Today,
+        Notes = string.Join('\n', notes)
+    };
+    new Client<Receipt>().Save(receipt, "");
+    
+    var invoiceReceipt = new InvoiceReceipt {
+        Notes = "Receipt for License Renewal",
+        Amount = renewal.Net
+    };
+    invoiceReceipt.InvoiceLink.ID = invoice.ID;
+    invoiceReceipt.ReceiptLink.ID = receipt.ID;
+    new Client<InvoiceReceipt>().Save(invoiceReceipt, "");
+}
+
+}

+ 6 - 0
prs.licensing/Templates/ping.txt

@@ -0,0 +1,6 @@
+@using GenHTTP.Api.Protocol
+@using GenHTTP.Modules.IO;
+@{
+Model.Respond()
+    .Status(ResponseStatus.OK);
+}