Files
member-console/internal/workflows/entitlements/activities.go
T
cgalo5758 1a19ebe971 Implement uniform conferral semantics
Replace product-kind branching and direct position writes with enclosed
database functions driven by structural product shape.

Migrate grant and provision data, unify operator issuance, update
subscription and expiry flows, and add migration and integration proofs.
2026-07-11 13:05:53 -05:00

120 lines
4.4 KiB
Go

package entitlements
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"github.com/google/uuid"
)
// Activities holds dependencies for entitlement-lifecycle workflows.
type Activities struct {
DB *sql.DB
Logger *slog.Logger
}
// NewActivities constructs an Activities ready for registration.
func NewActivities(db *sql.DB, logger *slog.Logger) *Activities {
return &Activities{DB: db, Logger: logger}
}
// ExpireGrantInput identifies the grant whose pools should transition off it.
type ExpireGrantInput struct {
GrantID string
}
// ExpireGrantOutput reports what the activity did. TransitionsRecorded is the
// number of positions ended by EndConferral for this grant source; Skipped is
// true when the grant was no longer active (already revoked/expired or absent),
// indicating the activity was a no-op.
type ExpireGrantOutput struct {
TransitionsRecorded int
Skipped bool
}
// ExpireGrantActivity decrees the grant expired (status active -> expired),
// ends every position conferred by this grant source via EndConferral, then
// re-materializes the org's default pool so entitlements settle to the default
// (or detach when no default is configured).
//
// The activity is branch-free and idempotent: ExpireGrant returns
// sql.ErrNoRows when the grant is already non-active (Skipped=true, a no-op),
// and EndConferral treats a source that resolves to nothing live as success
// (empty slice), so re-running against an already-ended grant records zero
// transitions rather than erroring.
func (a *Activities) ExpireGrantActivity(ctx context.Context, input ExpireGrantInput) (ExpireGrantOutput, error) {
if input.GrantID == "" {
return ExpireGrantOutput{}, fmt.Errorf("grant_id is required")
}
tx, err := a.DB.BeginTx(ctx, nil)
if err != nil {
return ExpireGrantOutput{}, fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()
q := entitlements.New(tx)
// Decree: flip status active -> expired. sql.ErrNoRows means the grant was
// not active (already revoked/expired or absent) — a no-op.
grant, err := q.ExpireGrant(ctx, input.GrantID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
a.Logger.Info("ExpireGrantActivity: grant not active, skipping",
slog.String("grant_id", input.GrantID))
return ExpireGrantOutput{Skipped: true}, nil
}
return ExpireGrantOutput{}, fmt.Errorf("expire grant %s: %w", input.GrantID, err)
}
// Resolve the org's default pool for re-materialization. A grant with no
// granted-to org has no pool to settle, so skip materialization for it.
var pool entitlements.ResourcePool
havePool := false
if grant.GrantedToOrgID.Valid {
pool, err = q.GetDefaultPoolByOrgID(ctx, grant.GrantedToOrgID.UUID.String())
if err != nil {
return ExpireGrantOutput{}, fmt.Errorf("default pool for org %s: %w", grant.GrantedToOrgID.UUID, err)
}
havePool = true
}
// End every position conferred by this grant source. Nothing live resolves
// to an empty slice (success), keeping the activity idempotent on re-run.
ended, err := q.EndConferral(ctx, entitlements.EndConferralParams{
GrantID: uuid.NullUUID{UUID: uuid.MustParse(input.GrantID), Valid: true},
ActorType: "system",
Reason: sql.NullString{String: fmt.Sprintf("grant-expiration:%s", input.GrantID), Valid: true},
})
if err != nil {
return ExpireGrantOutput{}, fmt.Errorf("end conferral for grant %s: %w", input.GrantID, err)
}
if havePool {
// Restore the org-type baseline when expiry left the default ladder
// vacant (guarded: a position held by another source is never
// superseded by the restoration).
if _, err := entitlements.ReapplyDefaultsIfVacant(ctx, tx, pool.PoolID, entitlements.Actor{
ActorType: "system",
Reason: fmt.Sprintf("grant-expiration:%s default restoration", input.GrantID),
}); err != nil {
return ExpireGrantOutput{}, fmt.Errorf("restore default for pool %s: %w", pool.PoolID, err)
}
if err := entitlements.MaterializePoolEntitlements(ctx, q, pool.PoolID); err != nil {
return ExpireGrantOutput{}, fmt.Errorf("materialize pool %s: %w", pool.PoolID, err)
}
}
if err := tx.Commit(); err != nil {
return ExpireGrantOutput{}, fmt.Errorf("commit: %w", err)
}
// Skipped stays false: the grant was active when this activity ran. A
// zero count is a legitimate idempotent re-run against nothing live.
return ExpireGrantOutput{TransitionsRecorded: len(ended)}, nil
}