backend phase 13 & frontend phase 6

This commit is contained in:
hamid
2026-07-09 04:09:35 +03:30
parent dc64472631
commit de53f9d8a6
97 changed files with 11969 additions and 77 deletions
@@ -0,0 +1,49 @@
#nullable enable
using Baya.Application.Contracts.Payments;
using Microsoft.Extensions.Options;
namespace Baya.Infrastructure.CrossCutting.Seams;
/// <summary>
/// A deterministic, network-free mock <see cref="IBankTransferProvider" /> for the PAYA/SATNA payout rail. It
/// moves <b>no money</b>: every instruction gets a deterministic <c>transfer_reference</c> and settles
/// <see cref="BankTransferStatus.Paid" /> (the mock collapses the real <c>submitted → paid</c> reconciliation
/// into one step). It <b>honours</b> the <see cref="PayoutInstruction.Method" /> chosen by the handler (PAYA vs
/// SATNA by the config threshold) and echoes it back. A config switch forces a deterministic failure so the
/// <c>partially_failed</c>/retry paths are testable: <see cref="BankTransferOptions.ForceFailure" /> fails every
/// instruction (→ whole-batch failure), and <see cref="BankTransferOptions.FailIban" /> fails just that one
/// destination (→ partial failure). A real transferor (Jibit / Vandar / Sadad payout) replaces this registration
/// only — the source settlement account, per-nurse Sheba, and the reconciliation callback are its concern.
/// </summary>
public sealed class MockBankTransferProvider(IOptions<SeamOptions> options) : IBankTransferProvider
{
private readonly BankTransferOptions _options = options.Value.BankTransfer;
public ValueTask<PayoutBatchSubmitResult> SubmitPayoutBatchAsync(
long payoutBatchId,
IReadOnlyList<PayoutInstruction> instructions,
string idempotencyKey,
CancellationToken cancellationToken = default)
{
var results = new List<PayoutInstructionResult>(instructions.Count);
foreach (var instruction in instructions)
{
var fail = _options.ForceFailure
|| (!string.IsNullOrEmpty(_options.FailIban)
&& string.Equals(instruction.Iban, _options.FailIban, StringComparison.Ordinal));
results.Add(fail
? new PayoutInstructionResult(instruction.PayoutId, BankTransferStatus.Failed, null, instruction.Method, "provider_declined")
: new PayoutInstructionResult(
instruction.PayoutId, BankTransferStatus.Paid,
TransferReference: $"mock-payout-{payoutBatchId}-{instruction.PayoutId}-{idempotencyKey}",
instruction.Method, FailureReason: null));
}
return ValueTask.FromResult(new PayoutBatchSubmitResult(
ExternalBatchRef: $"mock-batch-{payoutBatchId}-{idempotencyKey}", results));
}
public ValueTask<BankTransferStatus> GetPayoutStatusAsync(string externalBatchRef, CancellationToken cancellationToken = default)
=> ValueTask.FromResult(_options.ForceFailure ? BankTransferStatus.Failed : BankTransferStatus.Paid);
}
@@ -19,6 +19,24 @@ public sealed class SeamOptions
public MoadianOptions Moadian { get; set; } = new();
public BnplOptions Bnpl { get; set; } = new();
public CurrencyOptions Currency { get; set; } = new();
public BankTransferOptions BankTransfer { get; set; } = new();
}
/// <summary>
/// Tunes the mock <c>IBankTransferProvider</c> (b13 PAYA/SATNA payouts). By default every instruction settles
/// paid with a deterministic transfer reference and no money moves. Set <see cref="ForceFailure"/> to fail the
/// whole batch (→ <c>failed</c>) or <see cref="FailIban"/> to fail just one destination (→ <c>partially_failed</c>,
/// so the retry path is testable). The real transferor ignores these — the source settlement account, per-nurse
/// Sheba, and the reconciliation callback come from provider config.
/// </summary>
public sealed class BankTransferOptions
{
/// <summary>When true, every payout instruction is rejected so the whole-batch-failure path is testable.</summary>
public bool ForceFailure { get; set; }
/// <summary>A designated IBAN that is rejected while others succeed — exercises the <c>partially_failed</c>
/// batch outcome and the single-payout retry.</summary>
public string FailIban { get; set; } = string.Empty;
}
/// <summary>
@@ -73,6 +73,13 @@ public static class ServiceCollectionExtension
services.AddSingleton<IBnplProviderResolver, MockBnplProviderResolver>();
services.AddSingleton<ICurrencyNormalizer, MockCurrencyNormalizer>();
// Payout bank rail (backend-phase-13). The deterministic MockBankTransferProvider settles every PAYA/SATNA
// instruction paid with no money movement; a config switch forces whole-batch/single-row failures so the
// partially_failed + retry paths are testable. A real transferor (Jibit/Vandar/Sadad payout) with a
// registered source settlement account + reconciliation callback swaps in by a registration change only —
// the payout status machine + the nurse_payout_booking_links UNIQUE remain the irreversible-transfer backstop.
services.AddSingleton<IBankTransferProvider, MockBankTransferProvider>();
return services;
}
}