using Dapr; using EntitlementBLL.Common; using EntitlementBLL.Interfaces; using EntitlementBLL.Payment; using GB5Shared.DTO.Framework.Login; using Microsoft.AspNetCore.Mvc; namespace EntitlementSL.Subscriptions; // Entitlement↔Payment loose coupling (Docs/Platform-Architecture-Integration-Reference.md // §9.1). PAY publishes "payorder.statuschanged" on the "pubsub" Dapr component — NOT // "goodbooks-pubsub" (the component GoLiveDeclaredSubscriber/EngagementActivatedSubscriber use // for IDMS/EAP events). Confirmed live: PAY's own publish call reads // _config["Dapr:PubSubName"] with a "pubsub" fallback, and PAYSL/appsettings.json's real key is // the flat "DaprPubSubName" (never matching the nested "Dapr:PubSubName" path it actually // reads), so the config lookup always falls through to the "pubsub" default in practice — // exactly the component ClientProvisioningExecutedSubscriber.cs already documented using for // this same reason. Subscribing on "goodbooks-pubsub" here would silently never receive PAY's // events at all, with no error anywhere. // // PAY's own event payload carries only { PayOrderId, NewStatus, TenantId } — no SourceDocType/ // SourceDocId — so this subscriber must call back into PAY (GetPayOrder) to confirm the order // actually belongs to Entitlement (SourceDocType=EntitlementSubscription) before acting; some // other module's SourceDocType sharing the same event stream must be ignored, not assumed. // // This event is explicitly best-effort (PAY's own publish is wrapped in a non-fatal try/catch, // and skipped entirely for unrecognized webhook event types or when idempotency/locking // short-circuits the webhook handler) — PayOrderReconciliationJob is the required backstop, // not optional, per §9.1 item 4. [ApiController] public class PayOrderStatusChangedSubscriber : ControllerBase { private readonly ISubscriptionService _SubscriptionService; private readonly IPaySubscriptionClient _PayClient; private readonly IEntitlementLoginFactory _LoginFactory; private readonly ILogger _Logger; public PayOrderStatusChangedSubscriber( ISubscriptionService subscriptionService, IPaySubscriptionClient payClient, IEntitlementLoginFactory loginFactory, ILogger logger) { _SubscriptionService = subscriptionService; _PayClient = payClient; _LoginFactory = loginFactory; _Logger = logger; } [Topic("pubsub", "payorder.statuschanged")] [HttpPost("/lic/Subscriptions/PayOrderStatusChanged")] public async Task HandleAsync([FromBody] PayOrderStatusChangedEvent evt, CancellationToken ct) { try { // System login — PAY resolves TenantId internally from its own event payload, not // something this subscriber needs to forward; a system-wide login is enough to // call GetPayOrder (PAY doesn't filter this read by the caller's own tenant claim). var login = _LoginFactory.Create(-1); var order = await _PayClient.GetPayOrderAsync(evt.PayOrderId, login, ct).ConfigureAwait(false); if (order is null) { _Logger.LogWarning("PayOrderStatusChangedSubscriber: PayOrderId {PayOrderId} not found in PAY.", evt.PayOrderId); return Ok(); // nothing to act on — not a failure, just nothing found } if (order.SourceDocType != PaySourceDocType.EntitlementSubscription) return Ok(); // this order belongs to a different module's SourceDocType — not ours // Use the order's own live OrderStatus (just fetched), not the event's own NewStatus — // one fewer thing to trust from an at-least-once-delivered, best-effort payload. await _SubscriptionService.HandlePaymentOutcomeAsync(order.PayOrderId, order.OrderStatus, ct) .ConfigureAwait(false); _Logger.LogInformation( "PayOrderStatusChangedSubscriber handled PayOrderId {PayOrderId} OrderStatus {OrderStatus}", order.PayOrderId, order.OrderStatus); return Ok(); } catch (Exception ex) { _Logger.LogError(ex, "PayOrderStatusChangedSubscriber failed for PayOrderId {PayOrderId}", evt.PayOrderId); return StatusCode(500, ex.Message); } } public record PayOrderStatusChangedEvent(int PayOrderId, int NewStatus, int TenantId); }