backend phase 10

This commit is contained in:
hamid
2026-07-06 21:17:00 +03:30
parent 12c7e51c32
commit aae056b4e5
70 changed files with 8124 additions and 73 deletions
@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using Asp.Versioning;
using Baya.Application.Features.Payments.Queries.GetNursePayableBalance;
using Baya.Application.Models.Payments;
using Baya.WebFramework.Attributes;
using Baya.WebFramework.BaseController;
using Mediator;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Baya.Web.Api.Controllers.V1;
/// <summary>
/// The derived per-nurse payable balance (IRR digit-string), summed from the append-only ledger — never a
/// stored column. Authorized to the nurse themself or an admin/finance role. This is what b13 payouts read.
/// </summary>
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/nurses")]
[Authorize]
[Display(Description = "Derived nurse payable balance (ledger projection)")]
public sealed class NursePayableBalanceController(ISender sender) : BaseController
{
[HttpGet("{nurseId}/payable_balance")]
[ProducesOkApiResponseType<NursePayableBalanceDto>]
public async Task<IActionResult> PayableBalance(long nurseId, CancellationToken cancellationToken)
=> OperationResult(await sender.Send(new GetNursePayableBalanceQuery(nurseId), cancellationToken));
}
@@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Asp.Versioning;
using Baya.Application.Features.Payments.Commands.InitiatePayment;
using Baya.Application.Models.Payments;
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 card-payment start. A booking exists only on capture, so a payment is initiated against
/// the accepted <c>booking_requests</c> id; the money charged is the request's frozen gross. Rate-limited as a
/// money endpoint and idempotency-keyed (the <c>Idempotency-Key</c> header) so a retried start reuses the same
/// attempt. Internal account types are never exposed here — the response is just the redirect + the attempt id.
/// </summary>
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/bookings")]
[Authorize]
[Display(Description = "Card payment initiation against an accepted booking request")]
public sealed class PaymentsController(ISender sender) : BaseController
{
[HttpPost("{bookingRequestId}/payments")]
[EnableRateLimiting(RateLimitingServiceExtension.SensitivePolicy)]
[ProducesOkApiResponseType<InitiatePaymentResult>]
public async Task<IActionResult> Initiate(long bookingRequestId, CancellationToken cancellationToken)
{
var idempotencyKey = Request.Headers["Idempotency-Key"].FirstOrDefault();
return OperationResult(await sender.Send(new InitiatePaymentCommand(bookingRequestId, idempotencyKey), cancellationToken));
}
}
@@ -0,0 +1,40 @@
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Text;
using Asp.Versioning;
using Baya.Application.Features.Payments.Commands.HandlePaymentWebhook;
using Baya.Application.Models.Payments;
using Baya.WebFramework.Attributes;
using Baya.WebFramework.BaseController;
using Mediator;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Baya.Web.Api.Controllers.V1;
/// <summary>
/// The inbound PSP/BNPL 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, external_event_id)</c> before any money moves. The raw body is read verbatim and stored in
/// <c>payload_json</c>.
/// </summary>
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/webhooks")]
[AllowAnonymous]
[Display(Description = "PSP/BNPL payment callbacks (signature-authenticated, idempotent)")]
public sealed class WebhooksController(ISender sender) : BaseController
{
[HttpPost("payments/{provider}")]
[ProducesOkApiResponseType<WebhookIngestResult>]
public async Task<IActionResult> Payments(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 HandlePaymentWebhookCommand(provider, headers, rawBody), cancellationToken));
}
}
+1
View File
@@ -100,6 +100,7 @@ if (!app.Environment.IsEnvironment("Testing"))
{
await app.ApplyMigrationsAsync();
await app.SeedDefaultUsersAsync();
await app.SeedPaymentGatewaysAsync();
}
if (app.Environment.IsDevelopment())