Add an append-only ledger of entitlement set rule changes with per-pool effect rows, a preview-and-commit rule change flow, and an automatic drain that settles deferred recomputations. Rules gain a tier reduction policy, resource keys declare over-limit behavior, and the materializer now lowers limits when a rule stops applying. Add entitlement set rule change ledger and preview flow Add an append-only ledger of entitlement set rule changes with a preview-and-commit operator flow. Rule writes now go through an enclosed `core.commit_rule_change` function that files an act row and one obligation per carrying pool, with a drain workflow settling deferred recomputations. The preview dry-runs the materializer with a rule overlay and renders per-pool buckets, reduction-policy disclosures, and provider over-limit consequences. Materializing transactions take a shared advisory rendezvous that rule changes hold exclusively, enforced by a possession assertion. Add History and Entitlement changes surfaces, a rule-less warning on five product-selection surfaces, and a `tier_reduction_policy` column that gates FedWiki parking.
134 lines
5.7 KiB
SQL
134 lines
5.7 KiB
SQL
-- name: GetProductByID :one
|
|
SELECT * FROM core.products
|
|
WHERE product_id = $1;
|
|
|
|
-- name: ListActiveProducts :many
|
|
SELECT * FROM core.products
|
|
WHERE is_active = TRUE
|
|
ORDER BY name ASC;
|
|
|
|
-- name: ListPublicProducts :many
|
|
SELECT * FROM core.products
|
|
WHERE is_active = TRUE AND is_public = TRUE AND lifecycle_status = 'published'
|
|
ORDER BY name ASC;
|
|
|
|
-- name: ListPublicPlanProducts :many
|
|
-- Plan products are those attached to a ladder via plan_ladder_tiers
|
|
-- (Doc 31 Amendment #3); membership is the structural signal —
|
|
-- display_category is presentation-only and never branched on.
|
|
SELECT p.* FROM core.products p
|
|
WHERE p.is_active = TRUE AND p.is_public = TRUE AND p.lifecycle_status = 'published'
|
|
AND EXISTS (
|
|
SELECT 1 FROM core.plan_ladder_tiers t WHERE t.product_id = p.product_id
|
|
)
|
|
ORDER BY p.name ASC;
|
|
|
|
-- name: ListAllProducts :many
|
|
SELECT * FROM core.products
|
|
ORDER BY name ASC;
|
|
|
|
-- name: CreateProduct :one
|
|
-- `key` is the optional declarative address (entity-keys §3), root-scoped.
|
|
-- Product names deliberately carry no uniqueness constraint (two ladders may
|
|
-- each have a "Pro"), which is exactly why a caller outside the database
|
|
-- needs a key to name one row. The operator create form does not collect
|
|
-- one, so a UI-created product leaves it NULL.
|
|
INSERT INTO core.products (name, description, display_category, is_active, is_public, entitlement_set_id, lifecycle_status, key)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, sqlc.narg(key))
|
|
RETURNING *;
|
|
|
|
-- name: GetProductByKey :one
|
|
-- Root key resolver (entity-keys §5). uq_products_key guarantees at most one
|
|
-- row matches -- unlike GetProductByName, whose column has no unique
|
|
-- constraint at all.
|
|
SELECT * FROM core.products
|
|
WHERE key = $1;
|
|
|
|
-- name: UpdateProduct :one
|
|
UPDATE core.products
|
|
SET name = $2, description = $3, display_category = $4, is_active = $5, is_public = $6, entitlement_set_id = $7, metadata = $8
|
|
WHERE product_id = $1
|
|
RETURNING *;
|
|
|
|
-- name: CountPublishedProducts :one
|
|
-- Operator overview tile: the size of the live catalog. Counts only
|
|
-- lifecycle_status='published' — drafts are work in progress and retired
|
|
-- products are history, so neither belongs in a "what we sell today" number.
|
|
SELECT COUNT(*) FROM core.products
|
|
WHERE lifecycle_status = 'published';
|
|
|
|
-- name: GetProductShape :one
|
|
-- Diagnostics view (migrations 00006, 00018): independent structural
|
|
-- dimensions per product. Readiness reads set_present, billing_shape and
|
|
-- active_rule_count, which is the sole source of the answer to whether a
|
|
-- product's set provides anything (product-catalog). The two counts are
|
|
-- cast because the view's COALESCE over a grouped count gives sqlc no type
|
|
-- to infer.
|
|
SELECT * FROM core.product_shape
|
|
WHERE product_id = $1;
|
|
|
|
-- name: ListProductShapesByIDs :many
|
|
-- Batch reader for the products list's per-page verdict (product-management
|
|
-- "The verdict per page, from batches, never cached", design D4): the
|
|
-- product_shape view for every product on the page, one query instead of
|
|
-- one per row.
|
|
SELECT * FROM core.product_shape
|
|
WHERE product_id = ANY(sqlc.arg(product_ids)::uuid[]);
|
|
|
|
-- name: ListProductsPage :many
|
|
-- Products list operator surface (operator-list-scale; product-management
|
|
-- "The list takes the scaffold"): the paged, searched, lifecycle-filtered
|
|
-- successor to ListAllProducts, in the Organizations list's shape.
|
|
--
|
|
-- sqlc.narg(q): NULL matches every row; set, a case-insensitive substring
|
|
-- match against the product's name.
|
|
-- sqlc.narg(lifecycle_status): NULL matches every lifecycle state; set, an
|
|
-- exact facet match (the Draft/Published/Retired pills).
|
|
-- count(*) OVER() carries the true total for the filtered set (design D2),
|
|
-- so the stated total never reflects only the page.
|
|
SELECT *, count(*) OVER() AS total_count
|
|
FROM core.products
|
|
WHERE (sqlc.narg(q)::text IS NULL OR name ILIKE '%' || sqlc.narg(q)::text || '%')
|
|
AND (sqlc.narg(lifecycle_status)::text IS NULL OR lifecycle_status = sqlc.narg(lifecycle_status)::text)
|
|
ORDER BY name ASC
|
|
LIMIT sqlc.arg(page_limit)::int OFFSET sqlc.arg(page_offset)::int;
|
|
|
|
-- name: AnyProduct :one
|
|
-- Setup-checklist existence probe.
|
|
SELECT EXISTS(SELECT 1 FROM core.products);
|
|
|
|
-- name: AnyPublishedProduct :one
|
|
-- Setup-checklist existence probe.
|
|
SELECT EXISTS(SELECT 1 FROM core.products WHERE lifecycle_status = 'published');
|
|
|
|
-- name: ListProductsByNameCI :many
|
|
-- Product duplicate-name warning (entity-keys): products may
|
|
-- legitimately share a name (a "Pro" tier on two ladders), so
|
|
-- core.products.name carries no uniqueness constraint -- this powers a live,
|
|
-- non-blocking warning on the create/edit forms instead of a 422. Matches
|
|
-- case-insensitively; when exclude_product_id is set (the edit form's own
|
|
-- product), that product is excluded from its own duplicate check. LEFT
|
|
-- JOINs the ladder tables so a matching product that is on no ladder still
|
|
-- returns one row (ladder columns NULL); the caller groups rows by
|
|
-- product_id in Go and collects each product's ladder names.
|
|
SELECT
|
|
p.product_id,
|
|
p.name,
|
|
p.is_active,
|
|
l.plan_ladder_id AS ladder_id,
|
|
l.name AS ladder_name
|
|
FROM core.products p
|
|
LEFT JOIN core.plan_ladder_tiers t ON t.product_id = p.product_id
|
|
LEFT JOIN core.plan_ladders l ON l.plan_ladder_id = t.plan_ladder_id
|
|
WHERE lower(p.name) = lower(sqlc.arg(name)::text)
|
|
AND (sqlc.narg(exclude_product_id)::uuid IS NULL OR p.product_id <> sqlc.narg(exclude_product_id)::uuid)
|
|
ORDER BY p.name ASC, l.name ASC;
|
|
|
|
-- name: CountPublishedTierProducts :one
|
|
-- Operator catalog overview (ux-ia-naming 2.1): how many published products
|
|
-- are a tier on some plan ladder, vs. off-ladder. A product counts once even
|
|
-- when it is shared across more than one ladder (DISTINCT).
|
|
SELECT COUNT(DISTINCT t.product_id) FROM core.plan_ladder_tiers t
|
|
JOIN core.products p ON p.product_id = t.product_id
|
|
WHERE p.lifecycle_status = 'published';
|