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;
}