Browse Source

Finishing Off Licensing System

frogsoftware 1 year ago
parent
commit
c4e115ab0b

+ 1 - 1
prs.desktop/Panels/Tasks/KanbanResources.xaml

@@ -3,7 +3,7 @@
                     xmlns:wpf="clr-namespace:InABox.WPF;assembly=InABox.Wpf"
                     xmlns:local="clr-namespace:PRSDesktop"
                     x:Class="PRSDesktop.KanbanResources">
-    <wpf:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
+    <wpf:BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" TrueValue="Visible" FalseValue="Collapsed" />
     <local:ViewModeToKanbanTemplateConverter x:Key="TypeToTemplateConverter"/>
     <local:KanbanStatusToStringConverter x:Key="KanbanStatusToStringConverter"/>
 

+ 1 - 1
prs.desktop/prsdesktop.iss

@@ -5,7 +5,7 @@
 #pragma verboselevel 9
 
 #define MyAppName "PRS Desktop"
-#define MyAppVersion "7.56a"
+#define MyAppVersion "7.57b"
 #define MyAppPublisher "PRS Digital"
 #define MyAppURL "https://www.prs-software.com.au"
 #define MyAppExeName "PRSDesktop.exe"

+ 1 - 1
prs.licensing/Engine/LicensingEngine.cs

@@ -29,7 +29,7 @@ public class LicensingEngine : Engine<LicensingEngineProperties>
         }
 
         var transport = new RpcClientPipeTransport(DatabaseServerProperties.GetPipeName(Properties.Server, true));
-        ClientFactory.SetClientType(typeof(RpcClient<>), Platform.WebEngine, Version, transport);
+        ClientFactory.SetClientType(typeof(RpcClient<>), Platform.LicensingEngine, Version, transport);
         CheckConnection();
         
         Logger.Send(LogType.Information, "", "Registering Classes");

+ 29 - 27
prs.licensing/Engine/LicensingHandler.cs

@@ -25,14 +25,16 @@ public class LicensingHandler : Handler<LicensingHandlerProperties>
         _properties = properties;
     }
 
-    private IResponseBuilder LicenseSummary(IRequest request)
+    private IResponseBuilder RetrieveFees(IRequest request)
     {
         if (_properties == null)
             return request.Respond().Status(ResponseStatus.BadRequest);
         
-        var lsr = Serialization.Deserialize<LicenseSummaryRequest>(request.Content);
+        var lsr = Serialization.Deserialize<LicenseFeeRequest>(request.Content);
         if (lsr == null)
             return request.Respond().Status(ResponseStatus.BadRequest);
+        
+        Logger.Send(LogType.Information, "", $"License Enquiry Received ({lsr.RegistrationID})");
 
         var productids = _properties.EngineProperties.Mappings.Select(x => x.Product.ID).ToArray();
         if (!productids.Any())
@@ -72,7 +74,7 @@ public class LicensingHandler : Handler<LicensingHandlerProperties>
             ? query.Get<CustomerProduct>().Rows.Select(x => x.ToObject<CustomerProduct>()).ToArray()
             : new CustomerProduct[] { };
 
-        var result = new LicenseSummary();
+        var result = new LicenseFeeResponse();
         foreach (var mapping in _properties.EngineProperties.Mappings)
         {
 
@@ -117,25 +119,25 @@ public class LicensingHandler : Handler<LicensingHandlerProperties>
         }
         return months - 1;
     }
