306 lines
16 KiB
C#
306 lines
16 KiB
C#
using Baya.Application.Contracts.Common;
|
|
using Baya.Application.Contracts.Invoices;
|
|
using Baya.Application.Contracts.Payments;
|
|
using Baya.Application.Contracts.Reviews;
|
|
using Baya.Infrastructure.CrossCutting.Seams;
|
|
using Baya.Infrastructure.CrossCutting.Seams.Real;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Baya.Infrastructure.CrossCutting.ServiceConfiguration;
|
|
|
|
public static class ServiceCollectionExtension
|
|
{
|
|
/// <summary>
|
|
/// Registers the cross-cutting + vendor-rail seams. Each rail is <b>config-selected</b> (refinement-phase-8):
|
|
/// the deterministic mock is the default, and setting the rail's <c>Seams:*:Provider</c> to a real provider
|
|
/// token swaps in the real HTTP adapter behind the same Application contract — <b>callers never change</b>. An
|
|
/// unconfigured/typo'd provider falls closed to the mock. This makes a partial rollout the normal case (real SMS
|
|
/// + real geocoder while payments stay mocked in a pre-launch environment). Real adapters read credentials from
|
|
/// <c>Seams:*</c> (appsettings/environment) and get an <see cref="System.Net.Http.HttpClient"/> from the
|
|
/// <c>IHttpClientFactory</c>. (The real in-app <c>INotificationDispatcher</c> needs the database, so it is
|
|
/// registered in the Persistence layer.)
|
|
/// </summary>
|
|
public static IServiceCollection AddCrossCuttingSeams(this IServiceCollection services, IConfiguration configuration)
|
|
{
|
|
services.Configure<SeamOptions>(configuration.GetSection(SeamOptions.SectionName));
|
|
var seams = configuration.GetSection(SeamOptions.SectionName).Get<SeamOptions>() ?? new SeamOptions();
|
|
|
|
services.AddHttpClient();
|
|
services.AddMemoryCache();
|
|
|
|
services.AddSingleton<IDateTimeProvider, SystemDateTimeProvider>();
|
|
services.AddSingleton<IFieldEncryptor, SymmetricFieldEncryptor>();
|
|
services.AddSingleton<ICacheService, MemoryCacheService>();
|
|
|
|
RegisterObjectStorage(services, seams);
|
|
RegisterSms(services, seams);
|
|
RegisterTrustRails(services, seams);
|
|
RegisterGeocoder(services, seams);
|
|
RegisterPaymentRails(services, seams);
|
|
RegisterBnpl(services, seams);
|
|
RegisterPayoutRail(services, seams);
|
|
RegisterMoadian(services, seams);
|
|
|
|
// Payment-capture trigger (backend-phase-9). refinement-phase-8 (6.4): the real card capture (b10) supersedes
|
|
// it, so production registers the fail-closed DisabledPaymentCaptureSimulator (the dev-only bookings/convert
|
|
// endpoint fabricates nothing in a deployed env); Development/Testing re-register the real MockPaymentCaptureSimulator
|
|
// over this via AddDevelopmentPaymentCapture (last registration wins).
|
|
services.AddSingleton<IPaymentCaptureSimulator, DisabledPaymentCaptureSimulator>();
|
|
|
|
// Non-vendor mocks that stay as-is (real behaviour is out of this phase's scope / manual is the intended MVP).
|
|
services.AddSingleton<ICredentialVerifier, MockCredentialVerifier>(); // 5.6 manual = intended MVP
|
|
services.AddSingleton<IReviewModerationService, MockReviewModerationService>();
|
|
services.AddSingleton<ILicenseVerificationService, MockLicenseVerificationService>(); // 5.6 manual = intended MVP
|
|
|
|
return services;
|
|
}
|
|
|
|
// ---- object storage (5.5) --------------------------------------------------------------------------------
|
|
|
|
private static void RegisterObjectStorage(IServiceCollection services, SeamOptions seams)
|
|
{
|
|
if (Is(seams.ObjectStorage.Provider, SeamProviders.S3))
|
|
{
|
|
services.AddHttpClient(HttpClients.ObjectStorage);
|
|
services.AddSingleton<IObjectStorage>(sp => new S3ObjectStorage(
|
|
Client(sp, HttpClients.ObjectStorage),
|
|
sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>()));
|
|
}
|
|
else
|
|
{
|
|
services.AddSingleton<IObjectStorage, LocalDiskObjectStorage>();
|
|
}
|
|
}
|
|
|
|
// ---- SMS (5.1, launch-critical) --------------------------------------------------------------------------
|
|
|
|
private static void RegisterSms(IServiceCollection services, SeamOptions seams)
|
|
{
|
|
var provider = seams.Sms.Provider;
|
|
if (Is(provider, SeamProviders.Kavenegar))
|
|
{
|
|
services.AddHttpClient(HttpClients.Sms, c => c.BaseAddress = new Uri(BaseOrDefault(seams.Sms.BaseUrl, "https://api.kavenegar.com/")));
|
|
services.AddSingleton<ISmsSender>(sp => new KavenegarSmsSender(
|
|
Client(sp, HttpClients.Sms),
|
|
sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>(),
|
|
sp.GetRequiredService<ILogger<KavenegarSmsSender>>()));
|
|
}
|
|
else if (Is(provider, SeamProviders.Telegram))
|
|
{
|
|
// Development-only relay (telegram-otp-bot/) — a manual-testing convenience, not a gateway. The
|
|
// per-request timeout is generous because the relay's own hop to Telegram goes through a proxy.
|
|
var telegram = seams.Sms.Telegram;
|
|
services.AddHttpClient(HttpClients.Telegram, c =>
|
|
{
|
|
c.BaseAddress = new Uri(BaseOrDefault(telegram.BaseUrl, "http://127.0.0.1:5010").TrimEnd('/') + "/");
|
|
c.Timeout = TimeSpan.FromSeconds(telegram.TimeoutSeconds > 0 ? telegram.TimeoutSeconds : 10);
|
|
});
|
|
services.AddSingleton<ISmsSender>(sp => new TelegramSmsSender(
|
|
Client(sp, HttpClients.Telegram),
|
|
sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>(),
|
|
sp.GetRequiredService<ILogger<TelegramSmsSender>>()));
|
|
}
|
|
else if (Is(provider, SeamProviders.SmsIr) || Is(provider, SeamProviders.Ghasedak))
|
|
{
|
|
throw new NotSupportedException(
|
|
$"SMS provider '{provider}' is not implemented — only 'kavenegar' has a real adapter (refinement-phase-8). " +
|
|
"Add its adapter behind ISmsSender, or use 'mock'/'kavenegar'/'telegram'.");
|
|
}
|
|
else
|
|
{
|
|
// b2 log-only mock; the Development OTP-capture decorator is layered on in Program.cs (Development only,
|
|
// and only for the capture-safe providers — a real SMS gateway disables it so the OTP is never logged).
|
|
services.AddSingleton<ISmsSender, LoggingSmsSender>();
|
|
}
|
|
}
|
|
|
|
// ---- trust & identity (5.2, 5.3) — Finnotech-class KYC bridge --------------------------------------------
|
|
|
|
private static void RegisterTrustRails(IServiceCollection services, SeamOptions seams)
|
|
{
|
|
var anyFinnotech =
|
|
Is(seams.Shahkar.Provider, SeamProviders.Finnotech) ||
|
|
Is(seams.IdentityKyc.Provider, SeamProviders.Finnotech) ||
|
|
Is(seams.BankOwnership.Provider, SeamProviders.Finnotech);
|
|
|
|
if (anyFinnotech)
|
|
{
|
|
services.AddHttpClient(HttpClients.Finnotech);
|
|
services.AddSingleton(sp => new FinnotechClient(
|
|
Client(sp, HttpClients.Finnotech),
|
|
sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>()));
|
|
}
|
|
|
|
if (Is(seams.Shahkar.Provider, SeamProviders.Finnotech))
|
|
services.AddSingleton<IShahkarVerifier>(sp => new FinnotechShahkarVerifier(sp.GetRequiredService<FinnotechClient>()));
|
|
else
|
|
services.AddSingleton<IShahkarVerifier, MockShahkarVerifier>();
|
|
|
|
if (Is(seams.IdentityKyc.Provider, SeamProviders.Finnotech))
|
|
services.AddSingleton<IIdentityKycProvider>(sp => new FinnotechIdentityKycProvider(sp.GetRequiredService<FinnotechClient>()));
|
|
else
|
|
services.AddSingleton<IIdentityKycProvider, MockIdentityKycProvider>();
|
|
|
|
if (Is(seams.BankOwnership.Provider, SeamProviders.Finnotech))
|
|
services.AddSingleton<IBankAccountOwnershipVerifier>(sp => new FinnotechBankAccountOwnershipVerifier(sp.GetRequiredService<FinnotechClient>()));
|
|
else
|
|
services.AddSingleton<IBankAccountOwnershipVerifier, MockBankAccountOwnershipVerifier>();
|
|
}
|
|
|
|
// ---- geocoding (5.4) -------------------------------------------------------------------------------------
|
|
|
|
private static void RegisterGeocoder(IServiceCollection services, SeamOptions seams)
|
|
{
|
|
if (Is(seams.Geocoding.Provider, SeamProviders.Neshan))
|
|
{
|
|
services.AddHttpClient(HttpClients.Geocoding);
|
|
services.AddSingleton<IGeocoder>(sp => new NeshanGeocoder(
|
|
Client(sp, HttpClients.Geocoding),
|
|
sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>(),
|
|
sp.GetRequiredService<ILogger<NeshanGeocoder>>()));
|
|
}
|
|
else
|
|
{
|
|
services.AddSingleton<IGeocoder, MockGeocoder>();
|
|
}
|
|
}
|
|
|
|
// ---- card PSP + webhook signature + تسهیم split (6.1) ----------------------------------------------------
|
|
|
|
private static void RegisterPaymentRails(IServiceCollection services, SeamOptions seams)
|
|
{
|
|
var real = !Is(seams.Payments.Provider, SeamProviders.Mock) && !string.IsNullOrWhiteSpace(seams.Payments.Provider);
|
|
|
|
if (real)
|
|
{
|
|
services.AddHttpClient(HttpClients.Psp);
|
|
services.AddSingleton<IPaymentProvider>(sp => new ZarinPalPaymentProvider(
|
|
Client(sp, HttpClients.Psp),
|
|
sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>(),
|
|
sp.GetRequiredService<ILogger<ZarinPalPaymentProvider>>()));
|
|
services.AddSingleton<ISettlementSplitProvider>(sp => new ProviderSettlementSplitProvider(
|
|
Client(sp, HttpClients.Psp),
|
|
sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>(),
|
|
sp.GetRequiredService<ILogger<ProviderSettlementSplitProvider>>()));
|
|
// Per-provider HMAC over the raw callback body — never trust a callback alone (the confirm path still
|
|
// re-verifies server-side). Shared by the PSP + BNPL + payout-reconciliation callbacks.
|
|
services.AddSingleton<IWebhookVerifier, HmacWebhookVerifier>();
|
|
}
|
|
else
|
|
{
|
|
services.AddSingleton<IPaymentProvider, MockPaymentProvider>();
|
|
services.AddSingleton<ISettlementSplitProvider, MockSettlementSplitProvider>();
|
|
services.AddSingleton<IWebhookVerifier, MockWebhookVerifier>();
|
|
}
|
|
|
|
// Money-path mutex — in-proc today (the DB uniques/state-machine are the authoritative backstop);
|
|
// Redis-backed for >1 instance. Unchanged by the vendor swap.
|
|
services.AddSingleton<IDistributedLock, InProcessDistributedLock>();
|
|
}
|
|
|
|
// ---- BNPL (6.2) ------------------------------------------------------------------------------------------
|
|
|
|
private static void RegisterBnpl(IServiceCollection services, SeamOptions seams)
|
|
{
|
|
// The in-house net-of-fee model is always available — it stands in for the `balinyaar` provider_code even
|
|
// in real mode (no external API), and is the mock for every code in mock mode.
|
|
services.AddSingleton<MockBnplProvider>();
|
|
services.AddSingleton<ICurrencyNormalizer, MockCurrencyNormalizer>(); // config-driven multiplier = the real impl
|
|
|
|
if (string.Equals(seams.Bnpl.Provider, "real", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
services.AddHttpClient(HttpClients.BnplSnappPay);
|
|
services.AddHttpClient(HttpClients.BnplDigipay);
|
|
|
|
services.AddSingleton(sp => new SnappPayBnplProvider(
|
|
Client(sp, HttpClients.BnplSnappPay),
|
|
sp.GetRequiredService<ICurrencyNormalizer>(),
|
|
Connection(seams, Baya.Domain.Entities.Bnpl.BnplProviderCodes.SnappPay),
|
|
seams.Bnpl.WireCurrency,
|
|
sp.GetRequiredService<ILogger<SnappPayBnplProvider>>()));
|
|
services.AddSingleton(sp => new DigipayBnplProvider(
|
|
Client(sp, HttpClients.BnplDigipay),
|
|
sp.GetRequiredService<ICurrencyNormalizer>(),
|
|
Connection(seams, Baya.Domain.Entities.Bnpl.BnplProviderCodes.Digipay),
|
|
seams.Bnpl.WireCurrency,
|
|
sp.GetRequiredService<ILogger<DigipayBnplProvider>>()));
|
|
|
|
services.AddSingleton<IBnplProviderResolver, ConfiguredBnplProviderResolver>();
|
|
// The b11 refund `bnpl_revert` path injects IBnplProvider directly (not per-code); SnappPay is the
|
|
// default revert provider. Per-code revert resolution through the resolver is a documented follow-up.
|
|
services.AddSingleton<IBnplProvider>(sp => sp.GetRequiredService<SnappPayBnplProvider>());
|
|
}
|
|
else
|
|
{
|
|
services.AddSingleton<IBnplProvider>(sp => sp.GetRequiredService<MockBnplProvider>());
|
|
services.AddSingleton<IBnplProviderResolver, MockBnplProviderResolver>();
|
|
}
|
|
}
|
|
|
|
// ---- PAYA/SATNA payout rail (6.3) ------------------------------------------------------------------------
|
|
|
|
private static void RegisterPayoutRail(IServiceCollection services, SeamOptions seams)
|
|
{
|
|
if (Is(seams.BankTransfer.Provider, SeamProviders.Jibit))
|
|
{
|
|
services.AddHttpClient(HttpClients.BankTransfer);
|
|
services.AddSingleton<IBankTransferProvider>(sp => new JibitBankTransferProvider(
|
|
Client(sp, HttpClients.BankTransfer),
|
|
sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>(),
|
|
sp.GetRequiredService<ILogger<JibitBankTransferProvider>>()));
|
|
}
|
|
else
|
|
{
|
|
services.AddSingleton<IBankTransferProvider, MockBankTransferProvider>();
|
|
}
|
|
}
|
|
|
|
// ---- Moadian e-invoicing (6.5) ---------------------------------------------------------------------------
|
|
|
|
private static void RegisterMoadian(IServiceCollection services, SeamOptions seams)
|
|
{
|
|
if (Is(seams.Moadian.Provider, SeamProviders.Moadian))
|
|
{
|
|
services.AddHttpClient(HttpClients.Moadian);
|
|
services.AddSingleton<IMoadianClient>(sp => new MoadianClient(
|
|
Client(sp, HttpClients.Moadian),
|
|
sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>(),
|
|
sp.GetRequiredService<ILogger<MoadianClient>>()));
|
|
}
|
|
else
|
|
{
|
|
services.AddSingleton<IMoadianClient, MockMoadianClient>();
|
|
}
|
|
}
|
|
|
|
// ---- helpers ---------------------------------------------------------------------------------------------
|
|
|
|
private static bool Is(string? configured, string token)
|
|
=> string.Equals(configured, token, StringComparison.OrdinalIgnoreCase);
|
|
|
|
private static System.Net.Http.HttpClient Client(IServiceProvider sp, string name)
|
|
=> sp.GetRequiredService<System.Net.Http.IHttpClientFactory>().CreateClient(name);
|
|
|
|
private static string BaseOrDefault(string configured, string fallback)
|
|
=> string.IsNullOrWhiteSpace(configured) ? fallback : configured;
|
|
|
|
private static BnplProviderConnection Connection(SeamOptions seams, string code)
|
|
=> seams.Bnpl.Providers.TryGetValue(code, out var connection) ? connection : new BnplProviderConnection();
|
|
|
|
private static class HttpClients
|
|
{
|
|
public const string ObjectStorage = "seam-object-storage";
|
|
public const string Sms = "seam-sms";
|
|
public const string Telegram = "seam-sms-telegram";
|
|
public const string Finnotech = "seam-finnotech";
|
|
public const string Geocoding = "seam-geocoding";
|
|
public const string Psp = "seam-psp";
|
|
public const string BnplSnappPay = "seam-bnpl-snapppay";
|
|
public const string BnplDigipay = "seam-bnpl-digipay";
|
|
public const string BankTransfer = "seam-bank-transfer";
|
|
public const string Moadian = "seam-moadian";
|
|
}
|
|
}
|