refinement phase 8
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Asp.Versioning;
|
||||
using Baya.Application.Features.Payouts.Commands.ReconcilePayoutBatch;
|
||||
using Baya.WebFramework.Attributes;
|
||||
using Baya.WebFramework.BaseController;
|
||||
using Baya.WebFramework.ServiceConfiguration;
|
||||
using Mediator;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
|
||||
namespace Baya.Web.Api.Controllers.V1;
|
||||
|
||||
/// <summary>
|
||||
/// The async PAYA/SATNA payout <b>reconciliation callback</b> (refinement-phase-8, 6.3). The real bank rail
|
||||
/// accepts a payout as <c>submitted</c> and calls back later with the settled outcome, flipping each payout
|
||||
/// <c>submitted → paid/failed</c> (the mock rail collapsed this into the submit). Authenticated by
|
||||
/// <b>signature</b>, not a user session, so it is anonymous to the auth pipeline; the callback is HMAC-verified and
|
||||
/// idempotent (a replayed callback re-driving an already-settled payout is a no-op). Shares the deliberate
|
||||
/// bursty-tolerant <c>webhook</c> rate policy with the PSP/BNPL callbacks.
|
||||
/// </summary>
|
||||
[ApiVersion("1")]
|
||||
[ApiController]
|
||||
[Route("api/v{version:apiVersion}/webhooks")]
|
||||
[AllowAnonymous]
|
||||
[EnableRateLimiting(RateLimitingServiceExtension.WebhookPolicy)]
|
||||
[Display(Description = "PAYA/SATNA payout reconciliation callbacks (signature-authenticated, idempotent)")]
|
||||
public sealed class WebhooksPayoutsController(ISender sender) : BaseController
|
||||
{
|
||||
[HttpPost("payouts/{provider}")]
|
||||
[ProducesOkApiResponseType<bool>]
|
||||
public async Task<IActionResult> Payouts(string provider, CancellationToken cancellationToken)
|
||||
{
|
||||
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
|
||||
var rawBody = await reader.ReadToEndAsync(cancellationToken);
|
||||
|
||||
var headers = Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return OperationResult(await sender.Send(new ReconcilePayoutBatchCommand(provider, headers, rawBody), cancellationToken));
|
||||
}
|
||||
}
|
||||
@@ -86,10 +86,19 @@ builder.Services.AddApplicationServices()
|
||||
.AddRateLimitingPolicies();
|
||||
|
||||
// Development-only: capture each OTP in-memory so GET /api/v1/dev/last_otp/{phone} can complete a login
|
||||
// without an SMS gateway. Nothing here is wired in any other environment.
|
||||
if (builder.Environment.IsDevelopment())
|
||||
// without an SMS gateway. refinement-phase-8: the capture bridge runs ONLY while the log-only mock SMS sender is
|
||||
// selected — once a real gateway (Seams:Sms:Provider) ships, the OTP is delivered over the wire and never logged
|
||||
// or captured. Nothing here is wired in any other environment.
|
||||
var smsProvider = configuration["Seams:Sms:Provider"];
|
||||
var usingMockSms = string.IsNullOrWhiteSpace(smsProvider) || smsProvider.Equals("mock", StringComparison.OrdinalIgnoreCase);
|
||||
if (builder.Environment.IsDevelopment() && usingMockSms)
|
||||
builder.Services.AddDevelopmentOtpCapture();
|
||||
|
||||
// The IPaymentCaptureSimulator + bookings/convert path is a Development/Testing affordance (b10's real webhook
|
||||
// confirm supersedes it in production). Re-register the succeeding mock over the production fail-closed stand-in.
|
||||
if (builder.Environment.IsDevelopment() || builder.Environment.IsEnvironment("Testing"))
|
||||
builder.Services.AddDevelopmentPaymentCapture();
|
||||
|
||||
builder.Services.RegisterValidatorsAsServices();
|
||||
builder.Services.AddExceptionHandler<ExceptionHandler>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user