From ff992ed5be61f60ac9a1b90c945a955cb518ad73 Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Fri, 14 Nov 2025 11:10:51 -0800 Subject: [PATCH 01/13] Adds recharge auto-approval functionality Introduces a new feature that allows for automatic approval of recharge requests after a specified number of days. The number of days before auto-approval is configurable via settings, defaulting to 7 days. This is displayed in the financial approval email, informing approvers of the pending auto-approval. --- src/Payments.Core/Models/Configuration/FinanceSettings.cs | 4 +++- src/Payments.Emails/EmailService.cs | 6 +++++- src/Payments.Emails/Views/FinancialApprove.cshtml | 3 ++- src/Payments.Mvc/appsettings.json | 3 ++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Payments.Core/Models/Configuration/FinanceSettings.cs b/src/Payments.Core/Models/Configuration/FinanceSettings.cs index 183c3819..58f7ea8c 100644 --- a/src/Payments.Core/Models/Configuration/FinanceSettings.cs +++ b/src/Payments.Core/Models/Configuration/FinanceSettings.cs @@ -28,6 +28,8 @@ public class FinanceSettings public string RechargeSlothSourceName { get; set; } = "PaymentsRecharge"; - public bool ValidateRechargeFinancialSegmentString { get; set; } = false; //Maybe we want this to default to true? + public bool ValidateRechargeFinancialSegmentString { get; set; } = false; //Maybe we want this to default to true? + + public int RechargeAutoApproveDays { get; set; } = 7; } } diff --git a/src/Payments.Emails/EmailService.cs b/src/Payments.Emails/EmailService.cs index 2ee92bfd..2186544b 100644 --- a/src/Payments.Emails/EmailService.cs +++ b/src/Payments.Emails/EmailService.cs @@ -41,9 +41,12 @@ public class SparkpostEmailService : IEmailService private readonly MailAddress _refundAddress; - public SparkpostEmailService(IOptions sparkpostSettings, IMjmlServices mjmlServices) + private readonly FinanceSettings _financeSettings; + + public SparkpostEmailService(IOptions sparkpostSettings, IMjmlServices mjmlServices, IOptions financeSettings) { _mjmlServices = mjmlServices; + _financeSettings = financeSettings.Value; _sparkpostSettings = sparkpostSettings.Value; @@ -205,6 +208,7 @@ public async Task SendFinancialApprove(Invoice invoice, SendApprovalModel approv { var viewbag = GetViewData(); viewbag["Team"] = invoice.Team; + viewbag["DaysToAutoApprove"] = _financeSettings.RechargeAutoApproveDays; var model = new InvoiceViewModel { diff --git a/src/Payments.Emails/Views/FinancialApprove.cshtml b/src/Payments.Emails/Views/FinancialApprove.cshtml index bd1b0a05..efaed102 100644 --- a/src/Payments.Emails/Views/FinancialApprove.cshtml +++ b/src/Payments.Emails/Views/FinancialApprove.cshtml @@ -5,6 +5,7 @@ @{ Layout = "_EmailLayout"; var team = (Team) ViewData["Team"]; + var daysToAutoApprove = ViewData["DaysToAutoApprove"] ?? 7; var invoice = Model.Invoice; var extraText = "Recharge Invoice # " + invoice.GetFormattedId(); @@ -17,7 +18,7 @@

Invoice from @team.Name

@extraText

