⟨/⟩ 50_Domain/Cards/CardDocument.cs

163 строк · в начало

using System;
using System.Collections.Generic;
using Ban.Sdaid.Notation.Documents;
using Ban.Sdaid.Icd.Domain.Capture;
using Ban.Sdaid.Icd.Domain.Cards.Template;

namespace Ban.Sdaid.Icd.Domain.Cards
{
    /// <summary>Документ информационной карты — то, что заполнено по своему виду; может быть составным.</summary>
    [DomainEntity]
    public class CardDocument
    {
        /// <summary>Идентификатор документа.</summary>
        public Guid DocId { get; init; }

        /// <summary>Непосредственный родитель; пуст у документа верхнего уровня.</summary>
        public Guid? ParentId { get; init; }

        /// <summary>Вид, по которому заполнен документ.</summary>
        [Relation]
        public DocumentKind Kind { get; init; } = null!;

        /// <summary>Вложенные документы.</summary>
        [Relation(Kind = RelationKind.Composition)]
        public IReadOnlyList<CardDocument> NestedDocuments { get; init; } = new CardDocument[0];

        /// <summary>Заполненные атрибуты документа.</summary>
        [Relation(Kind = RelationKind.Composition)]
        public IReadOnlyList<CardDocumentAttribute> Attributes { get; init; } = new CardDocumentAttribute[0];
    }

    /// <summary>Атрибут документа карты — факт заполнения атрибута вида в конкретном документе.</summary>
    [DomainEntity]
    public class CardDocumentAttribute
    {
        /// <summary>Документ, которому принадлежит заполненный атрибут.</summary>
        [Relation]
        public CardDocument Document { get; init; } = null!;

        /// <summary>Атрибут вида документа, который заполнен.</summary>
        [Relation]
        public DocumentKindAttribute KindAttribute { get; init; } = null!;

        /// <summary>Значения в текстовом представлении; для выбора — коды вариантов. Пусто — не заполнено.</summary>
        public IReadOnlyList<string> Values { get; init; } = new string[0];

        /// <summary>Происхождение значения: распознано или введено пользователем.</summary>
        [Relation]
        public ValueOrigin Origin { get; init; }

        /// <summary>Пользователь, заполнивший значение.</summary>
        public Guid FilledByUserId { get; init; }

        /// <summary>Момент заполнения.</summary>
        public DateTime FilledAt { get; init; }
    }

    /// <summary>Описание доменной сущности <see cref="CardDocument"/>.</summary>
    public class CardDocumentSpec : IDomainEntityDocument
    {
        public static string _refName = "документ информационной карты";

        public Type Entity => typeof(CardDocument);

        public string Name => "Документ информационной карты";

        public string Description =>
            @"То, что заполнено на конкретной карте: документ своего вида с заполненными атрибутами и вложенными документами.";

        public string Version => "0.1";
        public string Status => "draft";

        public string[] Comments => new[]
        {
            "2026-08-19. Заведён по варианту 5 решения о структуре данных; заместил документ со значениями атрибутов.",
        };

        public static string S1_Pairing = """
            ## Пара к описанию

            Состав задан дважды параллельными парами. **Вид документа** и его **атрибут вида** —
            описание того, что бывает; **документ карты** и его **атрибут документа** — то, что
            заполнено на конкретной карте.

            Каждый уровень экземпляра ссылается на свой уровень описания: документ знает свой вид,
            атрибут документа знает атрибут вида. Через атрибут вида известны обязательность,
            код и наименование атрибута, его тип значения, порядок и участие в ядре.
            """;

        public static string S2_Nesting = """
            ## Вложение

            Документ ссылается на непосредственного родителя, образуя дерево заполненных документов
            внутри карты: эксперты, замеры величины pH, авторы, переписчики, владельцы, биотесты,
            примечания.
            Дерево экземпляров повторяет дерево видов: документ может быть вложен в другой, только
            если его вид входит в состав вида родителя.
            """;

        public static string S3_Invariants = """
            ## Инварианты

            - Вид документа не меняется после создания: смена вида — это другой документ.
            - Атрибут документа ссылается на атрибут **своего** вида; чужой атрибут вида в документ
              попасть не может.
            - Один атрибут вида заполняется в документе не более одного раза: повтор выражается
              не вторым заполнением, а списком значений либо вложенным документом.
            - Документ полон, когда заполнены все обязательные атрибуты его вида.
            """;

        public static string S99_Related = $"""
            ## Связанные артефакты

            - {nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_006_CardDataStructure)} — решение о структуре данных информационной карты
            """;
    }

    /// <summary>Описание доменной сущности <see cref="CardDocumentAttribute"/>.</summary>
    public class CardDocumentAttributeSpec : IDomainEntityDocument
    {
        public static string _refName = "атрибут документа карты";

        public Type Entity => typeof(CardDocumentAttribute);

        public string Name => "Атрибут документа карты";

        public string Description =>
            @"Заполнение атрибута в документе карты: значения, происхождение, кем и когда заполнено.";

        public string Version => "0.1";
        public string Status => "draft";
        public string[] Comments => new string[0];

        public static string S1_Purpose = """
            ## Назначение

            Здесь живёт **факт заполнения** — то, что не относится ни к описанию, ни к самому
            значению: кем и когда заполнено, распознано или введено вручную. В варианте без этой
            сущности такие сведения пришлось бы вешать либо на значение, либо на документ целиком,
            теряя привязку к полю.

            Значение — **поле** этой сущности, а не отдельная сущность: собственных реквизитов
            у него нет. Уверенность распознавания, кадр и область остались в пакете ввода, потому
            что это данные о распознавании, а не о результате.
            """;

        public static string S2_Values = """
            ## Значения

            Значения хранятся списком: тип может допускать несколько. Материалы основы, украшения
            и физико-механические повреждения отмечаются сразу несколькими клетками.

            Цена такого хранения — запрос «во всех картах, где среди материалов есть картон»
            становится поиском по содержимому списка, а не соединением таблиц. Это приемлемо:
            поиск за пределами ядра карты решено не поддерживать.

            Значения хранятся текстом, а правило их разбора известно из типа значения атрибута.
            Для выбора из перечисления и справочника хранятся коды вариантов, а не подписи:
            подпись зависит от бланка, код — нет.
            """;
    }
}