-    private LicenseData GenerateLicense(LicenseRenewal renewal){
-        var renewalPeriodInMonths = GetMonthDifference(renewal.DateRenewed, renewal.NewExpiry);
+    private LicenseData GenerateLicense(LicenseRenewalRequest renewalRequest){
+        var renewalPeriodInMonths = GetMonthDifference(renewalRequest.DateRenewed, renewalRequest.NewExpiry);
         var renewalAvailable = renewalPeriods
             .Where(x => renewalPeriodInMonths >= x.Item1)
             .MaxBy(x => x.Item1)
-            .Item2(renewal.NewExpiry);
-        
-        var newLicense = LicenseUtils.RenewLicense(renewal.OldLicense, renewal.DateRenewed, renewal.NewExpiry, renewalAvailable);
+            .Item2(renewalRequest.NewExpiry);
         
+        var newLicense = LicenseUtils.RenewLicense(renewalRequest.OldLicense, renewalRequest.DateRenewed, renewalRequest.NewExpiry, renewalAvailable, renewalRequest.Addresses);
+
         return newLicense;
     }
     
-    private string NewCustomerCode(LicenseRenewal renewal)
+    private string NewCustomerCode(LicenseRenewalRequest renewalRequest)
     {
         // Try to build a 5 character abbreviation of the company name
         // is ACME Incorporated should become ACMIN
         // while P T Barnum should become PTBAR and so on
         String code = "";
-        var codecomps = renewal.Company.CompanyName
+        var codecomps = renewalRequest.Company.CompanyName
             .ToUpper()
             .Split(' ')
             .ToList();
@@ -158,16 +160,16 @@ public class LicensingHandler : Handler<LicensingHandlerProperties>
         return $"{code}{i:D3}";
     }
     
-    private Customer CreateNewCustomer(LicenseRenewal renewal){
+    private Customer CreateNewCustomer(LicenseRenewalRequest renewalRequest){
         Logger.Send(LogType.Information, "", "Creating new customer");
         
         var customer = new Customer {
-            Code = NewCustomerCode(renewal),
-            Name = renewal.Company.CompanyName,
-            ABN = renewal.Company.ABN,
-            Delivery = renewal.Company.DeliveryAddress,
-            Email = renewal.Company.Email,
-            Postal = renewal.Company.PostalAddress
+            Code = NewCustomerCode(renewalRequest),
+            Name = renewalRequest.Company.CompanyName,
+            ABN = renewalRequest.Company.ABN,
+            Delivery = renewalRequest.Company.DeliveryAddress,
+            Email = renewalRequest.Company.Email,
+            Postal = renewalRequest.Company.PostalAddress
         };
         new Client<Customer>().Save(customer, "Created by License Renewal");
         return customer;
@@ -200,10 +202,10 @@ public class LicensingHandler : Handler<LicensingHandlerProperties>
         return customerDocument;
     }
     
-    private void CreateInvoice(Guid customerID, LicenseRenewal renewal){
+    private void CreateInvoice(Guid customerID, LicenseRenewalRequest renewalRequest){
         var invoiceLines = new List<InvoiceLine>();
         var notes = new List<string>();
-        foreach(var item in renewal.LicenseTracking){
+        foreach(var item in renewalRequest.LicenseTracking){
             var invoiceLine = new InvoiceLine {
                 Description = $"{item.Caption} - {item.Users} Users @ ${item.Rate:F2} per user",
                 ExTax = item.ExGST
@@ -212,8 +214,8 @@ public class LicensingHandler : Handler<LicensingHandlerProperties>
             notes.Add(invoiceLine.Description);
         }
         var discountLine = new InvoiceLine {
-            Description = $"${renewal.Discount:F2} discount",
-            ExTax = -renewal.Discount
+            Description = $"${renewalRequest.Discount:F2} discount",
+            ExTax = -renewalRequest.Discount
         };
         invoiceLines.Add(discountLine);
         notes.Add(discountLine.Description);
@@ -233,13 +235,13 @@ public class LicensingHandler : Handler<LicensingHandlerProperties>
         
         var receipt = new Receipt {
             Date = DateTime.Today,
-            Notes = $"PRS Renewal Invoice #{invoice.Number} ({renewal.TransactionID})"
+            Notes = $"PRS Renewal Invoice #{invoice.Number} ({renewalRequest.TransactionID})"
         };
         new Client<Receipt>().Save(receipt, "");
         
         var invoiceReceipt = new InvoiceReceipt {
             Notes = "Receipt for License Renewal",
-            Amount = renewal.Net
+            Amount = renewalRequest.Net
         };
         invoiceReceipt.InvoiceLink.ID = invoice.ID;
         invoiceReceipt.ReceiptLink.ID = receipt.ID;
@@ -249,7 +251,7 @@ public class LicensingHandler : Handler<LicensingHandlerProperties>
     private IResponseBuilder RenewLicense(IRequest request)
     {
         
-        var renewal = Serialization.Deserialize<LicenseRenewal>(request.Content);
+        var renewal = Serialization.Deserialize<LicenseRenewalRequest>(request.Content);
         if(renewal == null){
             return request.Respond().Status(ResponseStatus.BadRequest);
         }
@@ -304,10 +306,10 @@ public class LicensingHandler : Handler<LicensingHandlerProperties>
     {
         var endpoint = request.Target.Current?.Value.ToLower() ?? "";
         
-        if (endpoint.Equals(nameof(LicenseSummary).ToLower()))
-            return LicenseSummary(request);
+        if (endpoint.Equals(nameof(LicenseFeeRequest).ToLower()))
+            return RetrieveFees(request);
         
-        if (endpoint.Equals(nameof(LicenseRenewal).ToLower()))
+        if (endpoint.Equals(nameof(LicenseRenewalRequest).ToLower()))
             return RenewLicense(request);
         
         return request.Respond().Status(ResponseStatus.NotFound);

+ 64 - 44
prs.licensing/GUI/Console.xaml

@@ -6,11 +6,26 @@
         xmlns:local="clr-namespace:PRSLicensing"
         xmlns:console="clr-namespace:InABox.Wpf.Console;assembly=InABox.Wpf"
         xmlns:dg="clr-namespace:InABox.DynamicGrid;assembly=InABox.Wpf"
+        xmlns:wpf="clr-namespace:InABox.WPF;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="150"
+    <Window.Resources>
+        
+        <wpf:BooleanToBooleanConverter x:Key="Inverted" Invert="True"/>
+        
+        <wpf:BooleanToBrushConverter x:Key="NormalColor" TrueValue="LightGreen" FalseValue="WhiteSmoke"/>
+        <wpf:BooleanToBrushConverter x:Key="InvertedColor" TrueValue="WhiteSmoke" FalseValue="LightGreen"/>
+
+        <wpf:BooleanToImageSourceConverter x:Key="InstalledImage" TrueValue="../Resources/uninstall.png" FalseValue="../Resources/install.png"/>
+        <wpf:BooleanToStringConverter x:Key="InstalledText" TrueValue="Uninstall" FalseValue="Install"/>
+        
+        <wpf:BooleanToImageSourceConverter x:Key="RunningImage" TrueValue="../Resources/stop.png" FalseValue="../Resources/start.png"/>
+        <wpf:BooleanToStringConverter x:Key="RunningText" TrueValue="Stop" FalseValue="Start"/>
+        
+    </Window.Resources>
+    <dg:DynamicSplitPanel View="Combined" AllowableViews="Combined" Anchor="Master" AnchorWidth="130"
                           Margin="5,5,5,0"
                           DataContext="{Binding ElementName=Window}">
         <dg:DynamicSplitPanel.Header>
@@ -23,6 +38,7 @@
             <Border Background="WhiteSmoke" BorderBrush="Gray" BorderThickness="0.75" Margin="0,2,0,5" >
                 <Grid>
                     <Grid.RowDefinitions>
+                        <RowDefinition Height="Auto"/>
                         <RowDefinition Height="Auto"/>
                         <RowDefinition Height="Auto"/>
                         <RowDefinition Height="*"/>
@@ -35,11 +51,12 @@
                             BorderThickness="0" 
                             Background="Transparent" 
                             HorizontalAlignment="Stretch" 
-                            HorizontalContentAlignment="Stretch">
+                            HorizontalContentAlignment="Stretch"
+                            IsEnabled="{Binding IsRunning, Converter={StaticResource Inverted}}">
                         <Button.Content>
                             <Border 
                                 CornerRadius="5" 
-                                Background="White" 
+                                Background="{Binding IsRunning, Converter={StaticResource InvertedColor}}" 
                                 BorderBrush="Gray" 
                                 BorderThickness="0.75"
                                 Padding="10"
@@ -49,27 +66,16 @@
                                     Orientation="Vertical" 
                                     VerticalAlignment="Center" 
                                     HorizontalAlignment="Center" >
-                                    <Image Source="../Resources/service.png" Height="40" Width="40"/>
-                                    <Label>
-                                        <Label.Style>
-                                            <Style TargetType="Label">
-                                                <Setter Property="Content" Value="Install"/>
-                                                <Style.Triggers>
-                                                    <DataTrigger Binding="{Binding IsInstalled}" Value="True">
-                                                        <Setter Property="Content" Value="Uninstall"/>
-                                                    </DataTrigger>
-                                                </Style.Triggers>
-                                            </Style>
-                                        </Label.Style>
-                      
-                                    </Label>
+                                    <Image Source="{Binding IsInstalled, Converter={StaticResource InstalledImage}}" Height="40" Width="40"/>
+                                    <Label Content="{Binding IsInstalled, Converter={StaticResource InstalledText}}"/>
                                 </StackPanel>
                             </Border>
                         </Button.Content>
                     </Button>
                     
-                    <Button x:Name="StartButton"
-                            Click="StartButton_Click"
+                    <Button x:Name="EditButton"
+                            Click="EditButton_Click"
+                            IsEnabled="{Binding IsRunning, Converter={StaticResource Inverted}}"
                             Padding="4"
                             Grid.Row="1"
                             BorderThickness="0" 
@@ -79,7 +85,7 @@
                         <Button.Content>
                             <Border 
                                 CornerRadius="5" 
-                                Background="White" 
+                                Background="{Binding IsRunning, Converter={StaticResource InvertedColor}}" 
                                 BorderBrush="Gray" 
                                 BorderThickness="0.75" 
                                 Padding="10"
@@ -89,34 +95,48 @@
                                     Orientation="Vertical" 
                                     VerticalAlignment="Center" 
                                     HorizontalAlignment="Center" >
-                                    <Image Source="../Resources/tick.png" Height="40" Width="40"/>
-                                    <Label>
-                                        <Label.Style>
-                                            <Style TargetType="Label">
-                                                <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>
-                                        </Label.Style>
-                                    </Label>
+                                    <Image Source="../Resources/settings.png" Height="40" Width="40"/>
+                                    <Label Content="Settings" />
+                                </StackPanel>
+                            </Border>
+                        </Button.Content>
+                    </Button>
+                    
+                    <Button x:Name="StartButton"
+                            Click="StartButton_Click"
+                            Padding="4"
+                            Grid.Row="2"
+                            BorderThickness="0" 
+                            Background="Transparent" 
+                            HorizontalAlignment="Stretch" 
+                            HorizontalContentAlignment="Stretch"
+                            IsEnabled="{Binding IsInstalled}">
+                        <Button.Content>
+                            <Border 
+                                CornerRadius="5" 
+                                Background="{Binding IsInstalled, Converter={StaticResource NormalColor}}" 
+                                BorderBrush="Gray" 
+                                BorderThickness="0.75" 
+                                Padding="10"
+                                HorizontalAlignment="Stretch" 
+                                VerticalAlignment="Stretch">
+                                <StackPanel 
+                                    Orientation="Vertical" 
+                                    VerticalAlignment="Center" 
+                                    HorizontalAlignment="Center" >
+                                    <Image Source="{Binding IsRunning, Converter={StaticResource RunningImage}}" Height="40" Width="40" />
+                                    <Label Content="{Binding IsRunning, Converter={StaticResource RunningText}}"/>
                                 </StackPanel>
                             </Border>
                         </Button.Content>
                         
                     </Button>
                     
-                    <Button x:Name="EditButton"
-                            Click="EditButton_Click"
-                            IsEnabled="{Binding IsNotRunning}"
+                    <Button x:Name="GenerateButton"
+                            Click="GenerateButton_Click"
+                            IsEnabled="{Binding HasDbServer}"
                             Padding="4"
-                            Grid.Row="3"
+                            Grid.Row="4"
                             BorderThickness="0" 
                             Background="Transparent" 
                             HorizontalAlignment="Stretch" 
@@ -124,7 +144,7 @@
                         <Button.Content>
                             <Border 
                                 CornerRadius="5" 
-                                Background="White" 
+                                Background="{Binding HasDbServer, Converter={StaticResource NormalColor}}" 
                                 BorderBrush="Gray" 
                                 BorderThickness="0.75" 
                                 Padding="10"
@@ -134,8 +154,8 @@
                                     Orientation="Vertical" 
                                     VerticalAlignment="Center" 
                                     HorizontalAlignment="Center" >
-                                    <Image Source="../Resources/settings.png" Height="40" Width="40"/>
-                                    <Label Content="Settings" />
+                                    <Image Source="../Resources/generate.png" Height="40" Width="40"/>
+                                    <Label Content="Generate" />
                                 </StackPanel>
                             </Border>
                         </Button.Content>

+ 103 - 18
prs.licensing/GUI/Console.xaml.cs

@@ -6,6 +6,7 @@ using InABox.WPF;
 using PRSServices;
 using System;
 using System.ComponentModel;
+using System.IO;
 using System.Linq;
 using System.Runtime.CompilerServices;
 using System.Threading.Tasks;
@@ -16,10 +17,23 @@ using InABox.Rpc;
 using PRSServer;
 using Comal.Classes;
 using InABox.Wpf;
+using Microsoft.Win32;
 using PRS.Shared;
 
 namespace PRSLicensing;
 
+
+public class LicenseEditData : BaseObject
+{
+    [EditorSequence(1)]
+    public CustomerLink Customer { get; set; }
+
+    [EditorSequence(2)]
+    public DateTime ExpiryDate { get; set; }
+    
+    [EditorSequence(3)]
+    public bool IsDynamic { get; set; }
+}
 /// <summary>
 /// Interaction logic for MainWindow.xaml
 /// </summary>
@@ -39,11 +53,9 @@ public partial class Console : Window, INotifyPropertyChanged
         {
             _isRunning = value;
             OnPropertyChanged();
-            OnPropertyChanged(nameof(IsNotRunning));
         }
     }
-    public bool IsNotRunning => !_isRunning;
-
+    
     private bool _isInstalled = false;
     public bool IsInstalled
     {
@@ -55,6 +67,17 @@ public partial class Console : Window, INotifyPropertyChanged
         }
     }
 
+    private bool _hasDbServer = false;
+    public bool HasDbServer
+    {
+        get => _hasDbServer;
+        set
+        {
+            _hasDbServer = value;
+            OnPropertyChanged();
+        }
+    }
+
     private PipeClient<string>? _client;
     private Timer? RefreshTimer;
 
@@ -115,6 +138,7 @@ public partial class Console : Window, INotifyPropertyChanged
     {
         IsRunning = PRSServiceInstaller.IsRunning(Settings.GetServiceName());
         IsInstalled = PRSServiceInstaller.IsInstalled(Settings.GetServiceName());
+        HasDbServer = !String.IsNullOrWhiteSpace(GetProperties().Server);
 
         if(_client is null)
         {
@@ -297,10 +321,26 @@ public partial class Console : Window, INotifyPropertyChanged
         return properties;
     }
     
-    private void CheckConnection()
+    private bool CheckConnection()
     {
-        Client.Ping();
-        ClientFactory.SetBypass();
+        var properties = GetProperties();
+        if (!String.IsNullOrWhiteSpace(properties.Server))
+        {
+            ClientFactory.SetClientType(
+                typeof(RpcClient<>),
+                Platform.LicensingEngine,
+                CoreUtils.GetVersion(),
+                new RpcClientPipeTransport(DatabaseServerProperties.GetPipeName(properties.Server, true))
+            );
+
+            if (Client.Ping())
+            {
+                ClientFactory.SetBypass();
+                return true;
+            }
+        }
+        return false;
+        
     }
 
     private void EditButton_Click(object sender, RoutedEventArgs e)
@@ -323,17 +363,7 @@ public partial class Console : Window, INotifyPropertyChanged
         Settings.CommitChanges();
         var properties = GetProperties();
         
-        
-        if (!String.IsNullOrWhiteSpace(properties.Server))
-        {
-            ClientFactory.SetClientType(
-                typeof(RpcClient<>),
-                Platform.LicensingEngine,
-                CoreUtils.GetVersion(),
-                new RpcClientPipeTransport(DatabaseServerProperties.GetPipeName(properties.Server, true))
-            );
-            CheckConnection();
-        }
+        CheckConnection();
 
         if(grid.EditItems(new LicensingEngineProperties[] { properties }))
         {
@@ -365,5 +395,60 @@ public partial class Console : Window, INotifyPropertyChanged
     {
         PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
     }
-    
+
+    private void GenerateButton_Click(object sender, RoutedEventArgs e)
+    {
+
+        if (!CheckConnection())
+        {
+            MessageWindow.ShowMessage("Unable to connect to Server", "Error");
+            return;
+        }
+
+        var ofd = new OpenFileDialog()
+        {
+            FileName = "license.request",
+            Filter = "Request Files (*.request)|*.request"
+        };
+        if (ofd.ShowDialog() == true && System.IO.File.Exists(ofd.FileName))
+        {
+            var text = System.IO.File.ReadAllText(ofd.FileName);
+            if (LicenseUtils.TryDecryptLicenseRequest(text, out var request, out var _))
+            {
+                var data = new LicenseEditData();
+                data.Customer.ID = request.CustomerID;
+                data.IsDynamic = request.IsDynamic;
+                
+                var grid = new DynamicItemsListGrid<LicenseEditData>();
+                grid.OnValidate += (o, items, errors) =>
+                {
+                    if (items.Any(x => x.Customer.ID == Guid.Empty))
+                        errors.Add("Customer may not be blank!");
+                    if (items.Any(x => x.ExpiryDate <= DateTime.Today))
+                        errors.Add("Expiry must be in the future!");
+                };
+                if (grid.EditItems(new LicenseEditData[] { data }))
+                {
+                    var license = new LicenseData()
+                    {
+                        CustomerID = data.Customer.ID,
+                        Expiry = data.ExpiryDate,
+                        RenewalAvailable = data.ExpiryDate.AddMonths(-1),
+                        LastRenewal = DateTime.Today,
+                        Addresses = request.Addresses,
+                        IsDynamic = data.IsDynamic
+                    };
+
+                    SaveFileDialog sfd = new SaveFileDialog()
+                    {
+                        FileName = "license.key"
+                    };
+                    if (sfd.ShowDialog() == true)
+                        File.WriteAllText(sfd.FileName, LicenseUtils.EncryptLicense(license));
+                }
+            }
+            else
+                MessageWindow.ShowMessage("Invalid Request File","Error");
+        }
+    }
 }

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

@@ -10,6 +10,8 @@ using System.Timers;
 using System.Windows;
 using System.Windows.Media;
 using System.Windows.Threading;
+using Comal.Classes;
+using InABox.Core;
 
 namespace PRSLicensing;
 

+ 20 - 4
prs.licensing/PRSLicensing.csproj

@@ -5,6 +5,7 @@
     <TargetFramework>net6.0-windows</TargetFramework>
     <Nullable>enable</Nullable>
     <UseWPF>true</UseWPF>
+    <ApplicationIcon>reversed.ico</ApplicationIcon>
   </PropertyGroup>
 
   <ItemGroup>
@@ -26,18 +27,33 @@
 
   <ItemGroup>
     <Resource Include="Resources\splash-small.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </Resource>
     <None Remove="Resources\costsheettype.png" />
     <Resource Include="Resources\settings.png">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </Resource>
     <None Remove="Resources\service.png" />
-    <Resource Include="Resources\service.png">
+    <None Remove="Resources\tick.png" />
+    <None Remove="Resources\add.png" />
+    <None Remove="Resources\generate.png" />
+    <Resource Include="Resources\generate.png">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </Resource>
-    <None Remove="Resources\tick.png" />
-    <Resource Include="Resources\tick.png">
+    <None Remove="Resources\install.png" />
+    <Resource Include="Resources\install.png">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Resource>
+    <None Remove="Resources\start.png" />
+    <Resource Include="Resources\start.png">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Resource>
+    <None Remove="Resources\stop.png" />
+    <Resource Include="Resources\stop.png">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Resource>
+    <None Remove="Resources\uninstall.png" />
+    <Resource Include="Resources\uninstall.png">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </Resource>
   </ItemGroup>

+ 1 - 1
prs.licensing/PRSLicensing.iss

@@ -5,7 +5,7 @@
 #pragma verboselevel 9
 
 #define MyAppName "PRS Licensing"
-#define MyAppVersion "7.56a"
+#define MyAppVersion "7.57b"
 #define MyAppPublisher "PRS Digital"
 #define MyAppURL "https://www.prs-software.com.au"
 #define MyAppExeName "PRSLicensing.exe"

BIN
prs.licensing/Resources/generate.png


BIN
prs.licensing/Resources/install.png


BIN
prs.licensing/Resources/service.png


BIN
prs.licensing/Resources/settings.png


BIN
prs.licensing/Resources/start.png


BIN
prs.licensing/Resources/stop.png


BIN
prs.licensing/Resources/tick.png


BIN
prs.licensing/Resources/uninstall.png


BIN
prs.licensing/reversed.ico


+ 8 - 4
prs.server/Forms/DatabaseLicense/LicenseRenewalForm.xaml

@@ -7,7 +7,7 @@
         xmlns:dynamicgrid="clr-namespace:InABox.DynamicGrid;assembly=InABox.Wpf"
 		xmlns:wpf="clr-namespace:InABox.Wpf;assembly=InABox.Wpf"
         mc:Ignorable="d"
-        Title="License Renewal Form" Height="900" Width="500" WindowStartupLocation="CenterScreen"
+        Title="License Renewal Form" Height="800" Width="600" WindowStartupLocation="CenterScreen"
                     Loaded="Window_Loaded" x:Name="Window">
     <Grid Margin="5">
         <Grid.ColumnDefinitions>
@@ -69,10 +69,14 @@
         
         <DockPanel Grid.Row="8" Grid.Column="0">
             <Button x:Name="Help" Margin="5"  Height="30"  Width="30"/>
-            <Button x:Name="EnterKey" DockPanel.Dock="Left" Padding="10,0" Height="30" Margin="5" Content="Enter License Key"
-                    IsEnabled="False"
+            <Button x:Name="EnterKey" DockPanel.Dock="Left" Padding="10,0" Height="30" Margin="5" Content="Manual License"
                     Click="EnterKey_Click" ToolTipService.ShowOnDisabled="True">
-
+                <Button.ContextMenu>
+                    <ContextMenu>
+                        <MenuItem Header="Create Request File" Click="CreateManualRequest"/>
+                        <MenuItem Header="Load License File" Click="LoadManualResponse"/>
+                    </ContextMenu>
+                </Button.ContextMenu>
             </Button>
             
             <Label x:Name="RenewalAvailableLabel" Content="" DockPanel.Dock="Left" 

+ 188 - 87
prs.server/Forms/DatabaseLicense/LicenseRenewalForm.xaml.cs

@@ -11,11 +11,10 @@ using System.Windows.Controls;
 using System.Drawing;
 using Image = System.Windows.Controls.Image;
 using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Threading.Tasks;
 using FluentResults;
 using InABox.Client.Remote.Json;
 using InABox.Configuration;
+using Microsoft.Win32;
 using PRS.Shared;
 using Stripe;
 
@@ -37,25 +36,7 @@ namespace PRSServer.Forms.DatabaseLicense
         public LicenseData? CurrentLicense
         {
             get => _currentLicense;
-            set
-            {
-                if(value == null)
-                {
-                    return;
-                }
-
-                PayWithStripe.IsEnabled = false;
-                PayTooltip.Visibility = Visibility.Visible;
-                PayTooltip.Content = new Label { Content = "Loading..." };
-
-                _currentLicense = value;
-                LastRenewal.Value = value.LastRenewal;
-                CurrentExpiry.Value = value.Expiry;
-                //Modules.Renewed = value.LastRenewal;
-                RenewalAvailableFrom = value.RenewalAvailable;
-
-                Modules.Refresh(false, true);
-            }
+            set => _currentLicense = value;
         }
         
         public int RenewalPeriod
@@ -126,6 +107,8 @@ namespace PRSServer.Forms.DatabaseLicense
         {
             InitializeComponent();
 
+            Modules.OnCustomiseColumns += Modules_OnOnCustomiseColumns;
+
             // This should get us the RegistrationID, which we need in order
             // to get the License Pricing from the Server
             _licenseRegistrationDetails = _config.Load(false);
@@ -162,8 +145,6 @@ namespace PRSServer.Forms.DatabaseLicense
 
             Help.Content = new Image { Source = Properties.Resources.help.AsBitmapImage(Color.White) };
             Help.Click += Help_Click;
-
-            Modules.AfterRefresh += ModulesRefreshed;
             
         }
 
@@ -172,41 +153,93 @@ namespace PRSServer.Forms.DatabaseLicense
             Process.Start(new ProcessStartInfo("https://prsdigital.com.au/wiki/index.php/License_Renewal") { UseShellExecute = true });
         }
 
-        private void Window_Loaded(object sender, RoutedEventArgs e)
+        private void LoadData()
         {
-            if (!LicenseClient.Ping("ping"))
+
+            Result<bool> result = Progress.ShowModal<bool>("Getting License", progress =>
             {
-                MessageBox.Show("The PRS server is not available right now. Please try again later.");
-                Close();
-                return;
+                try
+                {
+                    progress.Report("Loading License");
+                    CurrentLicense = LoadCurrentLicense();
+                    
+                    progress.Report("Checking Server");
+                    if (!LicenseClient.Ping("ping"))
+                        return Result.Fail<bool>("Server Unavailable");
+                    
+                    progress.Report("Retrieving Data");
+                    RetrieveFees();
+                    
+                    progress.Report("Scanning Data");
+                    LoadUserTracking();
+                    
+                    foreach (var item in LicenseItems)
+                        item.Rate = LicenseUtils.GetLicenseFee(item.Type);
+                    
+                    return Result.Ok<bool>(true);
+                }
+                catch (Exception e)
+                {
+                    return Result.Fail<bool>(e.Message);
+                }
+            });
+
+            if (result.IsSuccess && result.Value)
+                UpdateWindow();
+            else
+            {
+                MessageWindow.ShowMessage(String.Join("\n",result.Errors.Select(x=>x.Message)), "Error");
+                UpdateWindow();
             }
+        }
+        
+        private void UpdateWindow()
+        {
 
-            Task feetask = Task.Run(RetrieveFees);
-            Task lictask = Task.Run(LoadUserTracking);
-            Task.WaitAll(feetask, lictask);
-            
-            foreach (var item in LicenseItems)
-                item.Rate = LicenseUtils.GetLicenseFee(item.Type);
-            Licenses = LicenseItems.OrderByDescending(x => x.Users).FirstOrDefault()?.Users ?? 0;
+            // Always a minimum of one license required!
+            Licenses = Math.Max(1, LicenseItems.OrderByDescending(x => x.Users).FirstOrDefault()?.Users ?? 0);
             
             Modules.Items = LicenseItems;
-            Modules.Refresh(true, false);
-
-            var license = LoadCurrentLicense();
-            if(license != null)
-                CurrentLicense = license;
-
+            Modules.Refresh(true, true);
+            
+            var lookups = new RenewalPeriodLookups(null).AsTable("RenewalPeriod");
+            RenewalPeriodEditor.LoadLookups(lookups);
+            RenewalPeriodEditor.Loaded = true;
+            
+            RenewalPeriod = LicenseUtils.TimeDiscountLevels().OrderBy(x => x).FirstOrDefault();
+            PayWithStripe.IsEnabled = CanRenew;
+            if (!PayWithStripe.IsEnabled)
+            {
+                PayTooltip.Visibility = Visibility.Visible;
+                PayTooltip.Content = new Label { Content = $"Renewal available from {RenewalAvailableFrom:dd MMM yyyy}" };
+            }
+            else
+                PayTooltip.Visibility = Visibility.Collapsed;
+            
             LastRenewal.Loaded = true;
             CurrentExpiry.Loaded = true;
             NewExpiry.Loaded = true;
-            RenewalPeriodEditor.Loaded = true;
+            
+            //PayWithStripe.IsEnabled = false;
+                
+            PayTooltip.Visibility = Visibility.Visible;
+            PayTooltip.Content = new Label { Content = "Loading..." };
+            
+            LastRenewal.Value = CurrentLicense?.LastRenewal ?? DateTime.MinValue;
+            CurrentExpiry.Value = CurrentLicense?.Expiry ?? DateTime.MinValue;
+            RenewalAvailableFrom = CurrentLicense?.RenewalAvailable ?? DateTime.MinValue;
+        }
+
+        private void Window_Loaded(object sender, RoutedEventArgs e)
+        {
+            LoadData();
         }
         
         private void RetrieveFees()
         {
-            var summary = LicenseClient.PostRequest<LicenseSummary>(
-                new LicenseSummaryRequest() { RegistrationID = CurrentLicense?.CustomerID ?? Guid.Empty },
-                nameof(LicenseSummary)
+            var summary = LicenseClient.PostRequest<LicenseFeeResponse>(
+                new LicenseFeeRequest() { RegistrationID = CurrentLicense?.CustomerID ?? Guid.Empty },
+                nameof(LicenseFeeRequest)
             );
             LicenseUtils.LoadSummary(summary);
         }
@@ -251,30 +284,10 @@ namespace PRSServer.Forms.DatabaseLicense
                     item.UserIDs.AddRange(users);
             }
             LicenseItems = result
-                .Where(x=>x.Users > 0)
                 .OrderBy(x=>x.Caption)
                 .ToList();
         }
 
-        private void ModulesRefreshed(object sender, AfterRefreshEventArgs args)
-        {
-            if (CurrentLicense == null) return;
-
-            var lookups = new RenewalPeriodLookups(null).AsTable("RenewalPeriod");
-            RenewalPeriodEditor.LoadLookups(lookups);
-
-            RenewalPeriod = LicenseUtils.TimeDiscountLevels().OrderBy(x => x).First();
-            PayWithStripe.IsEnabled = CanRenew;
-            if (!PayWithStripe.IsEnabled)
-            {
-                PayTooltip.Visibility = Visibility.Visible;
-                PayTooltip.Content = new Label { Content = $"Renewal available from {RenewalAvailableFrom:dd MMM yyyy}" };
-            }
-            else
-            {
-                PayTooltip.Visibility = Visibility.Collapsed;
-            }
-        }
 
         private static LicenseData? LoadCurrentLicense()
         {
@@ -301,16 +314,34 @@ namespace PRSServer.Forms.DatabaseLicense
         
         private void CalculateDiscounts()
         {
+            double CalcDiscount(double amount, int months)
+            {
+                return (amount * (LicenseUtils.GetUserDiscount(Licenses) / 100.0F)) +
+                       (amount * (LicenseUtils.GetTimeDiscount(months) / 100.0F));
+            }
+
             var periodInMonths = RenewalPeriod;
             
             NewExpiry.Value = NewExpiration;
             double total = 0.0F;
-            foreach (var row in Modules.Data.Rows)
-                total += row.Get<LicenseTrackingItem, double>(x => x.ExGST) * periodInMonths;
-            Gross = total;
-            Discount = total * (LicenseUtils.GetUserDiscount(Licenses) / 100.0F) +
-                        total * (LicenseUtils.GetTimeDiscount(periodInMonths) / 100.0F);
+            if (CurrentLicense?.IsDynamic == true)
+            {
+                foreach (var row in Modules.Data.Rows)
+                    total += row.Get<LicenseTrackingItem, double>(x => x.ExGST) * periodInMonths;
+                Gross = total;
+                Discount = CalcDiscount(total, periodInMonths);
+            }
+            else
+            {
+                foreach (var row in Modules.Data.Rows)
+                    total += Licenses * LicenseUtils.GetLicenseFee(row.Get<LicenseTrackingItem, String>(x => x.Type)) * periodInMonths;
+                Gross = Math.Round(total) - 0.05;
+                Gross = total;
+                Discount = Math.Round(CalcDiscount(total, periodInMonths) * 20F) / 20F;
+            }
 
+            
+            
             GrossLicenseFee.Value = Gross;
             DiscountEditor.Value = Discount;
             NettLicenseFee.Value = Net;
@@ -343,17 +374,22 @@ namespace PRSServer.Forms.DatabaseLicense
             {
                 _config.Save(_licenseRegistrationDetails);
                 Result<String> result = Result.Fail("Incomplete");
-                var renewal = CreateRenewal();
+                var renewalRequest = CreateRenewal();
                 Progress.ShowModal("Processing", progress =>
                 {
-                    // Process the Stripe Payment
-                    progress.Report("Processing Payment");
-                    result = ProcessStripePayment();
-                    if (result.IsFailed)
-                        return;
-                    
+                    if (renewalRequest.Net > 0.0F)
+                    {
+                        // Process the Stripe Payment
+                        progress.Report("Processing Payment");
+                        result = ProcessStripePayment();
+                        if (result.IsFailed)
+                            return;
+                    }
+                    else
+                        result = Result.Ok("no payment required");
+
                     progress.Report("Creating Renewal");
-                    result = ProcessRenewal(renewal, result.Value);
+                    result = RenewLicense(renewalRequest, result.Value);
 
                     if (result.IsFailed)
                         return;
@@ -420,9 +456,9 @@ namespace PRSServer.Forms.DatabaseLicense
 
         }
         
-        private LicenseRenewal CreateRenewal()
+        private LicenseRenewalRequest CreateRenewal()
         {
-            return new LicenseRenewal
+            return new LicenseRenewalRequest
             {
                 Company = _licenseRegistrationDetails.Company,
                 DateRenewed = RenewalDate,
@@ -431,16 +467,17 @@ namespace PRSServer.Forms.DatabaseLicense
                 LicenseTracking = LicenseItems.ToArray(),
                 Gross = Gross,
                 Discount = Discount,
-                Net = Net
+                Net = Net,
+                Addresses = LicenseUtils.GetMacAddresses(),
             };
         }
 
-        private Result<string> ProcessRenewal(LicenseRenewal renewal, String transactionID)
+        private Result<string> RenewLicense(LicenseRenewalRequest renewalRequest, String transactionID)
         {
-            renewal.TransactionID = transactionID;
+            renewalRequest.TransactionID = transactionID;
             try
             {
-                var result = LicenseClient.PostRequest<LicenseRenewalResult>(renewal, nameof(LicenseRenewal));
+                var result = LicenseClient.PostRequest<LicenseRenewalResult>(renewalRequest, nameof(LicenseRenewalRequest));
                 return Result.Ok(result.License);
             }
             catch (Exception e)
@@ -495,9 +532,73 @@ namespace PRSServer.Forms.DatabaseLicense
 
         private void EnterKey_Click(object sender, RoutedEventArgs e)
         {
-            // popup a text editor
-            // is it a valid licence key?
-                // Update the Database
+            if (EnterKey?.ContextMenu != null)
+                EnterKey.ContextMenu.IsOpen = true;
+        }
+
+        private void CreateManualRequest(object sender, RoutedEventArgs e)
+        {
+            var request = new LicenseRequest()
+            {
+                CustomerID = CurrentLicense?.CustomerID ?? Guid.Empty,
+                Addresses = LicenseUtils.GetMacAddresses(),
+                IsDynamic = CurrentLicense?.IsDynamic ?? false,
+            };
+            SaveFileDialog sfd = new SaveFileDialog()
+            {
+                FileName = "license.request"
+            };
+            if (sfd.ShowDialog() == true)
+            {
+                System.IO.File.WriteAllText(sfd.FileName, LicenseUtils.EncryptLicenseRequest(request));
+                MessageWindow.ShowMessage("Please email this file to support@prsdigital.com.au!","Request Created");
+            }
+
+        }
+
+        private void LoadManualResponse(object sender, RoutedEventArgs e)
+        {
+            var ofd = new OpenFileDialog()
+            {
+                FileName = "license.key",
+                Filter = "License Files (*.key)|*.key"
+            };
+            if (ofd.ShowDialog() == true && System.IO.File.Exists(ofd.FileName))
+            {
+                var text = System.IO.File.ReadAllText(ofd.FileName);
+                if (LicenseUtils.TryDecryptLicense(text, out var data, out var _))
+                {
+                    if (LicenseUtils.ValidateMacAddresses(data.Addresses))
+                    {
+                        SaveLicense(text);
+                        MessageWindow.ShowMessage("License Updated", "Success");
+                        Close();
+                    }
+                    else
+                        MessageWindow.ShowMessage("License Key is not valid!","Invalid Key");
+                }
+                else
+                    MessageWindow.ShowMessage("License Key is not valid!","Bad Key");
+            }
+        }
+
+        private void Modules_OnOnCustomiseColumns(object sender, DynamicGridColumns columns)
+        {
+            var userscol = columns.FirstOrDefault(x => String.Equals(x.ColumnName, nameof(LicenseTrackingItem.Users)));
+            if (userscol != null)
+                userscol.Alignment = Alignment.MiddleCenter;
+            
+            if (_currentLicense?.IsDynamic != true)
+            {
+                var ratecol = columns.FirstOrDefault(x => String.Equals(x.ColumnName, nameof(LicenseTrackingItem.Rate)));
+                if (ratecol != null)
+                    columns.Remove(ratecol);
+                
+                var exgstcol = columns.FirstOrDefault(x => String.Equals(x.ColumnName, nameof(LicenseTrackingItem.ExGST)));
+                if (exgstcol != null)
+                    columns.Remove(exgstcol);
+
+            }
         }
     }
 }

+ 1 - 1
prs.server/PRSServer.iss

@@ -5,7 +5,7 @@
 #pragma verboselevel 9
 
 #define MyAppName "PRS Server"
-#define MyAppVersion "7.56a"
+#define MyAppVersion "7.57b"
 #define MyAppPublisher "PRS Digital"
 #define MyAppURL "https://www.prs-software.com.au"
 #define MyAppExeName "PRSServer.exe"

+ 3 - 1
prs.shared/Objects/LicenseRenewal.cs → prs.shared/Objects/LicenseRenewalRequest.cs

@@ -85,7 +85,7 @@ namespace PRS.Shared
         public string Cvv { get; set; }
     }
     
-    public class LicenseRenewal : BaseObject
+    public class LicenseRenewalRequest : BaseObject
     {
         public LicenseRegistrationCompanyDetails Company { get; set; } = new ();
 
@@ -97,6 +97,8 @@ namespace PRS.Shared
 
         public LicenseTrackingItem[]? LicenseTracking { get; set; }
 
+        public String[] Addresses { get; set; } = Array.Empty<String>();
+
         public double Gross{ get; set; }
         public double Discount { get; set; }
         public double Net { get; set; }