fix(prod): payment/tax gateways never fake success outside Development
CI/CD / CI · API (dotnet build + test) (push) Successful in 41s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 29s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m7s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 51s
CI/CD / Deploy · all services (push) Successful in 1m31s

Production-readiness audit fixes — every mock fallback is now gated on
IsDevelopment; in production these paths fail loudly instead:

- ZarinPal/Tara/SnappPay init: missing credentials returned a MOCK
  payment URL whose callback verified as paid — a café could activate a
  paid plan without paying. Now: "Payment gateway is not configured."
- Tara/SnappPay verify: a forged MOCK-* trace/token on the callback was
  accepted as a verified payment in any environment. Now rejected
  outside Development.
- Taraz (سامانه مودیان): returned a fake MOCK-TARAZ tracking code as if
  invoices reached the tax authority. Now returns an honest error (the
  real integration is not built yet).
- Admin integrations: NextPay/Vandar removed — they were listed but have
  no gateway implementation (selecting them silently used ZarinPal).
- docker-compose: ASPNETCORE_ENVIRONMENT default flipped Development →
  Production so a missing env var can never run prod in dev mode.

86 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-12 10:16:01 +03:30
parent 00649d0248
commit 9765491f6f
7 changed files with 94 additions and 24 deletions
@@ -22,13 +22,14 @@ public class PlatformIntegrationService : IPlatformIntegrationService
public const string KeyKavenegarEnabled = "integrations.kavenegar.enabled";
public const string KeyKavenegarSender = "integrations.kavenegar.senderNumber";
// Only gateways the BillingPaymentOrchestrator actually implements. NextPay
// and Vandar were listed here before they had any implementation — enabling
// them silently fell back to ZarinPal, which is a trap for the operator.
private static readonly (string Id, string NameFa, string Prefix)[] Gateways =
[
("zarinpal", "زرین‌پال", "payment.zarinpal"),
("tara", "تارا", "payment.tara"),
("snapppay", "اسنپ‌پی", "payment.snapppay"),
("nextpay", "نکست‌پی", "payment.nextpay"),
("vandar", "وندار", "payment.vandar")
("snapppay", "اسنپ‌پی", "payment.snapppay")
];
private readonly AppDbContext _db;
@@ -97,11 +98,6 @@ public class PlatformIntegrationService : IPlatformIntegrationService
if (!string.IsNullOrWhiteSpace(gw.MerchantId))
await UpsertAsync($"{meta.Prefix}.merchantId", gw.MerchantId.Trim(), "payment", "مرچنت زرین‌پال", ct);
}
else if (gw.Id is "nextpay" or "vandar")
{
if (!string.IsNullOrWhiteSpace(gw.ApiKey) && !IsMaskedPlaceholder(gw.ApiKey))
await UpsertAsync($"{meta.Prefix}.apiKey", gw.ApiKey.Trim(), "payment", $"توکن {meta.NameFa}", ct);
}
if (gw.Credentials is not null)
await SaveCredentialsAsync(meta.Prefix, gw.Id, gw.Credentials, ct);
@@ -211,11 +207,6 @@ public class PlatformIntegrationService : IPlatformIntegrationService
merchantId = map.GetValueOrDefault($"{prefix}.merchantId");
hasSecret = HasSecret(map, $"{prefix}.merchantId");
}
else if (id is "nextpay" or "vandar")
{
apiKey = MaskSecret(map.GetValueOrDefault($"{prefix}.apiKey"));
hasSecret = HasSecret(map, $"{prefix}.apiKey");
}
else if (id == "tara")
{
credentials = new GatewayCredentialsDto(