ef15fd6247
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>
163 lines
5.9 KiB
C#
163 lines
5.9 KiB
C#
using Microsoft.AspNetCore.SignalR;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Meezi.API.Hubs;
|
|
using Meezi.API.Models.Notifications;
|
|
using Meezi.API.Models.Orders;
|
|
using Meezi.API.Models.Public;
|
|
using Meezi.API.Services.Printing;
|
|
using Meezi.Core.Entities;
|
|
using Meezi.Core.Enums;
|
|
using Meezi.Core.Interfaces;
|
|
using Meezi.Infrastructure.Data;
|
|
|
|
namespace Meezi.API.Services;
|
|
|
|
public class OrderNotificationService : IOrderNotificationService
|
|
{
|
|
private readonly AppDbContext _db;
|
|
private readonly IHubContext<KdsHub> _kdsHub;
|
|
private readonly IHubContext<GuestOrderHub> _guestHub;
|
|
private readonly ISmsService _sms;
|
|
private readonly ILogger<OrderNotificationService> _logger;
|
|
|
|
public OrderNotificationService(
|
|
AppDbContext db,
|
|
IHubContext<KdsHub> kdsHub,
|
|
IHubContext<GuestOrderHub> guestHub,
|
|
ISmsService sms,
|
|
ILogger<OrderNotificationService> logger)
|
|
{
|
|
_db = db;
|
|
_kdsHub = kdsHub;
|
|
_guestHub = guestHub;
|
|
_sms = sms;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task NotifyGuestOrderPlacedAsync(Order order, LiveOrderDto live, CancellationToken ct = default)
|
|
{
|
|
if (order.Source != OrderSource.GuestQr)
|
|
return;
|
|
|
|
var tableNumber = await ResolveTableNumberAsync(order, ct);
|
|
var orderNumber = ReceiptPrintFormatting.OrderNumberLabel(order.DisplayNumber > 0 ? order.DisplayNumber : ReceiptPrintFormatting.StableDisplayNumberFromId(order.Id));
|
|
var notification = new CafeNotification
|
|
{
|
|
CafeId = order.CafeId,
|
|
Type = "guest_order_new",
|
|
Title = $"سفارش جدید میز {tableNumber ?? "—"}",
|
|
Body = $"شماره {orderNumber} · {order.Total:N0} ت",
|
|
ReferenceId = order.Id,
|
|
TableNumber = tableNumber
|
|
};
|
|
_db.CafeNotifications.Add(notification);
|
|
await _db.SaveChangesAsync(ct);
|
|
|
|
var dto = MapNotification(notification);
|
|
await _kdsHub.Clients.Group(KdsHub.GroupName(order.CafeId))
|
|
.SendAsync("NotificationReceived", dto, ct);
|
|
|
|
await PushGuestTrackAsync(order, ct);
|
|
}
|
|
|
|
public async Task NotifyOrderStatusChangedAsync(Order order, CancellationToken ct = default)
|
|
{
|
|
await PushGuestTrackAsync(order, ct);
|
|
|
|
if (order.Source != OrderSource.GuestQr)
|
|
return;
|
|
|
|
if (order.Status == OrderStatus.Ready)
|
|
{
|
|
var tableNumber = await ResolveTableNumberAsync(order, ct);
|
|
var orderNumber = ReceiptPrintFormatting.OrderNumberLabel(order.DisplayNumber > 0 ? order.DisplayNumber : ReceiptPrintFormatting.StableDisplayNumberFromId(order.Id));
|
|
var readyNotification = new CafeNotification
|
|
{
|
|
CafeId = order.CafeId,
|
|
Type = "guest_order_ready",
|
|
Title = $"سفارش آماده — میز {tableNumber ?? "—"}",
|
|
Body = $"شماره {orderNumber}",
|
|
ReferenceId = order.Id,
|
|
TableNumber = tableNumber
|
|
};
|
|
_db.CafeNotifications.Add(readyNotification);
|
|
await _db.SaveChangesAsync(ct);
|
|
await _kdsHub.Clients.Group(KdsHub.GroupName(order.CafeId))
|
|
.SendAsync("NotificationReceived", MapNotification(readyNotification), ct);
|
|
|
|
await TrySmsGuestReadyAsync(order, tableNumber, orderNumber, ct);
|
|
}
|
|
}
|
|
|
|
private async Task PushGuestTrackAsync(Order order, CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrEmpty(order.GuestTrackingToken))
|
|
return;
|
|
|
|
var full = await _db.Orders
|
|
.Include(o => o.Items)
|
|
.ThenInclude(i => i.MenuItem)
|
|
.Include(o => o.Table)
|
|
.FirstOrDefaultAsync(o => o.Id == order.Id, cancellationToken);
|
|
if (full is null) return;
|
|
|
|
var track = OrderTrackingHelper.BuildTrackDto(full);
|
|
await _guestHub.Clients.Group(GuestOrderHub.OrderGroup(order.Id))
|
|
.SendAsync("OrderTrackUpdated", track, cancellationToken);
|
|
}
|
|
|
|
private async Task TrySmsGuestReadyAsync(
|
|
Order order,
|
|
string? tableNumber,
|
|
string orderNumber,
|
|
CancellationToken ct)
|
|
{
|
|
var phone = order.GuestPhone?.Trim();
|
|
if (string.IsNullOrEmpty(phone))
|
|
return;
|
|
|
|
try
|
|
{
|
|
var msg = $"میزی: سفارش {orderNumber} (میز {tableNumber ?? "—"}) آماده است.";
|
|
await _sms.SendMessageAsync(phone, msg, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Guest ready SMS failed for order {OrderId}", order.Id);
|
|
}
|
|
}
|
|
|
|
private async Task<string?> ResolveTableNumberAsync(Order order, CancellationToken cancellationToken)
|
|
{
|
|
if (order.Table is not null)
|
|
return order.Table.Number;
|
|
if (string.IsNullOrEmpty(order.TableId))
|
|
return null;
|
|
return await _db.Tables
|
|
.Where(t => t.Id == order.TableId)
|
|
.Select(t => t.Number)
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task NotifyCallWaiterAsync(string cafeId, string tableId, string tableNumber, CancellationToken ct = default)
|
|
{
|
|
var notification = new CafeNotification
|
|
{
|
|
CafeId = cafeId,
|
|
Type = "table_call_waiter",
|
|
Title = $"درخواست گارسون — میز {tableNumber}",
|
|
Body = "مشتری درخواست خدمت کرد",
|
|
ReferenceId = tableId,
|
|
TableNumber = tableNumber
|
|
};
|
|
_db.CafeNotifications.Add(notification);
|
|
await _db.SaveChangesAsync(ct);
|
|
|
|
await _kdsHub.Clients.Group(KdsHub.GroupName(cafeId))
|
|
.SendAsync("NotificationReceived", MapNotification(notification), ct);
|
|
}
|
|
|
|
private static CafeNotificationDto MapNotification(CafeNotification n) =>
|
|
new(n.Id, n.Type, n.Title, n.Body, n.ReferenceId, n.TableNumber, n.IsRead, n.CreatedAt);
|
|
}
|