---
phase: dataModel
kind: level
level: attributes
---

# Aspect — ATTRIBUTES (incl. computed formulas)

For each entity, define its `tablePrefix`, `tenantMode`, business attributes
(type, length, required, unique), computed formulas, indexes and an optional code
pattern. You auto-deduce attributes from the module's `use-case.md` and
`règles-métier.md` and present the complete result for review — you do NOT ask
the user field by field.

## How to deduce attributes (closed sources only)

- **Business rules** — `salary >= minimumWage` → `Salary` (decimal); `when
  status = 'active'` → `Status` (enum); "email unique per tenant" → `Email`
  (string, unique).
- **Use cases** — "fills in their leave dates" → `StartDate`, `EndDate`; "enters
  an approval note" → `ApprovalNote` (text); "the system calculates the total" →
  a **computed** attribute (see below).

**NEVER from domain knowledge or web search.** "Standard" fields (`FirstName`,
`Email`, `Status`, `TotalExclTax`…) are not a source: if no module UC/BR mentions
the field, do not add it — surface the gap and defer. For a `person: mandatory`
entity, identity fields (`FirstName`, `LastName`, `Email`) come from `auth_Users`
— never redeclare them (the decision is recorded by the entity's
`- **Personne**` line, see SKILL.md § Person extension pattern).

## tablePrefix (functional domain, not module location)

| Prefix | Domain | Prefix | Domain |
|--------|--------|--------|--------|
| `hr_` | Employee, Department, Timesheet | `crm_` | Customer, Lead, Opportunity |
| `ops_` | Project, Task, Resource | `fin_` | Invoice, Payment, Budget |
| `inv_` | Product, Warehouse, StockMovement | `cfg_` | tenant lookups |

Invent 2-5 lowercase letters + `_` for new domains (`edu_`, `med_`…). Match the
prefix of the module's existing entities when in doubt. Reserved prefixes are
blocked — see SKILL.md § Collision detection.

## tenantMode

| Mode | Use for |
|------|---------|
| `strict` (default) | tenant-isolated business data (employees, orders, invoices) |
| `optional` | shared resources with per-tenant overrides (email templates) |
| `none` | platform lookups single-set for all tenants (`Language`, `Country`) |

Default `strict`. These three values are exactly what `scaffold-entity` accepts —
there is no fourth mode. Use AskUserQuestion only when tenancy is genuinely
ambiguous.

**Say it in the model** — `- **Portée** : strict` (or `optional` / `none`),
once at the top of `entité.md` before the first `### ENT-` heading (inherited
by every entity), and per entity only to override. Without it the scaffolder
falls back to `strict`, and it is THAT default — not the model — which decides
the tenant-composite shape of every declared unique index (DM-028).

## Types & lengths — stick to these

**Strings:** 50 (codes), 100 (names/labels), 200 (titles), 256 (email/URL),
500/1000/2000 (descriptions), 4000 (JSON), `text` (unbounded). Never arbitrary
lengths — round to the nearest standard.

**Decimals:** money `decimal(18,8)`; percentages `decimal(5,4)`; quantities
`decimal(18,4)`; coordinates `decimal(9,6)`. Never `float`/`double` for money.

**Palette:** `string text int long short decimal float double bool datetime date
time guid enum json`.

**No `binary` type — file content is never an attribute.** A document/pièce
jointe is modeled as a METADATA entity (`FileName` string/256, `StoredFileName`
string/500 unique, `ContentType` string/100, `FileSizeBytes` long + parent FK);
the bytes live in the platform `IFileStorageService` (see SKILL.md rule C-6 and
`development/backend/data-layer/references/file-storage.md`). A
`binary`/`blob`/`varbinary`/`byte[]` attribute is rejected downstream
(PRD-053 err, `scaffold-entity` throws).

## Computed attributes (the `Calculé` column)

When a value is **deterministically derived from other attributes of the SAME
entity** (line total, consumption rate, age from a birth date), declare it as a
normal attribute and put the **C# formula** in the `Calculé` column of
`entité.md`. The dev cascade picks it up: DTO projection, repository LINQ
`.Select(…)` (single query, no N+1), read-only React column.

**Rules (load-bearing — they cascade to the dev pipeline):**
- The formula is a C# expression referencing **PascalCase properties of the same
  entity** (`Amount`, `Probability`, `BirthDate`).
