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
+169
View File
@@ -0,0 +1,169 @@
using FluentValidation;
using Meezi.API.Models.Menu;
using Meezi.API.Models.Orders;
using Meezi.API.Models.Tables;
using Meezi.Core.Constants;
using Meezi.Core.Utilities;
namespace Meezi.API.Validators;
public class CreateMenuCategoryRequestValidator : AbstractValidator<CreateMenuCategoryRequest>
{
public CreateMenuCategoryRequestValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.NameEn).NotEmpty().MaximumLength(200);
RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0);
RuleFor(x => x.DiscountPercent).InclusiveBetween(0, 100);
RuleFor(x => x.Icon).MaximumLength(32).When(x => x.Icon is not null);
RuleFor(x => x.IconPresetId)
.Must(id => id is null || CategoryIconPresets.IsValidPreset(id))
.WithMessage("Invalid icon preset.")
.MaximumLength(48)
.When(x => x.IconPresetId is not null);
RuleFor(x => x.IconStyle)
.Must(s => s is null || CategoryIconPresets.IsValidStyle(s))
.WithMessage("Invalid icon style.")
.MaximumLength(16)
.When(x => x.IconStyle is not null);
RuleFor(x => x.ImageUrl).MaximumLength(500).When(x => x.ImageUrl is not null);
RuleFor(x => x)
.Must(x => string.IsNullOrWhiteSpace(x.IconPresetId) == string.IsNullOrWhiteSpace(x.IconStyle))
.WithMessage("Icon preset and style must be set together.");
}
}
public class CreateMenuItemRequestValidator : AbstractValidator<CreateMenuItemRequest>
{
public CreateMenuItemRequestValidator()
{
RuleFor(x => x.CategoryId).NotEmpty();
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.NameEn).NotEmpty().MaximumLength(200);
RuleFor(x => x.Price).GreaterThanOrEqualTo(0);
RuleFor(x => x.DiscountPercent).InclusiveBetween(0, 100);
}
}
public class CreateTableRequestValidator : AbstractValidator<CreateTableRequest>
{
public CreateTableRequestValidator()
{
RuleFor(x => x.Number).NotEmpty().MaximumLength(50);
RuleFor(x => x.Capacity).GreaterThan(0);
}
}
public class PatchTableRequestValidator : AbstractValidator<PatchTableRequest>
{
public PatchTableRequestValidator()
{
RuleFor(x => x.Number).NotEmpty().MaximumLength(50).When(x => x.Number is not null);
RuleFor(x => x.Capacity).GreaterThan(0).When(x => x.Capacity.HasValue);
}
}
public class CreateBranchTableRequestValidator : AbstractValidator<CreateBranchTableRequest>
{
public CreateBranchTableRequestValidator()
{
RuleFor(x => x.Number).NotEmpty().MaximumLength(50);
RuleFor(x => x.Capacity).GreaterThan(0);
}
}
public class PatchBranchTableRequestValidator : AbstractValidator<PatchBranchTableRequest>
{
public PatchBranchTableRequestValidator()
{
RuleFor(x => x.Number).NotEmpty().MaximumLength(50).When(x => x.Number is not null);
RuleFor(x => x.Capacity).GreaterThan(0).When(x => x.Capacity.HasValue);
}
}
public class CreateTableSectionRequestValidator : AbstractValidator<CreateTableSectionRequest>
{
public CreateTableSectionRequestValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
}
}
public class PatchTableSectionRequestValidator : AbstractValidator<PatchTableSectionRequest>
{
public PatchTableSectionRequestValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(100).When(x => x.Name is not null);
}
}
public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderRequestValidator()
{
RuleFor(x => x.OrderType).IsInEnum();
RuleFor(x => x.Items).NotEmpty();
RuleFor(x => x.GuestName).MaximumLength(200).When(x => x.GuestName is not null);
RuleFor(x => x.GuestPhone)
.Must(p => PhoneNormalizer.IsValidIranMobile(PhoneNormalizer.Normalize(p!)))
.When(x => !string.IsNullOrWhiteSpace(x.GuestPhone));
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(i => i.MenuItemId).NotEmpty();
item.RuleFor(i => i.Quantity).GreaterThan(0);
});
}
}
public class UpdateOrderStatusRequestValidator : AbstractValidator<UpdateOrderStatusRequest>
{
public UpdateOrderStatusRequestValidator()
{
RuleFor(x => x.Status).IsInEnum();
}
}
public class RecordPaymentsRequestValidator : AbstractValidator<RecordPaymentsRequest>
{
public RecordPaymentsRequestValidator()
{
RuleFor(x => x.Payments).NotEmpty();
RuleForEach(x => x.Payments).ChildRules(p =>
{
p.RuleFor(x => x.Method).IsInEnum();
p.RuleFor(x => x.Amount).GreaterThan(0);
});
RuleFor(x => x.LoyaltyPointsToRedeem)
.GreaterThanOrEqualTo(0)
.When(x => x.LoyaltyPointsToRedeem.HasValue);
}
}
public class AppendOrderItemsRequestValidator : AbstractValidator<AppendOrderItemsRequest>
{
public AppendOrderItemsRequestValidator()
{
RuleFor(x => x.Items).NotEmpty();
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(i => i.MenuItemId).NotEmpty();
item.RuleFor(i => i.Quantity).GreaterThan(0);
});
}
}
public class UpdateOrderSessionRequestValidator : AbstractValidator<UpdateOrderSessionRequest>
{
public UpdateOrderSessionRequestValidator()
{
RuleFor(x => x.GuestName).MaximumLength(200).When(x => x.GuestName is not null);
RuleFor(x => x.GuestPhone)
.Must(p => PhoneNormalizer.IsValidIranMobile(PhoneNormalizer.Normalize(p!)))
.When(x => !string.IsNullOrWhiteSpace(x.GuestPhone));
}
}
public class SetTableCleaningRequestValidator : AbstractValidator<SetTableCleaningRequest>
{
public SetTableCleaningRequestValidator() { }
}