diff --git a/docker-compose.admin.yml b/docker-compose.admin.yml index 23d9323..cee7b16 100644 --- a/docker-compose.admin.yml +++ b/docker-compose.admin.yml @@ -26,7 +26,7 @@ services: redis: condition: service_healthy environment: - ASPNETCORE_ENVIRONMENT: "${ASPNETCORE_ENVIRONMENT:-Development}" + ASPNETCORE_ENVIRONMENT: "${ASPNETCORE_ENVIRONMENT:-Production}" ASPNETCORE_URLS: http://+:8080 RUN_MIGRATIONS: "${RUN_MIGRATIONS:-true}" ConnectionStrings__DefaultConnection: "${DB_CONNECTION_STRING:-Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass}" diff --git a/docker-compose.yml b/docker-compose.yml index bc98c77..f4c9e55 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -76,7 +76,7 @@ services: redis: condition: service_healthy environment: - ASPNETCORE_ENVIRONMENT: "${ASPNETCORE_ENVIRONMENT:-Development}" + ASPNETCORE_ENVIRONMENT: "${ASPNETCORE_ENVIRONMENT:-Production}" ASPNETCORE_URLS: http://+:8080 RUN_MIGRATIONS: "${RUN_MIGRATIONS:-true}" ConnectionStrings__DefaultConnection: "${DB_CONNECTION_STRING:-Host=postgres;Port=5432;Database=meezi;Username=meezi;Password=meezi_local_pass}" diff --git a/src/Meezi.Admin.API/Services/PlatformIntegrationService.cs b/src/Meezi.Admin.API/Services/PlatformIntegrationService.cs index 210aeab..b51ef65 100644 --- a/src/Meezi.Admin.API/Services/PlatformIntegrationService.cs +++ b/src/Meezi.Admin.API/Services/PlatformIntegrationService.cs @@ -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( diff --git a/src/Meezi.Infrastructure/ExternalServices/SnappPayGateway.cs b/src/Meezi.Infrastructure/ExternalServices/SnappPayGateway.cs index 7fad88e..587717c 100644 --- a/src/Meezi.Infrastructure/ExternalServices/SnappPayGateway.cs +++ b/src/Meezi.Infrastructure/ExternalServices/SnappPayGateway.cs @@ -4,6 +4,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Meezi.Core.Interfaces; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Meezi.Infrastructure.ExternalServices; @@ -19,15 +20,18 @@ public class SnappPayGateway : ISnappPayGateway private readonly HttpClient _httpClient; private readonly IPlatformRuntimeConfig _platform; + private readonly IHostEnvironment _environment; private readonly ILogger _logger; public SnappPayGateway( HttpClient httpClient, IPlatformRuntimeConfig platform, + IHostEnvironment environment, ILogger logger) { _httpClient = httpClient; _platform = platform; + _environment = environment; _logger = logger; } @@ -46,6 +50,13 @@ public class SnappPayGateway : ISnappPayGateway var creds = await LoadCredentialsAsync(cancellationToken); 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 mockUrl = $"{returnUrl}?paymentToken={mockToken}&transactionId={transactionId}&state=OK"; _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)) { + // 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); return new SnappPayVerifyResult(true, paymentToken, null); } diff --git a/src/Meezi.Infrastructure/ExternalServices/TaraPaymentGateway.cs b/src/Meezi.Infrastructure/ExternalServices/TaraPaymentGateway.cs index 61e082f..4b1d116 100644 --- a/src/Meezi.Infrastructure/ExternalServices/TaraPaymentGateway.cs +++ b/src/Meezi.Infrastructure/ExternalServices/TaraPaymentGateway.cs @@ -1,6 +1,7 @@ using System.Net.Http.Json; using System.Text.Json; using Meezi.Core.Interfaces; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Meezi.Infrastructure.ExternalServices; @@ -16,6 +17,7 @@ public class TaraPaymentGateway : ITaraPaymentGateway private readonly HttpClient _httpClient; private readonly IPlatformRuntimeConfig _platform; + private readonly IHostEnvironment _environment; private readonly ILogger _logger; private string? _cachedToken; private DateTime _tokenExpiresUtc = DateTime.MinValue; @@ -23,10 +25,12 @@ public class TaraPaymentGateway : ITaraPaymentGateway public TaraPaymentGateway( HttpClient httpClient, IPlatformRuntimeConfig platform, + IHostEnvironment environment, ILogger logger) { _httpClient = httpClient; _platform = platform; + _environment = environment; _logger = logger; } @@ -45,6 +49,13 @@ public class TaraPaymentGateway : ITaraPaymentGateway var creds = await LoadCredentialsAsync(cancellationToken); 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 mockUrl = $"{callbackUrl}?traceNumber={mockTrace}&status=OK"; _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)) { + // 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); return new TaraVerifyResult(true, traceNumber, null); } diff --git a/src/Meezi.Infrastructure/ExternalServices/TarazTaxService.cs b/src/Meezi.Infrastructure/ExternalServices/TarazTaxService.cs index f77a90b..7c8c248 100644 --- a/src/Meezi.Infrastructure/ExternalServices/TarazTaxService.cs +++ b/src/Meezi.Infrastructure/ExternalServices/TarazTaxService.cs @@ -1,17 +1,29 @@ using Meezi.Core.Interfaces; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Meezi.Infrastructure.ExternalServices; +/// +/// 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. +/// public class TarazTaxService : ITarazTaxService { private readonly IConfiguration _configuration; + private readonly IHostEnvironment _environment; private readonly ILogger _logger; - public TarazTaxService(IConfiguration configuration, ILogger logger) + public TarazTaxService( + IConfiguration configuration, + IHostEnvironment environment, + ILogger logger) { _configuration = configuration; + _environment = environment; _logger = logger; } @@ -20,20 +32,33 @@ public class TarazTaxService : ITarazTaxService DateTime dateUtc, CancellationToken cancellationToken = default) { - var username = _configuration["Taraz:Username"]; - if (string.IsNullOrWhiteSpace(username)) + if (_environment.IsDevelopment()) { - _logger.LogInformation( - "Taraz not configured — skip submit for cafe {CafeId} date {Date}", + _logger.LogWarning( + "[DEV TARAZ] Pretend-submitted invoices for cafe {CafeId} date {Date}", cafeId, dateUtc.Date); return Task.FromResult(new TarazSubmitResult( true, - "MOCK-TARAZ", - "Taraz API not configured; submission logged only.")); + "DEV-TARAZ", + "Development stub — nothing was sent to the tax authority.")); } - _logger.LogInformation("Taraz submit queued for cafe {CafeId} on {Date}", cafeId, dateUtc.Date); - return Task.FromResult(new TarazSubmitResult(true, null, "Submission queued.")); + var username = _configuration["Taraz:Username"]; + 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, + "اتصال مستقیم به سامانه مودیان هنوز در دست توسعه است.")); } } diff --git a/src/Meezi.Infrastructure/ExternalServices/ZarinPalGateway.cs b/src/Meezi.Infrastructure/ExternalServices/ZarinPalGateway.cs index 52a0e5e..3211325 100644 --- a/src/Meezi.Infrastructure/ExternalServices/ZarinPalGateway.cs +++ b/src/Meezi.Infrastructure/ExternalServices/ZarinPalGateway.cs @@ -2,6 +2,7 @@ using System.Net.Http.Json; using System.Text.Json.Serialization; using Meezi.Core.Interfaces; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Meezi.Infrastructure.ExternalServices; @@ -17,17 +18,20 @@ public class ZarinPalGateway : IZarinPalGateway private readonly HttpClient _httpClient; private readonly IConfiguration _configuration; private readonly IPlatformRuntimeConfig _platform; + private readonly IHostEnvironment _environment; private readonly ILogger _logger; public ZarinPalGateway( HttpClient httpClient, IConfiguration configuration, IPlatformRuntimeConfig platform, + IHostEnvironment environment, ILogger logger) { _httpClient = httpClient; _configuration = configuration; _platform = platform; + _environment = environment; _logger = logger; } @@ -40,6 +44,15 @@ public class ZarinPalGateway : IZarinPalGateway var merchantId = await GetMerchantIdAsync(cancellationToken); 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 mockUrl = $"{callbackUrl}?Authority={mockAuthority}&Status=OK"; _logger.LogInformation("ZarinPal mock payment {Authority} amount {Amount} Rials", mockAuthority, amountRials); @@ -81,6 +94,11 @@ public class ZarinPalGateway : IZarinPalGateway var merchantId = await GetMerchantIdAsync(cancellationToken); 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); return new ZarinPalVerifyResult(true, "MOCK-" + authority[..8], null); }