Files
baya-monorepo/server/src/Infrastructure/Baya.Infrastructure.CrossCutting/Seams/SeamOptions.cs
T

470 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace Baya.Infrastructure.CrossCutting.Seams;
/// <summary>
/// Options bound from the <c>Seams</c> configuration section. The mock seams read non-secret defaults
/// from here; deployed keys/paths come from the environment-specific appsettings file or environment variables.
///
/// <para><b>Provider selection (refinement-phase-8).</b> Each vendor rail carries a <c>Provider</c> selector
/// (default = the mock, so an unconfigured environment behaves exactly as before). Setting it to a real
/// provider token (e.g. <c>Seams:Sms:Provider = kavenegar</c>) swaps in the real HTTP adapter behind the same
/// contract — handlers never change. This makes a <b>partial rollout</b> the normal case: real SMS + real
/// geocoder while payments stay mocked in a pre-launch environment is just three config keys.</para>
/// </summary>
public sealed class SeamOptions
{
public const string SectionName = "Seams";
public FieldEncryptionOptions FieldEncryption { get; set; } = new();
public SmsOptions Sms { get; set; } = new();
public ObjectStorageOptions ObjectStorage { get; set; } = new();
public BankOwnershipOptions BankOwnership { get; set; } = new();
public GeocodingOptions Geocoding { get; set; } = new();
public ShahkarOptions Shahkar { get; set; } = new();
public IdentityKycOptions IdentityKyc { get; set; } = new();
public PaymentCaptureOptions PaymentCapture { get; set; } = new();
public PaymentsOptions Payments { get; set; } = new();
public MoadianOptions Moadian { get; set; } = new();
public BnplOptions Bnpl { get; set; } = new();
public CurrencyOptions Currency { get; set; } = new();
public BankTransferOptions BankTransfer { get; set; } = new();
public ReviewModerationOptions ReviewModeration { get; set; } = new();
public LicenseVerificationOptions LicenseVerification { get; set; } = new();
public FinnotechOptions Finnotech { get; set; } = new();
}
/// <summary>
/// Shared credentials for the Finnotech-class KYC bridge that fronts three trust rails — شاهکار
/// (<c>IShahkarVerifier</c>), e-KYC (<c>IIdentityKycProvider</c>), and استعلام شبا
/// (<c>IBankAccountOwnershipVerifier</c>). Each seam opts in with its own <c>Provider = finnotech</c> selector,
/// but they authenticate against the same tenant, so the connection facts live here once. All values are
/// secrets — the environment-specific appsettings file or environment variables.
/// </summary>
public sealed class FinnotechOptions
{
/// <summary>API host (defaults to Finnotech's public sandbox/production host at the adapter).</summary>
public string BaseUrl { get; set; } = string.Empty;
/// <summary>The tenant's client id (<c>NID</c>) — the Finnotech app identifier.</summary>
public string ClientId { get; set; } = string.Empty;
/// <summary>A pre-issued bearer access token (client-credential token exchange is out of scope for the MVP
/// adapter; a deployment supplies a current token, refreshed out-of-band).</summary>
public string AccessToken { get; set; } = string.Empty;
}
/// <summary>Stable provider tokens for the <c>Provider</c> selectors, so a typo fails closed to the mock.</summary>
public static class SeamProviders
{
public const string Mock = "mock";
public const string LocalDisk = "local";
// SMS gateways
public const string Kavenegar = "kavenegar";
public const string SmsIr = "smsir";
public const string Ghasedak = "ghasedak";
/// <summary><b>Broadcast relay, not an SMS gateway</b> — the standalone <c>telegram-otp-bot/</c> service
/// sends every code to a fixed list of Telegram chat ids. The pre-launch demo rail; replace with a real
/// gateway before onboarding customers outside the trusted group.</summary>
public const string Telegram = "telegram";
// Object storage
public const string S3 = "s3";
// Trust / identity (a Finnotech-class KYC bridge fronts Shahkar / e-KYC / استعلام شبا)
public const string Finnotech = "finnotech";
// Geocoding
public const string Neshan = "neshan";
// Card PSP acquirers
public const string ZarinPal = "zarinpal";
public const string Sadad = "sadad";
public const string Vandar = "vandar";
public const string Jibit = "jibit";
// BNPL
public const string SnappPay = "snapppay";
public const string Digipay = "digipay";
// e-invoicing
public const string Moadian = "moadian";
}
/// <summary>
/// The outbound SMS rail (<c>ISmsSender</c>). <see cref="Provider"/> = <c>mock</c> logs the OTP (the b2
/// <c>LoggingSmsSender</c>); set it to <c>kavenegar</c> / <c>smsir</c> / <c>ghasedak</c> to deliver over a real
/// Iranian gateway. <b>refinement-phase-8:</b> when a real gateway is selected the Development OTP-in-logs/echo
/// bridge is disabled — the OTP must never be logged once real SMS ships.
///
/// <para><c>telegram</c> is the one exception: it is a <b>Development convenience channel, not an SMS gateway</b>
/// (see <see cref="TelegramOptions"/>), so the OTP-capture bridge stays enabled alongside it.</para>
/// </summary>
public sealed class SmsOptions
{
/// <summary><c>mock</c> (default) | <c>kavenegar</c> | <c>smsir</c> | <c>ghasedak</c> | <c>telegram</c>
/// (Development only).</summary>
public string Provider { get; set; } = SeamProviders.Mock;
/// <summary>Gateway API key / token (secret — the environment-specific appsettings file or environment variables).</summary>
public string ApiKey { get; set; } = string.Empty;
/// <summary>The registered sender line (used by <c>SendAsync</c> free-form messages and non-template sends).</summary>
public string SenderLine { get; set; } = string.Empty;
/// <summary>Override the gateway base URL (defaults to the provider's public API host).</summary>
public string BaseUrl { get; set; } = string.Empty;
/// <summary>The approved OTP template/pattern name the gateway sends the code through (verify-lookup APIs).</summary>
public string OtpTemplate { get; set; } = string.Empty;
/// <summary>Connection facts for the <c>telegram</c> relay; ignored by every other provider.</summary>
public TelegramOptions Telegram { get; set; } = new();
}
/// <summary>
/// The Telegram OTP relay (the standalone <c>telegram-otp-bot/</c> Node service), selected by
/// <c>Seams:Sms:Provider = telegram</c>. It replaces "read the OTP out of the server log" — the tester gets the
/// code on their phone without an Iranian SMS gateway contract.
///
/// <para><b>It is not an SMS gateway.</b> There is no per-user routing: the relay <i>broadcasts</i> every code
/// to a fixed list of Telegram chat ids, so every configured recipient reads every login code. That is workable
/// for a trusted demo group — which is why the pre-launch <c>balinyaar.ir</c> deployment uses it — and
/// disqualifying once anyone outside that group can request a code. Switch <c>Seams:Sms:Provider</c> to
/// <c>kavenegar</c> at that point; nothing else changes.</para>
/// </summary>
public sealed class TelegramOptions
{
/// <summary>The relay's root URL, e.g. <c>http://127.0.0.1:5010</c>.</summary>
public string BaseUrl { get; set; } = string.Empty;
/// <summary>The shared secret sent as the relay's <c>X-Api-Key</c> header — it must equal the relay's
/// <c>API_KEY</c>. <b>Secret:</b> committed config carries an empty/placeholder value; the real one comes
/// from <c>Seams:Sms:Telegram:ApiKey</c> in appsettings or the environment.</summary>
public string ApiKey { get; set; } = string.Empty;
/// <summary>Per-request timeout. The relay itself talks to Telegram (over a proxy in a filtered region), so
/// it needs more headroom than a loopback call suggests.</summary>
public int TimeoutSeconds { get; set; } = 10;
}
/// <summary>
/// Tunes the mock <c>ILicenseVerificationService</c> (b15 partner-center eNamad / MoH establishment-permit
/// check). By default every check returns <c>NeedsManualReview</c> so <c>VerifyPartnerCenter</c> records the
/// human admin decision. Set <see cref="AutoApprove"/> to have the mock return <c>Valid</c> (test the
/// auto-approve path). The real eNamad / MoH registry adapter ignores these knobs.
/// </summary>
public sealed class LicenseVerificationOptions
{
/// <summary>When true, permit/eNamad checks auto-approve (return <c>Valid</c>) instead of requiring a manual decision.</summary>
public bool AutoApprove { get; set; }
}
/// <summary>
/// Tunes the mock <c>IReviewModerationService</c> (b14 AI review pre-screen). By default clean text returns a
/// human-review <c>Flag</c> (keeping the publish gate on); a banned-word hit returns <c>Reject</c>. Set
/// <see cref="AutoApproveClean"/> to have clean text auto-<c>Approve</c> (auto-publish). The real text
/// classifier / LLM endpoint ignores these knobs.
/// </summary>
public sealed class ReviewModerationOptions
{
/// <summary>When true, clean text is auto-approved (auto-published) instead of flagged for human review.</summary>
public bool AutoApproveClean { get; set; }
/// <summary>Case-insensitive substrings that mark a review for rejection.</summary>
public List<string> BannedWords { get; set; } = ["scam", "fraud", "کلاهبردار"];
}
/// <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><c>mock</c> (default) | <c>jibit</c> | <c>vandar</c> | <c>sadad</c> — the payout transferor.</summary>
public string Provider { get; set; } = SeamProviders.Mock;
/// <summary>Transferor API base URL.</summary>
public string BaseUrl { get; set; } = string.Empty;
/// <summary>Transferor API key / bearer token (secret).</summary>
public string ApiKey { get; set; } = string.Empty;
/// <summary>The platform's registered <b>source settlement account</b> the batch debits (IBAN/account id the
/// transferor recognises). Every PAYA/SATNA transfer originates here.</summary>
public string SourceSettlementAccount { get; set; } = string.Empty;
/// <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>
/// Tunes the mock <c>ICurrencyNormalizer</c> (b12). Toman → IRR multiplies by <see cref="TomanToIrrMultiplier"/>
/// (10); IRR passes through. The real adapter reads the multiplier from provider config. Conversion happens only
/// at the provider boundary, never internally.
/// </summary>
public sealed class CurrencyOptions
{
/// <summary>How many Rial one Toman is (10). A currency redenomination is a config change, not a code change.</summary>
public long TomanToIrrMultiplier { get; set; } = 10;
}
/// <summary>
/// Tunes the mock <c>IMoadianClient</c> (b11 e-invoicing). By default a submission stays <c>pending</c> with no
/// reference. Set <see cref="ForceRegistered"/> to make it return <c>registered</c> with a fake 22-digit ref so
/// the reconciliation/registered path is testable. The real سامانه مودیان adapter ignores these.
/// </summary>
public sealed class MoadianOptions
{
/// <summary><c>mock</c> (default) | <c>moadian</c> — the real سامانه مودیان submission adapter.</summary>
public string Provider { get; set; } = SeamProviders.Mock;
/// <summary>مودیان API base URL (the tax-authority endpoint).</summary>
public string BaseUrl { get; set; } = string.Empty;
/// <summary>The platform's مودیان memory/economic id (<c>memoryId</c> / شناسه یکتای حافظه مالیاتی).</summary>
public string MemoryId { get; set; } = string.Empty;
/// <summary>A pre-issued bearer token for the مودیان API (the signing-certificate token exchange is a
/// deploy-time concern; a deployment supplies a current token). Secret.</summary>
public string AccessToken { get; set; } = string.Empty;
/// <summary>When true, a submission returns <c>registered</c> + a deterministic fake 22-digit reference.</summary>
public bool ForceRegistered { get; set; }
}
/// <summary>
/// Tunes the deterministic mock <c>IBnplProvider</c> (b12, superset of the b11 revert-only stub). The mock
/// commission % drives the settle (net = order commission); the real adapter reads the actual deducted amount
/// from each settlement and ignores these knobs.
/// </summary>
public sealed class BnplOptions
{
/// <summary><c>mock</c> (default) | <c>real</c> — when <c>real</c>, <c>IBnplProviderResolver</c> maps each
/// <c>provider_code</c> to its concrete adapter (SnappPay / Digipay) instead of the one mock.</summary>
public string Provider { get; set; } = SeamProviders.Mock;
/// <summary>Per-provider connection facts, keyed by <c>provider_code</c> (<c>snapppay</c>/<c>digipay</c>).
/// Credentials proper (client id/secret) come from the encrypted <c>payment_gateways.config_json</c> in a
/// full deployment; the base URL + non-secret facts can be defaulted here.</summary>
public Dictionary<string, BnplProviderConnection> Providers { get; set; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>The currency the BNPL providers speak on the wire (<c>TOMAN</c> or <c>IRR</c>); conversion to IRR
/// happens only at the adapter boundary via <c>ICurrencyNormalizer</c>. SnappPay/Digipay speak Rial.</summary>
public string WireCurrency { get; set; } = "IRR";
/// <summary>When true, token/revert/update/cancel all fail so the provider-declined paths are testable.</summary>
public bool ForceFailure { get; set; }
/// <summary>When true, the mock reports the provider returned its commission (a non-null, zero reversal
/// placeholder) so the <c>provider_commission_reversed_amount</c> reconciliation is exercised.</summary>
public bool ReverseProviderCommission { get; set; }
/// <summary>The mock provider merchant commission rate (fraction) the settle deducts. Read from the response
/// by the handler, never hardcoded there; the real adapter reads the true per-contract deducted amount.</summary>
public decimal CommissionRate { get; set; } = 0.10m;
/// <summary>When true the mock settle stamps <c>settled_at = now</c>; false models the non-instant
/// (deferred/T+13/weekly) settlement with a null <c>settled_at</c>.</summary>
public bool SettlementInstant { get; set; } = true;
/// <summary>The provider credit ceiling (IRR); an order above it returns <c>ceiling_exceeded</c>.</summary>
public long CreditCeilingIrr { get; set; } = 2_000_000_000;
/// <summary>A designated test mobile that returns <c>not_eligible</c> so the fall-back-to-card path is testable.</summary>
public string NotEligibleMobile { get; set; } = "09120000099";
}
/// <summary>Non-secret connection facts for one BNPL provider (base URL, sandbox flag, merchant handle). The
/// secret client id/secret live in the encrypted <c>payment_gateways.config_json</c>; a real adapter reads both.</summary>
public sealed class BnplProviderConnection
{
public string BaseUrl { get; set; } = string.Empty;
public bool Sandbox { get; set; }
/// <summary>Optional non-secret merchant/terminal identifier the provider expects on requests.</summary>
public string MerchantId { get; set; } = string.Empty;
}
/// <summary>
/// Tunes the b10 money-path mocks (PSP acquirer, تسهیم split, webhook verifier). The real adapters ignore
/// these — production merchant ids / signing keys come from <c>payment_gateways.config_json</c> and secrets,
/// never from here.
/// </summary>
public sealed class PaymentsOptions
{
/// <summary><c>mock</c> (default) | <c>zarinpal</c> | <c>sadad</c> | <c>vandar</c> | <c>jibit</c> — the card
/// acquirer <c>IPaymentProvider</c> + <c>ISettlementSplitProvider</c> + <c>IWebhookVerifier</c> swap together.</summary>
public string Provider { get; set; } = SeamProviders.Mock;
/// <summary>Acquirer IPG base URL (the payment-request / verify / refund host).</summary>
public string BaseUrl { get; set; } = string.Empty;
/// <summary>The acquirer merchant id / terminal (non-secret handle). Production reads it from the encrypted
/// <c>payment_gateways.config_json</c>; this default enables a single-merchant deployment without the DB row.</summary>
public string MerchantId { get; set; } = string.Empty;
/// <summary>Where the acquirer sends the customer back after the hosted payment page (the return deep-link the
/// adapter passes as the callback URL when opening the IPG session).</summary>
public string CallbackUrl { get; set; } = string.Empty;
/// <summary>Per-provider webhook signing secret (HMAC key), keyed by <c>provider_code</c>. The real
/// <c>IWebhookVerifier</c> verifies the raw callback body against this; a provider with no signature falls back
/// to the mandatory server-side <c>verify</c> re-check.</summary>
public Dictionary<string, string> WebhookSigningSecrets { get; set; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>The header the provider carries its signature in (default <c>X-Signature</c>).</summary>
public string SignatureHeader { get; set; } = "X-Signature";
/// <summary>The platform's own registered IBAN (SHEBA) the split credits the commission leg to.</summary>
public string PlatformSheba { get; set; } = "IR000000000000000000000001";
/// <summary>A callback whose raw body contains this marker is treated as an <b>invalid signature</b> by the
/// mock verifier, so the "unverified callback mutates nothing" path is testable.</summary>
public string InvalidSignatureMarker { get; set; } = "INVALID_SIGNATURE";
}
/// <summary>
/// Tunes the mock <c>IPaymentCaptureSimulator</c> (backend-phase-9). By default every capture succeeds with
/// a fake gateway reference and <see cref="PspFeeAmount"/>. Set <see cref="ForceFailure"/> to exercise the
/// "capture failed → no booking" path. The real b10 card capture ignores these.
/// </summary>
public sealed class PaymentCaptureOptions
{
/// <summary>When true, every capture returns a failed result so conversion refuses to create a booking.</summary>
public bool ForceFailure { get; set; }
/// <summary>The PSP/gateway fee (IRR) the mock reports on a successful capture.</summary>
public long? PspFeeAmount { get; set; }
}
/// <summary>
/// Tunes the mock <c>IShahkarVerifier</c> (phone↔national-id binding). A submitted phone equal to
/// <see cref="SharedSimPhone"/> returns the explicit shared-SIM failure; a national id equal to
/// <see cref="MismatchNationalId"/> returns a plain mismatch; every other pair matches. The real vendor
/// implementation ignores these.
/// </summary>
public sealed class ShahkarOptions
{
/// <summary><c>mock</c> (default) | <c>finnotech</c> — the real شاهکار bridge (shares <c>Seams:Finnotech</c> creds).</summary>
public string Provider { get; set; } = SeamProviders.Mock;
/// <summary>The designated test phone that returns the shared-SIM failure state.</summary>
public string SharedSimPhone { get; set; } = "09120000000";
/// <summary>The designated test national id that returns a plain phone↔national-id mismatch.</summary>
public string MismatchNationalId { get; set; } = "1111111111";
}
/// <summary>
/// Tunes the mock <c>IIdentityKycProvider</c>. A national id equal to <see cref="FailNationalId"/> fails
/// KYC; every other (well-formed) national id passes with <see cref="MatchedName"/>. The real e-KYC vendor
/// implementation ignores these.
/// </summary>
public sealed class IdentityKycOptions
{
/// <summary><c>mock</c> (default) | <c>finnotech</c> — the real e-KYC bridge (shares <c>Seams:Finnotech</c> creds).</summary>
public string Provider { get; set; } = SeamProviders.Mock;
/// <summary>The designated test national id that fails identity KYC.</summary>
public string FailNationalId { get; set; } = "0000000000";
/// <summary>The name the mock reports as matched on a passing KYC (informational; the authoritative
/// identity name for credential cross-check comes from the users row).</summary>
public string MatchedName { get; set; } = "Verified Nurse";
}
/// <summary>
/// Tunes the mock <c>IGeocoder</c>. By default it resolves every address to a deterministic point around
/// the city centroid. Set <see cref="ReturnNullCoordinates"/> to force the unresolved path globally, or
/// embed <see cref="LowConfidenceMarker"/> in a single address to exercise the "saved without a map pin"
/// UI state per-request. The real vendor implementation ignores these.
/// </summary>
public sealed class GeocodingOptions
{
/// <summary><c>mock</c> (default) | <c>neshan</c> — the real Neshan geocoding adapter.</summary>
public string Provider { get; set; } = SeamProviders.Mock;
/// <summary>Neshan API key (secret). The real geocoder sends it as the <c>Api-Key</c> header.</summary>
public string ApiKey { get; set; } = string.Empty;
/// <summary>Neshan API base URL (defaults to the public host at the adapter).</summary>
public string BaseUrl { get; set; } = string.Empty;
/// <summary>When true, every geocode returns null coordinates with low confidence.</summary>
public bool ReturnNullCoordinates { get; set; }
/// <summary>An address whose text contains this marker resolves to null coordinates (testability).</summary>
public string LowConfidenceMarker { get; set; } = "NO_GEO";
/// <summary>Confidence returned for a successfully resolved address.</summary>
public double ResolvedConfidence { get; set; } = 0.9;
}
/// <summary>
/// Tunes the mock <c>IBankAccountOwnershipVerifier</c> (استعلام شبا). A submitted IBAN equal to
/// <see cref="MismatchIban"/> returns an ownership mismatch so the payout-gating path is testable; every
/// other IBAN returns a match. The real vendor implementation ignores these.
/// </summary>
public sealed class BankOwnershipOptions
{
/// <summary><c>mock</c> (default) | <c>finnotech</c> — the real استعلام شبا bridge (shares <c>Seams:Finnotech</c> creds).</summary>
public string Provider { get; set; } = SeamProviders.Mock;
/// <summary>The designated test IBAN that returns <c>matched_national_id = false</c>.</summary>
public string MismatchIban { get; set; } = "IR000000000000000000000000";
/// <summary>The account-holder name the mock echoes back for a matching inquiry.</summary>
public string MatchedHolderName { get; set; } = "Verified Account Holder";
/// <summary>The account-holder name the mock returns for the mismatch IBAN.</summary>
public string MismatchHolderName { get; set; } = "Unmatched Account Holder";
}
public sealed class FieldEncryptionOptions
{
/// <summary>Base64 32-byte AES key. Local-dev default only; override per environment.</summary>
public string Key { get; set; } = string.Empty;
/// <summary>Base64 HMAC key used for deterministic lookup hashes.</summary>
public string HashKey { get; set; } = string.Empty;
}
public sealed class ObjectStorageOptions
{
/// <summary><c>local</c> (default) | <c>s3</c> — S3/MinIO/ArvanCloud object storage with presigned PUT/GET.</summary>
public string Provider { get; set; } = SeamProviders.LocalDisk;
/// <summary>Filesystem root the local-disk mock writes blobs under.</summary>
public string RootPath { get; set; } = string.Empty;
/// <summary>S3-compatible endpoint host, e.g. <c>https://s3.ir-thr-at1.arvanstorage.ir</c> or a MinIO URL.</summary>
public string ServiceUrl { get; set; } = string.Empty;
/// <summary>The bucket blobs are stored in.</summary>
public string Bucket { get; set; } = string.Empty;
/// <summary>The S3 region (SigV4 credential scope; MinIO/ArvanCloud commonly use <c>us-east-1</c> or their own).</summary>
public string Region { get; set; } = "us-east-1";
/// <summary>S3 access key id (secret).</summary>
public string AccessKey { get; set; } = string.Empty;
/// <summary>S3 secret access key (secret).</summary>
public string SecretKey { get; set; } = string.Empty;
/// <summary>Use path-style addressing (<c>{endpoint}/{bucket}/{key}</c>) — required by MinIO/ArvanCloud; AWS
/// proper uses virtual-host style. Default true (path-style) since Iranian S3 endpoints expect it.</summary>
public bool UsePathStyle { get; set; } = true;
/// <summary>How long a presigned GET/PUT URL stays valid (seconds).</summary>
public int PresignExpirySeconds { get; set; } = 900;
}