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,142 @@
|
||||
using Meezi.API.Models.Kitchen;
|
||||
using Meezi.Core.Entities;
|
||||
using Meezi.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Meezi.API.Services;
|
||||
|
||||
public interface IKitchenStationService
|
||||
{
|
||||
Task<IReadOnlyList<KitchenStationDto>> ListAsync(string cafeId, CancellationToken ct = default);
|
||||
Task<KitchenStationDto?> CreateAsync(string cafeId, CreateKitchenStationRequest request, CancellationToken ct = default);
|
||||
Task<KitchenStationDto?> UpdateAsync(string cafeId, string id, UpdateKitchenStationRequest request, CancellationToken ct = default);
|
||||
Task<bool> DeleteAsync(string cafeId, string id, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class KitchenStationService : IKitchenStationService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public KitchenStationService(AppDbContext db) => _db = db;
|
||||
|
||||
public async Task<IReadOnlyList<KitchenStationDto>> ListAsync(string cafeId, CancellationToken ct = default)
|
||||
{
|
||||
var stations = await _db.KitchenStations
|
||||
.Where(s => s.CafeId == cafeId)
|
||||
.OrderBy(s => s.SortOrder)
|
||||
.ThenBy(s => s.Name)
|
||||
.Select(s => new
|
||||
{
|
||||
s.Id,
|
||||
s.BranchId,
|
||||
s.Name,
|
||||
s.PrinterIp,
|
||||
s.PrinterPort,
|
||||
s.SortOrder,
|
||||
CategoryCount = s.Categories.Count(c => c.DeletedAt == null)
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
return stations.Select(s => new KitchenStationDto(
|
||||
s.Id, s.BranchId, s.Name, s.PrinterIp, s.PrinterPort, s.SortOrder, s.CategoryCount)).ToList();
|
||||
}
|
||||
|
||||
public async Task<KitchenStationDto?> CreateAsync(
|
||||
string cafeId,
|
||||
CreateKitchenStationRequest request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(request.BranchId))
|
||||
{
|
||||
var branchOk = await _db.Branches.AnyAsync(
|
||||
b => b.Id == request.BranchId && b.CafeId == cafeId, ct);
|
||||
if (!branchOk) return null;
|
||||
}
|
||||
|
||||
var entity = new KitchenStation
|
||||
{
|
||||
CafeId = cafeId,
|
||||
BranchId = request.BranchId,
|
||||
Name = request.Name.Trim(),
|
||||
PrinterIp = string.IsNullOrWhiteSpace(request.PrinterIp) ? null : request.PrinterIp.Trim(),
|
||||
PrinterPort = request.PrinterPort > 0 ? request.PrinterPort : 9100,
|
||||
SortOrder = request.SortOrder
|
||||
};
|
||||
|
||||
_db.KitchenStations.Add(entity);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return await MapAsync(cafeId, entity.Id, ct);
|
||||
}
|
||||
|
||||
public async Task<KitchenStationDto?> UpdateAsync(
|
||||
string cafeId,
|
||||
string id,
|
||||
UpdateKitchenStationRequest request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var entity = await _db.KitchenStations
|
||||
.FirstOrDefaultAsync(s => s.Id == id && s.CafeId == cafeId, ct);
|
||||
if (entity is null) return null;
|
||||
|
||||
if (request.Name is not null) entity.Name = request.Name.Trim();
|
||||
if (request.BranchId is not null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(request.BranchId))
|
||||
{
|
||||
var branchOk = await _db.Branches.AnyAsync(
|
||||
b => b.Id == request.BranchId && b.CafeId == cafeId, ct);
|
||||
if (!branchOk) return null;
|
||||
}
|
||||
|
||||
entity.BranchId = string.IsNullOrEmpty(request.BranchId) ? null : request.BranchId;
|
||||
}
|
||||
|
||||
if (request.PrinterIp is not null)
|
||||
entity.PrinterIp = string.IsNullOrWhiteSpace(request.PrinterIp) ? null : request.PrinterIp.Trim();
|
||||
if (request.PrinterPort.HasValue)
|
||||
entity.PrinterPort = request.PrinterPort.Value > 0 ? request.PrinterPort.Value : 9100;
|
||||
if (request.SortOrder.HasValue)
|
||||
entity.SortOrder = request.SortOrder.Value;
|
||||
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return await MapAsync(cafeId, id, ct);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(string cafeId, string id, CancellationToken ct = default)
|
||||
{
|
||||
var entity = await _db.KitchenStations
|
||||
.FirstOrDefaultAsync(s => s.Id == id && s.CafeId == cafeId, ct);
|
||||
if (entity is null) return false;
|
||||
|
||||
var categories = await _db.MenuCategories
|
||||
.Where(c => c.KitchenStationId == id && c.CafeId == cafeId)
|
||||
.ToListAsync(ct);
|
||||
foreach (var cat in categories)
|
||||
cat.KitchenStationId = null;
|
||||
|
||||
entity.DeletedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<KitchenStationDto?> MapAsync(string cafeId, string id, CancellationToken ct)
|
||||
{
|
||||
var s = await _db.KitchenStations
|
||||
.Where(x => x.Id == id && x.CafeId == cafeId)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.BranchId,
|
||||
x.Name,
|
||||
x.PrinterIp,
|
||||
x.PrinterPort,
|
||||
x.SortOrder,
|
||||
CategoryCount = x.Categories.Count(c => c.DeletedAt == null)
|
||||
})
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
return s is null
|
||||
? null
|
||||
: new KitchenStationDto(s.Id, s.BranchId, s.Name, s.PrinterIp, s.PrinterPort, s.SortOrder, s.CategoryCount);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user