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
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1,26 @@
using Meezi.API.Services;
namespace Meezi.API.Jobs;
public class BranchPermanentDeleteJob
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<BranchPermanentDeleteJob> _logger;
public BranchPermanentDeleteJob(
IServiceScopeFactory scopeFactory,
ILogger<BranchPermanentDeleteJob> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
public async Task ExecuteAsync()
{
await using var scope = _scopeFactory.CreateAsyncScope();
var lifecycle = scope.ServiceProvider.GetRequiredService<IBranchLifecycleService>();
var purged = await lifecycle.PurgeExpiredDeletionsAsync();
if (purged > 0)
_logger.LogInformation("Permanently deleted {Count} expired branches", purged);
}
}
@@ -0,0 +1,61 @@
using Meezi.API.Services;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Meezi.API.Jobs;
public class GenerateYesterdayReportsJob
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<GenerateYesterdayReportsJob> _logger;
public GenerateYesterdayReportsJob(
IServiceScopeFactory scopeFactory,
ILogger<GenerateYesterdayReportsJob> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
public async Task ExecuteAsync()
{
await using var scope = _scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var reports = scope.ServiceProvider.GetRequiredService<IDailyReportService>();
var reportDate = IranCalendar.TodayInIran.AddDays(-1);
var branches = await db.Branches
.Where(b => b.IsActive && b.DeletedAt == null)
.Select(b => new { b.Id, b.CafeId })
.ToListAsync();
var success = 0;
var failed = 0;
foreach (var branch in branches)
{
try
{
await reports.GenerateReportAsync(branch.CafeId, branch.Id, reportDate);
success++;
}
catch (Exception ex)
{
failed++;
_logger.LogWarning(
ex,
"Failed daily report for cafe {CafeId} branch {BranchId} date {Date}",
branch.CafeId,
branch.Id,
reportDate);
}
}
_logger.LogInformation(
"Yesterday daily reports ({Date}): {Success} ok, {Failed} failed",
reportDate,
success,
failed);
}
}
@@ -0,0 +1,34 @@
using Hangfire;
using Meezi.Core.Delivery;
using Meezi.API.Services.Delivery;
namespace Meezi.API.Jobs;
public class ProcessDeliveryOrderJob
{
private readonly IDeliveryOrderProcessor _processor;
private readonly ILogger<ProcessDeliveryOrderJob> _logger;
public ProcessDeliveryOrderJob(
IDeliveryOrderProcessor processor,
ILogger<ProcessDeliveryOrderJob> logger)
{
_processor = processor;
_logger = logger;
}
[AutomaticRetry(Attempts = 3, DelaysInSeconds = [10, 60, 300])]
public async Task ExecuteAsync(
string webhookLogId,
UnifiedDeliveryOrder unified,
CancellationToken cancellationToken)
{
var result = await _processor.ProcessAsync(webhookLogId, unified, cancellationToken);
if (!result.Success && result.ErrorCode is not null)
_logger.LogWarning(
"Delivery process failed {Code}: {Message} (log {LogId})",
result.ErrorCode,
result.Message,
webhookLogId);
}
}
@@ -0,0 +1,60 @@
using Meezi.Core.Enums;
using Meezi.Core.Interfaces;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Meezi.API.Jobs;
public class SubscriptionRenewalReminderJob
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<SubscriptionRenewalReminderJob> _logger;
public SubscriptionRenewalReminderJob(
IServiceScopeFactory scopeFactory,
ILogger<SubscriptionRenewalReminderJob> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
public async Task ExecuteAsync()
{
await using var scope = _scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var sms = scope.ServiceProvider.GetRequiredService<ISmsService>();
var windowStart = DateTime.UtcNow.Date;
var windowEnd = windowStart.AddDays(3);
var cafes = await db.Cafes
.Where(c => c.PlanTier != PlanTier.Free
&& c.PlanExpiresAt != null
&& c.PlanExpiresAt >= windowStart
&& c.PlanExpiresAt <= windowEnd)
.ToListAsync();
foreach (var cafe in cafes)
{
var ownerPhone = await db.Employees
.Where(e => e.CafeId == cafe.Id && e.Role == EmployeeRole.Owner)
.Select(e => e.Phone)
.FirstOrDefaultAsync();
if (string.IsNullOrEmpty(ownerPhone)) continue;
var message =
$"میزی: اشتراک {cafe.PlanTier} شما تا {cafe.PlanExpiresAt:yyyy-MM-dd} منقضی می‌شود. از تنظیمات داشبورد تمدید کنید.";
try
{
await sms.SendMessageAsync(ownerPhone, message);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Renewal SMS failed for cafe {CafeId}", cafe.Id);
}
}
_logger.LogInformation("Subscription renewal reminders sent for {Count} cafes", cafes.Count);
}
}