109 lines
4.3 KiB
TypeScript
109 lines
4.3 KiB
TypeScript
// The enablement flow: one idempotent procedure pair an organization administrator runs, with the
|
|
// platform's checks in front of any configuration change and every denial a typed reason code the
|
|
// UI maps to an upgrade or policy state.
|
|
//
|
|
// enableDocumentAi: already enabled -> answers the current configuration, writes nothing.
|
|
// Otherwise: eligibility (plan capability + rollout flags) -> quota -> one transaction persists
|
|
// the configuration. Nothing is provisioned: retrieval is the platform's service (ADR-061), and an
|
|
// app keeps no vector store. A failure at any step writes nothing and is retirable: the same call
|
|
// again resumes from the first failing check.
|
|
//
|
|
// disableDocumentAi: preserves everything — collections, documents, the service's index and the
|
|
// recorded configuration stay; only `enabled` flips, so re-enabling resumes without duplication.
|
|
|
|
import { AppError } from "@conusai-kit/core"
|
|
import { config, type EnablementReason, type KnowledgeBackend } from "../contract/index.js"
|
|
import type { DocumentAiPlatform, KnowledgeOrgScope } from "./ports.js"
|
|
|
|
/** A typed enablement denial: the problem's code is the stable reason the UI maps to a state. */
|
|
export function enablementDenied(reason: EnablementReason, detail?: string): AppError {
|
|
const status = reason === "quota_exhausted" ? 429 : 403
|
|
const title =
|
|
reason === "plan_required"
|
|
? "The organization's plan does not include document AI"
|
|
: reason === "feature_disabled"
|
|
? "Document AI is disabled by policy"
|
|
: "The indexing quota is exhausted"
|
|
return new AppError({
|
|
type: `/problems/${reason}`,
|
|
title,
|
|
status,
|
|
code: reason,
|
|
...(detail === undefined ? {} : { detail }),
|
|
})
|
|
}
|
|
|
|
export interface SettingsView {
|
|
enabled: boolean
|
|
/** Who serves this organization once it has a configuration: only the platform does. */
|
|
backend: KnowledgeBackend | null
|
|
}
|
|
|
|
async function currentConfig(deps: { data: import("@conusai-kit/data").DataSource }, scope: KnowledgeOrgScope) {
|
|
const row = (await deps.data.forScope(scope).of(config).list({ limit: 1 })).items[0]
|
|
return row ?? null
|
|
}
|
|
|
|
export async function getSettings(
|
|
deps: { data: import("@conusai-kit/data").DataSource },
|
|
scope: KnowledgeOrgScope,
|
|
): Promise<SettingsView> {
|
|
const row = await currentConfig(deps, scope)
|
|
return { enabled: row?.enabled ?? false, backend: row ? "platform" : null }
|
|
}
|
|
|
|
export interface SettingsDeps {
|
|
data: import("@conusai-kit/data").DataSource
|
|
platform: DocumentAiPlatform
|
|
now?: () => number
|
|
}
|
|
|
|
export async function enableDocumentAi(deps: SettingsDeps, scope: KnowledgeOrgScope): Promise<SettingsView> {
|
|
const existing = await currentConfig(deps, scope)
|
|
if (existing?.enabled) return { enabled: true, backend: "platform" }
|
|
|
|
const eligibility = await deps.platform.checkEligibility(scope)
|
|
if (!eligibility.ok) throw enablementDenied(eligibility.reason, eligibility.detail)
|
|
|
|
const remaining = await deps.platform.indexingQuota(scope)
|
|
if (remaining <= 0) throw enablementDenied("quota_exhausted", "the indexing volume for today is spent")
|
|
|
|
await deps.data.unitOfWork.run(scope, async (tx) => {
|
|
const row = (await tx.of(config).list({ limit: 1 })).items[0]
|
|
const patch = {
|
|
enabled: true,
|
|
backend: "platform" as const,
|
|
source: "managed" as const,
|
|
serviceId: null,
|
|
url: null,
|
|
updatedBy: scope.actor.userId,
|
|
configuredAt: new Date((deps.now ?? Date.now)()).toISOString(),
|
|
}
|
|
if (row) await tx.of(config).update(row.id, patch, row.version)
|
|
else await tx.of(config).insert(patch)
|
|
})
|
|
return { enabled: true, backend: "platform" }
|
|
}
|
|
|
|
export async function disableDocumentAi(
|
|
deps: Pick<SettingsDeps, "data" | "now">,
|
|
scope: KnowledgeOrgScope,
|
|
): Promise<{ enabled: false }> {
|
|
await deps.data.unitOfWork.run(scope, async (tx) => {
|
|
const row = (await tx.of(config).list({ limit: 1 })).items[0]
|
|
if (!row?.enabled) return
|
|
// Everything else is preserved on purpose: the recorded configuration, every collection and
|
|
// document, and the service's index. Disabling fences new work; it deletes nothing.
|
|
await tx.of(config).update(
|
|
row.id,
|
|
{
|
|
enabled: false,
|
|
updatedBy: scope.actor.userId,
|
|
configuredAt: new Date((deps.now ?? Date.now)()).toISOString(),
|
|
},
|
|
row.version,
|
|
)
|
|
})
|
|
return { enabled: false }
|
|
}
|