Files
member-console/internal/billing/queries/products.sql
T
cgalo5758 dd3962990b Adopt entity keys and add invoice numbers
Replace the entity slugs on organizations, workspaces, resource pools,
and
plan ladders with nullable `key` columns and add keys to products,
prices,
and entitlement sets. Rename `providers.slug` to `provider` and add
partial
unique indexes for system and org role names.

Assign invoice numbers per billing account from a gapless transactional
counter; Stripe's number moves to the invoice mapping as an external
reference.

Seeds, fixtures, and the operator lookup address rows by key, and the
returning-login resync no longer blanks a display name when the IdP
sends
no `name` claim.
2026-08-29 20:12:04 -05:00

104 lines
4.2 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 (migration 00006): independent structural dimensions per
-- product. Readiness reads set_present and billing_shape.
SELECT * FROM core.product_shape
WHERE product_id = $1;
-- 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';