Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 60 additions & 50 deletions Api/Api.Controller/PayrunJobController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using PayrollEngine.Api.Core;
Expand All @@ -19,12 +19,13 @@ namespace PayrollEngine.Api.Controller;
/// API controller for the payrun jobs
/// </summary>
public abstract class PayrunJobController(ITenantService tenantService, IPayrunJobService payrunJobService,
IWebhookDispatchService webhookDispatcher, IControllerRuntime runtime)
IWebhookDispatchService webhookDispatcher, IPayrunJobQueue payrunJobQueue, IControllerRuntime runtime)
: RepositoryChildObjectController<ITenantService, IPayrunJobService,
ITenantRepository, IPayrunJobRepository,
Tenant, PayrunJob, ApiObject.PayrunJob>(tenantService, payrunJobService, runtime, new PayrunJobMap())
{
private IWebhookDispatchService WebhookDispatcher { get; } = webhookDispatcher ?? throw new ArgumentNullException(nameof(webhookDispatcher));
private IPayrunJobQueue PayrunJobQueue { get; } = payrunJobQueue ?? throw new ArgumentNullException(nameof(payrunJobQueue));
private PayrunJobServiceSettings ServiceSettings => Service.Settings;

public virtual async Task<ActionResult> QueryEmployeePayrunJobsAsync(int tenantId, int employeeId, Query query)
Expand Down Expand Up @@ -75,16 +76,15 @@ private async Task<ActionResult<long>> QueryEmployeeJobsCountAsync(int tenantId,
}

/// <summary>
/// Start a new payrun job
/// Start a new payrun job (asynchronously).
/// The job is queued for background processing and returns immediately with HTTP 202 Accepted.
/// Use the Location header to poll for job status.
/// </summary>
/// <param name="tenantId">The tenant id</param>
/// <param name="jobInvocation">The payrun jobs to add</param>
/// <returns>The started payrun job</returns>
/// <param name="jobInvocation">The payrun job invocation</param>
/// <returns>HTTP 202 Accepted with the payrun job and Location header for status polling</returns>
public virtual async Task<ActionResult<ApiObject.PayrunJob>> StartPayrunJobAsync(int tenantId, ApiObject.PayrunJobInvocation jobInvocation)
{
var stopwatch = new Stopwatch();
stopwatch.Start();

// tenant
var tenant = await ParentService.GetAsync(Runtime.DbContext, tenantId);
if (tenant == null)
Expand Down Expand Up @@ -135,53 +135,63 @@ private async Task<ActionResult<long>> QueryEmployeeJobsCountAsync(int tenantId,
}
}

// processor
try
{
// settings
var serverConfiguration = Runtime.Configuration.GetConfiguration<PayrollServerConfiguration>();

var processor = new PayrunProcessor(
tenant,
payrun,
new()
{
DbContext = Runtime.DbContext,
UserRepository = ServiceSettings.UserRepository,
DivisionRepository = ServiceSettings.DivisionRepository,
TaskRepository = ServiceSettings.TaskRepository,
LogRepository = ServiceSettings.LogRepository,
EmployeeRepository = ServiceSettings.EmployeeRepository,
GlobalCaseValueRepository = ServiceSettings.GlobalCaseValueRepository,
NationalCaseValueRepository = ServiceSettings.NationalCaseValueRepository,
CompanyCaseValueRepository = ServiceSettings.CompanyCaseValueRepository,
EmployeeCaseValueRepository = ServiceSettings.EmployeeCaseValueRepository,
PayrunRepository = ServiceSettings.PayrunRepository,
PayrunJobRepository = ServiceSettings.PayrunJobRepository,
RegulationLookupSetRepository = ServiceSettings.RegulationLookupSetRepository,
RegulationRepository = ServiceSettings.RegulationRepository,
RegulationShareRepository = ServiceSettings.RegulationShareRepository,
PayrollRepository = ServiceSettings.PayrollRepository,
PayrollResultRepository = ServiceSettings.PayrollResultRepository,
PayrollConsolidatedResultRepository = ServiceSettings.PayrollConsolidatedResultRepository,
PayrollResultSetRepository = ServiceSettings.PayrollResultSetRepository,
CalendarRepository = ServiceSettings.CalendarRepository,
PayrollCalculatorProvider = ServiceSettings.PayrollCalculatorProvider,
WebhookDispatchService = WebhookDispatcher,
FunctionLogTimeout = serverConfiguration.FunctionLogTimeout,
AssemblyCacheTimeout = serverConfiguration.AssemblyCacheTimeout,
ScriptProvider = Runtime.ScriptProvider,
});

// job
// Map API model to domain model
var domainJobInvocation = new PayrunJobInvocationMap().ToDomain(jobInvocation);
var payrunJob = await processor.Process(domainJobInvocation);

stopwatch.Stop();
Log.Debug($"Created job {payrunJob.Name}: {stopwatch.ElapsedMilliseconds} ms");
// Get payroll and division for job creation
var payroll = await ServiceSettings.PayrollRepository.GetAsync(Runtime.DbContext, tenantId, payrollId);
var division = await ServiceSettings.DivisionRepository.GetAsync(Runtime.DbContext, tenantId, payroll.DivisionId);

// Get calendar for period info
var calendarName = division.Calendar ?? tenant.Calendar;
Domain.Model.Calendar calendar = null;
if (!string.IsNullOrWhiteSpace(calendarName))
{
calendar = await ServiceSettings.CalendarRepository.GetByNameAsync(Runtime.DbContext, tenantId, calendarName);
}
calendar ??= new Domain.Model.Calendar(); // default calendar

// Get calculator for period info
var calculator = ServiceSettings.PayrollCalculatorProvider.CreateCalculator(
tenantId, domainJobInvocation.UserId,
CultureInfo.CurrentCulture,
calendar);

// Create the payrun job
var payrunJob = PayrunJobFactory.CreatePayrunJob(
jobInvocation: domainJobInvocation,
divisionId: division.Id,
payrollId: payrollId,
payrollCalculator: calculator);

// Set initial status for async processing
payrunJob.JobStart = Date.Now;
payrunJob.JobStatus = PayrunJobStatus.Process;
payrunJob.Message = "Payrun job queued for background processing";

// Persist the job to database
await ServiceSettings.PayrunJobRepository.CreateAsync(Runtime.DbContext, tenantId, payrunJob);

// Update invocation with job ID
domainJobInvocation.PayrunJobId = payrunJob.Id;

// Enqueue for background processing
await PayrunJobQueue.EnqueueAsync(new PayrunJobQueueItem
{
TenantId = tenantId,
PayrunJobId = payrunJob.Id,
Tenant = tenant,
Payrun = payrun,
JobInvocation = domainJobInvocation
});

Log.Information($"Queued payrun job {payrunJob.Id} for background processing");

// created resource
return new CreatedObjectResult(Request.Path, MapDomainToApi(payrunJob));
// Return HTTP 202 Accepted with Location header
var statusUrl = $"{Request.Path}/{payrunJob.Id}/status";
return new AcceptedObjectResult(statusUrl, MapDomainToApi(payrunJob));
}
catch (PayrunException exception)
{
Expand Down
11 changes: 11 additions & 0 deletions Api/Api.Core/AcceptedObjectResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using Microsoft.AspNetCore.Mvc;

namespace PayrollEngine.Api.Core;

/// <summary>
/// API controller accepted result for async operations.
/// Returns HTTP 202 Accepted with a Location header for status polling.
/// </summary>
public class AcceptedObjectResult(string statusLocationPath, object value)
: AcceptedResult(new Uri(statusLocationPath, UriKind.Relative), value);
13 changes: 8 additions & 5 deletions Backend.Controller/PayrunJobController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PayrollEngine.Api.Core;
using PayrollEngine.Domain.Application.Service;
Expand All @@ -15,8 +16,8 @@ public class PayrunJobController : Api.Controller.PayrunJobController
{
/// <inheritdoc/>
public PayrunJobController(ITenantService tenantService, IPayrunJobService payrunJobService,
IWebhookDispatchService webhookDispatcher, IControllerRuntime runtime) :
base(tenantService, payrunJobService, webhookDispatcher, runtime)
IWebhookDispatchService webhookDispatcher, IPayrunJobQueue payrunJobQueue, IControllerRuntime runtime) :
base(tenantService, payrunJobService, webhookDispatcher, payrunJobQueue, runtime)
{
}

Expand Down Expand Up @@ -89,13 +90,15 @@ public override async Task<ActionResult> QueryEmployeePayrunJobsAsync(
}

/// <summary>
/// Start a new payrun job
/// Start a new payrun job (asynchronously).
/// The job is queued for background processing and returns immediately.
/// Use the Location header to poll for job status.
/// </summary>
/// <param name="tenantId">The tenant id</param>
/// <param name="jobInvocation">The payrun job invocation</param>
/// <returns>The started payrun job</returns>
/// <returns>HTTP 202 Accepted with the payrun job and Location header for status polling</returns>
[HttpPost]
[CreatedResponse]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[NotFoundResponse]
[UnprocessableEntityResponse]
[ApiOperationId("StartPayrunJob")]
Expand Down
Loading