[{"name":"Identifiable","desc":"Root base schema. Provides a UUID primary key. All persisted entities extend this, either directly or via BaseDocument / OperationalDocument / OperationalSubDocument / TransactionalDocument / LedgerEntry.","provides":["id"],"properties":[{"n":"id","r":true,"t":"string","info":"UUID v4 primary key."}],"extendedBy":["Attribute Set","Brand","Classification","Franchise Group","Sales Channel","Vendor Dictionary","Advanced Shipping Notice","Advanced Shipping Notice Item","Bill","Bill Item","Delivery Notification","Purchase Order Acknowledgement Line","Ship Notification","Vendor Credit","Vendor Credit Item","Vendor Invoice"]},{"name":"BaseDocument","desc":"Standard document base for non-operational entities (e.g. reports, configurations). Adds audit trail, soft-delete, custom data, and tagging.","provides":["id","company","recentActions","createdBy","createdDate","customData","franchiseGroups","isDeleted","modifiedBy","modifiedDate","tags","uniqueValues"],"properties":[{"n":"company","r":true,"t":"entityDetail","re":"Company","info":"The Company tenant this document belongs to. Defines the hard isolation boundary for queries, permissions, replication, and export. Paired with franchiseGroups which provides sub-tenant segmentation within this Company."},{"n":"createdBy","t":"string","info":"User ID who created the document."},{"n":"createdDate","r":true,"t":"datetime","info":"ISO 8601 creation timestamp."},{"n":"customData","r":true,"t":"array","info":"Custom fields with label, fieldType, value, and sequence. Supports text, decimal, integer, date, flag, multiselect, lookup, and longtext."},{"n":"franchiseGroups","r":true,"t":"array","info":"EntityDetail references (code, name, alias, sequence) linking the document to franchise groups."},{"n":"isDeleted","r":true,"t":"boolean","info":"Soft-delete flag."},{"n":"modifiedBy","t":"string","info":"User ID who last modified the document."},{"n":"modifiedDate","r":true,"t":"datetime","info":"ISO 8601 last-modification timestamp."},{"n":"recentActions","r":true,"t":"array","info":"Denormalized snapshot of the most recent actions performed on this document (capped at the last 5 entries). The Entity Action Log is the system of record for the full audit trail. Each entry follows the Action inline schema: actionDate, actionedBy, code, label, actionType (FieldChange | Operation), and optional change detail."},{"n":"tags","r":true,"t":"array","info":"Free-form string tags for categorization."},{"n":"uniqueValues","r":true,"t":"array","info":"Alternative identifiers — array of Identifier (isPrimary, label, name, value)."}],"ext":"Identifiable","extendedBy":["Company"],"inlineSchemas":[{"name":"Action","schema":"schemas/common/Action.ts","properties":[{"info":"ISO 8601 timestamp when the action occurred.","name":"actionDate","type":"datetime","required":true},{"info":"User ID who performed the action.","name":"actionedBy","type":"string","required":true},{"info":"Discriminator: FieldChange for property-level mutations, Operation for domain events (e.g. status transition, approval).","name":"actionType","type":"enumeration","values":["FieldChange","Operation"],"required":true},{"info":"Field-level changes that occurred. Each entry is an ActionChange (fieldName, previousValue, newValue). Empty for Operation actions with no field-level side effects.","name":"changes","type":"array","required":true},{"info":"Stable, machine-readable action identifier (e.g. 'status.transition', 'price.update', 'approve', 'line.add'). Provides an enumerable key for reports, dashboards, and automation triggers. While actionType captures the category, code captures the semantic intent of the action.","name":"code","type":"string","required":true},{"info":"Human-readable summary of what happened (e.g. 'Status changed from Draft to Active').","name":"description","type":"string","required":true}]}]},{"name":"OperationalDocument","desc":"Extended document base for operational entities (Location, Vendor, Employee, etc.) and order-lifecycle entities (Sales Order, Purchase Order, etc.). Adds integration identifiers and an idempotency key on top of BaseDocument fields. Transactional entities extend this indirectly via TransactionalDocument.","provides":["id","company","recentActions","createdBy","createdDate","customData","franchiseGroups","identifiers","idmpKey","isDeleted","modifiedBy","modifiedDate","notes","tags","uniqueValues"],"properties":[{"n":"company","r":true,"t":"entityDetail","re":"Company","info":"The Company tenant this document belongs to. Defines the hard isolation boundary for queries, permissions, replication, and export. Paired with franchiseGroups which provides sub-tenant segmentation within this Company."},{"n":"createdBy","t":"string","info":"User ID who created the document."},{"n":"createdDate","r":true,"t":"datetime","info":"ISO 8601 creation timestamp."},{"n":"customData","r":true,"t":"array","info":"Custom fields with label, fieldType, value, and sequence. Supports text, decimal, integer, date, flag, multiselect, lookup, and longtext."},{"n":"franchiseGroups","r":true,"t":"array","info":"EntityDetail references (code, name, alias, sequence) linking the document to franchise groups."},{"n":"identifiers","r":true,"t":"array","info":"OperationalIdentifier array — extends Identifier with originatingSystemName for cross-system integration."},{"n":"idmpKey","r":true,"t":"string","info":"Idempotency / integration key for data-management deduplication."},{"n":"isDeleted","r":true,"t":"boolean","info":"Soft-delete flag."},{"n":"modifiedBy","t":"string","info":"User ID who last modified the document."},{"n":"modifiedDate","r":true,"t":"datetime","info":"ISO 8601 last-modification timestamp."},{"n":"notes","t":"string","info":"Free-text notes on this document."},{"n":"recentActions","r":true,"t":"array","info":"Denormalized snapshot of the most recent actions performed on this document (capped at the last 5 entries). The Entity Action Log is the system of record for the full audit trail. Each entry follows the Action inline schema defined on BaseDocument."},{"n":"tags","r":true,"t":"array","info":"Free-form string tags for categorization."},{"n":"uniqueValues","r":true,"t":"array","info":"Alternative identifiers — array of Identifier (isPrimary, label, name, value)."}],"ext":"Identifiable","extendedBy":["Product","Location","Employee","Vendor","Style","Sales Order","Purchase Order","Transfer Order","Shipment","Fulfillment Order","Ship Order","Catalog","Stock Take","Advanced Shipping Notice","Vendor Invoice","Vendor Credit","Customer","TransactionalDocument"]},{"name":"OperationalSubDocument","desc":"Lightweight operational base for line-items and child documents. Similar to OperationalDocument but without idmpKey or franchiseGroups. Used for entities that live within a parent document context.","provides":["id","recentActions","createdBy","createdDate","customData","identifiers","isDeleted","modifiedBy","modifiedDate","notes","tags","uniqueValues"],"properties":[{"n":"recentActions","r":true,"t":"array","info":"Denormalized snapshot of the most recent actions performed on this sub-document (capped at the last 5 entries). The Entity Action Log is the system of record for the full audit trail. Each entry follows the Action inline schema defined on BaseDocument."},{"n":"createdBy","t":"string","info":"User ID who created the sub-document."},{"n":"createdDate","r":true,"t":"datetime","info":"ISO 8601 creation timestamp."},{"n":"customData","r":true,"t":"array","info":"Custom fields with label, fieldType, value, and sequence."},{"n":"identifiers","r":true,"t":"array","info":"OperationalIdentifier array — extends Identifier with originatingSystemName."},{"n":"isDeleted","r":true,"t":"boolean","info":"Soft-delete flag."},{"n":"modifiedBy","t":"string","info":"User ID who last modified the sub-document."},{"n":"modifiedDate","r":true,"t":"datetime","info":"ISO 8601 last-modification timestamp."},{"n":"notes","t":"string","info":"Free-text notes on this sub-document."},{"n":"tags","r":true,"t":"array","info":"Free-form string tags for categorization."},{"n":"uniqueValues","r":true,"t":"array","info":"Alternative identifiers — array of Identifier (isPrimary, label, name, value)."}],"ext":"Identifiable","extendedBy":["Item","Advanced Shipping Notice Carton","Shipment Carton","Stock Transfer Carton"],"usedAsLinesBy":["Stock Adjustment","Stock Take","Stock Transfer","Transfer Order","Purchase Order","Goods Receipt","Vendor Invoice","Bill","Sales Order","Fulfillment Order","Ship Order","Fulfillment","Shipment","Sale","Advanced Shipping Notice Carton","Shipment Carton","Stock Transfer Carton","Purchase Order Acknowledgement"]},{"name":"TransactionalSubDocument","desc":"Lightweight transactional base for line-items and child documents within transactional entities. Extends OperationalSubDocument with transactionDate, fiscalDate, postedDate, and isVoided semantics. Used for sub-documents that participate in transactional workflows and may write to ledgers upon posting.","provides":["id","recentActions","createdBy","createdDate","customData","fiscalDate","identifiers","isDeleted","isVoided","modifiedBy","modifiedDate","notes","postedDate","tags","transactionDate","uniqueValues"],"properties":[{"n":"transactionDate","r":true,"t":"datetime","info":"The business date/time when the line-level transaction occurred. Typically inherited from the parent document but can be overridden for split-date scenarios."},{"n":"fiscalDate","r":true,"t":"datetime","info":"The fiscal calendar date assigned to this line. Used for period-based reporting. Typically inherited from the parent document."},{"n":"postedDate","t":"datetime","info":"Timestamp when this line was finalized and committed to its target ledger(s). Null while in a pre-posted state."},{"n":"isVoided","r":true,"t":"boolean","info":"Whether this line has been voided or reversed. Defaults to false. Voided lines remain for audit but are excluded from active reporting."}],"ext":"OperationalSubDocument","extendedBy":[]},{"name":"LookupEntity","desc":"Abstract base schema for all reference-data / dictionary entities. Provides the standard shape: code, name, alias, franchise-group scoping, active/default/deleted/readOnly flags, audit fields, and custom-data support. Not instantiated directly.","provides":["id","code","name","alias","company","franchiseGroups","idmpKey","isActive","isDefault","isDeleted","isReadOnly","sequence","createdBy","createdDate","modifiedBy","modifiedDate","customData"],"properties":[{"n":"alias","t":"schema","info":"Optional label/value pair for alternate display text (LookupEntityAlias)."},{"n":"code","r":true,"t":"string","info":"Human-readable key. Unique within scope."},{"n":"company","t":"entityDetail","re":"Company","info":"The Company tenant this lookup value belongs to. OPTIONAL on this base schema because a small set of dictionaries are platform-global (Currency, Locale, Country, Environment) and must have company null. All other Dictionary entities must declare company: required per the tenant-scoped-dictionary-declares-company convention."},{"n":"createdBy","t":"string","info":"User ID who created the record."},{"n":"createdDate","t":"datetime","info":"ISO 8601 creation timestamp."},{"n":"customData","r":true,"t":"array","info":"Extensible key/value pairs for tenant-specific metadata (DocumentCustomData)."},{"n":"franchiseGroups","r":true,"t":"array","info":"EntityDetail references scoping this lookup value to one or more franchise groups."},{"n":"idmpKey","r":true,"t":"string","info":"Idempotency / integration key for data-management deduplication."},{"n":"isActive","r":true,"t":"boolean","info":"Whether this lookup value is active. Defaults to true."},{"n":"isDefault","r":true,"t":"boolean","info":"Whether this is the default selection. Defaults to false."},{"n":"isDeleted","r":true,"t":"boolean","info":"Soft-delete flag. Defaults to false."},{"n":"isReadOnly","r":true,"t":"boolean","info":"Whether this lookup value is system-managed and not editable. Defaults to false."},{"n":"modifiedBy","t":"string","info":"User ID who last modified the record."},{"n":"modifiedDate","t":"datetime","info":"ISO 8601 last-modification timestamp."},{"n":"name","r":true,"t":"string","info":"Display name."},{"n":"sequence","t":"integer","info":"Display ordering hint."}],"ext":"Identifiable","extendedBy":["Currency","Locale","Tax Class","Price Level","Cost Level","Item Stock Group","Stock Limit Group","Purchasing Fee","Shipping Method","Loyalty Program"]},{"name":"LookupEntityValue","desc":"Base schema for inline value schemas within LookupEntity entities. Analogous to OperationalSubDocument for OperationalDocument — provides a lightweight identity and audit shape for nested value objects that exist only within the context of their parent LookupEntity. Carries its own id, identifiers, code, name, aliases, synonyms, and audit trail.\n\nCANONICAL VALUE CONTRACT (added 03 Sep 2026): a value in a LookupEntity's value collection is the CANONICAL form — the only spelling stored, displayed or reported. Two properties carry alternate text and they are deliberately separate, because they point in opposite directions:\n\n- 'aliases' is OUTBOUND. Alternative display names or label/value pairs used by the rendering layer (per-locale, per-channel). No cardinality rule; two values may legitimately share a label. Deleting one is cosmetic.\n- 'synonyms' is INBOUND. Alternate spellings that RESOLVE to this canonical on write, absorbing variants from vendor catalogs, POS imports and marketplace feeds. Must be unique within the parent's value collection. Deleting one breaks an import contract.\n\nOverloading a single property with both meanings means a display label added for a storefront silently becomes a resolution key and no alias can ever be safely retired. Keep them separate.\n\nNORMALIZATION: all text comparison on this schema — synonym matching and the code/name uniqueness rules — is performed on the platform normalization fold (NFKD normalize, strip diacritics, lowercase, remove all non-alphanumerics), never on raw text. The function is platform-owned and versioned; changing it is a data migration requiring every stored normalizedValue and every derived resolution index to be recomputed. Raw code, name and synonym value are always stored and displayed exactly as authored.\n\nUNIQUENESS: within one parent LookupEntity's value collection, the union of normalized codes, normalized names, and synonym normalizedValues must contain no duplicates. This single rule covers four cases: code unique in set; name unique in set; no two canonicals claiming the same synonym; and no synonym colliding with a canonical key (which would never fire, since the resolver matches code and name first, leaving a silently dead mapping). Because value collections are embedded, this is not expressible as a database unique index on the JSON array — concrete implementations should materialize a resolution-key projection keyed (company, parent, normalizedKey) inside the same transaction as the parent write, which both enforces the constraint under concurrency and turns import resolution into one indexed lookup instead of a per-row array scan.\n\nCANONICAL HYGIENE IS THE OPERATOR'S RESPONSIBILITY: the rules above catch mechanical duplication (Red / red / RED). They cannot catch two canonicals that merely MEAN the same thing. Systems should surface non-blocking similarity hints at authoring time — most usefully when a new canonical collides with an existing synonym — but must never apply fuzzy matching in the resolver, where a silent wrong merge surfaces months later as inventory variance.","provides":["id","identifiers","code","name","aliases","synonyms","isActive","isDeleted","isDefault","sequence","createdBy","createdDate","modifiedBy","modifiedDate","customData"],"properties":[{"n":"aliases","t":"array","info":"Alternative display names or label/value pairs for this value."},{"n":"code","r":true,"t":"string","info":"Human-readable key. Unique within the parent entity's value collection."},{"n":"createdBy","t":"string","info":"User ID who created the value."},{"n":"createdDate","t":"datetime","info":"ISO 8601 creation timestamp."},{"n":"customData","r":true,"t":"array","info":"Extensible key/value pairs for tenant-specific metadata (DocumentCustomData)."},{"n":"identifiers","r":true,"t":"array","info":"OperationalIdentifier array — extends Identifier with originatingSystemName for cross-system integration."},{"n":"isActive","r":true,"t":"boolean","info":"Whether this value is active. Defaults to true."},{"n":"isDefault","r":true,"t":"boolean","info":"Whether this is the default selection within the parent entity's value collection. Defaults to false."},{"n":"isDeleted","r":true,"t":"boolean","info":"Soft-delete flag. Defaults to false."},{"n":"modifiedBy","t":"string","info":"User ID who last modified the value."},{"n":"modifiedDate","t":"datetime","info":"ISO 8601 last-modification timestamp."},{"n":"name","r":true,"t":"string","info":"Display name of the value."},{"n":"sequence","t":"integer","info":"Display ordering hint within the parent entity's value collection."},{"n":"synonyms","r":true,"t":"array","info":"LookupValueSynonym array (default []). Alternate inbound spellings that resolve to this canonical value on write, so variant spellings from vendor catalogs, POS imports and marketplace feeds collapse onto one canonical instead of fracturing the value set. DISTINCT FROM 'aliases', which is outbound display text with no cardinality rule. Matching is on the computed normalizedValue, never raw text. Synonyms must be unique within the parent LookupEntity's value collection and must not collide with any value's canonical code or name in that collection - a synonym that duplicates a canonical key never fires, because the resolver matches code and name first. Declared on this base rather than on individual value schemas because AttributeValue, OptionValue and every other dictionary value set share the identical inbound-spelling exposure; one declaration means one normalization contract, one uniqueness rule, one resolver and one admin UI."}],"ext":"Identifiable","inlineSchemas":[{"name":"LookupValueSynonym","desc":"An alternate inbound spelling that resolves to a canonical LookupEntityValue on write. Exists to absorb variant spellings from vendor catalogs, POS imports and marketplace feeds (e.g. 'X-Large', 'Extra Large' -> canonical 'XL') so the canonical is the only value ever stored, displayed or reported. DISTINCT FROM 'aliases': aliases are OUTBOUND display text (per-locale, per-channel labels) with no cardinality rule; synonyms are INBOUND resolution keys that must be unique within the parent LookupEntity's value set. Conflating them means a display label added for a storefront silently becomes an import resolution key, and no alias can be retired without risking an import regression. Matching is performed on normalizedValue, never on raw text.","schema":"schemas/lookup-common/LookupValueSynonym.ts","properties":[{"info":"User ID who added the synonym. Matters when synonyms are learned from an import review rather than authored by hand.","name":"createdBy","type":"string","required":false},{"info":"ISO 8601 UTC creation timestamp.","name":"createdDate","type":"datetime","required":false},{"info":"Whether this synonym participates in resolution. Defaults to true. Retiring a synonym sets this false rather than deleting the row, preserving the audit trail of what a prior import was matching on.","name":"isActive","type":"boolean","required":true},{"info":"System-computed fold of 'value' produced by the platform lookup-value normalization function: NFKD normalize, strip diacritics, lowercase, remove all non-alphanumeric characters. Derived, immutable after write, and NEVER accepted from a client. This is the field the set-wide uniqueness constraint is declared on and the only field the resolver compares. Because the fold collapses 'X Large', 'X-Large', 'x_large' and 'xlarge' to the single key 'xlarge', a canonical typically needs far fewer synonym rows than it has observed variants.","name":"normalizedValue","type":"string","required":true},{"info":"Optionally scopes the synonym to one integration source, for the case where the same inbound string means different things in two systems. Null means the synonym applies to all sources. Mirrors OperationalIdentifier.originatingSystemName.","name":"originatingSystemName","type":"string","required":false},{"info":"Provenance of the mapping. 'platform' entries are seeded centrally (the universal Size and Color variant sets) and are read-only in the tenant UI; tenants extend with 'manual'. 'import' entries are written when an operator accepts a suggestion in the import-review UI, so the list improves through use. Provenance is required because unwinding a bad mapping depends on knowing whether it was seeded, hand-authored, learned, or dragged in by a migration - a scalar array of strings cannot express that.","name":"source","type":"enumeration","values":["platform","manual","import","vendor","migration"],"required":true},{"info":"The alternate exactly as authored or received, preserved verbatim for display in admin and for diagnosing where a mapping came from. Never used for matching - see normalizedValue.","name":"value","type":"string","required":true}]}]},{"name":"TransactionalDocument","desc":"Base schema for Transactional-class entities that record business events (sales, adjustments, receipts, transfers). Extends OperationalDocument with a standardized transaction date, posted date, and void/reversal semantics. Every TransactionalDocument writes to at least one Ledger upon posting.","provides":["id","company","recentActions","createdBy","createdDate","customData","fiscalDate","franchiseGroups","identifiers","idmpKey","isDeleted","isVoided","modifiedBy","modifiedDate","notes","postedDate","tags","transactionDate","uniqueValues"],"properties":[{"n":"transactionDate","r":true,"t":"datetime","info":"The business date/time when the transaction occurred. Distinct from createdDate (when the record was persisted) and postedDate (when it was finalized to ledgers). Maps to entity-specific names like Sale.date, StockAdjustment.adjustmentDate, Bill.billDate, GoodsReceipt.receivedDate, PriceAdjustment.effectiveDate, etc."},{"n":"fiscalDate","r":true,"t":"datetime","info":"The fiscal calendar date assigned to this transaction. Used for period-based reporting and accounting period assignment. May differ from transactionDate when transactions are recorded near period boundaries or when fiscal calendars do not align with calendar dates."},{"n":"postedDate","t":"datetime","info":"Timestamp when the transaction was finalized and committed to its target ledger(s). Null while the transaction is in a pre-posted state (e.g. Draft, Processing). Once set, the transaction is considered immutable for accounting purposes."},{"n":"isVoided","r":true,"t":"boolean","info":"Whether this transaction has been voided or reversed. Defaults to false. Voided transactions remain in the system for audit purposes but are excluded from active reporting. The void action writes a corresponding reversal entry to the relevant ledger(s)."}],"ext":"OperationalDocument","extendedBy":["Sale","Stock Adjustment","Stock Transfer","Goods Receipt","Purchase","Bill","Fulfillment","Price Adjustment","Cost Adjustment"]},{"name":"LedgerEntry","desc":"Base schema for Ledger-class entities — immutable, append-only audit records. Extends Identifiable directly (not OperationalDocument) because ledger entries must not be modified or deleted. Provides source-entity traceability and creation audit fields only. Individual ledger entities add domain-specific properties (e.g. ledgerLine, qty, amounts) on top. CURRENCY CONTRACT: any descendant carrying monetary decimals must declare its own required 'currencyCode' property of type string holding an ISO 4217 alpha-3 code — never entityDetail → Currency and never an enumeration of codes — because entries are immutable and replayed in bulk, so the currency must be a stable, self-contained value. FX is conditional on whether the amounts are transacted or authored: transacted amounts (Cost Ledger — vendor invoices in Vendor.orderCurrency; Stock Ledger — inbound cost in a location's operating currency) also declare 'exchangeRate' captured at posting plus a base-currency companion amount, since conversion to the Company currency reflects money that actually moved; authored amounts (Price Ledger — retail price is SET per market, never converted into a market) declare neither, because a converted price is a figure no customer was ever charged. Enforced by the ledger-currency-is-iso-string convention. Transactional and Operational documents keep entityDetail → Currency. The currency properties are deliberately NOT provided by this base schema, because half its descendants (Email Log, Entity Action Log, Announcement Acknowledgement) carry no monetary values. IDEMPOTENCY CONTRACT: this schema provides 'idmpKey' as an OPTIONAL string, and every descendant except those recording genuinely repeatable events must override it as required and unique within Company, documenting the natural key it is composed from — enforced by the ledger-idempotency-key-declared convention. Ledger writers are retryable by nature (queue redelivery, webhook replay, integration re-drive, fan-out retry), and because entries are immutable a duplicate can only be compensated, never corrected, by which point the side effect has already occurred. The property is optional here rather than required because the exemption test — is a duplicate a fact or a mistake? — has one genuine exemption: two identical Entity Action Log rows are two real events, and deduping them would destroy audit data. Composition is stated per entity rather than here because it is domain-specific. Note that existing per-entity guards do not substitute: Email Log.messageId holds an SES identifier assigned after the send is accepted, so it dedupes the log row but not the send; Stock Ledger.ledgerLine with Inventory Position.lastLedgerLine gives idempotent projection on the read side while a replayed posting still takes a fresh line. Placement mirrors the LookupEntity.company / tenant-scoped-dictionary-declares-company precedent, and the name deliberately matches OperationalDocument.idmpKey — one mechanism, one name, two base schemas.","provides":["id","company","entryDate","idmpKey","sourceEntityType","sourceEntityId","createdBy","createdDate"],"properties":[{"n":"company","r":true,"t":"entityDetail","re":"Company","info":"The Company tenant this ledger entry belongs to. Required at write time — ledger entries are immutable, so there is no opportunity to backfill company later. Also serves as the tenant partition key for ledger storage and per-tenant retention policies."},{"n":"createdBy","t":"string","info":"User ID or system principal that wrote this ledger entry."},{"n":"createdDate","r":true,"t":"datetime","info":"ISO 8601 system timestamp when the entry was persisted. Always system-generated, never back-dated (unlike entryDate which may reflect the business event date)."},{"n":"entryDate","r":true,"t":"datetime","info":"Chronological timestamp of the ledger entry — the business date/time the recorded event occurred. May differ from createdDate when entries are back-dated or batch-processed."},{"n":"idmpKey","t":"string","info":"Idempotency key guarding against duplicate postings from a retried or replayed writer. Same concept and same name as OperationalDocument.idmpKey; ledger entries need their own because they do not extend that schema. Uniqueness is scoped within Company (the inherited tenant partition key). OPTIONAL ON THIS BASE SCHEMA BY DESIGN — a ledger recording genuinely repeatable events, where two identical rows are two real facts rather than one fact written twice (Entity Action Log), must not dedupe. Every other descendant is required to declare this property required and unique, and to document its composition, per the ledger-idempotency-key-declared convention. Composition is domain-specific and cannot live on the base schema: sourceEntityId + sourceLineId for document-line ledgers, source entity + recipient + attempt for fan-out ledgers, target + actor + state for interaction ledgers."},{"n":"sourceEntityId","r":true,"t":"string","info":"UUID of the specific entity instance that triggered this ledger entry. Together with sourceEntityType, provides a complete back-reference to the originating transaction or event."},{"n":"sourceEntityType","r":true,"t":"string","info":"Entity class name that triggered this ledger entry (e.g. 'Sale', 'Stock Adjustment', 'Price Adjustment'). Used as a partition/index key alongside sourceEntityId for traceability queries."}],"ext":"Identifiable","extendedBy":["Stock Ledger","Price Ledger","Cost Ledger","Email Log","Entity Action Log"]},{"name":"TaxonomyEntity","desc":"Base schema for hierarchical classification entities that form tree structures (categories, departments, classes). Extends Identifiable with parent/child relationships, depth tracking, and path materialization for efficient tree traversal. Not instantiated directly — concrete taxonomy entities (ProductCategory, etc.) extend this.","provides":["id","company","ancestorIds","code","name","fullName","parent","children","depth","path","isLeaf","isRoot","sequence","isActive","isDeleted","createdBy","createdDate","modifiedBy","modifiedDate"],"properties":[{"n":"ancestorIds","r":true,"t":"array","info":"Ordered array of ancestor node IDs from root to immediate parent. Enables fast breadcrumb rendering and subtree queries without recursive lookups. Empty array for root nodes."},{"n":"children","t":"array","info":"Child nodes in the taxonomy tree."},{"n":"code","r":true,"t":"string","info":"Human-readable identifier. Unique within the taxonomy."},{"n":"company","r":true,"t":"entityDetail","re":"Company","info":"The Company tenant this taxonomy tree belongs to. Product categories and types are always tenant-specific."},{"n":"createdBy","t":"string","info":"User ID who created the node."},{"n":"createdDate","r":true,"t":"datetime","info":"ISO 8601 creation timestamp."},{"n":"depth","r":true,"t":"integer","info":"Zero-based depth in the tree. Root nodes have depth 0."},{"n":"fullName","t":"string","info":"Calculated field. The fully qualified name built from the materialized path, concatenating ancestor names from root to this node (e.g. 'Apparel > Men > Outerwear')."},{"n":"isActive","r":true,"t":"boolean","info":"Whether this taxonomy node is active. Defaults to true."},{"n":"isDeleted","r":true,"t":"boolean","info":"Soft-delete flag. Defaults to false."},{"n":"isLeaf","r":true,"t":"boolean","info":"Whether this node has no children. Computed from children array. Leaf nodes are selectable for entity assignment; branch nodes are for navigation only."},{"n":"isRoot","r":true,"t":"boolean","info":"Whether this is a top-level node with no parent. Computed from depth (depth === 0)."},{"n":"modifiedBy","t":"string","info":"User ID who last modified the node."},{"n":"modifiedDate","r":true,"t":"datetime","info":"ISO 8601 last-modification timestamp."},{"n":"name","r":true,"t":"string","info":"Display name of the taxonomy node."},{"n":"parent","t":"entityRef","info":"Self-referencing parent node. Null for root nodes."},{"n":"path","r":true,"t":"string","info":"Materialized path for efficient ancestor/descendant queries (e.g. '/root/parent/this'). Enables tree traversal without recursive queries."},{"n":"sequence","t":"integer","info":"Display ordering hint among siblings at the same depth."}],"ext":"Identifiable","extendedBy":["Product Category","Product Type"]},{"name":"TaxonomyEntityNode","desc":"Base schema for individual nodes within a taxonomy hierarchy (e.g. Classification Department, Classification Class, Classification Subclass). Extends Identifiable with the common properties shared by all taxonomy node entities — code, name, sequence, and audit fields. Simpler than TaxonomyEntity which represents the full tree structure; TaxonomyEntityNode represents a single level/node within that tree.","provides":["id","company","code","name","sequence","isActive","isDeleted","createdBy","createdDate","modifiedBy","modifiedDate"],"properties":[{"n":"code","r":true,"t":"string","info":"Human-readable identifier. Unique within the node type."},{"n":"company","r":true,"t":"entityDetail","re":"Company","info":"The Company tenant this taxonomy node belongs to. Classification Department/Class/Subclass are always tenant-specific."},{"n":"createdBy","t":"string","info":"User ID who created the node."},{"n":"createdDate","r":true,"t":"datetime","info":"ISO 8601 creation timestamp."},{"n":"isActive","r":true,"t":"boolean","info":"Whether this node is active. Defaults to true."},{"n":"isDeleted","r":true,"t":"boolean","info":"Soft-delete flag. Defaults to false."},{"n":"modifiedBy","t":"string","info":"User ID who last modified the node."},{"n":"modifiedDate","r":true,"t":"datetime","info":"ISO 8601 last-modification timestamp."},{"n":"name","r":true,"t":"string","info":"Display name of the taxonomy node."},{"n":"sequence","t":"integer","info":"Display ordering hint among sibling nodes."}],"ext":"Identifiable","extendedBy":["Classification Department","Classification Class","Classification Subclass"]},{"name":"CoreEntity","desc":"Base schema extending Identifiable. Shared foundation for entities that need common core fields beyond just `id` but don't fit the BaseDocument / OperationalDocument / TransactionalDocument lineage. Provides audit fields (createdBy/createdDate, modifiedBy/modifiedDate), soft-delete, customData, tags, slug, and a recentActions snapshot of the last five entity actions.","provides":["id","createdBy","createdDate","customData","isDeleted","modifiedBy","modifiedDate","recentActions","slug","tags"],"properties":[{"n":"createdBy","t":"string","info":"User ID who created the entity."},{"n":"createdDate","r":true,"t":"datetime","info":"ISO 8601 creation timestamp."},{"n":"customData","r":true,"t":"array","info":"Custom fields with label, fieldType, value, and sequence. Supports text, decimal, integer, date, flag, multiselect, lookup, and longtext."},{"n":"isDeleted","r":true,"t":"boolean","info":"Soft-delete flag."},{"n":"modifiedBy","t":"string","info":"User ID who last modified the entity."},{"n":"modifiedDate","r":true,"t":"datetime","info":"ISO 8601 last-modification timestamp."},{"n":"recentActions","r":true,"t":"array","info":"Denormalized snapshot of the most recent actions performed on this entity (capped at the last 5 entries). The Entity Action Log is the system of record for the full audit trail. Each entry follows the Action inline schema: actionDate, actionedBy, code, label, actionType (FieldChange | Operation), and optional change detail."},{"n":"slug","t":"string","u":true,"info":"URL-safe, human-readable identifier for the entity (e.g. used in routes or stable external references). Unique within the entity scope."},{"n":"tags","r":true,"t":"array","info":"Free-form string tags for categorization."}],"ext":"Identifiable","extendedBy":["Company","Organization","Business Entity"],"inlineSchemas":[{"name":"Action","schema":"schemas/common/Action.ts","properties":[{"n":"actionDate","r":true,"t":"datetime","info":"ISO 8601 timestamp when the action occurred."},{"n":"actionedBy","r":true,"t":"string","info":"User ID who performed the action."},{"n":"actionType","r":true,"t":"enumeration","v":["FieldChange","Operation"],"info":"Discriminator: FieldChange for property-level mutations, Operation for domain events (e.g. status transition, approval)."},{"n":"changes","r":true,"t":"array","info":"Field-level changes that occurred. Each entry is an ActionChange (fieldName, previousValue, newValue). Empty for Operation actions with no field-level side effects."},{"n":"code","r":true,"t":"string","info":"Stable, machine-readable action identifier (e.g. 'status.transition', 'price.update', 'approve', 'line.add'). Provides an enumerable key for reports, dashboards, and automation triggers. While actionType captures the category, code captures the semantic intent of the action."},{"n":"description","r":true,"t":"string","info":"Human-readable summary of what happened (e.g. 'Status changed from Draft to Active')."}]}]},{"name":"ContentDocument","desc":"Base schema for authored, published content documents — Announcement, Content Post, Content Page. Extends OperationalDocument with the publishing lifecycle (draft → scheduled → published → expired/archived), scheduling window, authorship, body/summary/title, audience targeting and attachments. Created 31 Aug 2026. RATIONALE: Announcement, a feed post and a knowledge page differ almost entirely in DISTRIBUTION and LIFECYCLE POLICY, not in content shape — an Announcement adds acknowledgement and multi-channel fan-out, a Post is chronological and un-acknowledged, a Page is evergreen and navigable. Without a shared base each would redeclare the same fourteen properties and drift. STATUS CONTRACT: descendants must not redeclare status, publishAt, publishedAt or expireAt; the state machine is uniform and publishedAt is system-set and immutable once written. Descendants MAY narrow the meaning of a state in their own businessValidation (a Content Page that never expires simply leaves expireAt null) but must not add or remove states, because the scheduler that promotes 'scheduled' to 'published' and expires past-expireAt rows is one job over all content types. AUDIENCE CONTRACT: the audience valueType is ContentAudience for every descendant. For fan-out content (Announcement) it resolves to a delivery set; for pull content (Content Page) it resolves to a visibility filter. Descendants must not redefine the resolution order — franchiseGroups (inherited) ∩ targetLocations ∩ targetRoles, plus includeUsers, minus excludeUsers. DELIVERY IS NOT MODELLED HERE: this base owns authoring and storage only. Which channels a document fans out to, and the resulting delivery ledger rows, are declared by the descendants that actually fan out (Announcement.deliveryChannels) and are recorded in the notification-side ledgers (Push Notification Log, Email Log). A ContentDocument never references a delivery ledger from the base. ATTACHMENTS AND HERO IMAGERY reference Media rather than carrying URLs, so every content type inherits the same asset governance — visibility, retention and franchise-group scoping — instead of each one minting its own public URL.","provides":["id","company","attachments","audience","author","body","createdBy","createdDate","customData","expireAt","franchiseGroups","heroMedia","identifiers","idmpKey","isDeleted","isPinned","modifiedBy","modifiedDate","notes","publishAt","publishedAt","recentActions","status","summary","tags","targetLocations","targetRoles","title","uniqueValues"],"properties":[{"n":"attachments","r":true,"t":"array","re":"Media","info":"EntityRef references to Media assets rendered alongside the body. Empty array default. Attachments are Media records with purpose='Attachment' or 'ContentAsset' — never inline URLs — so that visibility, retention and franchise-group scoping are governed in one place."},{"n":"audience","r":true,"t":"valueType","info":"ContentAudience value type. Targeting filter that, combined with inherited franchiseGroups and targetLocations/targetRoles, resolves the final delivery set (fan-out content) or visibility set (pull content)."},{"n":"author","r":true,"t":"entityRef","re":"User","info":"The User who authored the document. Only the author or a Company admin may edit prior to publish."},{"n":"body","r":true,"t":"string","info":"Document content. Supports markdown formatting."},{"n":"expireAt","t":"datetime","info":"When the document falls out of active surfaces and transitions to status='expired'. UTC. Null = no auto-expiry, which is the normal case for evergreen content."},{"n":"heroMedia","t":"entityRef","re":"Media","info":"Optional header image rendered at the top of the document on portal, feed and email surfaces. References Media rather than carrying a URL."},{"n":"isPinned","r":true,"t":"boolean","info":"When true, the document sticks to the top of its surface for its audience until expireAt or until unpinned. Per boolean-must-be-required."},{"n":"publishAt","t":"datetime","info":"Scheduled publish time in UTC. Null = publish immediately on save. Must be >= now() when set."},{"n":"publishedAt","t":"datetime","info":"Actual publish timestamp set by the system when status transitions to 'published'. Immutable thereafter. Never author-settable."},{"n":"status","r":true,"t":"enumeration","v":["draft","scheduled","published","expired","archived"],"info":"Lifecycle state. Transitions: draft -> scheduled -> published -> expired/archived; draft -> published is also valid; scheduled -> draft cancels a scheduled publish. No transitions back from published except archived. Uniform across all descendants — one scheduler promotes and expires every content type."},{"n":"summary","t":"string","info":"Short preview/teaser used in feed rows, search results and push notification bodies. Falls back to truncated body when null."},{"n":"targetLocations","r":true,"t":"array","re":"Location","info":"EntityDetail references narrowing the audience to specific Locations within the inherited franchiseGroups. Empty array = no Location narrowing. Per location-ref-uses-entity-detail and array-must-be-required."},{"n":"targetRoles","r":true,"t":"array","re":"Scope","info":"EntityDetail references narrowing the audience to specific Scopes/roles. Empty array = no role narrowing."},{"n":"title","r":true,"t":"string","info":"Display title shown in feeds, banners, page navigation, push notifications and email subjects."}],"ext":"OperationalDocument","extendedBy":["Announcement","Content Post","Content Page"]}]