models:
- name: DigestTip
collection: digest_tips
description: "DigestTip (Odoo digest.tip) — a once-per-user, group-gated usage tip appended to digest emails. SHARED data: no company column, no fence — one global carousel consumed in sequence order. The HTML payload is stored as authored and sanitized at render time (never before)."
fields:
id:
type: uuid
attributes: ["@id", "@default(uuid)"]
description: "Unique record id"
sequence:
type: integer
attributes: ["@default(1)"]
description: "Display/consumption order — the render picks the lowest-sequence tip the recipient has not yet consumed (the carousel is ordered, never random)."
name:
type: string
attributes: ["@required", "@max(255)"]
description: "Tip title (the record's display name)."
tip_description:
type: string
description: "The tip body HTML (stored as text — the DSL's string; the DDL emits TEXT), stored AS AUTHORED (un-sanitized at rest — the stored-unsanitized-HTML pattern; nothing in this module sanitizes on write) and html-sanitized AT RENDER TIME before it enters any mail. Sanitizing at rest would corrupt authored markup the sanitizer cannot round-trip; sanitizing at render is the load-bearing half of the pair."
group_key:
type: string?
attributes: ["@max(120)"]
description: "Visibility gate as a STABLE KEY STRING (the survey certification_badge_key precedent — no identity-module edge exists): the tip renders only for recipients whose key set (resolved through the host-composable recipient-context source, default-closed) contains this key; NULL = visible to everyone. The module never interprets the key's syntax."
metadata:
type: Metadata
attributes: ["@audit_metadata"]
description: "Audit metadata"
indexes:
- type: index
fields: [sequence]
description: "The consumption-ordered carousel's scan arm"
- name: DigestTipUser
collection: digest_tip_users
description: "DigestTipUser (Odoo digest.tip.user_ids) — the consumption marker: (tip, user) rows saying the user has already received this tip. The render picks the lowest-sequence tip with NO row here for the recipient. Fence-none by ruling: keyed by user, no tenant axis."
fields:
id:
type: uuid
attributes: ["@id", "@default(uuid)"]
description: "Unique record id"
tip_id:
type: uuid
attributes: ["@foreign_key(DigestTip.digest_tips)", "@indexed"]
description: "The consumed tip (relation-driven FK, ON DELETE CASCADE)."
user_id:
type: uuid
attributes: ["@exclude_from_foreign_key_check"]
description: "The consuming recipient # logical ref sapiens.User.id (no DB constraint)."
metadata:
type: Metadata
attributes: ["@audit_metadata"]
description: "Audit metadata"
relations:
tip:
type: DigestTip
attributes: ["@one", "@foreign_key(tip_id)", "@on_delete(cascade)"]
description: "The consumed tip (real FK digest.digest_tip_users.tip_id -> digest.digest_tips, cascade)"
indexes:
- type: unique
fields: [tip_id, user_id]
description: "Consumption is a set — one row per (tip, user), ever"