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,151 @@
|
||||
using Meezi.API.Models.Orders;
|
||||
using Meezi.Core.Entities;
|
||||
using Meezi.Core.Enums;
|
||||
using Meezi.Core.Interfaces;
|
||||
using Meezi.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Meezi.API.Services.Delivery;
|
||||
|
||||
public interface IDeliveryStatusSyncService
|
||||
{
|
||||
Task<bool> SyncInternalStatusAsync(
|
||||
string cafeId,
|
||||
string orderId,
|
||||
OrderStatus newStatus,
|
||||
CancellationToken ct = default);
|
||||
|
||||
Task<bool> ApplyPlatformStatusAsync(
|
||||
DeliveryPlatform platform,
|
||||
string externalOrderId,
|
||||
string platformStatus,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class DeliveryStatusSyncService : IDeliveryStatusSyncService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IKdsNotifier _kds;
|
||||
private readonly ISnappfoodClient _snappfood;
|
||||
private readonly ITap30Client _tap30;
|
||||
private readonly IInventoryService _inventory;
|
||||
private readonly ILogger<DeliveryStatusSyncService> _logger;
|
||||
|
||||
public DeliveryStatusSyncService(
|
||||
AppDbContext db,
|
||||
IKdsNotifier kds,
|
||||
ISnappfoodClient snappfood,
|
||||
ITap30Client tap30,
|
||||
IInventoryService inventory,
|
||||
ILogger<DeliveryStatusSyncService> logger)
|
||||
{
|
||||
_db = db;
|
||||
_kds = kds;
|
||||
_snappfood = snappfood;
|
||||
_tap30 = tap30;
|
||||
_inventory = inventory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> SyncInternalStatusAsync(
|
||||
string cafeId,
|
||||
string orderId,
|
||||
OrderStatus newStatus,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var order = await _db.Orders
|
||||
.FirstOrDefaultAsync(o => o.Id == orderId && o.CafeId == cafeId, ct);
|
||||
|
||||
if (order?.DeliveryPlatform is null || string.IsNullOrEmpty(order.ExternalOrderId))
|
||||
return false;
|
||||
|
||||
var platformStatus = MapToPlatformStatus(newStatus);
|
||||
await NotifyPlatformAsync(order.DeliveryPlatform.Value, order.ExternalOrderId, platformStatus, ct);
|
||||
|
||||
if (newStatus == OrderStatus.Delivered && !string.IsNullOrEmpty(order.SnappfoodOrderId))
|
||||
await _snappfood.NotifyOrderDeliveredAsync(order.SnappfoodOrderId, ct);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ApplyPlatformStatusAsync(
|
||||
DeliveryPlatform platform,
|
||||
string externalOrderId,
|
||||
string platformStatus,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var order = await _db.Orders
|
||||
.Include(o => o.Items)
|
||||
.ThenInclude(i => i.MenuItem)
|
||||
.FirstOrDefaultAsync(
|
||||
o => o.DeliveryPlatform == platform && o.ExternalOrderId == externalOrderId,
|
||||
ct);
|
||||
|
||||
if (order is null)
|
||||
return false;
|
||||
|
||||
var mapped = MapFromPlatformStatus(platformStatus);
|
||||
if (order.Status == mapped)
|
||||
return true;
|
||||
|
||||
if (mapped == OrderStatus.Cancelled)
|
||||
await RollbackInventoryPlaceholderAsync(order, ct);
|
||||
|
||||
order.Status = mapped;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
|
||||
await _kds.NotifyOrderStatusChangedAsync(order.CafeId, order.Id, mapped, ct);
|
||||
if (!string.IsNullOrEmpty(order.TableId))
|
||||
await _kds.NotifyTableStatusChangedAsync(order.CafeId, ct);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task NotifyPlatformAsync(
|
||||
DeliveryPlatform platform,
|
||||
string externalOrderId,
|
||||
string status,
|
||||
CancellationToken ct)
|
||||
{
|
||||
switch (platform)
|
||||
{
|
||||
case DeliveryPlatform.Snappfood:
|
||||
await _snappfood.NotifyOrderStatusAsync(externalOrderId, status, ct);
|
||||
break;
|
||||
case DeliveryPlatform.Tap30:
|
||||
await _tap30.NotifyOrderStatusAsync(externalOrderId, status, ct);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Task RollbackInventoryPlaceholderAsync(Order order, CancellationToken ct)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Delivery order {OrderId} cancelled — inventory rollback pending BOM linkage",
|
||||
order.Id);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string MapToPlatformStatus(OrderStatus status) => status switch
|
||||
{
|
||||
OrderStatus.Pending => "pending",
|
||||
OrderStatus.Confirmed => "confirmed",
|
||||
OrderStatus.Preparing => "preparing",
|
||||
OrderStatus.Ready => "ready",
|
||||
OrderStatus.Delivered => "delivered",
|
||||
OrderStatus.Cancelled => "cancelled",
|
||||
_ => "confirmed"
|
||||
};
|
||||
|
||||
private static OrderStatus MapFromPlatformStatus(string platformStatus) =>
|
||||
platformStatus.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"pending" => OrderStatus.Pending,
|
||||
"confirmed" => OrderStatus.Confirmed,
|
||||
"preparing" or "in_progress" => OrderStatus.Preparing,
|
||||
"ready" => OrderStatus.Ready,
|
||||
"delivered" or "completed" => OrderStatus.Delivered,
|
||||
"cancelled" or "canceled" => OrderStatus.Cancelled,
|
||||
_ => OrderStatus.Confirmed
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user