feat(api): .NET 10 multi-tenant REST API
Full backend implementation: - Multi-tenant cafe/restaurant management (menus, orders, tables, staff) - POS order flow with ZarinPal and Snappfood payment integration - OTP authentication via Kavenegar SMS - QR digital menu with public discover/finder endpoints - Customer loyalty, coupons, CRM - PostgreSQL via EF Core, Redis for caching/sessions - Background jobs, webhook handlers - Full migration history Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
using Meezi.API.Models.Billing;
|
||||
using Meezi.Core.Enums;
|
||||
using Meezi.Core.Interfaces;
|
||||
|
||||
namespace Meezi.API.Services;
|
||||
|
||||
public record PaymentInitResult(bool Success, string? Authority, string? PaymentUrl, string? ErrorMessage);
|
||||
|
||||
public record PaymentVerifyResult(bool Success, string? RefId, string? ErrorMessage);
|
||||
|
||||
public interface IBillingPaymentOrchestrator
|
||||
{
|
||||
Task<IReadOnlyList<PaymentMethodDto>> GetEnabledMethodsAsync(CancellationToken cancellationToken = default);
|
||||
Task<PaymentInitResult> InitiateAsync(
|
||||
PaymentProvider provider,
|
||||
long amountRials,
|
||||
string paymentId,
|
||||
string description,
|
||||
string callbackUrl,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<PaymentVerifyResult> VerifyAsync(
|
||||
PaymentProvider provider,
|
||||
string externalId,
|
||||
long amountRials,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class BillingPaymentOrchestrator : IBillingPaymentOrchestrator
|
||||
{
|
||||
private readonly IZarinPalGateway _zarinPal;
|
||||
private readonly ITaraPaymentGateway _tara;
|
||||
private readonly ISnappPayGateway _snappPay;
|
||||
private readonly IPlatformRuntimeConfig _platform;
|
||||
|
||||
private static readonly (string Id, string NameFa)[] AllMethods =
|
||||
[
|
||||
(PaymentProviderIds.ZarinPal, "زرینپال"),
|
||||
(PaymentProviderIds.Tara, "تارا (اعتباری)"),
|
||||
(PaymentProviderIds.SnappPay, "اسنپپی (اقساطی)")
|
||||
];
|
||||
|
||||
public BillingPaymentOrchestrator(
|
||||
IZarinPalGateway zarinPal,
|
||||
ITaraPaymentGateway tara,
|
||||
ISnappPayGateway snappPay,
|
||||
IPlatformRuntimeConfig platform)
|
||||
{
|
||||
_zarinPal = zarinPal;
|
||||
_tara = tara;
|
||||
_snappPay = snappPay;
|
||||
_platform = platform;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PaymentMethodDto>> GetEnabledMethodsAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var defaultId = await _platform.GetAsync("payment.activeGateway", cancellationToken) ?? PaymentProviderIds.ZarinPal;
|
||||
var list = new List<PaymentMethodDto>();
|
||||
|
||||
if (await _zarinPal.IsEnabledAsync(cancellationToken))
|
||||
list.Add(new PaymentMethodDto(PaymentProviderIds.ZarinPal, "زرینپال", defaultId == PaymentProviderIds.ZarinPal));
|
||||
if (await _tara.IsEnabledAsync(cancellationToken))
|
||||
list.Add(new PaymentMethodDto(PaymentProviderIds.Tara, "تارا (اعتباری)", defaultId == PaymentProviderIds.Tara));
|
||||
if (await _snappPay.IsEnabledAsync(cancellationToken))
|
||||
list.Add(new PaymentMethodDto(PaymentProviderIds.SnappPay, "اسنپپی (اقساطی)", defaultId == PaymentProviderIds.SnappPay));
|
||||
|
||||
if (list.Count == 0)
|
||||
list.Add(new PaymentMethodDto(PaymentProviderIds.ZarinPal, "زرینپال", true));
|
||||
|
||||
if (list.All(m => !m.IsDefault))
|
||||
list[0] = list[0] with { IsDefault = true };
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<PaymentInitResult> InitiateAsync(
|
||||
PaymentProvider provider,
|
||||
long amountRials,
|
||||
string paymentId,
|
||||
string description,
|
||||
string callbackUrl,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return provider switch
|
||||
{
|
||||
PaymentProvider.Tara => await InitiateTaraAsync(amountRials, paymentId, callbackUrl, cancellationToken),
|
||||
PaymentProvider.SnappPay => await InitiateSnappPayAsync(amountRials, paymentId, callbackUrl, cancellationToken),
|
||||
_ => await InitiateZarinPalAsync(amountRials, description, callbackUrl, cancellationToken)
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<PaymentVerifyResult> VerifyAsync(
|
||||
PaymentProvider provider,
|
||||
string externalId,
|
||||
long amountRials,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return provider switch
|
||||
{
|
||||
PaymentProvider.Tara =>
|
||||
Map(await _tara.VerifyPaymentAsync(externalId, cancellationToken)),
|
||||
PaymentProvider.SnappPay =>
|
||||
Map(await _snappPay.VerifyAndSettleAsync(externalId, cancellationToken)),
|
||||
_ =>
|
||||
Map(await _zarinPal.VerifyPaymentAsync(externalId, amountRials, cancellationToken))
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<PaymentInitResult> InitiateZarinPalAsync(
|
||||
long amountRials,
|
||||
string description,
|
||||
string callbackUrl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await _zarinPal.IsEnabledAsync(cancellationToken))
|
||||
return new PaymentInitResult(false, null, null, "ZarinPal is disabled.");
|
||||
|
||||
var r = await _zarinPal.RequestPaymentAsync(amountRials, description, callbackUrl, cancellationToken);
|
||||
return new PaymentInitResult(r.Success, r.Authority, r.PaymentUrl, r.ErrorMessage);
|
||||
}
|
||||
|
||||
private async Task<PaymentInitResult> InitiateTaraAsync(
|
||||
long amountRials,
|
||||
string paymentId,
|
||||
string callbackUrl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await _tara.IsEnabledAsync(cancellationToken))
|
||||
return new PaymentInitResult(false, null, null, "Tara is disabled.");
|
||||
|
||||
var r = await _tara.RequestPaymentAsync(amountRials, paymentId, callbackUrl, cancellationToken);
|
||||
return new PaymentInitResult(r.Success, r.TraceNumber, r.PaymentUrl, r.ErrorMessage);
|
||||
}
|
||||
|
||||
private async Task<PaymentInitResult> InitiateSnappPayAsync(
|
||||
long amountRials,
|
||||
string paymentId,
|
||||
string callbackUrl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await _snappPay.IsEnabledAsync(cancellationToken))
|
||||
return new PaymentInitResult(false, null, null, "Snapp Pay is disabled.");
|
||||
|
||||
var r = await _snappPay.RequestPaymentAsync(amountRials, paymentId, callbackUrl, cancellationToken);
|
||||
return new PaymentInitResult(r.Success, r.PaymentToken, r.PaymentUrl, r.ErrorMessage);
|
||||
}
|
||||
|
||||
private static PaymentVerifyResult Map(ZarinPalVerifyResult r) =>
|
||||
new(r.Success, r.RefId, r.ErrorMessage);
|
||||
|
||||
private static PaymentVerifyResult Map(TaraVerifyResult r) =>
|
||||
new(r.Success, r.RefId, r.ErrorMessage);
|
||||
|
||||
private static PaymentVerifyResult Map(SnappPayVerifyResult r) =>
|
||||
new(r.Success, r.RefId, r.ErrorMessage);
|
||||
}
|
||||
Reference in New Issue
Block a user