Files
member-console/internal/provisioning/workspace.go
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

97 lines
4.0 KiB
Go

package provisioning
import (
"context"
"database/sql"
"errors"
"fmt"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"git.coopcloud.tech/wiki-cafe/member-console/internal/organization"
)
// DefaultPoolKey is the key the console writes on every organization's
// default resource pool (entity-keys §4: the system writes a key only as a
// fixed constant for a row it creates by design). It is scoped to the
// organization by uq_resource_pools_org_id_key, so every organization has its
// own `default`.
const DefaultPoolKey = "default"
// WorkspaceCreationResult carries the records
// CreateWorkspaceWithPrimaryAssignment creates: the workspace and its primary
// assignment to the organization's default resource pool.
type WorkspaceCreationResult struct {
Workspace organization.Workspace
Pool entitlements.ResourcePool
PoolAssignment entitlements.PoolAssignment
}
// CreateWorkspaceWithPrimaryAssignment creates a workspace for orgID and gives
// it a primary assignment to the organization's default resource pool, all on
// the caller's transaction so the workspace insert, the default-pool
// resolution, and the assignment insert commit or roll back together. Both
// first-login auto-provisioning (AutoProvision) and the member "create
// workspace" handler call this, so the rule "a workspace never exists without
// a pool assignment" has exactly one implementation instead of two
// hand-synchronized copies (schema-hardening design D2).
//
// The organization's default pool is resolved by lookup; if none exists yet
// (a brand-new organization, as at signup) one is created here. An
// organization that already has a default pool (the ordinary
// member-created-workspace case) gets that pool, never a second one.
//
// The name is the only thing this function is given, and nothing is derived
// from it. The workspace gets no key: a key is a declarative address a caller
// outside the database supplies, and deriving one from a display name is the
// defect the retired derived-identifier column had (entity-keys §4). A name that duplicates
// a live workspace in the same organization is refused by
// uq_workspaces_org_id_name_ci, and the caller renders that as a field-level
// message rather than silently picking another name.
//
// The default pool, by contrast, does get a key: `default` is a fixed
// constant for the one pool the console creates for every organization by
// design, so a seed, a script, or an API client can name it without knowing
// the organization's generated pool ID. uq_resource_pools_one_default_per_org
// already guarantees at most one per organization, so the constant cannot
// collide with a second pool.
func CreateWorkspaceWithPrimaryAssignment(ctx context.Context, tx *sql.Tx, orgID, name string) (WorkspaceCreationResult, error) {
orgQ := organization.New(tx)
entQ := entitlements.New(tx)
workspace, err := orgQ.CreateWorkspace(ctx, organization.CreateWorkspaceParams{
OrgID: orgID,
Name: name,
})
if err != nil {
return WorkspaceCreationResult{}, fmt.Errorf("create workspace: %w", err)
}
pool, err := entQ.GetDefaultPoolByOrgID(ctx, orgID)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return WorkspaceCreationResult{}, fmt.Errorf("resolve default pool for org %s: %w", orgID, err)
}
pool, err = entQ.CreateResourcePool(ctx, entitlements.CreateResourcePoolParams{
OrgID: orgID,
Name: "Default",
PoolType: "default",
IsAutoManaged: true,
Key: sql.NullString{String: DefaultPoolKey, Valid: true},
})
if err != nil {
return WorkspaceCreationResult{}, fmt.Errorf("create default pool: %w", err)
}
}
assignment, err := entQ.CreatePoolAssignment(ctx, entitlements.CreatePoolAssignmentParams{
PoolID: pool.PoolID,
WorkspaceID: workspace.WorkspaceID,
IsPrimary: true,
})
if err != nil {
return WorkspaceCreationResult{}, fmt.Errorf("create pool assignment: %w", err)
}
return WorkspaceCreationResult{Workspace: workspace, Pool: pool, PoolAssignment: assignment}, nil
}