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:
soroush.asadi
2026-05-27 21:33:48 +03:30
parent 03376b3ea1
commit ef15fd6247
472 changed files with 120358 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
using Meezi.API.Models.Taxes;
using Meezi.Core.Entities;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Meezi.API.Services;
public interface ITaxService
{
Task<IReadOnlyList<TaxDto>> GetAllAsync(string cafeId, CancellationToken cancellationToken = default);
Task<TaxDto?> CreateAsync(string cafeId, CreateTaxRequest request, CancellationToken cancellationToken = default);
Task<TaxDto?> UpdateAsync(string cafeId, string id, UpdateTaxRequest request, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(string cafeId, string id, CancellationToken cancellationToken = default);
}
public class TaxService : ITaxService
{
private readonly AppDbContext _db;
public TaxService(AppDbContext db) => _db = db;
public async Task<IReadOnlyList<TaxDto>> GetAllAsync(string cafeId, CancellationToken cancellationToken = default)
{
return await _db.Taxes
.Where(t => t.CafeId == cafeId)
.OrderBy(t => t.Name)
.Select(t => ToDto(t))
.ToListAsync(cancellationToken);
}
public async Task<TaxDto?> CreateAsync(string cafeId, CreateTaxRequest request, CancellationToken cancellationToken = default)
{
if (request.IsDefault)
{
var existing = await _db.Taxes.Where(t => t.CafeId == cafeId && t.IsDefault).ToListAsync(cancellationToken);
foreach (var t in existing) t.IsDefault = false;
}
var entity = new Tax
{
CafeId = cafeId,
Name = request.Name,
Rate = request.Rate,
IsDefault = request.IsDefault,
IsRequired = request.IsRequired,
IsCompound = request.IsCompound
};
_db.Taxes.Add(entity);
await _db.SaveChangesAsync(cancellationToken);
return ToDto(entity);
}
public async Task<TaxDto?> UpdateAsync(
string cafeId,
string id,
UpdateTaxRequest request,
CancellationToken cancellationToken = default)
{
var entity = await _db.Taxes.FirstOrDefaultAsync(t => t.Id == id && t.CafeId == cafeId, cancellationToken);
if (entity is null) return null;
if (request.Name is not null) entity.Name = request.Name;
if (request.Rate.HasValue) entity.Rate = request.Rate.Value;
if (request.IsDefault == true)
{
var others = await _db.Taxes.Where(t => t.CafeId == cafeId && t.Id != id).ToListAsync(cancellationToken);
foreach (var t in others) t.IsDefault = false;
entity.IsDefault = true;
}
else if (request.IsDefault.HasValue) entity.IsDefault = request.IsDefault.Value;
if (request.IsRequired.HasValue) entity.IsRequired = request.IsRequired.Value;
if (request.IsCompound.HasValue) entity.IsCompound = request.IsCompound.Value;
await _db.SaveChangesAsync(cancellationToken);
return ToDto(entity);
}
public async Task<bool> DeleteAsync(string cafeId, string id, CancellationToken cancellationToken = default)
{
var entity = await _db.Taxes.FirstOrDefaultAsync(t => t.Id == id && t.CafeId == cafeId, cancellationToken);
if (entity is null) return false;
var wasDefault = entity.IsDefault;
entity.DeletedAt = DateTime.UtcNow;
entity.IsDefault = false;
var categories = await _db.MenuCategories
.Where(c => c.CafeId == cafeId && c.TaxId == id)
.ToListAsync(cancellationToken);
Tax? replacementDefault = null;
if (wasDefault)
{
replacementDefault = await _db.Taxes
.Where(t => t.CafeId == cafeId && t.Id != id)
.OrderBy(t => t.Name)
.FirstOrDefaultAsync(cancellationToken);
if (replacementDefault is not null)
replacementDefault.IsDefault = true;
}
foreach (var category in categories)
category.TaxId = replacementDefault?.Id;
await _db.SaveChangesAsync(cancellationToken);
return true;
}
private static TaxDto ToDto(Tax t) => new(t.Id, t.Name, t.Rate, t.IsDefault, t.IsRequired, t.IsCompound);
}