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,136 @@
|
||||
using Meezi.API.Models.Crm;
|
||||
using Meezi.Core.Entities;
|
||||
using Meezi.Core.Utilities;
|
||||
using Meezi.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Meezi.API.Services;
|
||||
|
||||
public interface ICustomerService
|
||||
{
|
||||
Task<IReadOnlyList<CustomerDto>> SearchAsync(string cafeId, string? query, CancellationToken cancellationToken = default);
|
||||
Task<CustomerDto?> GetAsync(string cafeId, string id, CancellationToken cancellationToken = default);
|
||||
Task<CustomerDto?> CreateAsync(string cafeId, CreateCustomerRequest request, CancellationToken cancellationToken = default);
|
||||
Task<CustomerDto?> UpdateAsync(string cafeId, string id, UpdateCustomerRequest request, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(string cafeId, string id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class CustomerService : ICustomerService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public CustomerService(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CustomerDto>> SearchAsync(
|
||||
string cafeId,
|
||||
string? query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var q = _db.Customers.Where(c => c.CafeId == cafeId && c.DeletedAt == null);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
var term = query.Trim();
|
||||
var normalizedPhone = PhoneNormalizer.Normalize(term);
|
||||
q = q.Where(c =>
|
||||
c.Name.Contains(term) ||
|
||||
c.Phone.Contains(term) ||
|
||||
(c.NationalId != null && c.NationalId.Contains(term)) ||
|
||||
(normalizedPhone.Length >= 10 && c.Phone.Contains(normalizedPhone)));
|
||||
}
|
||||
|
||||
var list = await q
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.Take(100)
|
||||
.ToListAsync(cancellationToken);
|
||||
return list.Select(ToDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<CustomerDto?> GetAsync(string cafeId, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await _db.Customers
|
||||
.FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId && c.DeletedAt == null, cancellationToken);
|
||||
return entity is null ? null : ToDto(entity);
|
||||
}
|
||||
|
||||
public async Task<CustomerDto?> CreateAsync(
|
||||
string cafeId,
|
||||
CreateCustomerRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var phone = PhoneNormalizer.Normalize(request.Phone);
|
||||
var exists = await _db.Customers.AnyAsync(
|
||||
c => c.CafeId == cafeId && c.Phone == phone, cancellationToken);
|
||||
if (exists) return null;
|
||||
|
||||
var entity = new Customer
|
||||
{
|
||||
CafeId = cafeId,
|
||||
Name = request.Name,
|
||||
Phone = phone,
|
||||
NationalId = request.NationalId,
|
||||
BirthDateJalali = request.BirthDateJalali,
|
||||
Group = request.Group,
|
||||
ReferredBy = request.ReferredBy
|
||||
};
|
||||
|
||||
_db.Customers.Add(entity);
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return ToDto(entity);
|
||||
}
|
||||
|
||||
public async Task<CustomerDto?> UpdateAsync(
|
||||
string cafeId,
|
||||
string id,
|
||||
UpdateCustomerRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await _db.Customers
|
||||
.FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId && c.DeletedAt == null, cancellationToken);
|
||||
if (entity is null) return null;
|
||||
|
||||
if (request.Name is not null) entity.Name = request.Name;
|
||||
if (request.Phone is not null)
|
||||
{
|
||||
var phone = PhoneNormalizer.Normalize(request.Phone);
|
||||
var phoneTaken = await _db.Customers.AnyAsync(
|
||||
c => c.CafeId == cafeId && c.Phone == phone && c.Id != id && c.DeletedAt == null,
|
||||
cancellationToken);
|
||||
if (phoneTaken) return null;
|
||||
entity.Phone = phone;
|
||||
}
|
||||
if (request.NationalId is not null) entity.NationalId = request.NationalId;
|
||||
if (request.BirthDateJalali is not null) entity.BirthDateJalali = request.BirthDateJalali;
|
||||
if (request.Group.HasValue) entity.Group = request.Group.Value;
|
||||
if (request.LoyaltyPoints.HasValue) entity.LoyaltyPoints = request.LoyaltyPoints.Value;
|
||||
if (request.ReferredBy is not null) entity.ReferredBy = request.ReferredBy;
|
||||
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return ToDto(entity);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(string cafeId, string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await _db.Customers
|
||||
.FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken);
|
||||
if (entity is null) return false;
|
||||
|
||||
entity.DeletedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static CustomerDto ToDto(Customer c) => new(
|
||||
c.Id,
|
||||
c.Name,
|
||||
c.Phone,
|
||||
c.NationalId,
|
||||
c.BirthDateJalali,
|
||||
c.Group,
|
||||
c.LoyaltyPoints,
|
||||
c.ReferredBy,
|
||||
c.CreatedAt);
|
||||
}
|
||||
Reference in New Issue
Block a user