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,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);
|
||||
}
|
||||
Reference in New Issue
Block a user