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