frontend phase 5 & backend phase 12
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Asp.Versioning;
|
||||
using Baya.Application.Features.Bnpl.Commands.RevertBnplOrder;
|
||||
using Baya.Application.Features.Bnpl.Commands.SettleBnplOrder;
|
||||
using Baya.Application.Features.Bnpl.Commands.VerifyBnplOrder;
|
||||
using Baya.Application.Features.Bnpl.Queries.GetBnplOrderStatus;
|
||||
using Baya.Application.Models.Bnpl;
|
||||
using Baya.Infrastructure.Identity.Identity.PermissionManager;
|
||||
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>
|
||||
/// Admin-only BNPL operations console: manually drive verify/settle (also driven by the provider callback) and
|
||||
/// the payout-/refund-sensitive full revert, plus read an order. The revert reverses through the provider only
|
||||
/// (customer ↔ provider ↔ Balinyaar) and surfaces the async ~7–10-business-day customer ETA. Rate-limited as
|
||||
/// money endpoints.
|
||||
/// </summary>
|
||||
[ApiVersion("1")]
|
||||
[ApiController]
|
||||
[Route("api/v{version:apiVersion}/admin_bnpl")]
|
||||
[Authorize(ConstantPolicies.DynamicPermission)]
|
||||
[EnableRateLimiting(RateLimitingServiceExtension.SensitivePolicy)]
|
||||
[Display(Description = "Admin BNPL verify/settle/revert + order status")]
|
||||
public sealed class AdminBnplController(ISender sender) : BaseController
|
||||
{
|
||||
[HttpPost("{id}/[action]")]
|
||||
[ProducesOkApiResponseType<bool>]
|
||||
public async Task<IActionResult> Verify(long id, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(new VerifyBnplOrderCommand(id), cancellationToken));
|
||||
|
||||
[HttpPost("{id}/[action]")]
|
||||
[ProducesOkApiResponseType<bool>]
|
||||
public async Task<IActionResult> Settle(long id, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(new SettleBnplOrderCommand(id), cancellationToken));
|
||||
|
||||
[HttpPost("{id}/[action]")]
|
||||
[ProducesOkApiResponseType<RevertBnplResult>]
|
||||
public async Task<IActionResult> Revert(long id, RevertBnplBody body, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(
|
||||
new RevertBnplOrderCommand(id, body.RefundPercentage, body.TicketId, body.ReasonNotes), cancellationToken));
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesOkApiResponseType<BnplOrderStatusDto>]
|
||||
public async Task<IActionResult> Get(long id, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(new GetBnplOrderStatusQuery(id, AdminView: true), cancellationToken));
|
||||
|
||||
/// <summary>The revert body (the order id comes from the route). Omit <c>refund_percentage</c> for a full revert.</summary>
|
||||
public record RevertBnplBody(decimal? RefundPercentage, long? TicketId, string? ReasonNotes);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Asp.Versioning;
|
||||
using Baya.Application.Features.Bnpl.Commands.InitiateBnplOrder;
|
||||
using Baya.Application.Features.Bnpl.Queries.CheckBnplEligibility;
|
||||
using Baya.Application.Features.Bnpl.Queries.GetBnplOrderStatus;
|
||||
using Baya.Application.Models.Bnpl;
|
||||
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 customer-facing BNPL checkout — the "pay with installments" alternative to the card path. Eligibility
|
||||
/// records the plan on a <c>bnpl_transactions</c> row; initiate issues the provider token + redirect. Both are
|
||||
/// tenancy-scoped to the caller and rate-limited as money endpoints. The order amount is always the request's
|
||||
/// frozen gross — never client-supplied.
|
||||
/// </summary>
|
||||
[ApiVersion("1")]
|
||||
[ApiController]
|
||||
[Route("api/v{version:apiVersion}/checkout_bnpl")]
|
||||
[Authorize]
|
||||
[EnableRateLimiting(RateLimitingServiceExtension.SensitivePolicy)]
|
||||
[Display(Description = "Customer BNPL checkout (eligibility + initiate)")]
|
||||
public sealed class CheckoutBnplController(ISender sender) : BaseController
|
||||
{
|
||||
[HttpPost("[action]")]
|
||||
[ProducesOkApiResponseType<BnplEligibilityDto>]
|
||||
public async Task<IActionResult> Eligibility(CheckBnplEligibilityQuery query, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(query, cancellationToken));
|
||||
|
||||
[HttpPost("[action]")]
|
||||
[ProducesOkApiResponseType<InitiateBnplResult>]
|
||||
public async Task<IActionResult> Initiate(InitiateBnplBody body, CancellationToken cancellationToken)
|
||||
{
|
||||
var idempotencyKey = Request.Headers["Idempotency-Key"].FirstOrDefault();
|
||||
return OperationResult(await sender.Send(
|
||||
new InitiateBnplOrderCommand(body.BookingRequestId, body.ProviderCode, idempotencyKey), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesOkApiResponseType<BnplOrderStatusDto>]
|
||||
public async Task<IActionResult> Get(long id, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(new GetBnplOrderStatusQuery(id, AdminView: false), cancellationToken));
|
||||
|
||||
/// <summary>The initiate body (the idempotency key comes from the <c>Idempotency-Key</c> header).</summary>
|
||||
public record InitiateBnplBody(long BookingRequestId, string ProviderCode);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Asp.Versioning;
|
||||
using Baya.Application.Features.Bnpl.Commands.HandleBnplCallback;
|
||||
using Baya.Application.Models.Bnpl;
|
||||
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 inbound BNPL provider-callback surface. Authenticated by <b>signature</b>, not a user session, so it is
|
||||
/// anonymous to the auth pipeline; at-least-once tolerant and idempotency-deduplicated on
|
||||
/// <c>(provider_code, external_event_id)</c> before any money moves. Rate-limited (per-IP). The raw body is read
|
||||
/// verbatim and stored in <c>payload_json</c>.
|
||||
/// </summary>
|
||||
[ApiVersion("1")]
|
||||
[ApiController]
|
||||
[Route("api/v{version:apiVersion}/webhooks_bnpl")]
|
||||
[AllowAnonymous]
|
||||
[EnableRateLimiting(RateLimitingServiceExtension.SensitivePolicy)]
|
||||
[Display(Description = "BNPL provider callbacks (signature-authenticated, idempotent)")]
|
||||
public sealed class WebhooksBnplController(ISender sender) : BaseController
|
||||
{
|
||||
[HttpPost("{provider}")]
|
||||
[ProducesOkApiResponseType<BnplCallbackResult>]
|
||||
public async Task<IActionResult> Callback(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 HandleBnplCallbackCommand(provider, headers, rawBody), cancellationToken));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user