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
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:
@@ -26,7 +26,7 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
ASPNETCORE_ENVIRONMENT: "${ASPNETCORE_ENVIRONMENT:-Development}"
|
ASPNETCORE_ENVIRONMENT: "${ASPNETCORE_ENVIRONMENT:-Production}"
|
||||||
ASPNETCORE_URLS: http://+:8080
|
ASPNETCORE_URLS: http://+:8080
|
||||||
RUN_MIGRATIONS: "${RUN_MIGRATIONS:-true}"
|
RUN_MIGRATIONS: "${RUN_MIGRATIONS:-true}"
|
||||||
ConnectionStrings__DefaultConnection: "${DB_CONNECTION_STRING:-Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass}"
|
ConnectionStrings__DefaultConnection: "${DB_CONNECTION_STRING:-Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass}"
|
||||||
|
|||||||
+1
-1
@@ -76,7 +76,7 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
ASPNETCORE_ENVIRONMENT: "${ASPNETCORE_ENVIRONMENT:-Development}"
|
ASPNETCORE_ENVIRONMENT: "${ASPNETCORE_ENVIRONMENT:-Production}"
|
||||||
ASPNETCORE_URLS: http://+:8080
|
ASPNETCORE_URLS: http://+:8080
|
||||||
RUN_MIGRATIONS: "${RUN_MIGRATIONS:-true}"
|
RUN_MIGRATIONS: "${RUN_MIGRATIONS:-true}"
|
||||||
ConnectionStrings__DefaultConnection: "${DB_CONNECTION_STRING:-Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass}"
|
ConnectionStrings__DefaultConnection: "${DB_CONNECTION_STRING:-Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass}"
|
||||||
|
|||||||
@@ -22,13 +22,14 @@ public class PlatformIntegrationService : IPlatformIntegrationService
|
|||||||
public const string KeyKavenegarEnabled = "integrations.kavenegar.enabled";
|
public const string KeyKavenegarEnabled = "integrations.kavenegar.enabled";
|
||||||
public const string KeyKavenegarSender = "integrations.kavenegar.senderNumber";
|
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 =
|
private static readonly (string Id, string NameFa, string Prefix)[] Gateways =
|
||||||
[
|
[
|
||||||
("zarinpal", "زرینپال", "payment.zarinpal"),
|
("zarinpal", "زرینپال", "payment.zarinpal"),
|
||||||
("tara", "تارا", "payment.tara"),
|
("tara", "تارا", "payment.tara"),
|
||||||
("snapppay", "اسنپپی", "payment.snapppay"),
|
("snapppay", "اسنپپی", "payment.snapppay")
|
||||||
("nextpay", "نکستپی", "payment.nextpay"),
|
|
||||||
("vandar", "وندار", "payment.vandar")
|
|
||||||
];
|
];
|
||||||
|
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
@@ -97,11 +98,6 @@ public class PlatformIntegrationService : IPlatformIntegrationService
|
|||||||
if (!string.IsNullOrWhiteSpace(gw.MerchantId))
|
if (!string.IsNullOrWhiteSpace(gw.MerchantId))
|
||||||
await UpsertAsync($"{meta.Prefix}.merchantId", gw.MerchantId.Trim(), "payment", "مرچنت زرینپال", ct);
|
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)
|
if (gw.Credentials is not null)
|
||||||
await SaveCredentialsAsync(meta.Prefix, gw.Id, gw.Credentials, ct);
|
await SaveCredentialsAsync(meta.Prefix, gw.Id, gw.Credentials, ct);
|
||||||
@@ -211,11 +207,6 @@ public class PlatformIntegrationService : IPlatformIntegrationService
|
|||||||
merchantId = map.GetValueOrDefault($"{prefix}.merchantId");
|
merchantId = map.GetValueOrDefault($"{prefix}.merchantId");
|
||||||
hasSecret = HasSecret(map, $"{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")
|
else if (id == "tara")
|
||||||
{
|
{
|
||||||
credentials = new GatewayCredentialsDto(
|
credentials = new GatewayCredentialsDto(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Meezi.Core.Interfaces;
|
using Meezi.Core.Interfaces;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Meezi.Infrastructure.ExternalServices;
|
namespace Meezi.Infrastructure.ExternalServices;
|
||||||
@@ -19,15 +20,18 @@ public class SnappPayGateway : ISnappPayGateway
|
|||||||
|
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly IPlatformRuntimeConfig _platform;
|
private readonly IPlatformRuntimeConfig _platform;
|
||||||
|
private readonly IHostEnvironment _environment;
|
||||||
private readonly ILogger<SnappPayGateway> _logger;
|
private readonly ILogger<SnappPayGateway> _logger;
|
||||||
|
|
||||||
public SnappPayGateway(
|
public SnappPayGateway(
|
||||||
HttpClient httpClient,
|
HttpClient httpClient,
|
||||||
IPlatformRuntimeConfig platform,
|
IPlatformRuntimeConfig platform,
|
||||||
|
IHostEnvironment environment,
|
||||||
ILogger<SnappPayGateway> logger)
|
ILogger<SnappPayGateway> logger)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_platform = platform;
|
_platform = platform;
|
||||||
|
_environment = environment;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +50,13 @@ public class SnappPayGateway : ISnappPayGateway
|
|||||||
var creds = await LoadCredentialsAsync(cancellationToken);
|
var creds = await LoadCredentialsAsync(cancellationToken);
|
||||||
if (!creds.IsConfigured)
|
if (!creds.IsConfigured)
|
||||||
{
|
{
|
||||||
|
// Mock checkout exists ONLY for local development — in production a
|
||||||
|
// fake-success would activate paid plans without real payment.
|
||||||
|
if (!_environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
_logger.LogError("SnappPay credentials missing — refusing payment init in {Env}", _environment.EnvironmentName);
|
||||||
|
return new SnappPayInitResult(false, null, null, "Payment gateway is not configured.");
|
||||||
|
}
|
||||||
var mockToken = "MOCK-SNAPP-" + transactionId;
|
var mockToken = "MOCK-SNAPP-" + transactionId;
|
||||||
var mockUrl = $"{returnUrl}?paymentToken={mockToken}&transactionId={transactionId}&state=OK";
|
var mockUrl = $"{returnUrl}?paymentToken={mockToken}&transactionId={transactionId}&state=OK";
|
||||||
_logger.LogInformation("SnappPay mock payment {TransactionId} amount {Amount} Rials", transactionId, amountRials);
|
_logger.LogInformation("SnappPay mock payment {TransactionId} amount {Amount} Rials", transactionId, amountRials);
|
||||||
@@ -114,6 +125,13 @@ public class SnappPayGateway : ISnappPayGateway
|
|||||||
{
|
{
|
||||||
if (paymentToken.StartsWith("MOCK-SNAPP-", StringComparison.Ordinal))
|
if (paymentToken.StartsWith("MOCK-SNAPP-", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
|
// Mock tokens are only honoured in local development — in production a
|
||||||
|
// forged MOCK- token on the callback would equal a free verified payment.
|
||||||
|
if (!_environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
_logger.LogError("Rejected MOCK SnappPay token {Token} in {Env}", paymentToken, _environment.EnvironmentName);
|
||||||
|
return new SnappPayVerifyResult(false, null, "Invalid payment token.");
|
||||||
|
}
|
||||||
_logger.LogInformation("SnappPay mock verify {Token}", paymentToken);
|
_logger.LogInformation("SnappPay mock verify {Token}", paymentToken);
|
||||||
return new SnappPayVerifyResult(true, paymentToken, null);
|
return new SnappPayVerifyResult(true, paymentToken, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Meezi.Core.Interfaces;
|
using Meezi.Core.Interfaces;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Meezi.Infrastructure.ExternalServices;
|
namespace Meezi.Infrastructure.ExternalServices;
|
||||||
@@ -16,6 +17,7 @@ public class TaraPaymentGateway : ITaraPaymentGateway
|
|||||||
|
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly IPlatformRuntimeConfig _platform;
|
private readonly IPlatformRuntimeConfig _platform;
|
||||||
|
private readonly IHostEnvironment _environment;
|
||||||
private readonly ILogger<TaraPaymentGateway> _logger;
|
private readonly ILogger<TaraPaymentGateway> _logger;
|
||||||
private string? _cachedToken;
|
private string? _cachedToken;
|
||||||
private DateTime _tokenExpiresUtc = DateTime.MinValue;
|
private DateTime _tokenExpiresUtc = DateTime.MinValue;
|
||||||
@@ -23,10 +25,12 @@ public class TaraPaymentGateway : ITaraPaymentGateway
|
|||||||
public TaraPaymentGateway(
|
public TaraPaymentGateway(
|
||||||
HttpClient httpClient,
|
HttpClient httpClient,
|
||||||
IPlatformRuntimeConfig platform,
|
IPlatformRuntimeConfig platform,
|
||||||
|
IHostEnvironment environment,
|
||||||
ILogger<TaraPaymentGateway> logger)
|
ILogger<TaraPaymentGateway> logger)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_platform = platform;
|
_platform = platform;
|
||||||
|
_environment = environment;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +49,13 @@ public class TaraPaymentGateway : ITaraPaymentGateway
|
|||||||
var creds = await LoadCredentialsAsync(cancellationToken);
|
var creds = await LoadCredentialsAsync(cancellationToken);
|
||||||
if (!creds.IsConfigured)
|
if (!creds.IsConfigured)
|
||||||
{
|
{
|
||||||
|
// Mock checkout exists ONLY for local development — in production a
|
||||||
|
// fake-success would activate paid plans without real payment.
|
||||||
|
if (!_environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
_logger.LogError("Tara credentials missing — refusing payment init in {Env}", _environment.EnvironmentName);
|
||||||
|
return new TaraInitResult(false, null, null, "Payment gateway is not configured.");
|
||||||
|
}
|
||||||
var mockTrace = "MOCK-TARA-" + invoiceNumber;
|
var mockTrace = "MOCK-TARA-" + invoiceNumber;
|
||||||
var mockUrl = $"{callbackUrl}?traceNumber={mockTrace}&status=OK";
|
var mockUrl = $"{callbackUrl}?traceNumber={mockTrace}&status=OK";
|
||||||
_logger.LogInformation("Tara mock payment trace {Trace} amount {Amount} Rials", mockTrace, amountRials);
|
_logger.LogInformation("Tara mock payment trace {Trace} amount {Amount} Rials", mockTrace, amountRials);
|
||||||
@@ -114,6 +125,13 @@ public class TaraPaymentGateway : ITaraPaymentGateway
|
|||||||
{
|
{
|
||||||
if (traceNumber.StartsWith("MOCK-TARA-", StringComparison.Ordinal))
|
if (traceNumber.StartsWith("MOCK-TARA-", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
|
// Mock traces are only honoured in local development — in production a
|
||||||
|
// forged MOCK- trace on the callback would equal a free verified payment.
|
||||||
|
if (!_environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
_logger.LogError("Rejected MOCK Tara trace {Trace} in {Env}", traceNumber, _environment.EnvironmentName);
|
||||||
|
return new TaraVerifyResult(false, null, "Invalid payment trace.");
|
||||||
|
}
|
||||||
_logger.LogInformation("Tara mock verify {Trace}", traceNumber);
|
_logger.LogInformation("Tara mock verify {Trace}", traceNumber);
|
||||||
return new TaraVerifyResult(true, traceNumber, null);
|
return new TaraVerifyResult(true, traceNumber, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
using Meezi.Core.Interfaces;
|
using Meezi.Core.Interfaces;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Meezi.Infrastructure.ExternalServices;
|
namespace Meezi.Infrastructure.ExternalServices;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Taraz (سامانه مودیان) invoice submission. The real API integration is not
|
||||||
|
/// built yet, so this service NEVER fakes a tracking code outside development —
|
||||||
|
/// a merchant must not believe invoices reached the tax authority when they
|
||||||
|
/// did not.
|
||||||
|
/// </summary>
|
||||||
public class TarazTaxService : ITarazTaxService
|
public class TarazTaxService : ITarazTaxService
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly IHostEnvironment _environment;
|
||||||
private readonly ILogger<TarazTaxService> _logger;
|
private readonly ILogger<TarazTaxService> _logger;
|
||||||
|
|
||||||
public TarazTaxService(IConfiguration configuration, ILogger<TarazTaxService> logger)
|
public TarazTaxService(
|
||||||
|
IConfiguration configuration,
|
||||||
|
IHostEnvironment environment,
|
||||||
|
ILogger<TarazTaxService> logger)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
|
_environment = environment;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,20 +32,33 @@ public class TarazTaxService : ITarazTaxService
|
|||||||
DateTime dateUtc,
|
DateTime dateUtc,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var username = _configuration["Taraz:Username"];
|
if (_environment.IsDevelopment())
|
||||||
if (string.IsNullOrWhiteSpace(username))
|
|
||||||
{
|
{
|
||||||
_logger.LogInformation(
|
_logger.LogWarning(
|
||||||
"Taraz not configured — skip submit for cafe {CafeId} date {Date}",
|
"[DEV TARAZ] Pretend-submitted invoices for cafe {CafeId} date {Date}",
|
||||||
cafeId,
|
cafeId,
|
||||||
dateUtc.Date);
|
dateUtc.Date);
|
||||||
return Task.FromResult(new TarazSubmitResult(
|
return Task.FromResult(new TarazSubmitResult(
|
||||||
true,
|
true,
|
||||||
"MOCK-TARAZ",
|
"DEV-TARAZ",
|
||||||
"Taraz API not configured; submission logged only."));
|
"Development stub — nothing was sent to the tax authority."));
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Taraz submit queued for cafe {CafeId} on {Date}", cafeId, dateUtc.Date);
|
var username = _configuration["Taraz:Username"];
|
||||||
return Task.FromResult(new TarazSubmitResult(true, null, "Submission queued."));
|
if (string.IsNullOrWhiteSpace(username))
|
||||||
|
{
|
||||||
|
return Task.FromResult(new TarazSubmitResult(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
"سرویس مودیان هنوز پیکربندی نشده است. لطفاً با پشتیبانی تماس بگیرید."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Credentials exist but the actual Taraz API call is not implemented yet.
|
||||||
|
// Be honest with the merchant instead of returning a fake tracking code.
|
||||||
|
_logger.LogWarning("Taraz submit requested for cafe {CafeId} but the integration is not implemented", cafeId);
|
||||||
|
return Task.FromResult(new TarazSubmitResult(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
"اتصال مستقیم به سامانه مودیان هنوز در دست توسعه است."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Net.Http.Json;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Meezi.Core.Interfaces;
|
using Meezi.Core.Interfaces;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Meezi.Infrastructure.ExternalServices;
|
namespace Meezi.Infrastructure.ExternalServices;
|
||||||
@@ -17,17 +18,20 @@ public class ZarinPalGateway : IZarinPalGateway
|
|||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly IPlatformRuntimeConfig _platform;
|
private readonly IPlatformRuntimeConfig _platform;
|
||||||
|
private readonly IHostEnvironment _environment;
|
||||||
private readonly ILogger<ZarinPalGateway> _logger;
|
private readonly ILogger<ZarinPalGateway> _logger;
|
||||||
|
|
||||||
public ZarinPalGateway(
|
public ZarinPalGateway(
|
||||||
HttpClient httpClient,
|
HttpClient httpClient,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IPlatformRuntimeConfig platform,
|
IPlatformRuntimeConfig platform,
|
||||||
|
IHostEnvironment environment,
|
||||||
ILogger<ZarinPalGateway> logger)
|
ILogger<ZarinPalGateway> logger)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_platform = platform;
|
_platform = platform;
|
||||||
|
_environment = environment;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +44,15 @@ public class ZarinPalGateway : IZarinPalGateway
|
|||||||
var merchantId = await GetMerchantIdAsync(cancellationToken);
|
var merchantId = await GetMerchantIdAsync(cancellationToken);
|
||||||
if (string.IsNullOrWhiteSpace(merchantId))
|
if (string.IsNullOrWhiteSpace(merchantId))
|
||||||
{
|
{
|
||||||
|
// Mock checkout exists ONLY for local development. In production a
|
||||||
|
// missing merchant id must fail loudly — a fake-success here would
|
||||||
|
// activate paid plans without any real payment.
|
||||||
|
if (!_environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
_logger.LogError("ZarinPal merchant id missing — refusing payment init in {Env}", _environment.EnvironmentName);
|
||||||
|
return new ZarinPalRequestResult(false, null, null, "Payment gateway is not configured.");
|
||||||
|
}
|
||||||
|
|
||||||
var mockAuthority = Guid.NewGuid().ToString("N")[..16];
|
var mockAuthority = Guid.NewGuid().ToString("N")[..16];
|
||||||
var mockUrl = $"{callbackUrl}?Authority={mockAuthority}&Status=OK";
|
var mockUrl = $"{callbackUrl}?Authority={mockAuthority}&Status=OK";
|
||||||
_logger.LogInformation("ZarinPal mock payment {Authority} amount {Amount} Rials", mockAuthority, amountRials);
|
_logger.LogInformation("ZarinPal mock payment {Authority} amount {Amount} Rials", mockAuthority, amountRials);
|
||||||
@@ -81,6 +94,11 @@ public class ZarinPalGateway : IZarinPalGateway
|
|||||||
var merchantId = await GetMerchantIdAsync(cancellationToken);
|
var merchantId = await GetMerchantIdAsync(cancellationToken);
|
||||||
if (string.IsNullOrWhiteSpace(merchantId))
|
if (string.IsNullOrWhiteSpace(merchantId))
|
||||||
{
|
{
|
||||||
|
if (!_environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
_logger.LogError("ZarinPal merchant id missing — refusing payment verify in {Env}", _environment.EnvironmentName);
|
||||||
|
return new ZarinPalVerifyResult(false, null, "Payment gateway is not configured.");
|
||||||
|
}
|
||||||
_logger.LogInformation("ZarinPal mock verify authority {Authority}", authority);
|
_logger.LogInformation("ZarinPal mock verify authority {Authority}", authority);
|
||||||
return new ZarinPalVerifyResult(true, "MOCK-" + authority[..8], null);
|
return new ZarinPalVerifyResult(true, "MOCK-" + authority[..8], null);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user