- **Cross-entity references are forbidden** (the audit rejects them). A value
  needing data from a related entity stays a backend service / business rule.
- Side-effect-free, EF-translatable (pure arithmetic / string concat). No method
  calls EF can't translate. Max ~1000 chars.
- A computed attribute has **no DB column and no setter** — it is read-only.

| Use case | Type | Formula |
|---|---|---|
| Weighted amount of an opportunity | decimal | `Amount * Probability` |
| Line total of an invoice item | decimal | `Quantity * UnitPrice * (1 + VatRate)` |
| Full name of a contact | string | `FirstName + " " + LastName` |
| Consumption rate of a budget | decimal | `(InitialAmount - CurrentBalance) / InitialAmount` |

**When NOT computed:** the value is stored & user-editable; or it needs a related
entity; or it's non-deterministic (random, time-now without an input attribute) →
omit the formula (`—` in the column).

## Framework fields — DO NOT list

`Id`, `CreatedAt`, `UpdatedAt`, `DeletedAt` and `TenantId` are implicit — the
generated `ExtensionBaseEntity` and the entity's tenancy add them, and
`scaffold-entity` DROPS them (with a warning) if listed. Show only `Id Guid PK` in
the table for readability; never the other four.

`CreatedBy`, `UpdatedBy` and `Scope` are **NOT** provided by the generated base
class: listed in the table, they become real columns. List them only when the
model wants those columns — never on the assumption that "the framework adds
them". (DM-027 says which of the two classes a listed framework name falls in.)

## Indexes & code patterns