You are one or more Financial Approvers needed to Approve or Deny the recharge for this invoice.
-
This will be auto approved in 7 days if no action is taken.
+
This will be auto approved in @daysToAutoApprove days if no action is taken.
@if(!string.IsNullOrWhiteSpace( invoice.Memo)) diff --git a/src/Payments.Mvc/appsettings.json b/src/Payments.Mvc/appsettings.json index e68ff920..4f722e32 100644 --- a/src/Payments.Mvc/appsettings.json +++ b/src/Payments.Mvc/appsettings.json @@ -45,7 +45,8 @@ "AutoApprove": true, "RechargeAutoApprove": false, "RechargeDisableJob": false, - "RechargeSlothSourceName": "PaymentsRecharge" + "RechargeSlothSourceName": "PaymentsRecharge", + "RechargeAutoApproveDays": 7 }, "Stackify": { "AppName": "payments.mvc", From 9710a991057e9be0de11ec084b287b1e00fb4cb5 Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Fri, 14 Nov 2025 14:23:55 -0800 Subject: [PATCH 02/13] Adds auto-approve job for recharge invoices Implements a new job that automatically approves recharge invoices meeting certain criteria (type, status, paid date). The job checks for invoices of type "Recharge" with "PendingApproval" status that have been paid for a specified number of days. If these conditions are met, the job automatically approves the invoice and the associated debit recharge accounts. Adds necessary configuration settings and database context setup. --- .../Payments.Jobs.AutoApprove.csproj | 28 +++++ Payments.Jobs.AutoApprove/Program.cs | 115 ++++++++++++++++++ .../Properties/launchSettings.json | 10 ++ Payments.Jobs.AutoApprove/appsettings.json | 27 ++++ Payments.Jobs.AutoApprove/run.cmd | 3 + Payments.Jobs.AutoApprove/settings.job | 3 + Payments.sln | 19 ++- .../Models/History/HistoryActionTypeCodes.cs | 1 + 8 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 Payments.Jobs.AutoApprove/Payments.Jobs.AutoApprove.csproj create mode 100644 Payments.Jobs.AutoApprove/Program.cs create mode 100644 Payments.Jobs.AutoApprove/Properties/launchSettings.json create mode 100644 Payments.Jobs.AutoApprove/appsettings.json create mode 100644 Payments.Jobs.AutoApprove/run.cmd create mode 100644 Payments.Jobs.AutoApprove/settings.job diff --git a/Payments.Jobs.AutoApprove/Payments.Jobs.AutoApprove.csproj b/Payments.Jobs.AutoApprove/Payments.Jobs.AutoApprove.csproj new file mode 100644 index 00000000..601496f2 --- /dev/null +++ b/Payments.Jobs.AutoApprove/Payments.Jobs.AutoApprove.csproj @@ -0,0 +1,28 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + PreserveNewest + PreserveNewest + + + Always + + + Always + + + + diff --git a/Payments.Jobs.AutoApprove/Program.cs b/Payments.Jobs.AutoApprove/Program.cs new file mode 100644 index 00000000..4453fe64 --- /dev/null +++ b/Payments.Jobs.AutoApprove/Program.cs @@ -0,0 +1,115 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Payments.Core.Data; +using Payments.Core.Domain; +using Payments.Core.Jobs; +using Payments.Core.Models.Configuration; +using Payments.Core.Models.History; +using Payments.Core.Services; +using Payments.Jobs.Core; +using Serilog; +using System; + +namespace Payments.Jobs.MoneyMovement +{ + public class Program : JobBase + { + private static ILogger _log; + + public static async Task Main(string[] args) + { + // setup env + Configure(); + + + _log = Log.Logger + .ForContext("jobname", "AutoApprove"); + + var assembyName = typeof(Program).Assembly.GetName(); + _log.Information("Running {job} build {build}", assembyName.Name, assembyName.Version); + _log.Information("AutoApprove Version 1"); + + // setup di + var provider = ConfigureServices(); + var dbContext = provider.GetService(); + + + try + { + if(dbContext == null) + { + throw new InvalidOperationException("Failed to obtain ApplicationDbContext from service provider."); + } + var financeSettings = provider.GetService>()?.Value; + if (financeSettings == null) + { + throw new InvalidOperationException("FinanceSettings configuration is missing or invalid."); + } + var daysToAutoApprove = financeSettings.RechargeAutoApproveDays; + var dateThreshold = DateTime.UtcNow.AddDays(-daysToAutoApprove); + + var invoices = await dbContext.Invoices + .Include(i => i.RechargeAccounts) + .Where(a => a.Type == Invoice.InvoiceTypes.Recharge && a.Status == Invoice.StatusCodes.PendingApproval && + a.PaidAt != null && a.PaidAt <= dateThreshold).ToListAsync(); + _log.Information("Found {count} invoices to auto-approve", invoices.Count); + foreach (var invoice in invoices) + { + invoice.Status = Invoice.StatusCodes.Approved; + foreach (var ra in invoice.RechargeAccounts.Where(a => a.Direction == RechargeAccount.CreditDebit.Debit && string.IsNullOrWhiteSpace( a.ApprovedByKerb))) + { + ra.ApprovedByKerb = "Automated"; + ra.ApprovedByName = "System"; + } + + invoice.Paid = true; + + var approvalAction = new History() + { + Type = HistoryActionTypes.RechargeApprovedByFinancialApprover.TypeCode, + ActionDateTime = DateTime.UtcNow, + Actor = "System", + Data = "All debit recharge accounts have been approved." + }; + + invoice.History.Add(approvalAction); + dbContext.Invoices.Update(invoice); + _log.Information("Auto-approved invoice {invoiceId}", invoice.Id); + + await dbContext.SaveChangesAsync(); + } + } + catch (Exception ex) + { + + _log.Error("Error running AutoApprove job", ex); + throw; + } + finally + { + await dbContext.SaveChangesAsync(); + } + + } + + private static ServiceProvider ConfigureServices() + { + IServiceCollection services = new ServiceCollection(); + + // options files + services.Configure(Configuration.GetSection("Finance")); + + + // db service + services.AddDbContext(options => + options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")) + ); + + + + return services.BuildServiceProvider(); + } + } +} diff --git a/Payments.Jobs.AutoApprove/Properties/launchSettings.json b/Payments.Jobs.AutoApprove/Properties/launchSettings.json new file mode 100644 index 00000000..75212399 --- /dev/null +++ b/Payments.Jobs.AutoApprove/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Payments.Jobs.AutoApprove": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Payments.Jobs.AutoApprove/appsettings.json b/Payments.Jobs.AutoApprove/appsettings.json new file mode 100644 index 00000000..41374c0e --- /dev/null +++ b/Payments.Jobs.AutoApprove/appsettings.json @@ -0,0 +1,27 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=.\\SQLExpress;Initial Catalog=Payments;Integrated Security=True;MultipleActiveResultSets=True" + }, + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "Finance": { + "RechargeAutoApproveDays": 7 + }, + "Stackify": { + "AppName": "payments.jobs.moneymovement", + "ApiKey": "[External]", + "Environment": "[External]" + }, + "AggieEnterprise": { + "GraphQlUrl": "[External]", + "Token": "[External]", + "ConsumerKey": "[External]", + "ConsumerSecret": "[External]", + "TokenEndpoint": "[External]", + "ScopeApp": "Payments-Job", + "ScopeEnv": "Test" + } +} diff --git a/Payments.Jobs.AutoApprove/run.cmd b/Payments.Jobs.AutoApprove/run.cmd new file mode 100644 index 00000000..da533950 --- /dev/null +++ b/Payments.Jobs.AutoApprove/run.cmd @@ -0,0 +1,3 @@ +@echo off + +dotnet Payments.Jobs.AutoApprove.dll diff --git a/Payments.Jobs.AutoApprove/settings.job b/Payments.Jobs.AutoApprove/settings.job new file mode 100644 index 00000000..15c998e8 --- /dev/null +++ b/Payments.Jobs.AutoApprove/settings.job @@ -0,0 +1,3 @@ +{ + "schedule": "0 0 6 * * *" +} diff --git a/Payments.sln b/Payments.sln index 38db1d1e..5df3c64b 100644 --- a/Payments.sln +++ b/Payments.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29326.143 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36705.20 d17.14 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D1C93383-2244-4990-B2E9-5EF966F0E5A5}" EndProject @@ -25,6 +25,8 @@ Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "Payments.Sql", "src\Payment EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Payments.Emails", "src\Payments.Emails\Payments.Emails.csproj", "{7A4BE093-ED90-4464-BF9C-9B5F748D5F10}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Payments.Jobs.AutoApprove", "Payments.Jobs.AutoApprove\Payments.Jobs.AutoApprove.csproj", "{67C14AEE-809F-4F73-8425-FFB06A35D678}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -121,6 +123,18 @@ Global {7A4BE093-ED90-4464-BF9C-9B5F748D5F10}.Release|x64.Build.0 = Release|Any CPU {7A4BE093-ED90-4464-BF9C-9B5F748D5F10}.Release|x86.ActiveCfg = Release|Any CPU {7A4BE093-ED90-4464-BF9C-9B5F748D5F10}.Release|x86.Build.0 = Release|Any CPU + {67C14AEE-809F-4F73-8425-FFB06A35D678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67C14AEE-809F-4F73-8425-FFB06A35D678}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67C14AEE-809F-4F73-8425-FFB06A35D678}.Debug|x64.ActiveCfg = Debug|Any CPU + {67C14AEE-809F-4F73-8425-FFB06A35D678}.Debug|x64.Build.0 = Debug|Any CPU + {67C14AEE-809F-4F73-8425-FFB06A35D678}.Debug|x86.ActiveCfg = Debug|Any CPU + {67C14AEE-809F-4F73-8425-FFB06A35D678}.Debug|x86.Build.0 = Debug|Any CPU + {67C14AEE-809F-4F73-8425-FFB06A35D678}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67C14AEE-809F-4F73-8425-FFB06A35D678}.Release|Any CPU.Build.0 = Release|Any CPU + {67C14AEE-809F-4F73-8425-FFB06A35D678}.Release|x64.ActiveCfg = Release|Any CPU + {67C14AEE-809F-4F73-8425-FFB06A35D678}.Release|x64.Build.0 = Release|Any CPU + {67C14AEE-809F-4F73-8425-FFB06A35D678}.Release|x86.ActiveCfg = Release|Any CPU + {67C14AEE-809F-4F73-8425-FFB06A35D678}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -133,6 +147,7 @@ Global {02AA0336-4F06-40C8-98A9-E29F510E3E57} = {515AA768-8E83-4026-AF1B-CAF89624E938} {1C67A279-CA98-494D-B9A6-DD5D99FD1A8F} = {D1C93383-2244-4990-B2E9-5EF966F0E5A5} {7A4BE093-ED90-4464-BF9C-9B5F748D5F10} = {D1C93383-2244-4990-B2E9-5EF966F0E5A5} + {67C14AEE-809F-4F73-8425-FFB06A35D678} = {D1C93383-2244-4990-B2E9-5EF966F0E5A5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {34045125-52CB-46C0-BEBC-371EB2F0DFEB} diff --git a/src/Payments.Core/Models/History/HistoryActionTypeCodes.cs b/src/Payments.Core/Models/History/HistoryActionTypeCodes.cs index 275b23ab..8e233c13 100644 --- a/src/Payments.Core/Models/History/HistoryActionTypeCodes.cs +++ b/src/Payments.Core/Models/History/HistoryActionTypeCodes.cs @@ -68,6 +68,7 @@ public static class HistoryActionTypes RechargePaidByCustomer, RechargeSentToFinancialApprovers, RechargeRejectedByFinancialApprover, + RechargeApprovedByFinancialApprover, RechargeRejected, }; From 2db486a047d127c70b8d77ad7f832dcc3445e5d8 Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Fri, 14 Nov 2025 14:41:32 -0800 Subject: [PATCH 03/13] Adds auto-approve job to the pipeline Adds tasks to the pipeline to build and publish the auto-approve job. Also adds .NET 8 SDK usage to the pipeline build steps. Relates to JCS/RechargeAutoApproveJob --- azure-pipelines.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 516eaf40..dba4ddd6 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,6 +25,12 @@ stages: packageType: "sdk" version: 6.x + - task: UseDotNet@2 + displayName: "Use .NET 8 sdk" + inputs: + packageType: "sdk" + version: 8.x + - task: NodeTool@0 displayName: "Use Node 18 (LTS)" inputs: @@ -45,6 +51,12 @@ stages: packageType: "sdk" version: 6.x + - task: UseDotNet@2 + displayName: "Use .NET 8 sdk" + inputs: + packageType: "sdk" + version: 8.x + - task: NodeTool@0 displayName: "Use Node 18 (LTS)" inputs: @@ -69,6 +81,15 @@ stages: projects: "./src/Payments.Jobs.MoneyMovement/Payments.Jobs.MoneyMovement.csproj" arguments: "--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/app_data/jobs/triggered" + - task: DotNetCoreCLI@2 + displayName: "Publish Payments.Jobs.AutoApprove" + inputs: + command: "publish" + publishWebProjects: false + zipAfterPublish: false + projects: "./Payments.Jobs.AutoApprove/Payments.Jobs.AutoApprove.csproj" + arguments: "--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/app_data/jobs/triggered" + - task: PublishBuildArtifacts@1 condition: eq(variables['Build.SourceBranch'], 'refs/heads/master') displayName: "Publish Build Artifacts for master branch builds" From 98daf29a67ca09888a06446de3b1817c52a6bb27 Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Fri, 14 Nov 2025 14:42:41 -0800 Subject: [PATCH 04/13] Removes AggieEnterprise settings Removes the AggieEnterprise configuration settings from the appsettings.json file. These settings are no longer needed for the RechargeAutoApproveJob. --- Payments.Jobs.AutoApprove/appsettings.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Payments.Jobs.AutoApprove/appsettings.json b/Payments.Jobs.AutoApprove/appsettings.json index 41374c0e..6756334c 100644 --- a/Payments.Jobs.AutoApprove/appsettings.json +++ b/Payments.Jobs.AutoApprove/appsettings.json @@ -14,14 +14,5 @@ "AppName": "payments.jobs.moneymovement", "ApiKey": "[External]", "Environment": "[External]" - }, - "AggieEnterprise": { - "GraphQlUrl": "[External]", - "Token": "[External]", - "ConsumerKey": "[External]", - "ConsumerSecret": "[External]", - "TokenEndpoint": "[External]", - "ScopeApp": "Payments-Job", - "ScopeEnv": "Test" } } From e06471fbce74bd0dccaa8dfd1a594c0b822165b7 Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Fri, 14 Nov 2025 14:58:09 -0800 Subject: [PATCH 05/13] Downgrades AutoApprove job to net6.0 Downgrades the AutoApprove job from .NET 8 to .NET 6 due to build agent compatibility issues. Removes .NET 8 SDK usage from the pipeline and updates the project target framework. Changes the Main method to synchronous and updates async calls to synchronous calls. --- .../Payments.Jobs.AutoApprove.csproj | 2 +- Payments.Jobs.AutoApprove/Program.cs | 10 +++++----- azure-pipelines.yml | 12 ------------ 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/Payments.Jobs.AutoApprove/Payments.Jobs.AutoApprove.csproj b/Payments.Jobs.AutoApprove/Payments.Jobs.AutoApprove.csproj index 601496f2..57247ed2 100644 --- a/Payments.Jobs.AutoApprove/Payments.Jobs.AutoApprove.csproj +++ b/Payments.Jobs.AutoApprove/Payments.Jobs.AutoApprove.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net6.0 enable enable diff --git a/Payments.Jobs.AutoApprove/Program.cs b/Payments.Jobs.AutoApprove/Program.cs index 4453fe64..ad787f6f 100644 --- a/Payments.Jobs.AutoApprove/Program.cs +++ b/Payments.Jobs.AutoApprove/Program.cs @@ -18,7 +18,7 @@ public class Program : JobBase { private static ILogger _log; - public static async Task Main(string[] args) + public static void Main(string[] args) { // setup env Configure(); @@ -50,10 +50,10 @@ public static async Task Main(string[] args) var daysToAutoApprove = financeSettings.RechargeAutoApproveDays; var dateThreshold = DateTime.UtcNow.AddDays(-daysToAutoApprove); - var invoices = await dbContext.Invoices + var invoices = dbContext.Invoices .Include(i => i.RechargeAccounts) .Where(a => a.Type == Invoice.InvoiceTypes.Recharge && a.Status == Invoice.StatusCodes.PendingApproval && - a.PaidAt != null && a.PaidAt <= dateThreshold).ToListAsync(); + a.PaidAt != null && a.PaidAt <= dateThreshold).ToList(); _log.Information("Found {count} invoices to auto-approve", invoices.Count); foreach (var invoice in invoices) { @@ -78,7 +78,7 @@ public static async Task Main(string[] args) dbContext.Invoices.Update(invoice); _log.Information("Auto-approved invoice {invoiceId}", invoice.Id); - await dbContext.SaveChangesAsync(); + dbContext.SaveChangesAsync(); } } catch (Exception ex) @@ -89,7 +89,7 @@ public static async Task Main(string[] args) } finally { - await dbContext.SaveChangesAsync(); + dbContext.SaveChangesAsync(); } } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index dba4ddd6..9f00bee7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,12 +25,6 @@ stages: packageType: "sdk" version: 6.x - - task: UseDotNet@2 - displayName: "Use .NET 8 sdk" - inputs: - packageType: "sdk" - version: 8.x - - task: NodeTool@0 displayName: "Use Node 18 (LTS)" inputs: @@ -51,12 +45,6 @@ stages: packageType: "sdk" version: 6.x - - task: UseDotNet@2 - displayName: "Use .NET 8 sdk" - inputs: - packageType: "sdk" - version: 8.x - - task: NodeTool@0 displayName: "Use Node 18 (LTS)" inputs: From 623dde204747e189b5c75862320963f2943a2bc0 Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Fri, 14 Nov 2025 15:00:24 -0800 Subject: [PATCH 06/13] Updates app name for auto-approve job Updates the Stackify app name in the configuration file to reflect the correct job name. This ensures that logging and monitoring are correctly associated with the auto-approve job. --- Payments.Jobs.AutoApprove/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Payments.Jobs.AutoApprove/appsettings.json b/Payments.Jobs.AutoApprove/appsettings.json index 6756334c..8050ae01 100644 --- a/Payments.Jobs.AutoApprove/appsettings.json +++ b/Payments.Jobs.AutoApprove/appsettings.json @@ -11,7 +11,7 @@ "RechargeAutoApproveDays": 7 }, "Stackify": { - "AppName": "payments.jobs.moneymovement", + "AppName": "payments.jobs.autoapprove", "ApiKey": "[External]", "Environment": "[External]" } From ce128ffcbebdecda455cde7a6660d2950789c581 Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Fri, 14 Nov 2025 15:02:29 -0800 Subject: [PATCH 07/13] Updates AutoApprove job project path Updates the project path for the AutoApprove job in the Azure Pipelines configuration to ensure the job is correctly located and published. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9f00bee7..efa59049 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -75,7 +75,7 @@ stages: command: "publish" publishWebProjects: false zipAfterPublish: false - projects: "./Payments.Jobs.AutoApprove/Payments.Jobs.AutoApprove.csproj" + projects: "./src/Payments.Jobs.AutoApprove/Payments.Jobs.AutoApprove.csproj" arguments: "--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/app_data/jobs/triggered" - task: PublishBuildArtifacts@1 From d4bf6522775a00ba1160927f2435b2d2a3c7a241 Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Fri, 14 Nov 2025 15:36:21 -0800 Subject: [PATCH 08/13] Removes unnecessary database save Removes the database save operation from the finally block in the AutoApprove job. This change streamlines the job's execution flow by removing a redundant database save operation which improves performance. Relates to JCS/RechargeAutoApproveJob --- Payments.Jobs.AutoApprove/Program.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Payments.Jobs.AutoApprove/Program.cs b/Payments.Jobs.AutoApprove/Program.cs index ad787f6f..bf16c5b6 100644 --- a/Payments.Jobs.AutoApprove/Program.cs +++ b/Payments.Jobs.AutoApprove/Program.cs @@ -87,10 +87,8 @@ public static void Main(string[] args) _log.Error("Error running AutoApprove job", ex); throw; } - finally - { - dbContext.SaveChangesAsync(); - } + + _log.Information("AutoApprove job completed"); } From 85b993522b8279d8787feb6709ae97e56a14ac0e Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Mon, 17 Nov 2025 08:08:11 -0800 Subject: [PATCH 09/13] Improves recharge invoice approval flow Updates the recharge invoice approval process by fixing a typo in auto-approval job, preventing access to draft/cancelled/sent invoices during approval, and ensuring the correct status is checked on the details page. This change enhances the user experience and ensures data integrity during the invoice approval workflow. --- Payments.Jobs.AutoApprove/Program.cs | 2 +- .../Controllers/RechargeController.cs | 23 ++++--------------- .../Views/Invoices/Details.cshtml | 2 +- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/Payments.Jobs.AutoApprove/Program.cs b/Payments.Jobs.AutoApprove/Program.cs index bf16c5b6..25785d72 100644 --- a/Payments.Jobs.AutoApprove/Program.cs +++ b/Payments.Jobs.AutoApprove/Program.cs @@ -78,7 +78,7 @@ public static void Main(string[] args) dbContext.Invoices.Update(invoice); _log.Information("Auto-approved invoice {invoiceId}", invoice.Id); - dbContext.SaveChangesAsync(); + dbContext.SaveChanges(); } } catch (Exception ex) diff --git a/src/Payments.Mvc/Controllers/RechargeController.cs b/src/Payments.Mvc/Controllers/RechargeController.cs index 77617751..1bc7e8f7 100644 --- a/src/Payments.Mvc/Controllers/RechargeController.cs +++ b/src/Payments.Mvc/Controllers/RechargeController.cs @@ -80,13 +80,6 @@ public async Task Preview(int id) var model = CreateRechargeInvoiceViewModel(invoice); - //var model = CreateRechargePaymentViewModel(invoice); - - if (invoice.Status != Invoice.StatusCodes.Sent) - { - //This is valid status to pay, but I think we want to allow the other statuses through so it can be viewed, but not edited. - } - ViewBag.Id = id; return View(model); @@ -151,10 +144,6 @@ public async Task Pay(string id) var model = CreateRechargeInvoiceViewModel(invoice); - if (invoice.Status == Invoice.StatusCodes.Sent) - { - //This is valid status to pay, but I think we want to allow the other statuses through so it can be viewed, but not edited. - } ViewBag.Id = id; @@ -345,12 +334,10 @@ public async Task Pay(string id, [FromBody] RechargeAccount[] mod await _dbContext.SaveChangesAsync(); //Maybe wait for all changes? - - - //Need to notify the approvers. This will require a new email template. + //TODO: The logic below will have to be duplicated if we allow this step to be skipped from the invoice details page. invoice.PaidAt = DateTime.UtcNow; - //TODO: Also set the paid flag? Or.... probably better, wait for the financial approve step. Then I can use the PaidAt to determine auto pay, and the Paid flag for the PaymentController download (receipt) + //Answered: Also set the paid flag? - NO. Or.... probably better, wait for the financial approve step. Then I can use the PaidAt to determine auto pay, and the Paid flag for the PaymentController download (receipt) invoice.Status = Invoice.StatusCodes.PendingApproval; @@ -367,7 +354,7 @@ public async Task Pay(string id, [FromBody] RechargeAccount[] mod await _invoiceService.SendFinancialApproverEmail(invoice, new SendApprovalModel() { emails = emails.ToArray(), - bccEmails = "" //TODO: Add any BCC emails if needed. Note customer is CC'd by default in the service. + bccEmails = "" // Add any BCC emails if needed. Note customer is CC'd by default in the service. }); var notificationAction = new History() @@ -387,7 +374,6 @@ public async Task Pay(string id, [FromBody] RechargeAccount[] mod invoice.History.Add(notificationAction); - //_dbContext.Invoices.Update(invoice); await _dbContext.SaveChangesAsync(); @@ -405,7 +391,7 @@ public async Task FinancialApprove(string id) .Include(i => i.Attachments) .Include(i => i.RechargeAccounts.Where(ra => ra.Direction == RechargeAccount.CreditDebit.Debit)) .FirstOrDefaultAsync(i => i.LinkId == id); - //I think his is ok. + //I think this is ok. if (invoice == null) { return PublicNotFound(); @@ -413,6 +399,7 @@ public async Task FinancialApprove(string id) if (invoice.Status == Invoice.StatusCodes.Draft || invoice.Status == Invoice.StatusCodes.Cancelled || invoice.Status == Invoice.StatusCodes.Sent) { + //Includes sent, because they can't approve it yet. return PublicNotFound(); } diff --git a/src/Payments.Mvc/Views/Invoices/Details.cshtml b/src/Payments.Mvc/Views/Invoices/Details.cshtml index ffcec077..b3b2b4f3 100644 --- a/src/Payments.Mvc/Views/Invoices/Details.cshtml +++ b/src/Payments.Mvc/Views/Invoices/Details.cshtml @@ -495,7 +495,7 @@ @(paymentPageHref) - if(Model.Status == Invoice.StatusCodes.PendingApproval) + if(Model.Status == Invoice.StatusCodes.PendingApproval || Model.Status == Invoice.StatusCodes.Approved || Model.Status == Invoice.StatusCodes.Completed) {
Recharge Financial Approval page
From 44544889dd26ae77bac1b3602b085c3a3855fe9b Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Mon, 17 Nov 2025 08:48:45 -0800 Subject: [PATCH 10/13] Streamlines recharge invoice approval Automatically approves recharge invoices when the customer email matches the user email and the invoice has debit recharge accounts. This speeds up the approval process for self-recharge scenarios. --- .../Controllers/InvoicesController.cs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Payments.Mvc/Controllers/InvoicesController.cs b/src/Payments.Mvc/Controllers/InvoicesController.cs index 481cec8f..f3598795 100644 --- a/src/Payments.Mvc/Controllers/InvoicesController.cs +++ b/src/Payments.Mvc/Controllers/InvoicesController.cs @@ -452,14 +452,37 @@ public async Task Send(int id, [FromBody] SendInvoiceModel model) await _invoiceService.SendInvoice(invoice, model); - if(invoice.Type == Invoice.InvoiceTypes.Recharge && invoice.Status == Invoice.StatusCodes.PendingApproval) + var user = await _userManager.GetUserAsync(User); + + // check if the user email is the same as the customer email for recharges so it goes directly to the pending approval. + if (invoice.Type == Invoice.InvoiceTypes.Recharge && + string.Equals(user.Email, invoice.CustomerEmail, StringComparison.OrdinalIgnoreCase) && + invoice.Status == Invoice.StatusCodes.Sent && + invoice.RechargeAccounts.Where(a => a.Direction == RechargeAccount.CreditDebit.Debit).Any()) + { + invoice.PaidAt = DateTime.UtcNow; + invoice.Status = Invoice.StatusCodes.PendingApproval; + var approvalAction = new History() + { + Type = HistoryActionTypes.RechargePaidByCustomer.TypeCode, + ActionDateTime = DateTime.UtcNow, + Actor = user.Name, + Data = new RechargePaidByCustomerHistoryActionType().SerializeData(new RechargePaidByCustomerHistoryActionType.DataType + { + RechargeAccounts = invoice.RechargeAccounts.Where(a => a.Direction == RechargeAccount.CreditDebit.Debit).ToArray() + }) + }; + invoice.History.Add(approvalAction); + } + + if (invoice.Type == Invoice.InvoiceTypes.Recharge && invoice.Status == Invoice.StatusCodes.PendingApproval) { //Need to resend these ones too await _invoiceService.SendFinancialApproverEmail(invoice, null); //Will pull them with a private method } // record action - var user = await _userManager.GetUserAsync(User); + var action = new History() { Type = HistoryActionTypes.InvoiceSent.TypeCode, From 51b01436febaeb12fc30987864db0545d1daca7c Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Tue, 18 Nov 2025 08:52:40 -0800 Subject: [PATCH 11/13] Adds recharge approval history Adds a history record for when a recharge invoice is sent to financial approvers. This allows tracking of when and to whom approval requests were sent. Updates the invoice service to return the approval model. --- .../Controllers/InvoicesController.cs | 21 ++++++++++++++++++- src/Payments.Mvc/Services/InvoiceService.cs | 8 ++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Payments.Mvc/Controllers/InvoicesController.cs b/src/Payments.Mvc/Controllers/InvoicesController.cs index f3598795..a869afe0 100644 --- a/src/Payments.Mvc/Controllers/InvoicesController.cs +++ b/src/Payments.Mvc/Controllers/InvoicesController.cs @@ -478,7 +478,26 @@ public async Task Send(int id, [FromBody] SendInvoiceModel model) if (invoice.Type == Invoice.InvoiceTypes.Recharge && invoice.Status == Invoice.StatusCodes.PendingApproval) { //Need to resend these ones too - await _invoiceService.SendFinancialApproverEmail(invoice, null); //Will pull them with a private method + var sentTo = await _invoiceService.SendFinancialApproverEmail(invoice, null); //Will pull them with a private method + + if (sentTo != null) + { + var approvalSentAction = new History() + { + Type = HistoryActionTypes.RechargeSentToFinancialApprovers.TypeCode, + ActionDateTime = DateTime.UtcNow, + Actor = "System", + Data = new RechargeSentToFinancialApproversHistoryActionType().SerializeData(new RechargeSentToFinancialApproversHistoryActionType.DataType + { + FinancialApprovers = sentTo.emails.Select(a => new RechargeSentToFinancialApproversHistoryActionType.FinancialApprover() + { + Name = a.Name, + Email = a.Email + }).ToArray() + }) + }; + invoice.History.Add(approvalSentAction); + } } // record action diff --git a/src/Payments.Mvc/Services/InvoiceService.cs b/src/Payments.Mvc/Services/InvoiceService.cs index e85ee8c1..5ddfcbb4 100644 --- a/src/Payments.Mvc/Services/InvoiceService.cs +++ b/src/Payments.Mvc/Services/InvoiceService.cs @@ -418,11 +418,11 @@ public string SetInvoiceKey(Invoice invoice) return linkId; } - public async Task SendFinancialApproverEmail(Invoice invoice, SendApprovalModel model) + public async Task SendFinancialApproverEmail(Invoice invoice, SendApprovalModel model) { if (invoice.Type != Invoice.InvoiceTypes.Recharge) { - return; + return null; } if(model == null) @@ -433,6 +433,8 @@ public async Task SendFinancialApproverEmail(Invoice invoice, SendApprovalModel //The cc emails are actually going to be the to emails in this case. //We might want to change it a little to have the names as well. await _emailService.SendFinancialApprove(invoice, model); + + return model; } private async Task GetInvoiceApprovers(Invoice invoice) @@ -483,7 +485,7 @@ public interface IInvoiceService Task SendInvoice(Invoice invoice, SendInvoiceModel model); - Task SendFinancialApproverEmail(Invoice invoice, SendApprovalModel model); + Task SendFinancialApproverEmail(Invoice invoice, SendApprovalModel model); Task RefundInvoice(Invoice invoice, PaymentEvent payment, string refundReason, User user); From 1a64af755d56d76785f71ee9be736e7ae7629257 Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Wed, 19 Nov 2025 08:01:55 -0800 Subject: [PATCH 12/13] Not used from here --- src/Payments.Core/Models/Configuration/PaymentsApiSettings.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Payments.Core/Models/Configuration/PaymentsApiSettings.cs b/src/Payments.Core/Models/Configuration/PaymentsApiSettings.cs index 1f8062f5..2358bbf4 100644 --- a/src/Payments.Core/Models/Configuration/PaymentsApiSettings.cs +++ b/src/Payments.Core/Models/Configuration/PaymentsApiSettings.cs @@ -10,8 +10,5 @@ public class PaymentsApiSettings public string ApiKey { get; set; } - public string RechargeApiKey { get; set; } - - public string RechargeSourceName { get; set; } } } From 6953dc615e4a6ce3fb27a5cfd498d55a58af6a4d Mon Sep 17 00:00:00 2001 From: Jason Sylvestre Date: Wed, 19 Nov 2025 08:08:15 -0800 Subject: [PATCH 13/13] Enables recharge financial segment validation In sloth This change is made to ensure that recharge transactions are properly validated from the start, improving data integrity and reducing the risk of errors. --- src/Payments.Core/Models/Configuration/FinanceSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Payments.Core/Models/Configuration/FinanceSettings.cs b/src/Payments.Core/Models/Configuration/FinanceSettings.cs index 58f7ea8c..c0a3e062 100644 --- a/src/Payments.Core/Models/Configuration/FinanceSettings.cs +++ b/src/Payments.Core/Models/Configuration/FinanceSettings.cs @@ -28,7 +28,7 @@ public class FinanceSettings public string RechargeSlothSourceName { get; set; } = "PaymentsRecharge"; - public bool ValidateRechargeFinancialSegmentString { get; set; } = false; //Maybe we want this to default to true? + public bool ValidateRechargeFinancialSegmentString { get; set; } = true; public int RechargeAutoApproveDays { get; set; } = 7; }