Files
meezi/src/Meezi.API/Validators/PosValidators.cs
T
soroush.asadi 024a455ab3 fix: menu item/category create, demo banner reach, token refresh, blog publish
Dashboard & API bug fixes for owner-reported breakage:

- MenuController validators (PosValidators): NameEn was required but the
  dashboard sends null when blank, so every manual menu-item create failed
  and category create failed 100% (the form never sends nameEn). Now optional.
- DemoDataBanner: only showed when a cafe was exactly empty, so
  showcase-seeded cafes (2-3 cats / 3-5 items) could never trigger the
  one-click seed. Widened gate to sparse menus (<5 cats && <10 items) and
  added a clear "nothing to add" message when already populated.
- client.ts: added one-time JWT refresh-and-retry on 401 (shared in-flight
  promise) before bouncing to /login. Expired access tokens silently broke
  ticket list, add-table, and other reads.
- Surface API errors as toasts on menu + table mutations (were swallowed
  silently, so failures looked like "nothing happens").
- Admin blog editor: saving an edit dropped IsPublished (defaulted false,
  silently unpublishing the post on every save); now persisted with a
  toggle. Also hoisted the inner Field component to module scope - it was
  remounting every input on each keystroke and dropping focus.
- Admin integrations: replaced raw radio gateway selector with a styled
  RadioDot matching the iOS toggles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 18:25:34 +03:30

170 lines
5.8 KiB
C#

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).MaximumLength(200).When(x => !string.IsNullOrWhiteSpace(x.NameEn));
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).MaximumLength(200).When(x => !string.IsNullOrWhiteSpace(x.NameEn));
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() { }
}