TorbatYar/frontend/modules/commercial/adapters/workspace.ts
Mortezakoohjani 0d424c500a feat(platform): complete commercial runtime consolidation
Unify commercial runtime ownership across backend and frontend so platform, experience, and hospitality modules use the shared commercial source of truth.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 11:11:53 +03:30

197 lines
7.0 KiB
TypeScript

import { api } from "@/lib/api";
import type {
AdapterResult,
CommercialProduct,
CommercialWorkspaceSnapshot,
SetupChecklistItem,
TrialState,
} from "../types";
import { resolveProductLaunchUrl } from "../types";
import { evaluateDomainPolicy } from "./policies";
import { loadCommercialUsage } from "./usage";
import { commercialGet, commercialPost } from "./http";
function computeTrial(sub: {
status?: string | null;
trial_ends_at?: string | null;
} | null): TrialState | null {
if (!sub?.status) return null;
const active = sub.status === "trialing";
let days_remaining: number | null = null;
if (sub.trial_ends_at) {
const end = new Date(sub.trial_ends_at).getTime();
days_remaining = Math.max(0, Math.ceil((end - Date.now()) / 86400000));
}
return { active, days_remaining, ends_at: sub.trial_ends_at ?? null, quotas: [] };
}
/**
* Entitlement check — Commercial Runtime only. Safe deny when unavailable.
*/
export async function checkFeatureAccess(
tenantId: string,
featureKey: string
): Promise<import("../types").EntitlementCheckResult> {
const commercial = await commercialPost<{
has_access?: boolean;
allowed?: boolean;
reason?: string | null;
required_plan?: string | null;
required_capability?: string | null;
required_bundle?: string | null;
}>("/api/v1/commercial/entitlements/check", {
tenant_id: tenantId,
feature_key: featureKey,
capability_code: featureKey,
});
if (commercial.ok) {
const allowed = Boolean(commercial.data.has_access ?? commercial.data.allowed);
return {
has_access: allowed,
reason: commercial.data.reason ?? null,
required_plan: commercial.data.required_plan ?? null,
required_capability: commercial.data.required_capability ?? null,
required_bundle: commercial.data.required_bundle ?? null,
};
}
return {
has_access: false,
reason: commercial.message || "بررسی entitlement در دسترس نیست",
unavailable: true,
};
}
export async function loadWorkspaceSnapshot(): Promise<
AdapterResult<CommercialWorkspaceSnapshot | null>
> {
try {
const tenantCtx = await api.tenantContext.current();
const tenantId = tenantCtx.tenant.id;
const dash = await commercialGet<{
subscription?: {
status?: string | null;
plan_code?: string | null;
bundle_code?: string | null;
trial_ends_at?: string | null;
} | null;
pinned_product_codes?: string[];
recent_product_codes?: string[];
quick_actions?: CommercialWorkspaceSnapshot["quick_actions"];
locked_features?: CommercialWorkspaceSnapshot["locked_features"];
workspace_health?: string | null;
checklist_completion_percent?: number | null;
product_codes?: string[];
capability_codes?: string[];
bundle_code?: string | null;
}>(`/api/v1/commercial/dashboard?tenant_id=${encodeURIComponent(tenantId)}`);
const commercialSub = dash.ok ? dash.data.subscription ?? null : null;
const status =
commercialSub?.status ?? tenantCtx.subscription_status ?? null;
const trial = computeTrial(commercialSub);
const usage = await loadCommercialUsage(tenantId);
if (trial && usage.availability === "ready") {
trial.quotas = usage.data;
}
const domainEval = await evaluateDomainPolicy({
tenant_id: tenantId,
subscription_status: status,
});
const pinned = dash.ok ? dash.data.pinned_product_codes ?? [] : [];
const recent = dash.ok ? dash.data.recent_product_codes ?? [] : [];
const quick = dash.ok ? dash.data.quick_actions ?? [] : [];
const domainData = domainEval.data;
const params = domainData.parameters || {};
const snapshot: CommercialWorkspaceSnapshot = {
tenant_id: tenantId,
tenant_name: tenantCtx.tenant.name,
slug: tenantCtx.tenant.slug,
status: tenantCtx.tenant.status,
plan_name: commercialSub?.plan_code ?? tenantCtx.plan_name,
subscription_status: status,
onboarding_completed: tenantCtx.tenant.onboarding_completed,
primary_domain: tenantCtx.primary_domain,
domains: tenantCtx.domains.map((d) => ({
id: d.id,
domain: d.domain,
domain_type: d.domain_type,
is_verified: d.is_verified,
})),
role: tenantCtx.role,
trial,
custom_domain_unlocked: Boolean(domainData.custom_domain_allowed),
domain_policy: {
...domainData,
ssl_ready:
domainData.ssl_ready ??
(typeof params.ssl_ready === "boolean" ? params.ssl_ready : undefined),
dns_instructions:
domainData.dns_instructions ??
(Array.isArray(params.dns_instructions)
? (params.dns_instructions as NonNullable<
CommercialWorkspaceSnapshot["domain_policy"]
>["dns_instructions"])
: undefined),
verification_state:
domainData.verification_state ??
(typeof params.verification_state === "string" ? params.verification_state : null),
},
pinned_product_codes: pinned,
recent_product_codes: recent,
quick_actions: quick,
product_codes: dash.ok ? dash.data.product_codes ?? [] : [],
capability_codes: dash.ok ? dash.data.capability_codes ?? [] : [],
bundle_code: dash.ok
? dash.data.bundle_code ?? commercialSub?.bundle_code ?? null
: commercialSub?.bundle_code ?? null,
locked_features: dash.ok ? dash.data.locked_features ?? [] : [],
workspace_health: dash.ok ? dash.data.workspace_health ?? null : null,
checklist_completion_percent: dash.ok
? dash.data.checklist_completion_percent ?? null
: null,
};
return { availability: "ready", data: snapshot, source: "commercial.runtime" };
} catch (err) {
return {
availability: "error",
data: null,
message: err instanceof Error ? err.message : "خطا در بارگذاری workspace",
source: "none",
};
}
}
/** Checklist from discovered products' launch metadata — no hardcoded product list. */
export function buildSetupChecklist(
products: CommercialProduct[],
opts?: { installedCodes?: string[] }
): SetupChecklistItem[] {
const installed = new Set(opts?.installedCodes ?? []);
const fromProducts = products
.filter((p) => p.visibility !== "hidden")
.map((p) => ({
id: `product:${p.product_code}`,
label: p.display_name,
href: resolveProductLaunchUrl(p),
product_code: p.product_code,
done: installed.has(p.product_code),
required: false,
}));
const workspaceActions =
products.length === 0
? [
{ id: "workspace", label: "Workspace", href: "/dashboard", required: true },
{ id: "billing", label: "Billing", href: "/billing", required: false },
]
: [];
return [...workspaceActions, ...fromProducts];
}