Files
meezi/src/Meezi.API/Services/OrderNotificationService.cs
T
soroush.asadi ef15fd6247 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>
2026-05-27 21:33:48 +03:30

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);
}