food-market/src/food-market.api/Controllers/Organizations/OrganizationSettingsController.cs
nns 9886b5dee1 feat(org-settings): настройка ShowMinMaxStock для мин/макс остатков
Добавлена Organization.ShowMinMaxStock (bool, default false) — флаг
видимости полей «Минимальный / Максимальный остаток» на карточке
товара. В UI настроек магазина появилась соответствующая галка
с подсказкой. По умолчанию выключено — большинству магазинов
эти поля не нужны.

Миграция Phase5f_ShowMinMaxStock добавляет колонку.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 19:02:53 +05:00

109 lines
4.1 KiB
C#

using foodmarket.Application.Common.Tenancy;
using foodmarket.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace foodmarket.Api.Controllers.Organizations;
[ApiController]
[Authorize]
[Route("api/organization")]
public class OrganizationSettingsController : ControllerBase
{
private readonly AppDbContext _db;
private readonly ITenantContext _tenant;
public OrganizationSettingsController(AppDbContext db, ITenantContext tenant)
{
_db = db;
_tenant = tenant;
}
public record OrgSettingsDto(
Guid Id,
string Name,
string CountryCode,
Guid? DefaultCurrencyId,
string? DefaultCurrencyCode,
string? DefaultCurrencySymbol,
bool MultiCurrencyEnabled,
// VAT read-only: из страны организации (countries.VatRate). Источник правды — справочник стран.
decimal VatRate,
bool ShowVatEnabledOnProduct,
bool ShowServiceOnProduct,
bool ShowMarkedOnProduct,
bool ShowMinMaxStock);
// DefaultCurrencyId не принимается — он read-only, выводится из страны (Country.DefaultCurrencyId).
public record OrgSettingsInput(
string Name,
string CountryCode,
bool MultiCurrencyEnabled,
bool ShowVatEnabledOnProduct,
bool ShowServiceOnProduct,
bool ShowMarkedOnProduct,
bool ShowMinMaxStock);
[HttpGet("settings")]
public async Task<ActionResult<OrgSettingsDto>> Get(CancellationToken ct)
{
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
var o = await _db.Organizations
.Include(o => o.DefaultCurrency)
.FirstOrDefaultAsync(o => o.Id == orgId, ct);
if (o is null) return NotFound();
var vat = await ReadVatRateAsync(o.CountryCode, ct);
return Project(o, vat);
}
[HttpPut("settings"), Authorize(Roles = "Admin,Manager")]
public async Task<ActionResult<OrgSettingsDto>> Update([FromBody] OrgSettingsInput input, CancellationToken ct)
{
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
var o = await _db.Organizations
.Include(o => o.DefaultCurrency)
.FirstOrDefaultAsync(o => o.Id == orgId, ct);
if (o is null) return NotFound();
o.Name = input.Name;
o.CountryCode = input.CountryCode;
// Валюта организации жёстко следует за страной — не принимается от клиента.
o.DefaultCurrencyId = await _db.Countries
.Where(c => c.Code == input.CountryCode)
.Select(c => c.DefaultCurrencyId)
.FirstOrDefaultAsync(ct);
o.MultiCurrencyEnabled = input.MultiCurrencyEnabled;
o.ShowVatEnabledOnProduct = input.ShowVatEnabledOnProduct;
o.ShowServiceOnProduct = input.ShowServiceOnProduct;
o.ShowMarkedOnProduct = input.ShowMarkedOnProduct;
o.ShowMinMaxStock = input.ShowMinMaxStock;
await _db.SaveChangesAsync(ct);
await _db.Entry(o).Reference(x => x.DefaultCurrency).LoadAsync(ct);
var vat = await ReadVatRateAsync(o.CountryCode, ct);
return Project(o, vat);
}
private async Task<decimal> ReadVatRateAsync(string countryCode, CancellationToken ct)
{
var rate = await _db.Countries
.Where(c => c.Code == countryCode)
.Select(c => (decimal?)c.VatRate)
.FirstOrDefaultAsync(ct);
return rate ?? 0m;
}
private static OrgSettingsDto Project(foodmarket.Domain.Organizations.Organization o, decimal vat) => new(
o.Id, o.Name, o.CountryCode,
o.DefaultCurrencyId,
o.DefaultCurrency?.Code,
o.DefaultCurrency?.Symbol,
o.MultiCurrencyEnabled,
vat,
o.ShowVatEnabledOnProduct,
o.ShowServiceOnProduct,
o.ShowMarkedOnProduct,
o.ShowMinMaxStock);
}