refactor(currencies): убрать IsActive и MinorUnit из UI/API

- Currency.IsActive удалён полностью (domain/DTO/API/web/миграция).
  Валюты — глобальный справочник; «архивировать» USD глобально
  бессмысленно, а per-tenant видимости у валют нет.
- MinorUnit остаётся в БД (нужен для форматирования цен), но скрыт
  из UI: убран CurrencyDto.MinorUnit, CurrencyInput.MinorUnit,
  колонка «Знаки» из списка.
- Форма валюты — 3 поля: Код / Название / Символ.
- Миграция Phase5e_DropCurrencyIsActive дропает колонку.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nns 2026-04-24 19:01:13 +05:00
parent 3ed6fe25be
commit d6dcc75aa0
7 changed files with 1915 additions and 16 deletions

View file

@ -33,14 +33,12 @@ public async Task<ActionResult<PagedResult<CurrencyDto>>> List([FromQuery] Paged
("name", true) => q.OrderByDescending(c => c.Name),
("symbol", false) => q.OrderBy(c => c.Symbol),
("symbol", true) => q.OrderByDescending(c => c.Symbol),
("isActive", false) => q.OrderBy(c => c.IsActive).ThenBy(c => c.Code),
("isActive", true) => q.OrderByDescending(c => c.IsActive).ThenBy(c => c.Code),
("code", true) => q.OrderByDescending(c => c.Code),
_ => q.OrderBy(c => c.Code),
};
var items = await q
.Skip(req.Skip).Take(req.Take)
.Select(c => new CurrencyDto(c.Id, c.Code, c.Name, c.Symbol, c.MinorUnit, c.IsActive))
.Select(c => new CurrencyDto(c.Id, c.Code, c.Name, c.Symbol))
.ToListAsync(ct);
return new PagedResult<CurrencyDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
}
@ -49,7 +47,7 @@ public async Task<ActionResult<PagedResult<CurrencyDto>>> List([FromQuery] Paged
public async Task<ActionResult<CurrencyDto>> Get(Guid id, CancellationToken ct)
{
var c = await _db.Currencies.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
return c is null ? NotFound() : new CurrencyDto(c.Id, c.Code, c.Name, c.Symbol, c.MinorUnit, c.IsActive);
return c is null ? NotFound() : new CurrencyDto(c.Id, c.Code, c.Name, c.Symbol);
}
[HttpPost, Authorize(Roles = "SuperAdmin")]
@ -60,13 +58,11 @@ public async Task<ActionResult<CurrencyDto>> Create([FromBody] CurrencyInput inp
Code = input.Code.Trim().ToUpper(),
Name = input.Name,
Symbol = input.Symbol,
MinorUnit = input.MinorUnit,
IsActive = input.IsActive,
};
_db.Currencies.Add(e);
await _db.SaveChangesAsync(ct);
return CreatedAtAction(nameof(Get), new { id = e.Id },
new CurrencyDto(e.Id, e.Code, e.Name, e.Symbol, e.MinorUnit, e.IsActive));
new CurrencyDto(e.Id, e.Code, e.Name, e.Symbol));
}
[HttpPut("{id:guid}"), Authorize(Roles = "SuperAdmin")]
@ -77,8 +73,6 @@ public async Task<IActionResult> Update(Guid id, [FromBody] CurrencyInput input,
e.Code = input.Code.Trim().ToUpper();
e.Name = input.Name;
e.Symbol = input.Symbol;
e.MinorUnit = input.MinorUnit;
e.IsActive = input.IsActive;
await _db.SaveChangesAsync(ct);
return NoContent();
}

View file

@ -8,7 +8,7 @@ public record CountryDto(
decimal VatRate);
public record CurrencyDto(Guid Id, string Code, string Name, string Symbol, int MinorUnit, bool IsActive);
public record CurrencyDto(Guid Id, string Code, string Name, string Symbol);
public record UnitOfMeasureDto(
Guid Id, string Code, string Name, string? Description, bool IsActive);
@ -55,7 +55,7 @@ public record ProductDto(
public record CountryInput(
string Code, string Name,
Guid? DefaultCurrencyId = null, decimal VatRate = 0m);
public record CurrencyInput(string Code, string Name, string Symbol, int MinorUnit = 2, bool IsActive = true);
public record CurrencyInput(string Code, string Name, string Symbol);
public record UnitOfMeasureInput(string Code, string Name, string? Description = null, bool IsActive = true);
public record PriceTypeInput(string Name, bool IsDefault = false, bool IsRetail = false, int SortOrder = 0, bool IsActive = true);
public record StoreInput(

View file

@ -8,6 +8,7 @@ public class Currency : Entity
public string Code { get; set; } = null!; // ISO 4217, e.g. "KZT"
public string Name { get; set; } = null!;
public string Symbol { get; set; } = null!; // "₸"
public int MinorUnit { get; set; } = 2; // 2 = two decimal places
public bool IsActive { get; set; } = true;
// Количество знаков после запятой для форматирования цен. Не редактируется
// в UI — задаётся сидером/миграцией по ISO 4217.
public int MinorUnit { get; set; } = 2;
}

View file

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace foodmarket.Infrastructure.Persistence.Migrations
{
/// <summary>Удаляет currencies.IsActive. Валюты — глобальный справочник,
/// «архивировать» USD не имеет смысла. Если какая-то валюта не нужна
/// конкретному магазину — пользователь её просто не выбирает.</summary>
public partial class Phase5e_DropCurrencyIsActive : Migration
{
protected override void Up(MigrationBuilder b)
{
b.DropColumn(name: "IsActive", schema: "public", table: "currencies");
}
protected override void Down(MigrationBuilder b)
{
b.AddColumn<bool>(
name: "IsActive", schema: "public", table: "currencies",
type: "boolean", nullable: false, defaultValue: true);
}
}
}

View file

@ -27,7 +27,7 @@ export interface Country {
defaultCurrencySymbol: string | null
vatRate: number
}
export interface Currency { id: string; code: string; name: string; symbol: string; minorUnit: number; isActive: boolean }
export interface Currency { id: string; code: string; name: string; symbol: string }
export interface UnitOfMeasure { id: string; code: string; name: string; description: string | null; isActive: boolean }
export interface PriceType { id: string; name: string; isDefault: boolean; isRetail: boolean; sortOrder: number; isActive: boolean }
export interface Store {

View file

@ -28,8 +28,6 @@ export function CurrenciesPage() {
{ header: 'Код', width: '90px', sortKey: 'code', cell: (r) => <span className="font-mono">{r.code}</span> },
{ header: 'Название', sortKey: 'name', cell: (r) => r.name },
{ header: 'Символ', width: '100px', sortKey: 'symbol', cell: (r) => <span className="text-lg">{r.symbol}</span> },
{ header: 'Знаки', width: '100px', className: 'text-right', cell: (r) => r.minorUnit },
{ header: 'Активна', width: '100px', sortKey: 'isActive', cell: (r) => r.isActive ? '✓' : '—' },
]}
/>
</ListPageShell>