feat: add production templates API and documentation for manufacturing phase 2
- Implemented CRUD operations for production templates, including listing, retrieving, creating, updating, and deactivating templates. - Introduced a new API contract for production runs, detailing the lifecycle from creation to completion, including handling of stock inputs and outputs. - Documented the architecture, requirements, entity model, and API contract for the manufacturing phase 2, ensuring clarity on the production process and its integration with existing systems.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Production;
|
||||
|
||||
// Production run contract (docs/30-BACKEND-PHASE2.md §D.2–D.3).
|
||||
//
|
||||
// Statuses are never client-settable (02-SECURITY §B.6): a run's status and every stage
|
||||
// status move only through the stage-action endpoints. Requests here carry quantities and
|
||||
// references, nothing else.
|
||||
|
||||
// --- responses ---------------------------------------------------------------
|
||||
|
||||
/// <summary>Per-status stage counts driving the board's progress strip (FR-MFG-18, docs/21 §3).</summary>
|
||||
public sealed record StageSummaryDto(int Waiting, int Ready, int InProgress, int Done, int Approved);
|
||||
|
||||
/// <summary>Row on the run board (docs/30 §D.2 <c>GET /production-runs</c>).</summary>
|
||||
public sealed record RunSummaryDto(
|
||||
int RunId, string DocNo, int TemplateId, string TemplateName,
|
||||
decimal TargetQty, ProductionRunStatus Status, int ReworkCount,
|
||||
StageSummaryDto StageSummary, int WarehouseId,
|
||||
int? FinishedItemId, string? FinishedItemName,
|
||||
int CreatedBy, DateTime CreatedAt, DateTime? CompletedAt);
|
||||
|
||||
/// <summary>
|
||||
/// The run cost pool (FR-MFG-13). Always derived from the stage inputs, never stored —
|
||||
/// surfaced so the UI can preview the finished unit cost before approving the terminal.
|
||||
/// </summary>
|
||||
public sealed record CostPoolDto(decimal Consumed, decimal Returned, decimal Net);
|
||||
|
||||
public sealed record RunStageInputDto(
|
||||
int RunInputId, StageInputSource Source, int? ItemId, int? FromRunOutputId, int UomId,
|
||||
decimal PlannedQty, decimal ConsumedQty, decimal ConsumedValue,
|
||||
decimal DeliveredQty, decimal ReturnedQty, decimal ReturnedValue);
|
||||
|
||||
/// <summary>
|
||||
/// One output of a run stage. <c>AvailableToTransfer</c> is derived — produced − scrapped −
|
||||
/// transferred (FR-MFG-12) — and never stored.
|
||||
/// </summary>
|
||||
public sealed record RunStageOutputDto(
|
||||
int RunOutputId, int? ItemId, string Name, int UomId,
|
||||
decimal PlannedQty, decimal ProducedQty, decimal ScrappedQty, int? ScrapReasonCodeId,
|
||||
decimal TransferredQty, decimal AvailableToTransfer);
|
||||
|
||||
/// <summary>
|
||||
/// One stage of a run. <c>IsTerminal</c> (no outbound edge) and <c>IsEntry</c> (no inbound
|
||||
/// edge) are derived from the run edge set rather than stored, so they cannot drift from it.
|
||||
/// <c>ActualMinutes</c> is the elapsed whole minutes once the stage has finished, null while
|
||||
/// it is still running (FR-MFG-19). <c>FieldValues</c> is the raw jsonb captured at complete,
|
||||
/// passed through verbatim.
|
||||
/// </summary>
|
||||
public sealed record RunStageDto(
|
||||
int RunStageId, int? TemplateStageId, string Name, string? RoleLabel,
|
||||
int EstimatedMinutes, decimal PosX, decimal PosY,
|
||||
ProductionStageStatus Status, bool IsTerminal, bool IsEntry,
|
||||
DateTime? ActualStartAt, DateTime? ActualEndAt, int? ActualMinutes,
|
||||
IReadOnlyList<FieldDefDto> FieldDefs, JsonElement? FieldValues,
|
||||
IReadOnlyList<RunStageInputDto> Inputs, IReadOnlyList<RunStageOutputDto> Outputs);
|
||||
|
||||
public sealed record RunEdgeDto(int RunEdgeId, int ParentRunStageId, int ChildRunStageId);
|
||||
|
||||
public sealed record RunEventDto(
|
||||
int EventId, int? RunStageId, RunStageEventType EventType, string? Note,
|
||||
JsonElement? Payload, int UserId, DateTime CreatedAt);
|
||||
|
||||
/// <summary>Full run graph (docs/30 §D.2 <c>GET /production-runs/{id}</c>).</summary>
|
||||
public sealed record RunGraphDto(
|
||||
int RunId, string DocNo, int TemplateId, string TemplateName,
|
||||
int WarehouseId, int? OutputBinId,
|
||||
decimal TargetQty, decimal ScaleFactor,
|
||||
ProductionRunStatus Status, int ReworkCount, int? CancelReasonCodeId,
|
||||
CostPoolDto CostPool,
|
||||
IReadOnlyList<RunStageDto> Stages, IReadOnlyList<RunEdgeDto> Edges, IReadOnlyList<RunEventDto> Events,
|
||||
int CreatedBy, DateTime CreatedAt, DateTime? CompletedAt);
|
||||
|
||||
// --- requests ----------------------------------------------------------------
|
||||
|
||||
public sealed class CreateRunRequest
|
||||
{
|
||||
[Range(1, int.MaxValue)]
|
||||
public int TemplateId { get; set; }
|
||||
|
||||
/// <summary>Quantity of the finished item; drives the whole graph's scale factor (FR-MFG-08).</summary>
|
||||
[Range(0.0001, double.MaxValue)]
|
||||
public decimal TargetQty { get; set; }
|
||||
|
||||
[Range(1, int.MaxValue)]
|
||||
public int WarehouseId { get; set; }
|
||||
|
||||
/// <summary>Optional bin for the finished goods; must belong to <see cref="WarehouseId"/>.</summary>
|
||||
public int? OutputBinId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-run scaling override (FR-MFG-08). Only accepted while the stage has not started
|
||||
/// (<c>409 STAGE_NOT_EDITABLE</c>).
|
||||
/// </summary>
|
||||
public sealed class UpdateStageQuantitiesRequest
|
||||
{
|
||||
public List<StageQuantityLine> Inputs { get; set; } = new();
|
||||
public List<StageQuantityLine> Outputs { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class StageQuantityLine
|
||||
{
|
||||
/// <summary>The <c>runInputId</c> or <c>runOutputId</c> being adjusted.</summary>
|
||||
[Range(1, int.MaxValue)]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Range(0.0001, double.MaxValue)]
|
||||
public decimal PlannedQty { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Production;
|
||||
|
||||
// Stage-action contract (docs/30-BACKEND-PHASE2.md §D.3). Every action is transactional,
|
||||
// guards on the stage's current status, and returns the refreshed stage plus whatever
|
||||
// stock effects it caused.
|
||||
|
||||
// --- start -------------------------------------------------------------------
|
||||
|
||||
/// <summary>One FIFO layer a stage start drew from, at that layer's cost.</summary>
|
||||
public sealed record ConsumedLayerDto(int LayerId, decimal Qty, decimal UnitCost);
|
||||
|
||||
/// <summary>What a single Stock input consumed at start (FR-MFG-10).</summary>
|
||||
public sealed record ConsumedInputDto(
|
||||
int RunInputId, int ItemId, decimal Qty, decimal Value, IReadOnlyList<ConsumedLayerDto> ConsumedLayers);
|
||||
|
||||
public sealed record StartStageResultDto(
|
||||
int RunStageId, ProductionStageStatus Status, DateTime? ActualStartAt,
|
||||
IReadOnlyList<ConsumedInputDto> Consumed, IReadOnlyList<int> LedgerRefs,
|
||||
RunStageDto Stage);
|
||||
|
||||
// --- complete ----------------------------------------------------------------
|
||||
|
||||
public sealed class CompleteStageRequest
|
||||
{
|
||||
[Required, MinLength(1)]
|
||||
public List<CompleteOutputLine> Outputs { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Values for the stage's custom fields, keyed by <c>fieldDefs[].key</c>. Every field
|
||||
/// marked required must be present and non-empty (<c>400 REQUIRED_FIELD_MISSING</c>).
|
||||
/// </summary>
|
||||
public JsonElement? FieldValues { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CompleteOutputLine
|
||||
{
|
||||
[Range(1, int.MaxValue)]
|
||||
public int RunOutputId { get; set; }
|
||||
|
||||
[Range(0, double.MaxValue)]
|
||||
public decimal ProducedQty { get; set; }
|
||||
|
||||
[Range(0, double.MaxValue)]
|
||||
public decimal ScrappedQty { get; set; }
|
||||
|
||||
/// <summary>Mandatory once <see cref="ScrappedQty"/> > 0; must be a Production reason.</summary>
|
||||
public int? ScrapReasonCodeId { get; set; }
|
||||
}
|
||||
|
||||
// --- approve / transfer ------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// One WIP hand-off from a parent output to a child input. Deliveries route by
|
||||
/// <c>fromRunOutputId</c>, not by edge — see the note on <c>RunEdge</c>.
|
||||
/// </summary>
|
||||
public sealed record TransferDto(
|
||||
int RunOutputId, int RunInputId, int ChildRunStageId, decimal Qty,
|
||||
decimal ChildDeliveredQty, ProductionStageStatus ChildStatus);
|
||||
|
||||
/// <summary>The finished-goods layer a terminal approve created (FR-MFG-13).</summary>
|
||||
public sealed record ReceiptDto(
|
||||
int LayerId, int ItemId, int WarehouseId, int? BinId,
|
||||
decimal QtyReceived, decimal UnitCost, decimal Value);
|
||||
|
||||
public sealed record ApproveStageResultDto(
|
||||
int RunStageId, ProductionStageStatus Status, ProductionRunStatus RunStatus,
|
||||
IReadOnlyList<TransferDto> Transfers,
|
||||
ReceiptDto? Receipt, CostPoolDto? CostPool, IReadOnlyList<int> LedgerRefs,
|
||||
RunStageDto Stage);
|
||||
|
||||
public sealed class ApproveStageRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional partial transfers. Omitted or empty means transfer the full available
|
||||
/// quantity of every output (FR-MFG-12).
|
||||
/// </summary>
|
||||
public List<TransferLine> Transfers { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class TransferLine
|
||||
{
|
||||
[Range(1, int.MaxValue)]
|
||||
public int RunOutputId { get; set; }
|
||||
|
||||
[Range(0.0001, double.MaxValue)]
|
||||
public decimal Qty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional explicit target. When an output feeds several child inputs the server
|
||||
/// otherwise fills them in <c>runInputId</c> order; naming one removes the ambiguity.
|
||||
/// </summary>
|
||||
public int? RunInputId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class TransferRemainderRequest
|
||||
{
|
||||
[Range(1, int.MaxValue)]
|
||||
public int RunOutputId { get; set; }
|
||||
|
||||
[Range(0.0001, double.MaxValue)]
|
||||
public decimal Qty { get; set; }
|
||||
|
||||
public int? RunInputId { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TransferResultDto(
|
||||
int RunStageId, IReadOnlyList<TransferDto> Transfers, RunStageDto Stage);
|
||||
|
||||
// --- leftover return ---------------------------------------------------------
|
||||
|
||||
public sealed class ReturnLeftoverRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Quantity to return, in the item's <b>base</b> UOM — the same unit
|
||||
/// <c>consumedQty</c>/<c>returnedQty</c> are stored in, since the return posts straight
|
||||
/// to stock. Cannot exceed consumed − already returned.
|
||||
/// </summary>
|
||||
[Range(0.0001, double.MaxValue)]
|
||||
public decimal Qty { get; set; }
|
||||
|
||||
/// <summary>Mandatory, and must be a Production-context reason (FR-MFG-14).</summary>
|
||||
public int? ReasonCodeId { get; set; }
|
||||
}
|
||||
|
||||
public sealed record ProductionCreatedLayerDto(int LayerId, decimal UnitCost);
|
||||
|
||||
public sealed record ReturnLeftoverResultDto(
|
||||
int RunInputId, decimal ReturnedQty, decimal ReturnedValue,
|
||||
ProductionCreatedLayerDto CreatedLayer, IReadOnlyList<int> LedgerRefs, CostPoolDto CostPool);
|
||||
|
||||
// --- rejection / rework ------------------------------------------------------
|
||||
|
||||
public sealed class RejectRequest
|
||||
{
|
||||
[StringLength(500)]
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>One parent whose delivered work was pulled back by a reject-intake (FR-MFG-15).</summary>
|
||||
public sealed record PulledBackDto(
|
||||
int RunInputId, int ParentRunStageId, int ParentRunOutputId,
|
||||
decimal Qty, ProductionStageStatus PriorParentStatus, ProductionStageStatus ParentStatus);
|
||||
|
||||
public sealed record RejectIntakeResultDto(
|
||||
int RunStageId, ProductionStageStatus Status,
|
||||
IReadOnlyList<PulledBackDto> PulledBack, RunGraphDto Run);
|
||||
|
||||
public sealed record TerminalRejectResultDto(
|
||||
int RunStageId, int ReworkCount, RunGraphDto Run);
|
||||
|
||||
// --- cancel ------------------------------------------------------------------
|
||||
|
||||
public sealed class CancelRunRequest
|
||||
{
|
||||
public int? ReasonCodeId { get; set; }
|
||||
|
||||
[StringLength(500)]
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
|
||||
public sealed record CancelReturnDto(int ItemId, decimal Qty, decimal UnitCost, int LayerId);
|
||||
|
||||
/// <summary>
|
||||
/// Scrapped output quantities written off by a cancel. Recorded on the event only — scrap
|
||||
/// sits on outputs, which never entered stock, so there is nothing to return (FR-MFG-17).
|
||||
/// </summary>
|
||||
public sealed record ScrapWriteOffDto(int RunOutputId, string Name, decimal Qty);
|
||||
|
||||
public sealed record CancelRunResultDto(
|
||||
int RunId, ProductionRunStatus Status,
|
||||
IReadOnlyList<CancelReturnDto> Returns,
|
||||
IReadOnlyList<ScrapWriteOffDto> ScrappedWrittenOff,
|
||||
IReadOnlyList<int> LedgerRefs);
|
||||
@@ -0,0 +1,177 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Production;
|
||||
|
||||
// Production template contract (docs/30-BACKEND-PHASE2.md §D.1).
|
||||
//
|
||||
// The `Key` vocabulary: every stage and every output carries a client-facing string key
|
||||
// alongside its database id. On a GET the key IS the stringified id; on a save the client
|
||||
// echoes those keys back for rows it kept and mints "tmp-<uuid>" keys for rows it just
|
||||
// drew. Edges and Upstream inputs then reference stages/outputs *by key* only, which is
|
||||
// what lets one payload shape — and one validator — serve both POST (nothing has an id
|
||||
// yet) and PUT (most things do).
|
||||
|
||||
// --- responses ---------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Row on the template list (docs/30 §D.1 <c>GET /production-templates</c>).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>StageNames</c> is an addition to the documented shape. The template overview is a
|
||||
/// canvas showing every template as a production line with its stages left-to-right
|
||||
/// (docs/21 §1), so it needs the names for every listed row — without them the client would
|
||||
/// have to fetch each template's full graph just to label the boxes. Ordered by stage id,
|
||||
/// matching the graph endpoint.
|
||||
/// </remarks>
|
||||
public sealed record TemplateSummaryDto(
|
||||
int TemplateId, string Code, string Name, EntityStatus Status,
|
||||
int StageCount, IReadOnlyList<string> StageNames,
|
||||
int ActiveRunCount, int CreatedBy, DateTime CreatedAt);
|
||||
|
||||
/// <summary>One custom field definition, serialized verbatim into the stage's <c>field_defs</c> jsonb.</summary>
|
||||
public sealed record FieldDefDto(
|
||||
string Key, string Label, CustomFieldType Type, IReadOnlyList<string>? Options, bool Required);
|
||||
|
||||
public sealed record StageInputDto(
|
||||
int InputId, StageInputSource Source, int? ItemId,
|
||||
int? FromOutputId, string? FromOutputKey, int UomId, decimal QtyPerBatch);
|
||||
|
||||
public sealed record StageOutputDto(
|
||||
int OutputId, string Key, int? ItemId, string Name, int UomId, decimal QtyPerBatch);
|
||||
|
||||
public sealed record TemplateStageDto(
|
||||
int StageId, string Key, string Name, string? RoleLabel, int EstimatedMinutes,
|
||||
decimal PosX, decimal PosY, IReadOnlyList<FieldDefDto> FieldDefs,
|
||||
IReadOnlyList<StageInputDto> Inputs, IReadOnlyList<StageOutputDto> Outputs);
|
||||
|
||||
public sealed record TemplateEdgeDto(
|
||||
int EdgeId, int ParentStageId, int ChildStageId, string ParentKey, string ChildKey);
|
||||
|
||||
/// <summary>
|
||||
/// A canvas-only grouping box or divider line. Round-tripped verbatim: no server-side
|
||||
/// meaning whatsoever, and invisible to the graph validator.
|
||||
/// </summary>
|
||||
public sealed record CanvasAnnotationDto(
|
||||
string Kind, decimal PosX, decimal PosY, decimal Width, decimal Height,
|
||||
string? Label, decimal? Rotation);
|
||||
|
||||
/// <summary>Full graph (docs/30 §D.1 <c>GET /production-templates/{id}</c>).</summary>
|
||||
/// <remarks>
|
||||
/// <c>ActiveRunCount</c> and <c>Annotations</c> are additions to the documented shape.
|
||||
/// The first is what puts the builder into its edit-locked state (docs/21 §2) and mirrors
|
||||
/// the condition <c>UpdateAsync</c> enforces — without it the builder would need a second
|
||||
/// request to the list endpoint just to know whether to disable itself.
|
||||
/// </remarks>
|
||||
public sealed record TemplateGraphDto(
|
||||
int TemplateId, string Code, string Name, string? Description, EntityStatus Status,
|
||||
IReadOnlyList<TemplateStageDto> Stages, IReadOnlyList<TemplateEdgeDto> Edges,
|
||||
IReadOnlyList<CanvasAnnotationDto> Annotations, int ActiveRunCount,
|
||||
int CreatedBy, DateTime CreatedAt);
|
||||
|
||||
// --- requests ----------------------------------------------------------------
|
||||
// Narrow by design (02-SECURITY §B.6): no status, no ids, no createdBy, no timestamps.
|
||||
// POST and PUT share this shape; PUT replaces the whole graph.
|
||||
|
||||
public sealed class SaveTemplateRequest
|
||||
{
|
||||
[Required, StringLength(30, MinimumLength = 1)]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
[Required, StringLength(150, MinimumLength = 1)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[Required, MinLength(1)]
|
||||
public List<SaveStageRequest> Stages { get; set; } = new();
|
||||
|
||||
/// <summary>Empty is legal — a single-stage template is both entry and terminal.</summary>
|
||||
public List<SaveEdgeRequest> Edges { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Canvas boxes/lines. Capped because this is free-form client state going into a jsonb
|
||||
/// column — a hand-rolled request should not be able to store an unbounded blob.
|
||||
/// </summary>
|
||||
[MaxLength(200)]
|
||||
public List<CanvasAnnotationDto> Annotations { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class SaveStageRequest
|
||||
{
|
||||
/// <summary>Existing stage id as a string, or a client-minted <c>tmp-*</c> key for a new stage.</summary>
|
||||
[Required, StringLength(60, MinimumLength = 1)]
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
[Required, StringLength(150, MinimumLength = 1)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(60)]
|
||||
public string? RoleLabel { get; set; }
|
||||
|
||||
[Range(0, 1_000_000)]
|
||||
public int EstimatedMinutes { get; set; }
|
||||
|
||||
/// <summary>Canvas coordinates, stored verbatim and never interpreted server-side.</summary>
|
||||
public decimal PosX { get; set; }
|
||||
public decimal PosY { get; set; }
|
||||
|
||||
public List<FieldDefDto> FieldDefs { get; set; } = new();
|
||||
public List<SaveInputRequest> Inputs { get; set; } = new();
|
||||
public List<SaveOutputRequest> Outputs { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class SaveInputRequest
|
||||
{
|
||||
[Required]
|
||||
public StageInputSource Source { get; set; }
|
||||
|
||||
/// <summary>Required when <see cref="Source"/> is Stock; must be null when Upstream.</summary>
|
||||
public int? ItemId { get; set; }
|
||||
|
||||
/// <summary>Required when <see cref="Source"/> is Upstream; must name an output of a direct parent.</summary>
|
||||
[StringLength(60)]
|
||||
public string? FromOutputKey { get; set; }
|
||||
|
||||
[Range(1, int.MaxValue)]
|
||||
public int UomId { get; set; }
|
||||
|
||||
[Range(0.0001, double.MaxValue)]
|
||||
public decimal QtyPerBatch { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SaveOutputRequest
|
||||
{
|
||||
/// <summary>Existing output id as a string, or a <c>tmp-*</c> key. Referenced by Upstream inputs.</summary>
|
||||
[Required, StringLength(60, MinimumLength = 1)]
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Required on the terminal stage's single output; must be null on every other output.</summary>
|
||||
public int? ItemId { get; set; }
|
||||
|
||||
[Required, StringLength(150, MinimumLength = 1)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Range(1, int.MaxValue)]
|
||||
public int UomId { get; set; }
|
||||
|
||||
[Range(0.0001, double.MaxValue)]
|
||||
public decimal QtyPerBatch { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SaveEdgeRequest
|
||||
{
|
||||
[Required, StringLength(60, MinimumLength = 1)]
|
||||
public string ParentKey { get; set; } = string.Empty;
|
||||
|
||||
[Required, StringLength(60, MinimumLength = 1)]
|
||||
public string ChildKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Body of <c>PATCH /production-templates/{id}/status</c> (docs/30 §D.1).</summary>
|
||||
public sealed class UpdateTemplateStatusRequest
|
||||
{
|
||||
[Required]
|
||||
public EntityStatus Status { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user