// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo // Package usage converges the `fedwiki_sites` reservation counter on the // site rows FedWiki owns (design D2 of slice3-walk-fixes). // // It sits below both callers -- the web handlers and the Temporal activities, // which cannot import each other -- and below the integration root, which runs // the boot repair. package usage import ( "context" "database/sql" "errors" "git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements" fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store" ) // ResourceKey is the entitlements resource key sites are counted under. const ResourceKey = "fedwiki_sites" // ActiveSites counts a workspace's active site rows: the display truth for // every quota surface (design D1). func ActiveSites(ctx context.Context, siteQ fwmod.Querier, workspaceID string) (int64, error) { return siteQ.CountActiveSitesByWorkspace(ctx, workspaceID) } // ReconcileWorkspace raises the workspace's counter to its active site row // count, never lowering it: between a create workflow's gated increment and // its row insert the counter legitimately exceeds the rows, and lowering there // would erase that reservation. Real deletions are lowered by the decrement // and status-drift paths, and by the exact set at boot. // // Returns the observed active row count. A workspace with no primary pool // assignment has no counter to converge on and is not an error. func ReconcileWorkspace(ctx context.Context, entQ entitlements.Querier, siteQ fwmod.Querier, workspaceID string) (int64, error) { observed, poolID, err := observe(ctx, entQ, siteQ, workspaceID) if err != nil || poolID == "" { return observed, err } _, err = entQ.RaiseNumericUsageTo(ctx, entitlements.RaiseNumericUsageToParams{ PoolID: poolID, ResourceKey: ResourceKey, Observed: observed, }) return observed, err } // SetWorkspaceExact sets the workspace's counter to its active site row count, // for boot reconciliation only: no create workflow holds an unmaterialized // reservation then, so the rows are the whole truth and an overstated counter // (which refuses creates) comes back down. // // Reports whether a counter row was updated; a workspace whose pool has no // usage row for the key has nothing to repair (the materializer creates the // row when the entitlement is materialized). func SetWorkspaceExact(ctx context.Context, entQ entitlements.Querier, siteQ fwmod.Querier, workspaceID string) (observed int64, updated bool, err error) { observed, poolID, err := observe(ctx, entQ, siteQ, workspaceID) if err != nil || poolID == "" { return observed, false, err } res, err := entQ.SetNumericUsage(ctx, entitlements.SetNumericUsageParams{ PoolID: poolID, ResourceKey: ResourceKey, Observed: observed, }) if err != nil { return observed, false, err } rows, err := res.RowsAffected() if err != nil { return observed, false, err } return observed, rows > 0, nil } // observe resolves the workspace's primary pool (the resolution every quota // read uses) and counts its active site rows. An absent assignment yields an // empty pool id rather than an error: nothing to reconcile. func observe(ctx context.Context, entQ entitlements.Querier, siteQ fwmod.Querier, workspaceID string) (int64, string, error) { assignment, err := entQ.GetPrimaryPoolAssignmentByWorkspace(ctx, workspaceID) if errors.Is(err, sql.ErrNoRows) { return 0, "", nil } if err != nil { return 0, "", err } observed, err := siteQ.CountActiveSitesByWorkspace(ctx, workspaceID) if err != nil { return 0, "", err } return observed, assignment.PoolID, nil }