- **Indexes** — only those a UC/BR query pattern justifies. Unique for business
  uniqueness ("no two users share an email" → `(Email) unique`); composite for
  frequent filters (`(Status, CreatedAt)`). No need to index FK columns **for
  performance** (EF auto-indexes them) — but UNIQUENESS on an FK is a business
  rule and MUST be declared like any other: a 1↔1 ("un collaborateur n'est
  habilité qu'une fois" → `(UserId) unique`) or an FK-bearing composite
  (`(DrivingLicenceId, Kind, ThresholdDays) unique` — what physically prevents
  a double notification) is real index grammar, carried downstream to
  `scaffold-entity` (`relations[].unique` / `indexes[]`) and gated by
  DEV-API-031. In `entité.md`, list as `(Field)` / `(Field) unique` /
  `(A, B) unique`.
- **Never list `TenantId` in an index.** The discriminator is synthesised from
  the entity's tenancy: the scaffolder prefixes every declared unique with it
  (`(Code) unique` → `(TenantId, Code)` on a tenant entity) and indexes
  `TenantId` on its own. A declared `(TenantId, Code) unique` used to emit an
  anonymous type naming `TenantId` twice — CS0833, uncompilable. DM-023 errs on it.

### When an entity deserves a code (the decision test)

Four species of identifier — decide which one FIRST, for every entity:

| Species | What it is | Data-model marker |
|---------|------------|-------------------|
| **Allocated code** | a number the SOCLE generates at insert (`CodedEntitySaveHandler`) | `code` attribute + a `**Code pattern**` line |
| **Typed code** | a short business key the USER types (`Matricule`, `Reference`) | the attribute + a `- **Code saisi** : <Attribut>` bullet (EN alias `**Typed code**`) — **no** `**Code pattern**` line |
| **Decided code** | a code on a **reference table**, wanted by the USER | a `- **Code décidé** : <what the code names> — décision utilisateur du <AAAA-MM-JJ>` bullet (EN alias `**Decided code**`) |
| **No code** | the name/label IS the human handle | `name`/`label` + `**Affichage**` |

> **Decided code and Typed code are ORTHOGONAL, not variants.** `**Code saisi**`
> answers *how* the code is produced (typed, never allocated); `**Code décidé**`
> answers *why this reference table has one at all*. On a lookup they legitimately
> co-exist — write both.

**The business may not call it « code ».** An attribute named after the
code-like lexicon below, shaped like a business key (string + unique), MUST be
classified into one of the four species — DM-021 warns on an undecided one
(and errs when an unparsable `Code pattern` declaration lives in the same
block). A typed key keeps its business name (`Reference`, `Matricule`) — mark
it with `- **Code saisi** : <Attribut>` so the audit knows the species was
DECIDED, not forgotten. An allocated code stays the `code` attribute — give it
its business display name through the `libellé « … »` facet of the
`**Code pattern**` line instead of renaming the column.

<!-- code-like-lexicon:v1 -->
Code-like attribute words (FR/EN, accent-folded, matched whole-word over the
PascalCase/separator split — `NumeroClient` matches, `Preference` and
FK-shaped `ReferenceId` never do): `reference`, `ref`, `numero`, `number`,
`num`, `matricule`.
<!-- /code-like-lexicon:v1 -->

An ALLOCATED code is justified **only when the object is referenced OUTSIDE the
UI** — at least one of:

1. **Human communication across channels** — the number is quoted on the phone,
   in an email, at a support desk (order, ticket, customer number).
2. **Outbound document / cross-system reference** — it appears on a document that
   leaves the system (invoice, PO, delivery note) or is reconciled in another
   system (accounting, ERP).
3. **Legal / audit / fiscal requirement** — sequential traceable numbering is
   mandated (invoices in most jurisdictions; the socle is gapless by default).
4. **Long-lived case tracking** — the object lives a long lifecycle and is
   re-referenced over years (contract, claim, dossier).

One-sentence test: *would someone dictate this reference over the phone, or look
it up from a piece of paper?* If no criterion holds, do NOT author a code — the
entity's handle is its `name`/`label`.
NEVER an allocated code on: lookups (see the doctrine below — no code at all by
default), child/line entities (numbered relative to their parent at most —
express the parent in the format, never an own sequence), technical entities,
entities whose natural handle is a unique name the business already uses. A code
authored "because every entity has one" is a modelling error **DM-022** flags on
a reference table — not a convention.

### A reference value does not carry a code

**A reference value's LABEL is its identity and its natural key.** A `lookup`
gets no `code` by default — not an allocated one, not a typed one.

**Only the USER may decide that one of these tables carries a code.** That
decision, written in `entité.md` and **dated**, **overrides the rule**: no audit
re-argues it, no backfill undoes it, no pass proposes it on its own initiative.

- **`- **Code décidé** : <what the code names> — décision utilisateur du <AAAA-MM-JJ>`**
  (EN alias `**Decided code**` … `user decision of <AAAA-MM-JJ>`), a BULLET at
  the head of the `### ENT-NNN` block, never a table cell.
  1. **An agent NEVER authors this line on its own initiative** — it transcribes
     a decision the user took, and it carries its **date**.
  2. **Without it**, a `code` attribute on an entity classed `lookup` is a
     defect (**DM-022**, err).
  3. **With it**, no audit discusses that code any more — no consolation warn,
     no "consider removing".
  4. A line that MENTIONS the decision without being analysable — a table cell,
     an unbolded bullet, or a bullet with **no real ISO date** — is an **err**,
     never a silence. An undated « decision » is a habit, not a decision.
  5. A **Decided code** is *typed*, never *allocated*: the `**Code pattern**`
     prohibition below stays whole, and it is the one thing DM-022 still judges
     on a decided entity.
- **The price, to weigh BEFORE deciding.** Dropping the code moves the seed's
  natural key onto the label (`**Valeurs initiales** : clé <Libellé>`), and a key
  on a label **breaks if someone renames the row** — the seed then re-creates it
  instead of finding it. That is the trade, and the user must see it at decision
  time, not six months later.
- The full inventory — table by table, with the VERBATIM citations of each
  code's values across the business rules, the acceptance criteria, the PRD and
  the other seeded tables — comes from:

  ```bash
  npx --prefer-offline tsx skills/business-analyse/create-data-model/cli/derive-referential-codes/index.ts \
    --spec '{"baRoot":".smartstack/ba","app":"<APP>","module":"<MODULE>","mode":"check"}'
  ```

  It reads the BA, not the C#: an absence of citation is not a proof of absence.

- **Code pattern** — only for entities that PASS the decision test above. A
  `codePattern` is the data-model side of a
  `numbering` business rule (`/ba-create-business-rules`) and carries:
  - **format** — literals + tokens (`AFF-{YY}-{SEQ:4}` → prefix + 2-digit year +
    4-digit zero-padded counter). The vocabulary is the socle's and is **CLOSED**:
    `{YYYY}` `{YY}` `{MM}` `{DD}` `{TENANT}` `{SEQ:n}`, plus the field-derived
    `{FIELD:Champ}` `{UPPER:Champ}` `{LOWER:Champ}` `{SLUG:Champ}`
    `{INITIALS:Champ}` `{ABBR:Champ:n}`. `{SEQ}` / `{SEQ:n}` is REQUIRED unless the
    format derives from ≥1 field token. Anything invented — `{NNNN}`, `{YEAR}`,
    `{SEQUENCE}` — is rejected by the engine at every insert
    (`Unrecognised token(s)`), so never coin a token ;
  - **scope** — `tenant` (default, tenant-isolated data) / `global` (platform-wide).
    Those two are the whole enum (`CodeScopeKind`) ; **must be consistent with
    `tenantMode`**. A counter *per parent row* is NOT supported — put the parent in
    the format instead (`{ABBR:ClientName:3}-{SEQ:4}`) ;
  - **reset** — `none` / `yearly` / `monthly` / `daily`, aligned with a date token
    in the format (yearly ⇒ `{YY}`/`{YYYY}`; misalignment is rejected) ;
  - **gapless** — `true` (legal/accounting, no gaps — the socle default) / `false`
    (high-throughput HiLo, gaps tolerated).
  - **libellé** (optional) — the BUSINESS name of the code in the UI/i18n:
    `libellé « Référence »` (EN alias `label "Reference"`). The column stays
    `Code` (the socle `ICodedEntity` contract); the label travels to every
    list column, form and detail field through the pagespec flag. Author it
    whenever the business never says « code » (Référence, Numéro, Matricule…).
  - **surchargeable à la création** (optional, EN alias `supplied on create`) —
    an OPTIONAL user/import-supplied code is sanctioned on the CREATE surface:
    the generated Create command gains `string? Code = null`, validated by the
    socle's `ISuppliedCodeGuard` and applied BEFORE save (`HasCode`
    short-circuits the engine — the idempotency contract; imports, semis and
    reprises arrive WITH their number and keep it). Updates NEVER carry a
    Code. Decision test: *does the object already arrive numbered?* On a
    `{SEQ}` format, DM-017 check 7 warns about the deferred unique-index
    collision corridor (keep supplied codes out of the mask's shape).

  Reference the `numbering` rule code (`BR-…`) in the entity's **Traçabilité**.
  **Never model the counter.** The socle allocates the code atomically at insert
  and an admin retunes the format in **Administration → Configuration → Code
  patterns** (no seed, no migration — a DB row only overrides the default). A
  `{Entity}Sequence` / `nextValue` table duplicates the engine and is flagged
  (DM-017, capability `code-generation`). Lookups, junctions and audit logs get no
  code pattern. This stays true even on a table whose code the user DECIDED: a
  **Decided code** is SAISI (typed), never ALLOUÉ (allocated) — the two must not
  be confused, and DM-022 errs on a `**Code pattern**` sitting on a reference
  table.

### Valeurs initiales (business-fixed rows)

When the UCs/BRs FIX an entity's rows (a precondition names them: "les 9 types
existent : expertise, vignette, …"), declare them as DATA with a
`- **Valeurs initiales** : clé <Attribut> — …` line + a markdown table whose
columns are the entity's attributes (grammar in `create-data-model/SKILL.md`
§ "Reference data"). Prose in a use case is a format no CLI reads: an entity
whose screens declare neither `create` nor `delete` AND that nothing seeds is
DEFINITIVELY empty — its UCs are unsatisfiable while every audit stays green.
The seed key is a natural attribute, never an allocated `Code pattern` code —
and on a reference table WITHOUT a `**Code décidé**`, that attribute is the
**label** (see `create-data-model/SKILL.md` § Reference data for how it
normalises, and for what a label key costs when a row is renamed).

## Quality checks (per entity)

- `tablePrefix` matches `^[a-z]{2,5}_$`, not reserved; `tenantMode` set.
- ≥1 attribute; every string has a length, every decimal precision/scale, every
  enum non-empty values.
- Computed attributes reference only same-entity PascalCase props (no
  cross-entity).
- A UC precondition that NAMES fixed rows ⇒ the entity carries
  `**Valeurs initiales**` (or the screens expose `create`).
- No framework field listed. Then continue to relationships (`./relationships.md`).
