using AccountsBLL.Warehouse; using Dapr; using GB5Shared.DTO.Framework.Login; using Microsoft.AspNetCore.Mvc; namespace AccountsSL.Subscriptions { // Dapr CloudEvents subscriber that receives voucher-saved events and enqueues delta // posting work into TWAREHOUSECHANGEQUEUE for each POSTINGMODE=1 fact. // // VoucherBLL is NOT aware of the warehouse — it publishes existing Dapr events normally. // This subscriber is the sole coupling point between the voucher domain and the warehouse. // // The event payload must carry ClientId and DatabaseName so this system subscriber can // route to the correct tenant DB without a user Login header (Dapr sidecar invocations // are system-to-system and don't forward user auth headers). [ApiController] public class WarehouseChangeSubscriber : ControllerBase { private readonly IWarehouseChangeQueueBLL _queueBll; private readonly ILogger _logger; public WarehouseChangeSubscriber( IWarehouseChangeQueueBLL queueBll, ILogger logger) { _queueBll = queueBll; _logger = logger; } [Topic("pubsub", "accounting.voucher.saved")] [HttpPost("/Accounts/Subscribe/VoucherSaved")] public async Task HandleVoucherSavedAsync( [FromBody] VoucherSavedEvent evt, CancellationToken ct) { _logger.LogInformation( "accounting.voucher.saved received: VoucherId {VoucherId} OuId {OuId} Date {Date} TenantId {TenantId}", evt.VoucherId, evt.OuId, evt.VoucherDate, evt.TenantId); try { // Build a minimal LoginDTO for DB routing — this subscriber runs as a system // operation, so we use the event's own tenant context rather than a user session. // Only ClientId and DatabaseName are needed; IQueryExecutor uses them for routing. // ConnectionName = MSERVERCONFIG.CONNECTIONNAME (not the physical DB name). // Set ConnectionDatabaseName — the field background/system callers use so the // resolver picks it over DatabaseName when both are populated. var login = new LoginDTO { ClientId = evt.TenantId, ConnectionDatabaseName = evt.ConnectionName }; await _queueBll.EnqueueVoucherChangeAsync(evt.OuId, evt.VoucherDate, login, ct); return Ok(); } catch (Exception ex) { _logger.LogError(ex, "Failed to enqueue warehouse change for VoucherId {VoucherId}", evt.VoucherId); // Return 500 so Dapr retries delivery (Dapr treats non-2xx as transient failure). return StatusCode(500, ex.Message); } } // CloudEvent data payload for "accounting.voucher.saved". // VoucherBLL must include TenantId and ConnectionName in the event data: // TenantId = login.ClientId (for WHERE TENANTID filter) // ConnectionName = login.DatabaseName (= MSERVERCONFIG.CONNECTIONNAME for real // user sessions set by AuthenticationBLL) // ConnectionName is NOT the physical database name — it is the MSERVERCONFIG lookup key. public record VoucherSavedEvent( int VoucherId, int OuId, DateTime VoucherDate, int TenantId, string ConnectionName); // Second module wired into the same generic delta pipeline (per the architecture note: // "only the publish side stays module-local... the drain and PostFactAsync engine are // already generic"). MMHeadBLL publishes "mm.stock.posted" the same best-effort way // VoucherBLL publishes "accounting.voucher.saved" -- this handler just reuses the exact // same EnqueueVoucherChangeAsync call (it's already fact-generic: it loops every // MWAREHOUSEFACT with POSTINGMODE=1, nothing voucher-specific inside it), so no new // AccountsBLL code was needed for this. [Topic("pubsub", "mm.stock.posted")] [HttpPost("/Accounts/Subscribe/MMStockPosted")] public async Task HandleMMStockPostedAsync( [FromBody] MMStockPostedEvent evt, CancellationToken ct) { _logger.LogInformation( "mm.stock.posted received: OuId {OuId} Date {Date} TenantId {TenantId}", evt.OuId, evt.ActivityDate, evt.TenantId); try { var login = new LoginDTO { ClientId = evt.TenantId, ConnectionDatabaseName = evt.ConnectionName }; await _queueBll.EnqueueVoucherChangeAsync(evt.OuId, evt.ActivityDate, login, ct); return Ok(); } catch (Exception ex) { _logger.LogError(ex, "Failed to enqueue warehouse change for OuId {OuId}", evt.OuId); return StatusCode(500, ex.Message); } } // CloudEvent data payload for "mm.stock.posted" -- see MMHeadBLL // .PublishStockPostedEventAsync for the publish side. public record MMStockPostedEvent( int OuId, DateTime ActivityDate, int TenantId, string ConnectionName); // Third module event wired into the same generic delta pipeline -- Sales Overview // Dashboard (Phase 1, parallel DW-posting track). MMHeadBLL publishes "mm.sales.posted" // from both its pending-allocation branch (Sales Enquiry/Quotation/Order) and its // stock-posting branch (Sales Invoice/Return) -- see MMHeadBLL.PublishSalesPostedEventAsync. // Same EnqueueVoucherChangeAsync reuse, no new AccountsBLL code needed. [Topic("pubsub", "mm.sales.posted")] [HttpPost("/Accounts/Subscribe/MMSalesPosted")] public async Task HandleMMSalesPostedAsync( [FromBody] MMSalesPostedEvent evt, CancellationToken ct) { _logger.LogInformation( "mm.sales.posted received: OuId {OuId} Date {Date} TenantId {TenantId}", evt.OuId, evt.ActivityDate, evt.TenantId); try { var login = new LoginDTO { ClientId = evt.TenantId, ConnectionDatabaseName = evt.ConnectionName }; await _queueBll.EnqueueVoucherChangeAsync(evt.OuId, evt.ActivityDate, login, ct); return Ok(); } catch (Exception ex) { _logger.LogError(ex, "Failed to enqueue warehouse change for OuId {OuId}", evt.OuId); return StatusCode(500, ex.Message); } } // CloudEvent data payload for "mm.sales.posted" -- see MMHeadBLL // .PublishSalesPostedEventAsync for the publish side. public record MMSalesPostedEvent( int OuId, DateTime ActivityDate, int TenantId, string ConnectionName); } }