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 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user