Skip to content
Merged
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
1 change: 1 addition & 0 deletions ShopInventory/Configuration/QuartzConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public static IServiceCollection AddShopInventoryQuartz(
AddIntervalJob<InventoryTransferPostingJob>(q, "inventory-transfer-posting", TimeSpan.FromSeconds(10));
AddIntervalJob<IncomingPaymentPostingJob>(q, "incoming-payment-posting", TimeSpan.FromSeconds(10));
AddIntervalJob<ReservationCleanupJob>(q, "reservation-cleanup", TimeSpan.FromMinutes(1), startDelay: TimeSpan.FromSeconds(30));
AddIntervalJob<SalesOrderReconciliationJob>(q, "sales-order-reconciliation", TimeSpan.FromMinutes(2), startDelay: TimeSpan.FromMinutes(1));

if (sap.AutoSyncEnabled)
{
Expand Down
1 change: 1 addition & 0 deletions ShopInventory/Services/INewFeatureServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public interface ISalesOrderService
Task<SalesOrderDto> MarkAsFulfilledAsync(int id, int? invoiceId, CancellationToken cancellationToken = default);
Task<string> GenerateOrderNumberAsync(CancellationToken cancellationToken = default);
Task<SalesOrderDto> PostToSAPAsync(int id, Guid userId, CancellationToken cancellationToken = default);
Task<int> ReconcileUnlinkedSapSalesOrdersAsync(TimeSpan lookback, int maxOrders, CancellationToken cancellationToken = default);
}

/// <summary>
Expand Down
59 changes: 59 additions & 0 deletions ShopInventory/Services/Jobs/SalesOrderReconciliationJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Microsoft.Extensions.DependencyInjection;
using Quartz;

namespace ShopInventory.Services;

/// <summary>
/// Quartz job that links local sales orders back to SAP documents that were created but never
/// recorded locally. A SAP create can commit while the response, the local save, or the short
/// in-request reconciliation window fails, which leaves the order showing Pending with no
/// document number until somebody re-approves it by hand. Scheduling, clustering and misfire
/// handling are owned by Quartz (see QuartzConfiguration).
/// </summary>
[DisallowConcurrentExecution]
public sealed class SalesOrderReconciliationJob : IJob
{
private static readonly TimeSpan Lookback = TimeSpan.FromDays(7);
private const int MaxOrdersPerRun = 25;

private readonly IServiceProvider _serviceProvider;
private readonly ILogger<SalesOrderReconciliationJob> _logger;

public SalesOrderReconciliationJob(
IServiceProvider serviceProvider,
ILogger<SalesOrderReconciliationJob> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}

public async Task Execute(IJobExecutionContext context)
{
using var scope = _serviceProvider.CreateScope();
var salesOrderService = scope.ServiceProvider.GetRequiredService<ISalesOrderService>();

try
{
var linkedCount = await salesOrderService.ReconcileUnlinkedSapSalesOrdersAsync(
Lookback,
MaxOrdersPerRun,
context.CancellationToken);

if (linkedCount > 0)
{
_logger.LogInformation(
"Sales order reconciliation linked {LinkedCount} local order(s) to their existing SAP documents",
linkedCount);
}
}
catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
// Never let a SAP outage fault the trigger; the next run retries the same candidates.
_logger.LogError(ex, "Sales order SAP reconciliation sweep failed");
}
}
}
Loading
Loading