Enforce 10j's verified gaps (schema-hardening change): - Migration 00010: partial unique indexes for one default pool and one primary assignment per workspace, plus CHECKs pinning pool/provider/subscription vocabularies and provider lifecycle timestamps. - Workspace creation shares a transactional provisioning function; extension validates its target pool; last-tier deletion of a defaulted ladder is guarded; signup completes plan-less on a broken ladder. - Boot asserts integration slug parity and validates declared config enums; Stripe invoice amounts are range-checked; domain cancellation runs a final evidence probe; rule authoring is additive-only.
76 lines
2.7 KiB
Go
76 lines
2.7 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"
|
|
)
|
|
|
|
// 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.
|
|
func CreateWorkspaceWithPrimaryAssignment(ctx context.Context, tx *sql.Tx, orgID, name, slug string) (WorkspaceCreationResult, error) {
|
|
orgQ := organization.New(tx)
|
|
entQ := entitlements.New(tx)
|
|
|
|
workspace, err := orgQ.CreateWorkspace(ctx, organization.CreateWorkspaceParams{
|
|
OrgID: orgID,
|
|
Name: name,
|
|
Slug: slug,
|
|
})
|
|
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",
|
|
Slug: "default",
|
|
PoolType: "default",
|
|
IsAutoManaged: 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
|
|
}
|