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:
soroush.asadi
2026-05-27 21:33:48 +03:30
parent 03376b3ea1
commit ef15fd6247
472 changed files with 120358 additions and 0 deletions
@@ -0,0 +1,9 @@
namespace Meezi.Core.Interfaces;
/// <summary>Optional branch scope from JWT (staff terminal / POS session).</summary>
public interface IBranchContext
{
string? CafeId { get; }
string? BranchId { get; }
bool HasBranch { get; }
}
@@ -0,0 +1,8 @@
namespace Meezi.Core.Interfaces;
public interface IPlatformRuntimeConfig
{
Task<string?> GetAsync(string key, CancellationToken cancellationToken = default);
Task<IReadOnlyDictionary<string, string>> GetByPrefixAsync(string prefix, CancellationToken cancellationToken = default);
void InvalidateCache();
}
+7
View File
@@ -0,0 +1,7 @@
namespace Meezi.Core.Interfaces;
public interface ISmsService
{
Task SendOtpAsync(string phone, string otp, CancellationToken cancellationToken = default);
Task SendMessageAsync(string phone, string message, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,18 @@
namespace Meezi.Core.Interfaces;
public record SnappPayInitResult(bool Success, string? PaymentToken, string? PaymentUrl, string? ErrorMessage);
public record SnappPayVerifyResult(bool Success, string? RefId, string? ErrorMessage);
public interface ISnappPayGateway
{
Task<bool> IsEnabledAsync(CancellationToken cancellationToken = default);
Task<SnappPayInitResult> RequestPaymentAsync(
long amountRials,
string transactionId,
string returnUrl,
CancellationToken cancellationToken = default);
Task<SnappPayVerifyResult> VerifyAndSettleAsync(
string paymentToken,
CancellationToken cancellationToken = default);
}
@@ -0,0 +1,11 @@
namespace Meezi.Core.Interfaces;
public interface ISnappfoodClient
{
Task AcknowledgeOrderAsync(string snappfoodOrderId, CancellationToken cancellationToken = default);
Task NotifyOrderDeliveredAsync(string snappfoodOrderId, CancellationToken cancellationToken = default);
Task NotifyOrderStatusAsync(
string snappfoodOrderId,
string status,
CancellationToken cancellationToken = default);
}
+10
View File
@@ -0,0 +1,10 @@
namespace Meezi.Core.Interfaces;
public interface ITap30Client
{
Task AcknowledgeOrderAsync(string tap30OrderId, CancellationToken cancellationToken = default);
Task NotifyOrderStatusAsync(
string tap30OrderId,
string status,
CancellationToken cancellationToken = default);
}
@@ -0,0 +1,18 @@
namespace Meezi.Core.Interfaces;
public record TaraInitResult(bool Success, string? TraceNumber, string? PaymentUrl, string? ErrorMessage);
public record TaraVerifyResult(bool Success, string? RefId, string? ErrorMessage);
public interface ITaraPaymentGateway
{
Task<bool> IsEnabledAsync(CancellationToken cancellationToken = default);
Task<TaraInitResult> RequestPaymentAsync(
long amountRials,
string invoiceNumber,
string callbackUrl,
CancellationToken cancellationToken = default);
Task<TaraVerifyResult> VerifyPaymentAsync(
string traceNumber,
CancellationToken cancellationToken = default);
}
@@ -0,0 +1,8 @@
namespace Meezi.Core.Interfaces;
public record TarazSubmitResult(bool Success, string? TrackingCode, string? Message);
public interface ITarazTaxService
{
Task<TarazSubmitResult> SubmitDailyInvoicesAsync(string cafeId, DateTime dateUtc, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,17 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Interfaces;
public interface ITenantContext
{
string? UserId { get; }
string? CafeId { get; }
EmployeeRole? Role { get; }
PlanTier? PlanTier { get; }
string? Language { get; }
/// <summary>Active branch from JWT when employee is branch-scoped.</summary>
string? BranchId { get; }
bool IsSystemAdmin { get; }
bool IsAuthenticated { get; }
bool IsCafeOwner => Role == EmployeeRole.Owner;
}
@@ -0,0 +1,14 @@
namespace Meezi.Core.Interfaces;
public interface IWebsiteService
{
Task<(IReadOnlyList<object> Posts, int Total)> GetPostsAsync(
string locale, int page, int limit, CancellationToken ct = default);
Task<object?> GetPostAsync(string slug, string locale, CancellationToken ct = default);
Task<IReadOnlyList<object>> GetCommentsAsync(string slug, CancellationToken ct = default);
Task<object> AddCommentAsync(string slug, string authorName, string? email,
string content, string? ip, CancellationToken ct = default);
Task<object> CreateDemoRequestAsync(string contactName, string businessName,
string phone, string? email, string branchCount, string? notes, string source,
CancellationToken ct = default);
}
@@ -0,0 +1,21 @@
namespace Meezi.Core.Interfaces;
public record ZarinPalRequestResult(bool Success, string? Authority, string? PaymentUrl, string? ErrorMessage);
public record ZarinPalVerifyResult(bool Success, string? RefId, string? ErrorMessage);
public interface IZarinPalGateway
{
Task<bool> IsEnabledAsync(CancellationToken cancellationToken = default);
Task<ZarinPalRequestResult> RequestPaymentAsync(
long amountRials,
string description,
string callbackUrl,
CancellationToken cancellationToken = default);
Task<ZarinPalVerifyResult> VerifyPaymentAsync(
string authority,
long amountRials,
CancellationToken cancellationToken = default);
}