Read both grants pages from one listing query
ListGrantsWithDelivery now takes optional organization, search, product, state and paging arguments and serves the grants page and the organization page's ledger. The grouped SELECT's columns and grouping moved unchanged into a CTE named listed, whose WHERE now holds the organization filter and the search filter (with the organizations join the search needs); the outer statement filters on the state computed once, counts with count(*) OVER() after that filter, orders by created_at DESC and pages (a NULL limit or offset reads every row). ListGrantsWithDeliveryPage, toDeliveryRows and loadGrantsListData's unused success and error-message arguments are deleted. The generated row is field for field the old page row. Product names are still matched in Go, so % and _ in a search keep matching themselves there. The two pages keep the pool difference: with an organization the waits are evaluated on its active default pool, without one on every pool. How the commit was shown to change no output: - The row golden, run first on PostgreSQL against the merged query, passed with every file under testdata/grants_listing/ and testdata/org_enrollment/ unchanged. LIMIT NULL OFFSET NULL returns all rows, and EXPLAIN ANALYZE of the organization-only call and of the search, state and page call shows no CTE scan on listed (it is inlined). - internal/server (1398 results) and internal/entitlements (180) give the same test and subtest names and results at the parent and at this commit; no test was added, renamed or deleted. - 13 breaks, each caught by the old and the new test, plus 3 breaks caught on the old side only: the adapter's copies of the waits, the default-duplicate and the restored-after columns, which are deleted with the adapter. No break was dropped because the old test missed it. - Test calls were re-pointed to the merged query with their assertions unchanged (every row of the assertion map is same). The comments of the query, the grants template, docs/models/entitlements.md and docs/operator-ux-conventions.md now name the one query. Unprobed: the order of grants that share created_at (a tier removal writes one default grant per vacated pool in one transaction). The merged query's plan may order such a tie differently from either old query, no fixture holds a tie, and the order is accepted until the tie-break by grant id, as for positions. The zero UUID pool of an organization with no active default pool: no fixture organization lacks one. total_count on the organization call, which no caller reads. The adapter's other 22 copied columns on the old side: the goldens read each, three are probed, and the adapter is gone so no copy remains to break.
This commit is contained in:
@@ -45,7 +45,7 @@ Database objects sit in the `core` schema; baseline DDL in `internal/db/migratio
|
||||
| Materializer | `MaterializePoolEntitlements`: full in-transaction re-evaluation of a pool's active provisions into limits, contributions, and boolean grants | `internal/entitlements/materialize.go` |
|
||||
| Conferral boundary | Typed wrappers over the five functions, error mapping, and `ConferGrantTx` — decree, confer, materialize in one transaction | `internal/entitlements/conferral.go` |
|
||||
| Default restoration | Mints and confers the baseline default grant (see the ladders card for the floor guard) | `internal/entitlements/reapply_defaults.go` |
|
||||
| Delivery-state query | `ListGrantsWithDelivery` derives each grant's live/on_hold/superseded/inactive label (`on_hold` read from `core.grant_waits`, whose resuming rows are `core.resumable_grants`, together with what an on-hold grant waits on, `resumes`, `behind`, `no_ladder` or `blocked`, and the products its badge's hover names; plus supersession lineage — `extends_grant_id` and, for a superseded row, the grant that replaced it) by joining onto provisions at read time; one shared query an optional org filter narrows, so the grants index and the per-organization grants table render from the exact same derivation and can never disagree | `internal/entitlements/queries/grants.sql` |
|
||||
| Delivery-state query | `ListGrantsWithDelivery` derives each grant's live/on_hold/superseded/inactive label (`on_hold` read from `core.grant_waits`, whose resuming rows are `core.resumable_grants`, together with what an on-hold grant waits on, `resumes`, `behind`, `no_ladder` or `blocked`, and the products its badge's hover names; plus supersession lineage — `extends_grant_id` and, for a superseded row, the grant that replaced it) by joining onto provisions at read time; one query serves the grants index and the per-organization grants table, with optional organization, search, state and paging arguments, so both render from the same derivation; the organization argument also chooses the pool `core.grant_waits` evaluates (the organization's active default pool; every pool when it is unset) | `internal/entitlements/queries/grants.sql` |
|
||||
| Rule authoring | Operator set and rule CRUD: `rule_type` derived from the key's kind, stacking fixed to `additive` (the selector left the form), constraint violations mapped to field errors | `internal/server/operator_entitlement_sets.go` |
|
||||
| Grant issuance | The operator grant forms (quantity capped at 1..1,000,000): issuing composes through `ConferGrantTx`. After issuance, one Manage action on a named grant changes its Valid until, note and resumption mark, acting on the head of the grant's lineage as it stands at submit, under the organization's default pool lock. A changed Valid until extends the head: a head that delivers goes through `ExtendGrantTx` (decree, confer as a transfer, materialize), and a head that delivers nothing through `ExtendWaitingGrantTx`, which writes a successor grant in its place and confers nothing; the successor carries the submitted note and mark, and both paths refuse a retired product. An unchanged Valid until writes the mark and the note to the head in place and nothing else. The Issue Grant form discloses that recording the grant is immediate while any provider-side follow-through it triggers is separately queued work | `internal/entitlements/grant_acts.go`; `internal/server/operator_enrollment.go` |
|
||||
| Grant surfaces | The grants index (`/operator/grants`) and the per-organization composite both render `ListGrantsWithDelivery`'s rows: delivery state as a badge whose hover says what an on-hold grant waits on (never `grants.status` under a bare "Status" header), the stored mark as a Resumes column reading Yes or No, lineage, and an in-place ledger-vs-delivery explanation | `internal/server/operator_partials.go` (index); `internal/server/operator_enrollment.go` (org detail) |
|
||||
|
||||
@@ -237,7 +237,7 @@ Every mutation waits for server confirmation before the UI updates. HTMX's reque
|
||||
| Inline `<script>` in templates | Put JS in `internal/embeds/static/` and reference via `<script defer src>` (CSP `script-src 'self'` blocks inline) |
|
||||
| `http.Redirect(w, r, ...)` as the only answer to a navigating mutation, regardless of `HX-Request` | `redirectToRecord` (`internal/server/redirect.go`, design D9): `HX-Redirect` + 200 with an empty body when the submit is htmx, `http.Redirect(..., http.StatusSeeOther)` when it is native |
|
||||
| Validation in the client only (HTML5 `required` alone) | A declared `forms.FormSpec`, parsed server-side through `spec.Parse(r)` (`form-library`; `docs/design-system.md` §8); `required` stays on the control, generated from the same declaration, for assistive technology |
|
||||
| `for _, g := range grants { if g.Status != "active" { continue } … }` to build an "active grants" list | Use a query that joins to active provisions (`ListDeliveringGrantsByOrgID` / `ListGrantsWithDeliveryByOrgID`) — see §9a |
|
||||
| `for _, g := range grants { if g.Status != "active" { continue } … }` to build an "active grants" list | Use a query that joins to active provisions (`ListDeliveringGrantsByOrgID` / `ListGrantsWithDelivery` with the organization argument) — see §9a |
|
||||
| `hx-get="/operator/foo/{{ .ID }}"` — interpolated URL string in any `hx-*` or `data-action-url` attribute | `hx-get="{{ routeURL "/operator/foo/{id}" .ID }}"` — `member-console lint` cross-checks the pattern against `mux.HandleFunc` registrations. Helper is `web.RouteURL` in `internal/web/route_url.go`; the surface constructors (`web.MemberTemplates`, `web.OperatorTemplates`) register it on every set. |
|
||||
| `"Failed to save: " + err.Error()` in a render/toast/`http.Error` call | `web.FieldErrorsFromDB` → 422 + `FieldErrors` for constraint violations; `slog.Error` + generic message otherwise (§4; enforced by the `raw-error-render` lint rule) |
|
||||
| Writing the row first and running the invariant guard after it (`CreateGrant` then the same-tier check) | Every guard runs before the first write — a guard that short-circuits after the row exists leaves an orphan no later flow can reach |
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
more.
|
||||
|
||||
Delivery renders the derived state from the shared delivery-state
|
||||
derivation (ux-honest-surfaces design decision 1; the paged query
|
||||
ListGrantsWithDeliveryPage repeats the identical CASE expression,
|
||||
design D4): the same vocabulary and badge treatment the org-detail
|
||||
composite's grants ledger uses, so the two surfaces can
|
||||
never disagree. Issuance (grants.status) is what the record says
|
||||
happened to the grant and is deliberately NOT a colored state badge
|
||||
(Delivery above is); each column header carries a help icon (design
|
||||
D23) explaining the distinction instead of a standing paragraph.
|
||||
derivation (ux-honest-surfaces design decision 1; both pages read
|
||||
ListGrantsWithDelivery, which computes the state once): the same
|
||||
vocabulary and badge treatment the org-detail composite's grants ledger
|
||||
uses, so the two surfaces can never disagree. Issuance (grants.status) is
|
||||
what the record says happened to the grant and is deliberately NOT a
|
||||
colored state badge (Delivery above is); each column header carries a
|
||||
help icon (design D23) explaining the distinction instead of a standing
|
||||
paragraph.
|
||||
|
||||
Governed by operator-list-scale: server-side search, the
|
||||
delivery-state filter, and true-total pagination
|
||||
|
||||
@@ -694,7 +694,7 @@ func deliveryStates(w *resumeWorld) map[string]string {
|
||||
|
||||
func listDelivery(w *resumeWorld) map[string]entitlements.ListGrantsWithDeliveryRow {
|
||||
w.t.Helper()
|
||||
rows, err := w.q.ListGrantsWithDelivery(w.ctx, orgArg(w.org))
|
||||
rows, err := w.q.ListGrantsWithDelivery(w.ctx, entitlements.ListGrantsWithDeliveryParams{OrgID: orgArg(w.org)})
|
||||
if err != nil {
|
||||
w.t.Fatalf("ListGrantsWithDelivery: %v", err)
|
||||
}
|
||||
|
||||
@@ -192,11 +192,11 @@ func checkNoStandby(t *testing.T, rows map[string]entitlements.ListGrantsWithDel
|
||||
func checkStateFilter(t *testing.T, w *resumeWorld, rows map[string]entitlements.ListGrantsWithDeliveryRow, ids []string) {
|
||||
t.Helper()
|
||||
for _, state := range []string{"on_hold"} {
|
||||
page, err := w.q.ListGrantsWithDeliveryPage(w.ctx, entitlements.ListGrantsWithDeliveryPageParams{
|
||||
page, err := w.q.ListGrantsWithDelivery(w.ctx, entitlements.ListGrantsWithDeliveryParams{
|
||||
Q: sql.NullString{String: "no organization is named this " + uuid.NewString(), Valid: true},
|
||||
ProductIds: ids,
|
||||
DeliveryState: sql.NullString{String: state, Valid: true},
|
||||
PageLimit: 100,
|
||||
PageLimit: sql.NullInt32{Int32: 100, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("page %s: %v", state, err)
|
||||
|
||||
@@ -576,10 +576,10 @@ WITH RECURSIVE waits AS (
|
||||
w.preceding_grant_ids::uuid[] AS preceding_grant_ids
|
||||
FROM core.grant_waits(
|
||||
now(),
|
||||
CASE WHEN $1::uuid IS NULL THEN NULL::uuid
|
||||
CASE WHEN $4::uuid IS NULL THEN NULL::uuid
|
||||
ELSE COALESCE((
|
||||
SELECT rp.pool_id FROM core.resource_pools rp
|
||||
WHERE rp.org_id = $1::uuid
|
||||
WHERE rp.org_id = $4::uuid
|
||||
AND rp.pool_type = 'default' AND rp.status = 'active'
|
||||
LIMIT 1), '00000000-0000-0000-0000-000000000000'::uuid)
|
||||
END
|
||||
@@ -642,13 +642,14 @@ wait_lines AS (
|
||||
roots (grant_id, root_id, depth) AS (
|
||||
SELECT r0.grant_id, r0.grant_id, 0 FROM core.grants r0
|
||||
WHERE r0.extends_grant_id IS NULL
|
||||
AND ($1::uuid IS NULL OR r0.granted_to_org_id = $1::uuid)
|
||||
AND ($4::uuid IS NULL OR r0.granted_to_org_id = $4::uuid)
|
||||
UNION ALL
|
||||
SELECT c.grant_id, r.root_id, r.depth + 1
|
||||
FROM roots r
|
||||
JOIN core.grants c ON c.extends_grant_id = r.grant_id
|
||||
WHERE r.depth < 100
|
||||
)
|
||||
),
|
||||
listed AS (
|
||||
SELECT
|
||||
g.grant_id,
|
||||
g.granted_to_org_id,
|
||||
@@ -754,11 +755,31 @@ FROM core.grants g
|
||||
LEFT JOIN core.pool_provisions p ON p.grant_id = g.grant_id
|
||||
LEFT JOIN core.pool_provision_ladders l ON l.provision_id = p.provision_id
|
||||
LEFT JOIN wait_lines wl ON wl.grant_id = g.grant_id
|
||||
WHERE $1::uuid IS NULL OR g.granted_to_org_id = $1::uuid
|
||||
LEFT JOIN core.organizations org ON org.org_id = g.granted_to_org_id
|
||||
WHERE ($4::uuid IS NULL OR g.granted_to_org_id = $4::uuid)
|
||||
AND ($5::text IS NULL
|
||||
OR org.name ILIKE ('%' || $5::text || '%')
|
||||
OR g.product_id = ANY($6::uuid[]))
|
||||
GROUP BY g.grant_id, g.granted_to_org_id, g.product_id, g.grant_reason, g.description, g.quantity, g.status, g.created_at, g.extends_grant_id
|
||||
ORDER BY g.created_at DESC
|
||||
)
|
||||
SELECT listed.grant_id, listed.granted_to_org_id, listed.product_id, listed.grant_reason, listed.description, listed.quantity, listed.grant_status, listed.created_at, listed.extends_grant_id, listed.granted_by_person_id, listed.resumes_after_supersession, listed.delivery_state, listed.replaced_by_grant_id, listed.activated_at, listed.ended_at, listed.valid_until, listed.holder_product_name, listed.waits_on, listed.blocking_product_names, listed.manage_offered, listed.will_not_resume, listed.resuming_product_names, listed.floor_after_revocation, listed.duplicates_default, listed.restored_after_product_names, count(*) OVER() AS total_count
|
||||
FROM listed
|
||||
WHERE $1::text IS NULL
|
||||
OR listed.delivery_state = $1::text
|
||||
ORDER BY listed.created_at DESC
|
||||
LIMIT $3
|
||||
OFFSET $2
|
||||
`
|
||||
|
||||
type ListGrantsWithDeliveryParams struct {
|
||||
DeliveryState sql.NullString `json:"delivery_state"`
|
||||
PageOffset sql.NullInt32 `json:"page_offset"`
|
||||
PageLimit sql.NullInt32 `json:"page_limit"`
|
||||
OrgID uuid.NullUUID `json:"org_id"`
|
||||
Q sql.NullString `json:"q"`
|
||||
ProductIds []string `json:"product_ids"`
|
||||
}
|
||||
|
||||
type ListGrantsWithDeliveryRow struct {
|
||||
GrantID string `json:"grant_id"`
|
||||
GrantedToOrgID uuid.NullUUID `json:"granted_to_org_id"`
|
||||
@@ -785,6 +806,7 @@ type ListGrantsWithDeliveryRow struct {
|
||||
FloorAfterRevocation bool `json:"floor_after_revocation"`
|
||||
DuplicatesDefault bool `json:"duplicates_default"`
|
||||
RestoredAfterProductNames []string `json:"restored_after_product_names"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
}
|
||||
|
||||
// The one shared delivery-state derivation (ux-honest-surfaces design
|
||||
@@ -870,12 +892,38 @@ type ListGrantsWithDeliveryRow struct {
|
||||
// The lineage sub-ordering (the replaced_by_grant_id subquery below) stays
|
||||
// child.created_at DESC.
|
||||
//
|
||||
// sqlc.narg(org_id): NULL returns every grant system-wide (the grants
|
||||
// index, /operator/grants) and evaluates every pool; set, scopes to one
|
||||
// organization (org-detail) and evaluates that organization's default pool.
|
||||
// One query serves both grant listings. Every argument is optional:
|
||||
//
|
||||
// org_id set: that organization's grants, with core.grant_waits
|
||||
// evaluated on its active default pool (the zero UUID
|
||||
// when it has none) and the lineage roots cut to its
|
||||
// grants. NULL: every grant, with waits evaluated on
|
||||
// every pool (grant-resumption D3). The two can read a
|
||||
// waiting grant differently only where its replacement
|
||||
// sits outside the organization's active default pool,
|
||||
// which the console does not write.
|
||||
// q matches the organization's name with ILIKE, so % and _
|
||||
// match any text, or a product id in product_ids, which
|
||||
// the caller matches by name in Go.
|
||||
// delivery_state keeps the rows of that state. The state is computed
|
||||
// once, in listed, and the outer statement filters on it.
|
||||
// page_limit, page_offset
|
||||
// NULL reads every row, from the first.
|
||||
//
|
||||
// total_count counts the rows every filter keeps, before the page. Rows
|
||||
// come newest first by created_at alone (tier-changes-ledger D5).
|
||||
// Each grant's lineage root, for the default-duplicate line.
|
||||
func (q *Queries) ListGrantsWithDelivery(ctx context.Context, orgID uuid.NullUUID) ([]ListGrantsWithDeliveryRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listGrantsWithDelivery, orgID)
|
||||
// One row per grant that passes the org and search filters, with its
|
||||
// delivery state computed once; the outer SELECT filters on it.
|
||||
func (q *Queries) ListGrantsWithDelivery(ctx context.Context, arg ListGrantsWithDeliveryParams) ([]ListGrantsWithDeliveryRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listGrantsWithDelivery,
|
||||
arg.DeliveryState,
|
||||
arg.PageOffset,
|
||||
arg.PageLimit,
|
||||
arg.OrgID,
|
||||
arg.Q,
|
||||
pq.Array(arg.ProductIds),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -883,336 +931,6 @@ func (q *Queries) ListGrantsWithDelivery(ctx context.Context, orgID uuid.NullUUI
|
||||
items := []ListGrantsWithDeliveryRow{}
|
||||
for rows.Next() {
|
||||
var i ListGrantsWithDeliveryRow
|
||||
if err := rows.Scan(
|
||||
&i.GrantID,
|
||||
&i.GrantedToOrgID,
|
||||
&i.ProductID,
|
||||
&i.GrantReason,
|
||||
&i.Description,
|
||||
&i.Quantity,
|
||||
&i.GrantStatus,
|
||||
&i.CreatedAt,
|
||||
&i.ExtendsGrantID,
|
||||
&i.GrantedByPersonID,
|
||||
&i.ResumesAfterSupersession,
|
||||
&i.DeliveryState,
|
||||
&i.ReplacedByGrantID,
|
||||
&i.ActivatedAt,
|
||||
&i.EndedAt,
|
||||
&i.ValidUntil,
|
||||
&i.HolderProductName,
|
||||
&i.WaitsOn,
|
||||
pq.Array(&i.BlockingProductNames),
|
||||
&i.ManageOffered,
|
||||
&i.WillNotResume,
|
||||
pq.Array(&i.ResumingProductNames),
|
||||
&i.FloorAfterRevocation,
|
||||
&i.DuplicatesDefault,
|
||||
pq.Array(&i.RestoredAfterProductNames),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listGrantsWithDeliveryPage = `-- name: ListGrantsWithDeliveryPage :many
|
||||
WITH RECURSIVE waits AS (
|
||||
SELECT w.grant_id::uuid AS grant_id,
|
||||
w.pool_id::uuid AS pool_id,
|
||||
w.holder_grant_id::uuid AS holder_grant_id,
|
||||
w.holder_subscription_id::uuid AS holder_subscription_id,
|
||||
w.holder_delivers::boolean AS holder_delivers,
|
||||
w.top_delivers::boolean AS top_delivers,
|
||||
w.resumes::boolean AS resumes,
|
||||
w.on_no_ladder::boolean AS on_no_ladder,
|
||||
w.blocking_provision_ids::uuid[] AS blocking_provision_ids,
|
||||
w.preceding_grant_ids::uuid[] AS preceding_grant_ids
|
||||
FROM core.grant_waits(now(), NULL::uuid)
|
||||
AS w(grant_id, pool_id, replaced_at, holder_grant_id, holder_subscription_id,
|
||||
holder_delivers, top_grant_id, top_subscription_id, top_delivers,
|
||||
resumes, on_no_ladder, blocking_provision_ids, preceding_grant_ids)
|
||||
),
|
||||
wait_lines AS (
|
||||
SELECT w.grant_id,
|
||||
((w.resumes AND w.holder_delivers)
|
||||
OR (wg.resumes_after_supersession AND wg.status = 'active'
|
||||
AND wg.valid_from <= now()
|
||||
AND (wg.valid_until IS NULL OR wg.valid_until > now())
|
||||
AND w.top_delivers)) AS on_hold,
|
||||
(NOT wg.resumes_after_supersession AND w.top_delivers
|
||||
AND (wg.valid_until IS NULL OR wg.valid_until > now())) AS unmarked_under_live_top,
|
||||
(wg.resumes_after_supersession AND wg.status = 'active'
|
||||
AND NOT (w.top_delivers
|
||||
AND (wg.valid_until IS NULL OR wg.valid_until > now()))) AS will_not_resume,
|
||||
CASE
|
||||
WHEN w.holder_grant_id IS NOT NULL THEN (
|
||||
SELECT hp.name FROM core.grants hg
|
||||
JOIN core.products hp ON hp.product_id = hg.product_id
|
||||
WHERE hg.grant_id = w.holder_grant_id)
|
||||
ELSE (
|
||||
SELECT sp.name FROM core.pool_provisions spp
|
||||
JOIN core.products sp ON sp.product_id = spp.product_id
|
||||
WHERE spp.subscription_id = w.holder_subscription_id
|
||||
AND spp.pool_id = w.pool_id
|
||||
AND spp.status <> 'ended'
|
||||
ORDER BY EXISTS (
|
||||
SELECT 1 FROM core.product_conferral_shapes a
|
||||
JOIN core.product_conferral_shapes b ON b.plan_ladder_id = a.plan_ladder_id
|
||||
WHERE a.product_id = spp.product_id AND b.product_id = wg.product_id
|
||||
) DESC,
|
||||
spp.activated_at DESC, spp.provision_id
|
||||
LIMIT 1)
|
||||
END AS holder_product_name,
|
||||
CASE
|
||||
WHEN w.resumes AND w.holder_delivers THEN 'resumes'
|
||||
WHEN w.holder_grant_id IS NOT NULL AND NOT w.holder_delivers THEN 'behind'
|
||||
WHEN w.on_no_ladder THEN 'no_ladder'
|
||||
WHEN cardinality(w.blocking_provision_ids) > 0
|
||||
OR cardinality(w.preceding_grant_ids) > 0 THEN 'blocked'
|
||||
ELSE ''
|
||||
END AS waits_on,
|
||||
ARRAY(
|
||||
SELECT DISTINCT bp.name FROM core.products bp
|
||||
WHERE bp.product_id IN (
|
||||
SELECT pp.product_id FROM core.pool_provisions pp
|
||||
WHERE pp.provision_id = ANY (w.blocking_provision_ids)
|
||||
UNION
|
||||
SELECT pg.product_id FROM core.grants pg
|
||||
WHERE pg.grant_id = ANY (w.preceding_grant_ids))
|
||||
ORDER BY bp.name
|
||||
) AS blocking_product_names
|
||||
FROM waits w
|
||||
JOIN core.grants wg ON wg.grant_id = w.grant_id
|
||||
),
|
||||
roots (grant_id, root_id, depth) AS (
|
||||
SELECT r0.grant_id, r0.grant_id, 0 FROM core.grants r0
|
||||
WHERE r0.extends_grant_id IS NULL
|
||||
UNION ALL
|
||||
SELECT c.grant_id, r.root_id, r.depth + 1
|
||||
FROM roots r
|
||||
JOIN core.grants c ON c.extends_grant_id = r.grant_id
|
||||
WHERE r.depth < 100
|
||||
)
|
||||
SELECT
|
||||
g.grant_id,
|
||||
g.granted_to_org_id,
|
||||
g.product_id,
|
||||
g.grant_reason,
|
||||
g.description,
|
||||
g.quantity,
|
||||
g.status AS grant_status,
|
||||
g.created_at,
|
||||
g.extends_grant_id,
|
||||
g.granted_by_person_id,
|
||||
g.resumes_after_supersession,
|
||||
CASE
|
||||
WHEN BOOL_OR(p.status = 'active') THEN 'live'
|
||||
WHEN BOOL_OR(wl.on_hold) THEN 'on_hold'
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM core.grants child
|
||||
WHERE child.extends_grant_id = g.grant_id
|
||||
) THEN 'superseded'
|
||||
ELSE 'inactive'
|
||||
END AS delivery_state,
|
||||
MAX((
|
||||
SELECT child.grant_id::text FROM core.grants child
|
||||
WHERE child.extends_grant_id = g.grant_id
|
||||
ORDER BY child.created_at DESC
|
||||
LIMIT 1
|
||||
)) AS replaced_by_grant_id,
|
||||
MAX(l.activated_at) AS activated_at,
|
||||
CASE WHEN BOOL_OR(p.status <> 'ended') THEN NULL
|
||||
ELSE MAX(CASE WHEN l.status = 'ended' THEN l.ended_at END)
|
||||
END AS ended_at,
|
||||
g.valid_until,
|
||||
COALESCE(MAX(wl.holder_product_name), '')::text AS holder_product_name,
|
||||
COALESCE(MAX(wl.waits_on), '')::text AS waits_on,
|
||||
COALESCE(MAX(wl.blocking_product_names), '{}')::text[] AS blocking_product_names,
|
||||
(g.status = 'active'
|
||||
AND g.grant_reason <> 'default'
|
||||
AND NOT EXISTS (SELECT 1 FROM core.grants child WHERE child.extends_grant_id = g.grant_id)
|
||||
AND (COALESCE(BOOL_OR(p.status = 'active'), FALSE)
|
||||
OR COALESCE(BOOL_OR(wl.on_hold), FALSE)
|
||||
OR (NOT COALESCE(BOOL_OR(p.status <> 'ended'), FALSE)
|
||||
AND COALESCE(BOOL_OR(wl.unmarked_under_live_top), FALSE))))::boolean AS manage_offered,
|
||||
COALESCE(BOOL_OR(wl.will_not_resume), FALSE)::boolean AS will_not_resume,
|
||||
ARRAY(
|
||||
SELECT rp.name FROM waits rw
|
||||
JOIN core.grants rg ON rg.grant_id = rw.grant_id
|
||||
JOIN core.products rp ON rp.product_id = rg.product_id
|
||||
WHERE rw.resumes AND rw.holder_grant_id = g.grant_id
|
||||
ORDER BY rg.created_at DESC, rg.grant_id DESC
|
||||
)::text[] AS resuming_product_names,
|
||||
(EXISTS (
|
||||
SELECT 1 FROM core.organizations fo
|
||||
JOIN core.org_types fot ON fot.org_type = fo.org_type
|
||||
WHERE fo.org_id = g.granted_to_org_id
|
||||
AND fot.default_plan_ladder_id IS NOT NULL)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM core.pool_provision_ladders fl
|
||||
JOIN core.pool_provisions fp ON fp.provision_id = fl.provision_id
|
||||
JOIN core.resource_pools frp ON frp.pool_id = fl.pool_id
|
||||
WHERE frp.org_id = g.granted_to_org_id
|
||||
AND frp.pool_type = 'default'
|
||||
AND frp.status = 'active'
|
||||
AND fl.status IN ('active', 'suspended')
|
||||
AND fp.grant_id IS DISTINCT FROM g.grant_id))::boolean AS floor_after_revocation,
|
||||
(g.status = 'active'
|
||||
AND NOT EXISTS (SELECT 1 FROM core.pool_provisions dp WHERE dp.grant_id = g.grant_id)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM core.pool_provisions lp
|
||||
JOIN core.resource_pools lrp ON lrp.pool_id = lp.pool_id
|
||||
JOIN roots lr ON lr.grant_id = lp.grant_id
|
||||
JOIN core.grants lroot ON lroot.grant_id = lr.root_id
|
||||
WHERE lp.product_id = g.product_id
|
||||
AND lp.status = 'active'
|
||||
AND lrp.org_id = g.granted_to_org_id
|
||||
AND lrp.pool_type = 'default'
|
||||
AND lroot.grant_reason = 'default'))::boolean AS duplicates_default,
|
||||
ARRAY(
|
||||
SELECT x.name FROM (
|
||||
SELECT epr.name, min(et.transition_id::text) AS first_written
|
||||
FROM core.pool_provision_transitions ft
|
||||
JOIN core.pool_provision_transitions et
|
||||
ON et.pool_id = ft.pool_id
|
||||
AND et.effective_at = ft.effective_at
|
||||
AND et.transition_type = 'end'
|
||||
JOIN core.pool_provisions ep ON ep.provision_id = et.provision_id
|
||||
JOIN core.products epr ON epr.product_id = ep.product_id
|
||||
WHERE g.grant_reason = 'default'
|
||||
AND ft.transition_type = 'initiate'
|
||||
AND ft.provision_id = (
|
||||
SELECT fp.provision_id FROM core.pool_provisions fp
|
||||
WHERE fp.grant_id = g.grant_id
|
||||
ORDER BY fp.activated_at, fp.provision_id
|
||||
LIMIT 1)
|
||||
AND (ft.reason IN ('post-cancellation default restoration', 'post-revocation default restoration')
|
||||
OR (ft.reason LIKE 'grant-expiration:%' AND ft.reason LIKE '% default restoration'))
|
||||
GROUP BY epr.name
|
||||
) x
|
||||
ORDER BY x.first_written
|
||||
)::text[] AS restored_after_product_names,
|
||||
count(*) OVER() AS total_count
|
||||
FROM core.grants g
|
||||
LEFT JOIN core.pool_provisions p ON p.grant_id = g.grant_id
|
||||
LEFT JOIN core.pool_provision_ladders l ON l.provision_id = p.provision_id
|
||||
LEFT JOIN wait_lines wl ON wl.grant_id = g.grant_id
|
||||
LEFT JOIN core.organizations org ON org.org_id = g.granted_to_org_id
|
||||
WHERE
|
||||
$1::text IS NULL
|
||||
OR org.name ILIKE ('%' || $1::text || '%')
|
||||
OR g.product_id = ANY($2::uuid[])
|
||||
GROUP BY g.grant_id, g.granted_to_org_id, g.product_id, g.grant_reason, g.description, g.quantity, g.status, g.created_at, g.extends_grant_id
|
||||
HAVING
|
||||
$3::text IS NULL
|
||||
OR (
|
||||
CASE
|
||||
WHEN BOOL_OR(p.status = 'active') THEN 'live'
|
||||
WHEN BOOL_OR(wl.on_hold) THEN 'on_hold'
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM core.grants child
|
||||
WHERE child.extends_grant_id = g.grant_id
|
||||
) THEN 'superseded'
|
||||
ELSE 'inactive'
|
||||
END
|
||||
) = $3::text
|
||||
ORDER BY g.created_at DESC
|
||||
LIMIT $5
|
||||
OFFSET $4
|
||||
`
|
||||
|
||||
type ListGrantsWithDeliveryPageParams struct {
|
||||
Q sql.NullString `json:"q"`
|
||||
ProductIds []string `json:"product_ids"`
|
||||
DeliveryState sql.NullString `json:"delivery_state"`
|
||||
PageOffset int32 `json:"page_offset"`
|
||||
PageLimit int32 `json:"page_limit"`
|
||||
}
|
||||
|
||||
type ListGrantsWithDeliveryPageRow struct {
|
||||
GrantID string `json:"grant_id"`
|
||||
GrantedToOrgID uuid.NullUUID `json:"granted_to_org_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
GrantReason string `json:"grant_reason"`
|
||||
Description sql.NullString `json:"description"`
|
||||
Quantity int32 `json:"quantity"`
|
||||
GrantStatus string `json:"grant_status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ExtendsGrantID uuid.NullUUID `json:"extends_grant_id"`
|
||||
GrantedByPersonID uuid.NullUUID `json:"granted_by_person_id"`
|
||||
ResumesAfterSupersession bool `json:"resumes_after_supersession"`
|
||||
DeliveryState string `json:"delivery_state"`
|
||||
ReplacedByGrantID interface{} `json:"replaced_by_grant_id"`
|
||||
ActivatedAt interface{} `json:"activated_at"`
|
||||
EndedAt interface{} `json:"ended_at"`
|
||||
ValidUntil sql.NullTime `json:"valid_until"`
|
||||
HolderProductName string `json:"holder_product_name"`
|
||||
WaitsOn string `json:"waits_on"`
|
||||
BlockingProductNames []string `json:"blocking_product_names"`
|
||||
ManageOffered bool `json:"manage_offered"`
|
||||
WillNotResume bool `json:"will_not_resume"`
|
||||
ResumingProductNames []string `json:"resuming_product_names"`
|
||||
FloorAfterRevocation bool `json:"floor_after_revocation"`
|
||||
DuplicatesDefault bool `json:"duplicates_default"`
|
||||
RestoredAfterProductNames []string `json:"restored_after_product_names"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
}
|
||||
|
||||
// Paginated, searchable, filterable system-wide variant of
|
||||
// ListGrantsWithDelivery for /operator/grants (operator-list-scale,
|
||||
// design D4). Same derivation, join shape, and ordering as
|
||||
// ListGrantsWithDelivery above -- do not let the two drift, and never
|
||||
// scope this one to an org_id; the org-detail composite keeps calling
|
||||
// ListGrantsWithDelivery unpaged. Ordered by created_at DESC alone
|
||||
// (tier-changes-ledger design D5): the Live facet and the Active tab
|
||||
// already select the delivering rows, so ordering does not also pretend
|
||||
// to filter. core.grant_waits is called with a NULL pool, evaluating every
|
||||
// pool; this is the one reader that does (grant-resumption D3).
|
||||
//
|
||||
// sqlc.narg(q): NULL means search is inactive and every row matches.
|
||||
// Non-NULL matches a grant when the granted-to organization's name
|
||||
// ILIKE's the term, OR the grant's product_id is present in
|
||||
// sqlc.narg(product_ids) -- the grants module cannot query the billing
|
||||
// schema directly, so the caller (Go) pre-resolves product IDs whose name
|
||||
// matches the search term and passes them here; a NULL or empty array
|
||||
// with a non-NULL q simply means the q criterion rides on org name alone.
|
||||
//
|
||||
// sqlc.narg(delivery_state): NULL returns every delivery state; otherwise
|
||||
// only rows whose derived state equals the value. The HAVING clause below
|
||||
// repeats the exact CASE expression from the SELECT list (HAVING cannot
|
||||
// reference a SELECT-list alias) so the filter can never disagree with
|
||||
// what the SELECT list -- and therefore the Go derivation reading it --
|
||||
// renders as that row's state (a mixed-fixture test pins this agreement).
|
||||
//
|
||||
// count(*) OVER() is evaluated after GROUP BY/HAVING, so with the GROUP
|
||||
// BY below it counts grouped (one-per-grant) rows that passed WHERE and
|
||||
// HAVING -- the true total of matching grants, not a join-multiplied
|
||||
// count of the underlying pool_provisions/pool_provision_ladders rows.
|
||||
func (q *Queries) ListGrantsWithDeliveryPage(ctx context.Context, arg ListGrantsWithDeliveryPageParams) ([]ListGrantsWithDeliveryPageRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listGrantsWithDeliveryPage,
|
||||
arg.Q,
|
||||
pq.Array(arg.ProductIds),
|
||||
arg.DeliveryState,
|
||||
arg.PageOffset,
|
||||
arg.PageLimit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []ListGrantsWithDeliveryPageRow{}
|
||||
for rows.Next() {
|
||||
var i ListGrantsWithDeliveryPageRow
|
||||
if err := rows.Scan(
|
||||
&i.GrantID,
|
||||
&i.GrantedToOrgID,
|
||||
|
||||
@@ -308,42 +308,30 @@ type Querier interface {
|
||||
// The lineage sub-ordering (the replaced_by_grant_id subquery below) stays
|
||||
// child.created_at DESC.
|
||||
//
|
||||
// sqlc.narg(org_id): NULL returns every grant system-wide (the grants
|
||||
// index, /operator/grants) and evaluates every pool; set, scopes to one
|
||||
// organization (org-detail) and evaluates that organization's default pool.
|
||||
// One query serves both grant listings. Every argument is optional:
|
||||
//
|
||||
// org_id set: that organization's grants, with core.grant_waits
|
||||
// evaluated on its active default pool (the zero UUID
|
||||
// when it has none) and the lineage roots cut to its
|
||||
// grants. NULL: every grant, with waits evaluated on
|
||||
// every pool (grant-resumption D3). The two can read a
|
||||
// waiting grant differently only where its replacement
|
||||
// sits outside the organization's active default pool,
|
||||
// which the console does not write.
|
||||
// q matches the organization's name with ILIKE, so % and _
|
||||
// match any text, or a product id in product_ids, which
|
||||
// the caller matches by name in Go.
|
||||
// delivery_state keeps the rows of that state. The state is computed
|
||||
// once, in listed, and the outer statement filters on it.
|
||||
// page_limit, page_offset
|
||||
// NULL reads every row, from the first.
|
||||
//
|
||||
// total_count counts the rows every filter keeps, before the page. Rows
|
||||
// come newest first by created_at alone (tier-changes-ledger D5).
|
||||
// Each grant's lineage root, for the default-duplicate line.
|
||||
ListGrantsWithDelivery(ctx context.Context, orgID uuid.NullUUID) ([]ListGrantsWithDeliveryRow, error)
|
||||
// Paginated, searchable, filterable system-wide variant of
|
||||
// ListGrantsWithDelivery for /operator/grants (operator-list-scale,
|
||||
// design D4). Same derivation, join shape, and ordering as
|
||||
// ListGrantsWithDelivery above -- do not let the two drift, and never
|
||||
// scope this one to an org_id; the org-detail composite keeps calling
|
||||
// ListGrantsWithDelivery unpaged. Ordered by created_at DESC alone
|
||||
// (tier-changes-ledger design D5): the Live facet and the Active tab
|
||||
// already select the delivering rows, so ordering does not also pretend
|
||||
// to filter. core.grant_waits is called with a NULL pool, evaluating every
|
||||
// pool; this is the one reader that does (grant-resumption D3).
|
||||
//
|
||||
// sqlc.narg(q): NULL means search is inactive and every row matches.
|
||||
// Non-NULL matches a grant when the granted-to organization's name
|
||||
// ILIKE's the term, OR the grant's product_id is present in
|
||||
// sqlc.narg(product_ids) -- the grants module cannot query the billing
|
||||
// schema directly, so the caller (Go) pre-resolves product IDs whose name
|
||||
// matches the search term and passes them here; a NULL or empty array
|
||||
// with a non-NULL q simply means the q criterion rides on org name alone.
|
||||
//
|
||||
// sqlc.narg(delivery_state): NULL returns every delivery state; otherwise
|
||||
// only rows whose derived state equals the value. The HAVING clause below
|
||||
// repeats the exact CASE expression from the SELECT list (HAVING cannot
|
||||
// reference a SELECT-list alias) so the filter can never disagree with
|
||||
// what the SELECT list -- and therefore the Go derivation reading it --
|
||||
// renders as that row's state (a mixed-fixture test pins this agreement).
|
||||
//
|
||||
// count(*) OVER() is evaluated after GROUP BY/HAVING, so with the GROUP
|
||||
// BY below it counts grouped (one-per-grant) rows that passed WHERE and
|
||||
// HAVING -- the true total of matching grants, not a join-multiplied
|
||||
// count of the underlying pool_provisions/pool_provision_ladders rows.
|
||||
ListGrantsWithDeliveryPage(ctx context.Context, arg ListGrantsWithDeliveryPageParams) ([]ListGrantsWithDeliveryPageRow, error)
|
||||
// One row per grant that passes the org and search filters, with its
|
||||
// delivery state computed once; the outer SELECT filters on it.
|
||||
ListGrantsWithDelivery(ctx context.Context, arg ListGrantsWithDeliveryParams) ([]ListGrantsWithDeliveryRow, error)
|
||||
// The position of a pool on every plan ladder it holds, newest attachment
|
||||
// first, in one round trip. The Products page, Checkout and the switch read
|
||||
// it through positionLoader (internal/server/plan_offer.go), and the switch
|
||||
|
||||
@@ -94,9 +94,26 @@ ORDER BY created_at DESC;
|
||||
-- The lineage sub-ordering (the replaced_by_grant_id subquery below) stays
|
||||
-- child.created_at DESC.
|
||||
--
|
||||
-- sqlc.narg(org_id): NULL returns every grant system-wide (the grants
|
||||
-- index, /operator/grants) and evaluates every pool; set, scopes to one
|
||||
-- organization (org-detail) and evaluates that organization's default pool.
|
||||
-- One query serves both grant listings. Every argument is optional:
|
||||
--
|
||||
-- org_id set: that organization's grants, with core.grant_waits
|
||||
-- evaluated on its active default pool (the zero UUID
|
||||
-- when it has none) and the lineage roots cut to its
|
||||
-- grants. NULL: every grant, with waits evaluated on
|
||||
-- every pool (grant-resumption D3). The two can read a
|
||||
-- waiting grant differently only where its replacement
|
||||
-- sits outside the organization's active default pool,
|
||||
-- which the console does not write.
|
||||
-- q matches the organization's name with ILIKE, so % and _
|
||||
-- match any text, or a product id in product_ids, which
|
||||
-- the caller matches by name in Go.
|
||||
-- delivery_state keeps the rows of that state. The state is computed
|
||||
-- once, in listed, and the outer statement filters on it.
|
||||
-- page_limit, page_offset
|
||||
-- NULL reads every row, from the first.
|
||||
--
|
||||
-- total_count counts the rows every filter keeps, before the page. Rows
|
||||
-- come newest first by created_at alone (tier-changes-ledger D5).
|
||||
WITH RECURSIVE waits AS (
|
||||
SELECT w.grant_id::uuid AS grant_id,
|
||||
w.pool_id::uuid AS pool_id,
|
||||
@@ -183,7 +200,10 @@ roots (grant_id, root_id, depth) AS (
|
||||
FROM roots r
|
||||
JOIN core.grants c ON c.extends_grant_id = r.grant_id
|
||||
WHERE r.depth < 100
|
||||
)
|
||||
),
|
||||
-- One row per grant that passes the org and search filters, with its
|
||||
-- delivery state computed once; the outer SELECT filters on it.
|
||||
listed AS (
|
||||
SELECT
|
||||
g.grant_id,
|
||||
g.granted_to_org_id,
|
||||
@@ -289,246 +309,20 @@ FROM core.grants g
|
||||
LEFT JOIN core.pool_provisions p ON p.grant_id = g.grant_id
|
||||
LEFT JOIN core.pool_provision_ladders l ON l.provision_id = p.provision_id
|
||||
LEFT JOIN wait_lines wl ON wl.grant_id = g.grant_id
|
||||
WHERE sqlc.narg(org_id)::uuid IS NULL OR g.granted_to_org_id = sqlc.narg(org_id)::uuid
|
||||
GROUP BY g.grant_id, g.granted_to_org_id, g.product_id, g.grant_reason, g.description, g.quantity, g.status, g.created_at, g.extends_grant_id
|
||||
ORDER BY g.created_at DESC;
|
||||
|
||||
-- name: ListGrantsWithDeliveryPage :many
|
||||
-- Paginated, searchable, filterable system-wide variant of
|
||||
-- ListGrantsWithDelivery for /operator/grants (operator-list-scale,
|
||||
-- design D4). Same derivation, join shape, and ordering as
|
||||
-- ListGrantsWithDelivery above -- do not let the two drift, and never
|
||||
-- scope this one to an org_id; the org-detail composite keeps calling
|
||||
-- ListGrantsWithDelivery unpaged. Ordered by created_at DESC alone
|
||||
-- (tier-changes-ledger design D5): the Live facet and the Active tab
|
||||
-- already select the delivering rows, so ordering does not also pretend
|
||||
-- to filter. core.grant_waits is called with a NULL pool, evaluating every
|
||||
-- pool; this is the one reader that does (grant-resumption D3).
|
||||
--
|
||||
-- sqlc.narg(q): NULL means search is inactive and every row matches.
|
||||
-- Non-NULL matches a grant when the granted-to organization's name
|
||||
-- ILIKE's the term, OR the grant's product_id is present in
|
||||
-- sqlc.narg(product_ids) -- the grants module cannot query the billing
|
||||
-- schema directly, so the caller (Go) pre-resolves product IDs whose name
|
||||
-- matches the search term and passes them here; a NULL or empty array
|
||||
-- with a non-NULL q simply means the q criterion rides on org name alone.
|
||||
--
|
||||
-- sqlc.narg(delivery_state): NULL returns every delivery state; otherwise
|
||||
-- only rows whose derived state equals the value. The HAVING clause below
|
||||
-- repeats the exact CASE expression from the SELECT list (HAVING cannot
|
||||
-- reference a SELECT-list alias) so the filter can never disagree with
|
||||
-- what the SELECT list -- and therefore the Go derivation reading it --
|
||||
-- renders as that row's state (a mixed-fixture test pins this agreement).
|
||||
--
|
||||
-- count(*) OVER() is evaluated after GROUP BY/HAVING, so with the GROUP
|
||||
-- BY below it counts grouped (one-per-grant) rows that passed WHERE and
|
||||
-- HAVING -- the true total of matching grants, not a join-multiplied
|
||||
-- count of the underlying pool_provisions/pool_provision_ladders rows.
|
||||
WITH RECURSIVE waits AS (
|
||||
SELECT w.grant_id::uuid AS grant_id,
|
||||
w.pool_id::uuid AS pool_id,
|
||||
w.holder_grant_id::uuid AS holder_grant_id,
|
||||
w.holder_subscription_id::uuid AS holder_subscription_id,
|
||||
w.holder_delivers::boolean AS holder_delivers,
|
||||
w.top_delivers::boolean AS top_delivers,
|
||||
w.resumes::boolean AS resumes,
|
||||
w.on_no_ladder::boolean AS on_no_ladder,
|
||||
w.blocking_provision_ids::uuid[] AS blocking_provision_ids,
|
||||
w.preceding_grant_ids::uuid[] AS preceding_grant_ids
|
||||
FROM core.grant_waits(now(), NULL::uuid)
|
||||
AS w(grant_id, pool_id, replaced_at, holder_grant_id, holder_subscription_id,
|
||||
holder_delivers, top_grant_id, top_subscription_id, top_delivers,
|
||||
resumes, on_no_ladder, blocking_provision_ids, preceding_grant_ids)
|
||||
),
|
||||
wait_lines AS (
|
||||
SELECT w.grant_id,
|
||||
((w.resumes AND w.holder_delivers)
|
||||
OR (wg.resumes_after_supersession AND wg.status = 'active'
|
||||
AND wg.valid_from <= now()
|
||||
AND (wg.valid_until IS NULL OR wg.valid_until > now())
|
||||
AND w.top_delivers)) AS on_hold,
|
||||
(NOT wg.resumes_after_supersession AND w.top_delivers
|
||||
AND (wg.valid_until IS NULL OR wg.valid_until > now())) AS unmarked_under_live_top,
|
||||
(wg.resumes_after_supersession AND wg.status = 'active'
|
||||
AND NOT (w.top_delivers
|
||||
AND (wg.valid_until IS NULL OR wg.valid_until > now()))) AS will_not_resume,
|
||||
CASE
|
||||
WHEN w.holder_grant_id IS NOT NULL THEN (
|
||||
SELECT hp.name FROM core.grants hg
|
||||
JOIN core.products hp ON hp.product_id = hg.product_id
|
||||
WHERE hg.grant_id = w.holder_grant_id)
|
||||
ELSE (
|
||||
SELECT sp.name FROM core.pool_provisions spp
|
||||
JOIN core.products sp ON sp.product_id = spp.product_id
|
||||
WHERE spp.subscription_id = w.holder_subscription_id
|
||||
AND spp.pool_id = w.pool_id
|
||||
AND spp.status <> 'ended'
|
||||
ORDER BY EXISTS (
|
||||
SELECT 1 FROM core.product_conferral_shapes a
|
||||
JOIN core.product_conferral_shapes b ON b.plan_ladder_id = a.plan_ladder_id
|
||||
WHERE a.product_id = spp.product_id AND b.product_id = wg.product_id
|
||||
) DESC,
|
||||
spp.activated_at DESC, spp.provision_id
|
||||
LIMIT 1)
|
||||
END AS holder_product_name,
|
||||
CASE
|
||||
WHEN w.resumes AND w.holder_delivers THEN 'resumes'
|
||||
WHEN w.holder_grant_id IS NOT NULL AND NOT w.holder_delivers THEN 'behind'
|
||||
WHEN w.on_no_ladder THEN 'no_ladder'
|
||||
WHEN cardinality(w.blocking_provision_ids) > 0
|
||||
OR cardinality(w.preceding_grant_ids) > 0 THEN 'blocked'
|
||||
ELSE ''
|
||||
END AS waits_on,
|
||||
ARRAY(
|
||||
SELECT DISTINCT bp.name FROM core.products bp
|
||||
WHERE bp.product_id IN (
|
||||
SELECT pp.product_id FROM core.pool_provisions pp
|
||||
WHERE pp.provision_id = ANY (w.blocking_provision_ids)
|
||||
UNION
|
||||
SELECT pg.product_id FROM core.grants pg
|
||||
WHERE pg.grant_id = ANY (w.preceding_grant_ids))
|
||||
ORDER BY bp.name
|
||||
) AS blocking_product_names
|
||||
FROM waits w
|
||||
JOIN core.grants wg ON wg.grant_id = w.grant_id
|
||||
),
|
||||
roots (grant_id, root_id, depth) AS (
|
||||
SELECT r0.grant_id, r0.grant_id, 0 FROM core.grants r0
|
||||
WHERE r0.extends_grant_id IS NULL
|
||||
UNION ALL
|
||||
SELECT c.grant_id, r.root_id, r.depth + 1
|
||||
FROM roots r
|
||||
JOIN core.grants c ON c.extends_grant_id = r.grant_id
|
||||
WHERE r.depth < 100
|
||||
)
|
||||
SELECT
|
||||
g.grant_id,
|
||||
g.granted_to_org_id,
|
||||
g.product_id,
|
||||
g.grant_reason,
|
||||
g.description,
|
||||
g.quantity,
|
||||
g.status AS grant_status,
|
||||
g.created_at,
|
||||
g.extends_grant_id,
|
||||
g.granted_by_person_id,
|
||||
g.resumes_after_supersession,
|
||||
CASE
|
||||
WHEN BOOL_OR(p.status = 'active') THEN 'live'
|
||||
WHEN BOOL_OR(wl.on_hold) THEN 'on_hold'
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM core.grants child
|
||||
WHERE child.extends_grant_id = g.grant_id
|
||||
) THEN 'superseded'
|
||||
ELSE 'inactive'
|
||||
END AS delivery_state,
|
||||
MAX((
|
||||
SELECT child.grant_id::text FROM core.grants child
|
||||
WHERE child.extends_grant_id = g.grant_id
|
||||
ORDER BY child.created_at DESC
|
||||
LIMIT 1
|
||||
)) AS replaced_by_grant_id,
|
||||
MAX(l.activated_at) AS activated_at,
|
||||
CASE WHEN BOOL_OR(p.status <> 'ended') THEN NULL
|
||||
ELSE MAX(CASE WHEN l.status = 'ended' THEN l.ended_at END)
|
||||
END AS ended_at,
|
||||
g.valid_until,
|
||||
COALESCE(MAX(wl.holder_product_name), '')::text AS holder_product_name,
|
||||
COALESCE(MAX(wl.waits_on), '')::text AS waits_on,
|
||||
COALESCE(MAX(wl.blocking_product_names), '{}')::text[] AS blocking_product_names,
|
||||
(g.status = 'active'
|
||||
AND g.grant_reason <> 'default'
|
||||
AND NOT EXISTS (SELECT 1 FROM core.grants child WHERE child.extends_grant_id = g.grant_id)
|
||||
AND (COALESCE(BOOL_OR(p.status = 'active'), FALSE)
|
||||
OR COALESCE(BOOL_OR(wl.on_hold), FALSE)
|
||||
OR (NOT COALESCE(BOOL_OR(p.status <> 'ended'), FALSE)
|
||||
AND COALESCE(BOOL_OR(wl.unmarked_under_live_top), FALSE))))::boolean AS manage_offered,
|
||||
COALESCE(BOOL_OR(wl.will_not_resume), FALSE)::boolean AS will_not_resume,
|
||||
ARRAY(
|
||||
SELECT rp.name FROM waits rw
|
||||
JOIN core.grants rg ON rg.grant_id = rw.grant_id
|
||||
JOIN core.products rp ON rp.product_id = rg.product_id
|
||||
WHERE rw.resumes AND rw.holder_grant_id = g.grant_id
|
||||
ORDER BY rg.created_at DESC, rg.grant_id DESC
|
||||
)::text[] AS resuming_product_names,
|
||||
(EXISTS (
|
||||
SELECT 1 FROM core.organizations fo
|
||||
JOIN core.org_types fot ON fot.org_type = fo.org_type
|
||||
WHERE fo.org_id = g.granted_to_org_id
|
||||
AND fot.default_plan_ladder_id IS NOT NULL)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM core.pool_provision_ladders fl
|
||||
JOIN core.pool_provisions fp ON fp.provision_id = fl.provision_id
|
||||
JOIN core.resource_pools frp ON frp.pool_id = fl.pool_id
|
||||
WHERE frp.org_id = g.granted_to_org_id
|
||||
AND frp.pool_type = 'default'
|
||||
AND frp.status = 'active'
|
||||
AND fl.status IN ('active', 'suspended')
|
||||
AND fp.grant_id IS DISTINCT FROM g.grant_id))::boolean AS floor_after_revocation,
|
||||
(g.status = 'active'
|
||||
AND NOT EXISTS (SELECT 1 FROM core.pool_provisions dp WHERE dp.grant_id = g.grant_id)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM core.pool_provisions lp
|
||||
JOIN core.resource_pools lrp ON lrp.pool_id = lp.pool_id
|
||||
JOIN roots lr ON lr.grant_id = lp.grant_id
|
||||
JOIN core.grants lroot ON lroot.grant_id = lr.root_id
|
||||
WHERE lp.product_id = g.product_id
|
||||
AND lp.status = 'active'
|
||||
AND lrp.org_id = g.granted_to_org_id
|
||||
AND lrp.pool_type = 'default'
|
||||
AND lroot.grant_reason = 'default'))::boolean AS duplicates_default,
|
||||
ARRAY(
|
||||
SELECT x.name FROM (
|
||||
SELECT epr.name, min(et.transition_id::text) AS first_written
|
||||
FROM core.pool_provision_transitions ft
|
||||
JOIN core.pool_provision_transitions et
|
||||
ON et.pool_id = ft.pool_id
|
||||
AND et.effective_at = ft.effective_at
|
||||
AND et.transition_type = 'end'
|
||||
JOIN core.pool_provisions ep ON ep.provision_id = et.provision_id
|
||||
JOIN core.products epr ON epr.product_id = ep.product_id
|
||||
WHERE g.grant_reason = 'default'
|
||||
AND ft.transition_type = 'initiate'
|
||||
AND ft.provision_id = (
|
||||
SELECT fp.provision_id FROM core.pool_provisions fp
|
||||
WHERE fp.grant_id = g.grant_id
|
||||
ORDER BY fp.activated_at, fp.provision_id
|
||||
LIMIT 1)
|
||||
AND (ft.reason IN ('post-cancellation default restoration', 'post-revocation default restoration')
|
||||
OR (ft.reason LIKE 'grant-expiration:%' AND ft.reason LIKE '% default restoration'))
|
||||
GROUP BY epr.name
|
||||
) x
|
||||
ORDER BY x.first_written
|
||||
)::text[] AS restored_after_product_names,
|
||||
count(*) OVER() AS total_count
|
||||
FROM core.grants g
|
||||
LEFT JOIN core.pool_provisions p ON p.grant_id = g.grant_id
|
||||
LEFT JOIN core.pool_provision_ladders l ON l.provision_id = p.provision_id
|
||||
LEFT JOIN wait_lines wl ON wl.grant_id = g.grant_id
|
||||
LEFT JOIN core.organizations org ON org.org_id = g.granted_to_org_id
|
||||
WHERE
|
||||
sqlc.narg(q)::text IS NULL
|
||||
OR org.name ILIKE ('%' || sqlc.narg(q)::text || '%')
|
||||
OR g.product_id = ANY(sqlc.narg(product_ids)::uuid[])
|
||||
WHERE (sqlc.narg(org_id)::uuid IS NULL OR g.granted_to_org_id = sqlc.narg(org_id)::uuid)
|
||||
AND (sqlc.narg(q)::text IS NULL
|
||||
OR org.name ILIKE ('%' || sqlc.narg(q)::text || '%')
|
||||
OR g.product_id = ANY(sqlc.narg(product_ids)::uuid[]))
|
||||
GROUP BY g.grant_id, g.granted_to_org_id, g.product_id, g.grant_reason, g.description, g.quantity, g.status, g.created_at, g.extends_grant_id
|
||||
HAVING
|
||||
sqlc.narg(delivery_state)::text IS NULL
|
||||
OR (
|
||||
CASE
|
||||
WHEN BOOL_OR(p.status = 'active') THEN 'live'
|
||||
WHEN BOOL_OR(wl.on_hold) THEN 'on_hold'
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM core.grants child
|
||||
WHERE child.extends_grant_id = g.grant_id
|
||||
) THEN 'superseded'
|
||||
ELSE 'inactive'
|
||||
END
|
||||
) = sqlc.narg(delivery_state)::text
|
||||
ORDER BY g.created_at DESC
|
||||
LIMIT sqlc.arg(page_limit)
|
||||
OFFSET sqlc.arg(page_offset);
|
||||
)
|
||||
SELECT listed.*, count(*) OVER() AS total_count
|
||||
FROM listed
|
||||
WHERE sqlc.narg(delivery_state)::text IS NULL
|
||||
OR listed.delivery_state = sqlc.narg(delivery_state)::text
|
||||
ORDER BY listed.created_at DESC
|
||||
LIMIT sqlc.narg(page_limit)
|
||||
OFFSET sqlc.narg(page_offset);
|
||||
|
||||
-- name: ListDeliveringGrantsByOrgID :many
|
||||
-- Returns only grants currently delivering entitlements to a pool in the
|
||||
|
||||
@@ -345,7 +345,9 @@ func (w *grWorld) scalar(query string, args ...any) int {
|
||||
// delivery is the grant's derived delivery state through the shared query.
|
||||
func (w *grWorld) delivery(orgID, grantID string) entitlements.ListGrantsWithDeliveryRow {
|
||||
w.t.Helper()
|
||||
rows, err := entitlements.New(w.db).ListGrantsWithDelivery(w.ctx, uuid.NullUUID{UUID: uuid.MustParse(orgID), Valid: true})
|
||||
rows, err := entitlements.New(w.db).ListGrantsWithDelivery(w.ctx, entitlements.ListGrantsWithDeliveryParams{
|
||||
OrgID: uuid.NullUUID{UUID: uuid.MustParse(orgID), Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
w.t.Fatalf("list grants: %v", err)
|
||||
}
|
||||
@@ -649,8 +651,8 @@ func checkOnHoldFilter(t *testing.T, w *waitingWorld) {
|
||||
w.underSub: true, w.underEval: true, w.ahead: true, w.evalHolder: true,
|
||||
w.outranked: true, w.behind: true, w.blocked: true, w.noLadder: true,
|
||||
}
|
||||
rows, err := w.h.handler.EntitlementsQ.ListGrantsWithDeliveryPage(w.ctx, entitlements.ListGrantsWithDeliveryPageParams{
|
||||
Q: sql.NullString{String: w.sfx, Valid: true}, DeliveryState: sql.NullString{String: "on_hold", Valid: true}, PageLimit: 1000,
|
||||
rows, err := w.h.handler.EntitlementsQ.ListGrantsWithDelivery(w.ctx, entitlements.ListGrantsWithDeliveryParams{
|
||||
Q: sql.NullString{String: w.sfx, Valid: true}, DeliveryState: sql.NullString{String: "on_hold", Valid: true}, PageLimit: sql.NullInt32{Int32: 1000, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("filter on_hold: %v", err)
|
||||
@@ -1890,11 +1892,11 @@ func TestIssueGrant_RecordsTheMark(t *testing.T) {
|
||||
|
||||
// TestGrantsListDeliveryStateAgreesWithGoDerivation_WaitingStates pins the
|
||||
// index filter to the rendered state for the waiting states too: over every
|
||||
// grant in the database, the paged query's HAVING selects exactly the rows
|
||||
// the shared derivation classifies under each state.
|
||||
// grant in the database, the state filter selects exactly the rows whose
|
||||
// state column names that state.
|
||||
func TestGrantsListDeliveryStateAgreesWithGoDerivation_WaitingStates(t *testing.T) {
|
||||
w := buildWaitingWorld(t)
|
||||
all, err := w.h.handler.EntitlementsQ.ListGrantsWithDelivery(w.ctx, uuid.NullUUID{})
|
||||
all, err := w.h.handler.EntitlementsQ.ListGrantsWithDelivery(w.ctx, entitlements.ListGrantsWithDeliveryParams{})
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
@@ -1910,8 +1912,8 @@ func TestGrantsListDeliveryStateAgreesWithGoDerivation_WaitingStates(t *testing.
|
||||
t.Errorf("%d grants read standby, a state that no longer exists", len(goByState["standby"]))
|
||||
}
|
||||
for _, state := range states {
|
||||
rows, err := w.h.handler.EntitlementsQ.ListGrantsWithDeliveryPage(w.ctx, entitlements.ListGrantsWithDeliveryPageParams{
|
||||
DeliveryState: sql.NullString{String: state, Valid: true}, PageLimit: 1_000_000,
|
||||
rows, err := w.h.handler.EntitlementsQ.ListGrantsWithDelivery(w.ctx, entitlements.ListGrantsWithDeliveryParams{
|
||||
DeliveryState: sql.NullString{String: state, Valid: true}, PageLimit: sql.NullInt32{Int32: 1_000_000, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("page %s: %v", state, err)
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
|
||||
package server
|
||||
|
||||
// The grants listing characterized: for a fixture of fifteen organizations
|
||||
// and thirty-three grants, the rows each of the two listing queries returns
|
||||
// (the organization's, and the search's that the Grants page pages), what the
|
||||
// Grants page and the organization's grants ledger show of them, and a check
|
||||
// that the two queries return the same columns for the same grant. The
|
||||
// The grants listing characterized: for a fixture of fifteen organizations and
|
||||
// thirty-three grants, the rows the listing query returns for each organization
|
||||
// and for the search that the Grants page pages, what the Grants page and the
|
||||
// organization's grants ledger show of them, and a check that the organization
|
||||
// call and the search call return the same columns for the same grant. The
|
||||
// files under testdata/grants_listing are the output the code produced when
|
||||
// they were written, read against the fixture before they were committed;
|
||||
// -update rewrites them. The fixture is the waiting fixture of the grant
|
||||
// resumption tests plus the acts below, each committed in its own
|
||||
// transaction, so no two grants share created_at and the order of the
|
||||
// listing never depends on a tie.
|
||||
// resumption tests plus the acts below, each committed in its own transaction,
|
||||
// so no two grants share created_at and the order of the listing never depends
|
||||
// on a tie.
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
@@ -205,7 +205,7 @@ func (w *listingWorld) listingRows(t *testing.T, c listingCall) (rows []listingR
|
||||
t.Helper()
|
||||
q := entitlements.New(w.db)
|
||||
if c.orgID != "" {
|
||||
got, err := q.ListGrantsWithDelivery(w.ctx, nullUUID(c.orgID))
|
||||
got, err := q.ListGrantsWithDelivery(w.ctx, entitlements.ListGrantsWithDeliveryParams{OrgID: nullUUID(c.orgID)})
|
||||
if err != nil {
|
||||
t.Fatalf("list the organization's grants: %v", err)
|
||||
}
|
||||
@@ -214,11 +214,13 @@ func (w *listingWorld) listingRows(t *testing.T, c listingCall) (rows []listingR
|
||||
}
|
||||
return rows, 0
|
||||
}
|
||||
p := entitlements.ListGrantsWithDeliveryPageParams{
|
||||
Q: sql.NullString{String: w.sfx, Valid: true}, PageLimit: c.limit, PageOffset: c.offset,
|
||||
p := entitlements.ListGrantsWithDeliveryParams{
|
||||
Q: sql.NullString{String: w.sfx, Valid: true},
|
||||
PageLimit: sql.NullInt32{Int32: c.limit, Valid: true},
|
||||
PageOffset: sql.NullInt32{Int32: c.offset, Valid: true},
|
||||
DeliveryState: sql.NullString{String: c.state, Valid: c.state != ""},
|
||||
}
|
||||
got, err := q.ListGrantsWithDeliveryPage(w.ctx, p)
|
||||
got, err := q.ListGrantsWithDelivery(w.ctx, p)
|
||||
if err != nil {
|
||||
t.Fatalf("search the grants: %v", err)
|
||||
}
|
||||
@@ -330,7 +332,7 @@ func TestGrantsListingRowsGolden(t *testing.T) {
|
||||
t.Run("drift", func(t *testing.T) { w.checkListingsAgree(t) })
|
||||
}
|
||||
|
||||
// checkListingsAgree holds the organization's query and the search's to the
|
||||
// checkListingsAgree holds the organization call and the search call to the
|
||||
// same columns, total_count apart, for every fixture grant, and fails with
|
||||
// the grant's name and each column that differs.
|
||||
func (w *listingWorld) checkListingsAgree(t *testing.T) {
|
||||
@@ -364,7 +366,7 @@ func (w *listingWorld) checkListingsAgree(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if len(differ) > 0 {
|
||||
t.Errorf("grant %s differs between the two queries in %s", w.norm.apply(id), w.normalize(t, strings.Join(differ, ", ")))
|
||||
t.Errorf("grant %s differs between the organization call and the search call in %s", w.norm.apply(id), w.normalize(t, strings.Join(differ, ", ")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1763,15 +1763,14 @@ func (h *OperatorPartialsHandler) loadOrgGrants(r *http.Request, data *OrgEnroll
|
||||
h.Logger.Error("failed to parse org ID for grants query", slog.Any("error", err), slog.String("org_id", orgID))
|
||||
data.GrantsError = "Failed to load grants."
|
||||
} else {
|
||||
grants, err := h.EntitlementsQ.ListGrantsWithDelivery(r.Context(), uuid.NullUUID{UUID: orgUUID, Valid: true})
|
||||
grants, err := h.EntitlementsQ.ListGrantsWithDelivery(r.Context(), entitlements.ListGrantsWithDeliveryParams{OrgID: uuid.NullUUID{UUID: orgUUID, Valid: true}})
|
||||
if err != nil {
|
||||
h.Logger.Error("failed to list grants", slog.Any("error", err), slog.String("org_id", orgID))
|
||||
data.GrantsError = "Failed to load grants."
|
||||
} else {
|
||||
// resolveOrgName=false: this composite is already scoped to one
|
||||
// named organization, so every row's OrgName would just repeat
|
||||
// it. ListGrantsWithDelivery orders live-first, then created_at
|
||||
// DESC (see queries/grants.sql).
|
||||
// it. ListGrantsWithDelivery lists the newest grant first.
|
||||
all := h.buildGrantViewModels(r, grants, false)
|
||||
filtered := make([]GrantViewModel, 0, len(all))
|
||||
q := strings.ToLower(strings.TrimSpace(gParams.Q))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package server
|
||||
|
||||
// operator_grants_list_test.go — DB-backed coverage for
|
||||
// ListGrantsWithDeliveryPage and its wiring into GetGrantsPage
|
||||
// ListGrantsWithDelivery and its wiring into GetGrantsPage
|
||||
// (operator-list-scale tasks 2.3 + 3.2, design D4): pins the SQL-vs-Go
|
||||
// delivery-state agreement, org-name search with a true total, and
|
||||
// LIMIT/OFFSET paging math.
|
||||
@@ -314,7 +314,7 @@ func issueAgreementGrants(t *testing.T, database *sql.DB, h *glHarness, f glFixt
|
||||
// and buildGrantViewModels would.
|
||||
func listingByState(ctx context.Context, t *testing.T, h *glHarness) map[string]map[string]bool {
|
||||
t.Helper()
|
||||
allRows, err := h.handler.EntitlementsQ.ListGrantsWithDelivery(ctx, uuid.NullUUID{})
|
||||
allRows, err := h.handler.EntitlementsQ.ListGrantsWithDelivery(ctx, entitlements.ListGrantsWithDeliveryParams{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListGrantsWithDelivery: %v", err)
|
||||
}
|
||||
@@ -327,7 +327,7 @@ func listingByState(ctx context.Context, t *testing.T, h *glHarness) map[string]
|
||||
return goByState
|
||||
}
|
||||
|
||||
// checkStateFilter holds the paged query's HAVING-filtered rows for one facet
|
||||
// checkStateFilter holds the state-filtered rows for one facet
|
||||
// value to exactly the grant IDs the Go derivation classified under that
|
||||
// state. PageLimit is generous because TEST_DATABASE_URL is shared,
|
||||
// non-transactional storage across this package's tests -- every grant any
|
||||
@@ -335,13 +335,13 @@ func listingByState(ctx context.Context, t *testing.T, h *glHarness) map[string]
|
||||
// of them, not just this fixture's three.
|
||||
func checkStateFilter(ctx context.Context, t *testing.T, h *glHarness, state string, goIDs map[string]bool) {
|
||||
t.Helper()
|
||||
rows, err := h.handler.EntitlementsQ.ListGrantsWithDeliveryPage(ctx, entitlements.ListGrantsWithDeliveryPageParams{
|
||||
rows, err := h.handler.EntitlementsQ.ListGrantsWithDelivery(ctx, entitlements.ListGrantsWithDeliveryParams{
|
||||
DeliveryState: sql.NullString{String: state, Valid: true},
|
||||
PageLimit: 1_000_000,
|
||||
PageOffset: 0,
|
||||
PageLimit: sql.NullInt32{Int32: 1_000_000, Valid: true},
|
||||
PageOffset: sql.NullInt32{Int32: 0, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListGrantsWithDeliveryPage(%s): %v", state, err)
|
||||
t.Fatalf("ListGrantsWithDelivery(%s): %v", state, err)
|
||||
}
|
||||
sqlIDs := make(map[string]bool, len(rows))
|
||||
for _, row := range rows {
|
||||
@@ -366,11 +366,12 @@ func checkStateFilter(ctx context.Context, t *testing.T, h *glHarness, state str
|
||||
}
|
||||
|
||||
// TestGrantsListDeliveryStateAgreesWithGoDerivation pins design D4's
|
||||
// central invariant: for every facet value, the rows
|
||||
// ListGrantsWithDeliveryPage's HAVING clause selects are exactly the rows
|
||||
// the Go derivation (ListGrantsWithDelivery + buildGrantViewModels) would
|
||||
// classify under that same state. If the paged query's repeated CASE
|
||||
// expression ever drifts from the shared one, this test is where it shows.
|
||||
// central invariant: for every facet value, the state filter selects exactly
|
||||
// the rows whose state column names that state, which are the rows the Go
|
||||
// derivation (ListGrantsWithDelivery + buildGrantViewModels) would classify
|
||||
// under that same state. The query filters on the state it computes once (no
|
||||
// HAVING clause), so the filter and the column cannot drift apart; this test
|
||||
// is where a change that let them would show.
|
||||
func TestGrantsListDeliveryStateAgreesWithGoDerivation(t *testing.T) {
|
||||
database := dbtest.Open(t)
|
||||
f := newGLFixture(t, database)
|
||||
@@ -406,11 +407,11 @@ func TestGrantsListDeliveryStateAgreesWithGoDerivation(t *testing.T) {
|
||||
{"superseded", "grant A", g.extended},
|
||||
{"inactive", "grant B", g.revoked},
|
||||
} {
|
||||
rows, err := h.handler.EntitlementsQ.ListGrantsWithDeliveryPage(ctx, entitlements.ListGrantsWithDeliveryPageParams{
|
||||
DeliveryState: sql.NullString{String: c.state, Valid: true}, PageLimit: 1_000_000,
|
||||
rows, err := h.handler.EntitlementsQ.ListGrantsWithDelivery(ctx, entitlements.ListGrantsWithDeliveryParams{
|
||||
DeliveryState: sql.NullString{String: c.state, Valid: true}, PageLimit: sql.NullInt32{Int32: 1_000_000, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListGrantsWithDeliveryPage(%s): %v", c.state, err)
|
||||
t.Fatalf("ListGrantsWithDelivery(%s): %v", c.state, err)
|
||||
}
|
||||
if !containsGrantID(rows, c.id) {
|
||||
t.Errorf("%s filter: missing %s %s", c.state, c.what, c.id)
|
||||
@@ -418,7 +419,7 @@ func TestGrantsListDeliveryStateAgreesWithGoDerivation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func containsGrantID(rows []entitlements.ListGrantsWithDeliveryPageRow, grantID string) bool {
|
||||
func containsGrantID(rows []entitlements.ListGrantsWithDeliveryRow, grantID string) bool {
|
||||
for _, row := range rows {
|
||||
if row.GrantID == grantID {
|
||||
return true
|
||||
@@ -431,9 +432,8 @@ func containsGrantID(rows []entitlements.ListGrantsWithDeliveryPageRow, grantID
|
||||
// "Search narrows the list server-side" scenario end to end through
|
||||
// GetGrantsPage: a search term matching two organizations' names (and not
|
||||
// a third) narrows the rendered rows and reports the true total, both by
|
||||
// org name and — since the grants module cannot join into billing to
|
||||
// search product names in SQL — by the Go-resolved product-name path
|
||||
// (matchingProductIDs).
|
||||
// org name and — since % and _ must match themselves in product names —
|
||||
// by the Go-resolved product-name path (matchingProductIDs).
|
||||
func TestGrantsListSearchNarrowsWithTotals(t *testing.T) {
|
||||
database := dbtest.Open(t)
|
||||
f := newGLFixture(t, database)
|
||||
@@ -467,13 +467,13 @@ func TestGrantsListSearchNarrowsWithTotals(t *testing.T) {
|
||||
|
||||
// Query-level cross-check: the true total independent of any HTML
|
||||
// formatting.
|
||||
rows, err := h.handler.EntitlementsQ.ListGrantsWithDeliveryPage(context.Background(), entitlements.ListGrantsWithDeliveryPageParams{
|
||||
rows, err := h.handler.EntitlementsQ.ListGrantsWithDelivery(context.Background(), entitlements.ListGrantsWithDeliveryParams{
|
||||
Q: sql.NullString{String: marker, Valid: true},
|
||||
PageLimit: 50,
|
||||
PageOffset: 0,
|
||||
PageLimit: sql.NullInt32{Int32: 50, Valid: true},
|
||||
PageOffset: sql.NullInt32{Int32: 0, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListGrantsWithDeliveryPage: %v", err)
|
||||
t.Fatalf("ListGrantsWithDelivery: %v", err)
|
||||
}
|
||||
if len(rows) != 2 {
|
||||
t.Fatalf("expected 2 rows for q=%q, got %d", marker, len(rows))
|
||||
@@ -533,13 +533,13 @@ func TestGrantsListPagingMath(t *testing.T) {
|
||||
offsets := []int32{0, 2, 4}
|
||||
wantCounts := []int{2, 2, 1}
|
||||
for i, offset := range offsets {
|
||||
rows, err := h.handler.EntitlementsQ.ListGrantsWithDeliveryPage(ctx, entitlements.ListGrantsWithDeliveryPageParams{
|
||||
rows, err := h.handler.EntitlementsQ.ListGrantsWithDelivery(ctx, entitlements.ListGrantsWithDeliveryParams{
|
||||
Q: sql.NullString{String: marker, Valid: true},
|
||||
PageLimit: 2,
|
||||
PageOffset: offset,
|
||||
PageLimit: sql.NullInt32{Int32: 2, Valid: true},
|
||||
PageOffset: sql.NullInt32{Int32: offset, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListGrantsWithDeliveryPage offset=%d: %v", offset, err)
|
||||
t.Fatalf("ListGrantsWithDelivery offset=%d: %v", offset, err)
|
||||
}
|
||||
if len(rows) != wantCounts[i] {
|
||||
t.Fatalf("offset=%d: expected %d rows, got %d", offset, wantCounts[i], len(rows))
|
||||
@@ -597,13 +597,13 @@ func TestGrantsListOrdering_CreatedAtRegardlessOfDelivery(t *testing.T) {
|
||||
// scenario is exactly a newer non-live grant outranking an older one.
|
||||
newerInactiveID := glInsertGrant(t, database, org, f.prodB, f.operatorID)
|
||||
|
||||
rows, err := h.handler.EntitlementsQ.ListGrantsWithDeliveryPage(context.Background(), entitlements.ListGrantsWithDeliveryPageParams{
|
||||
rows, err := h.handler.EntitlementsQ.ListGrantsWithDelivery(context.Background(), entitlements.ListGrantsWithDeliveryParams{
|
||||
Q: sql.NullString{String: marker, Valid: true},
|
||||
PageLimit: 50,
|
||||
PageOffset: 0,
|
||||
PageLimit: sql.NullInt32{Int32: 50, Valid: true},
|
||||
PageOffset: sql.NullInt32{Int32: 0, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListGrantsWithDeliveryPage: %v", err)
|
||||
t.Fatalf("ListGrantsWithDelivery: %v", err)
|
||||
}
|
||||
if len(rows) != 2 {
|
||||
t.Fatalf("expected 2 rows for marker %q, got %d", marker, len(rows))
|
||||
|
||||
@@ -351,7 +351,7 @@ func (h *OperatorPartialsHandler) ownedResourceKeys(ctx context.Context, provide
|
||||
// exists" — grants: the derived Live/On hold/Superseded/Inactive states,
|
||||
// design D4; grant-resumption D9, one On hold pill for every marked grant
|
||||
// that waits). Values match
|
||||
// sqlc.narg(delivery_state) in ListGrantsWithDeliveryPage and the
|
||||
// sqlc.narg(delivery_state) in ListGrantsWithDelivery and the
|
||||
// DeliveryState strings buildGrantViewModels renders; labels are the
|
||||
// badges'.
|
||||
var grantsFacetOptions = []FacetOption{
|
||||
@@ -369,7 +369,7 @@ var grantsFacetOptions = []FacetOption{
|
||||
//
|
||||
// Governed by operator-list-scale: server-side search (org name or product
|
||||
// name), the delivery-state filter, and true-total pagination via
|
||||
// ListGrantsWithDeliveryPage (design D4). FetchPage's load closure ignores
|
||||
// ListGrantsWithDelivery (design D4). FetchPage's load closure ignores
|
||||
// its own limit/offset arguments and instead reads params.Limit()/Offset()
|
||||
// directly — safe because FetchPage always calls load with exactly those
|
||||
// values (or, on the past-the-end clamp retry, with p.Page already reset
|
||||
@@ -382,7 +382,7 @@ func (h *OperatorPartialsHandler) GetGrantsPage(w http.ResponseWriter, r *http.R
|
||||
|
||||
var bodyData GrantsData
|
||||
grants, total, _ := FetchPage(¶ms, func(limit, offset int32) ([]GrantViewModel, int64, error) {
|
||||
bodyData = h.loadGrantsListData(r, params, "", "")
|
||||
bodyData = h.loadGrantsListData(r, params)
|
||||
return bodyData.Grants, bodyData.Nav.Total, nil
|
||||
})
|
||||
bodyData.Grants = grants
|
||||
|
||||
@@ -588,32 +588,20 @@ type ProductViewModel struct {
|
||||
// the template renders a single banner.
|
||||
//
|
||||
// params carries the current search/facet/page state (operator-list-scale).
|
||||
// The query itself is ListGrantsWithDeliveryPage (design D4): a paginated,
|
||||
// searchable, filterable sibling of the shared ListGrantsWithDelivery
|
||||
// query the org-detail composite still uses unpaged. Search matches the
|
||||
// granted-to organization's name in SQL; product-name search cannot join
|
||||
// into the billing schema from the entitlements module, so matchingProductIDs
|
||||
// pre-resolves candidate product IDs in Go and passes them as the query's
|
||||
// product_ids criterion. Delivery-state filtering happens in the query's
|
||||
// HAVING clause on the identical CASE expression ListGrantsWithDelivery
|
||||
// uses, so the SQL filter can never disagree with the Go derivation below.
|
||||
//
|
||||
// The rows are adapted (toDeliveryRows) to the shape buildGrantViewModels
|
||||
// already consumes so the paged query and ListGrantsWithDelivery render
|
||||
// through the exact same view-model derivation (design decision 1, "one
|
||||
// query, two consumers" — now "one derivation, two queries").
|
||||
func (h *OperatorPartialsHandler) loadGrantsListData(r *http.Request, params ListParams, success string, errMsg string) GrantsData {
|
||||
data := GrantsData{
|
||||
Success: success,
|
||||
Error: errMsg,
|
||||
}
|
||||
if errMsg != "" {
|
||||
return data
|
||||
}
|
||||
// One query, ListGrantsWithDelivery, serves this page and the org-detail
|
||||
// ledger; here it runs with no organization and with the page's search,
|
||||
// state and paging. Search matches the granted-to organization's name in
|
||||
// SQL. Product names are matched here (matchingProductIDs) and passed as
|
||||
// product ids, because an ILIKE in the query would make % and _ in the
|
||||
// search match any text in product names too (the organization match
|
||||
// already does). The query filters on the state it computes once, so the
|
||||
// filter cannot disagree with the Go derivation below.
|
||||
func (h *OperatorPartialsHandler) loadGrantsListData(r *http.Request, params ListParams) GrantsData {
|
||||
data := GrantsData{}
|
||||
|
||||
queryParams := entitlements.ListGrantsWithDeliveryPageParams{
|
||||
PageLimit: params.Limit(),
|
||||
PageOffset: params.Offset(),
|
||||
queryParams := entitlements.ListGrantsWithDeliveryParams{
|
||||
PageLimit: sql.NullInt32{Int32: params.Limit(), Valid: true},
|
||||
PageOffset: sql.NullInt32{Int32: params.Offset(), Valid: true},
|
||||
}
|
||||
if params.Q != "" {
|
||||
queryParams.Q = sql.NullString{String: params.Q, Valid: true}
|
||||
@@ -623,7 +611,7 @@ func (h *OperatorPartialsHandler) loadGrantsListData(r *http.Request, params Lis
|
||||
queryParams.DeliveryState = sql.NullString{String: params.Facet, Valid: true}
|
||||
}
|
||||
|
||||
rows, err := h.EntitlementsQ.ListGrantsWithDeliveryPage(r.Context(), queryParams)
|
||||
rows, err := h.EntitlementsQ.ListGrantsWithDelivery(r.Context(), queryParams)
|
||||
if err != nil {
|
||||
h.Logger.Error("failed to list grants", slog.Any("error", err))
|
||||
data.Error = "Failed to load grants"
|
||||
@@ -633,19 +621,17 @@ func (h *OperatorPartialsHandler) loadGrantsListData(r *http.Request, params Lis
|
||||
if len(rows) > 0 {
|
||||
data.Nav.Total = rows[0].TotalCount
|
||||
}
|
||||
data.Grants = h.buildGrantViewModels(r, toDeliveryRows(rows), true)
|
||||
data.Grants = h.buildGrantViewModels(r, rows, true)
|
||||
return data
|
||||
}
|
||||
|
||||
// matchingProductIDs resolves the product IDs whose name case-insensitively
|
||||
// contains q. The grants module (internal/entitlements) cannot query the
|
||||
// billing schema directly, so ListGrantsWithDeliveryPage's product-name
|
||||
// search criterion rides on IDs resolved here rather than a SQL join into
|
||||
// core.products. ListAllProducts (not just published/active products) is
|
||||
// used because a grant issued while its product was published must stay
|
||||
// searchable by name after the product is later unpublished. Returns nil
|
||||
// (not an error) on a query failure — search degrades to org-name-only
|
||||
// rather than failing the whole page.
|
||||
// contains q. ListGrantsWithDelivery's search takes the ids, so % and _ in q
|
||||
// match themselves in product names. ListAllProducts (not just
|
||||
// published/active products) is used because a grant issued while its
|
||||
// product was published must stay searchable by name after the product is
|
||||
// later unpublished. Returns nil (not an error) on a query failure — search
|
||||
// degrades to org-name-only rather than failing the whole page.
|
||||
func (h *OperatorPartialsHandler) matchingProductIDs(r *http.Request, q string) []string {
|
||||
products, err := h.BillingQ.ListAllProducts(r.Context())
|
||||
if err != nil {
|
||||
@@ -662,45 +648,6 @@ func (h *OperatorPartialsHandler) matchingProductIDs(r *http.Request, q string)
|
||||
return ids
|
||||
}
|
||||
|
||||
// toDeliveryRows adapts ListGrantsWithDeliveryPage's rows (which carry the
|
||||
// extra TotalCount window-function column) to entitlements.ListGrantsWithDeliveryRow,
|
||||
// the shape buildGrantViewModels already consumes — the two queries share
|
||||
// every column except TotalCount, by construction (design D4: the paged
|
||||
// query must never let its derivation drift from the shared one).
|
||||
func toDeliveryRows(rows []entitlements.ListGrantsWithDeliveryPageRow) []entitlements.ListGrantsWithDeliveryRow {
|
||||
out := make([]entitlements.ListGrantsWithDeliveryRow, len(rows))
|
||||
for i, row := range rows {
|
||||
out[i] = entitlements.ListGrantsWithDeliveryRow{
|
||||
GrantID: row.GrantID,
|
||||
GrantedToOrgID: row.GrantedToOrgID,
|
||||
ProductID: row.ProductID,
|
||||
GrantReason: row.GrantReason,
|
||||
Description: row.Description,
|
||||
Quantity: row.Quantity,
|
||||
GrantStatus: row.GrantStatus,
|
||||
CreatedAt: row.CreatedAt,
|
||||
ExtendsGrantID: row.ExtendsGrantID,
|
||||
GrantedByPersonID: row.GrantedByPersonID,
|
||||
ResumesAfterSupersession: row.ResumesAfterSupersession,
|
||||
DeliveryState: row.DeliveryState,
|
||||
ReplacedByGrantID: row.ReplacedByGrantID,
|
||||
ActivatedAt: row.ActivatedAt,
|
||||
EndedAt: row.EndedAt,
|
||||
ValidUntil: row.ValidUntil,
|
||||
HolderProductName: row.HolderProductName,
|
||||
WaitsOn: row.WaitsOn,
|
||||
BlockingProductNames: row.BlockingProductNames,
|
||||
ManageOffered: row.ManageOffered,
|
||||
WillNotResume: row.WillNotResume,
|
||||
ResumingProductNames: row.ResumingProductNames,
|
||||
FloorAfterRevocation: row.FloorAfterRevocation,
|
||||
DuplicatesDefault: row.DuplicatesDefault,
|
||||
RestoredAfterProductNames: row.RestoredAfterProductNames,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// buildGrantViewModels converts rows from the shared ListGrantsWithDelivery
|
||||
// query into GrantViewModels, resolving organization/product names and the
|
||||
// supersession-lineage labels (ReplacedBy / Extends). Shared by the grants
|
||||
@@ -715,12 +662,11 @@ func toDeliveryRows(rows []entitlements.ListGrantsWithDeliveryPageRow) []entitle
|
||||
// replacing/extended grant is almost always present in the same rows
|
||||
// already loaded for this surface.
|
||||
//
|
||||
// granted_by_person_id rides on the ListGrantsWithDelivery /
|
||||
// ListGrantsWithDeliveryPage rows (acceptance-fixes, 2026-09-02). Names
|
||||
// then resolve from the distinct set of granting-person IDs, one lookup
|
||||
// per distinct person rather than one per row -- the same dedup-then-map
|
||||
// shape loadRecentActivity (operator.go) uses for the landing activity
|
||||
// feed's actor names.
|
||||
// granted_by_person_id rides on the ListGrantsWithDelivery rows
|
||||
// (acceptance-fixes, 2026-09-02). Names then resolve from the distinct set of
|
||||
// granting-person IDs, one lookup per distinct person rather than one per
|
||||
// row -- the same dedup-then-map shape loadRecentActivity (operator.go) uses
|
||||
// for the landing activity feed's actor names.
|
||||
func (h *OperatorPartialsHandler) buildGrantViewModels(r *http.Request, rows []entitlements.ListGrantsWithDeliveryRow, resolveOrgName bool) []GrantViewModel {
|
||||
byID := make(map[string]entitlements.ListGrantsWithDeliveryRow, len(rows))
|
||||
for _, g := range rows {
|
||||
|
||||
Reference in New Issue
Block a user