AkurAI Build
Menu

Public repository / 3a8e394f852804fb6cb8b400b55e2afbf0b6dddd

AkurAI-Platform / backend/catalog.ts

Latest change 3a8e394f852804fb6cb8b400b55e2afbf0b6dddd - feat: wire checkout to shared product catalog, remove Gateway from purchasable (AKURAI-PLATFORM-28) by Ólafur Búi Ólafsson

/**
 * Canonical typed product catalog — single source of truth for the landing page
 * suite tiles and subscription checkout. Replaces backend/subscriptions.json.
 *
 * Every product has a stable id, locale keys for name/kind/description (both
 * en.json and is.json must define the key), a public URL or null, a canonical
 * AkurAI Build repository name, optional bundle membership, and a fulfillment
 * strategy required when purchasable.
 *
 * Validation:
 *  - purchasable products MUST have a non-null fulfillment target
 *  - every nameKey/kindKey/descriptionKey MUST exist in en.json AND is.json
 *  - no duplicate product ids
 *  - dev/hidden products are flagged (not rejected — they are legitimate state)
 */

import { readFileSync, existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

// ── Types ──────────────────────────────────────────────────────────────────

export type ProductVisibility = "visible" | "dev" | "hidden";
export type FulfillmentTarget = "provisioned" | "external" | "manual";

export interface Product {
  /** Stable kebab-case identifier, never changes */
  id: string;
  /** Locale key for display name (e.g. "app.mail.name") */
  nameKey: string;
  /** Locale key for the kind/tagline (e.g. "app.mail.kind") */
  kindKey: string;
  /** Locale key for the value proposition (e.g. "app.mail.valueprop") */
  descriptionKey: string;
  /** Category id matching locale keys (e.g. "communication" → category.communication) */
  category: string;
  /** User-facing visibility */
  visibility: ProductVisibility;
  /** Whether this product can be purchased through subscription */
  purchasable: boolean;
  /** Display name (English, for backward compat with payment article descriptions) */
  name: string;
  /** Available for purchase (backward compat: visibility === "visible" && purchasable) */
  available: boolean;
  /** Public app URL (null if not deployed / no web UI) */
  url: string | null;
  /** Canonical AkurAI Build repository name */
  buildRepo: string;
  /** Product ids included when this one is purchased (bundle) */
  bundle: string[];
  /** How fulfilment is delivered; required when purchasable is true */
  fulfillment: FulfillmentTarget | null;
}

export interface Catalog {
  currency: "EUR";
  monthly_unit_cents: number;
  annual_discount_percent: number;
  products: Product[];
}

// ── The data ───────────────────────────────────────────────────────────────

export const products: Product[] = [
  // ── Communication ──
  {
    id: "mail",
    nameKey: "app.mail.name",
    kindKey: "app.mail.kind",
    descriptionKey: "app.mail.valueprop",
    category: "communication",
    visibility: "visible",
    purchasable: true,
    name: "AkurAI Mail",
    available: true,
    url: "https://mail.olibuijr.com",
    buildRepo: "AkurAI-Mail",
    bundle: [],
    fulfillment: "provisioned",
  },

  // ── Security & privacy ──
  {
    id: "auth",
    nameKey: "app.auth.name",
    kindKey: "app.auth.kind",
    descriptionKey: "app.auth.valueprop",
    category: "security",
    visibility: "visible",
    purchasable: true,
    name: "AkurAI Auth",
    available: true,
    url: "https://auth.olibuijr.com",
    buildRepo: "AkurAI-IDP",
    bundle: [],
    fulfillment: "provisioned",
  },
  {
    id: "vpn",
    nameKey: "app.vpn.name",
    kindKey: "app.vpn.kind",
    descriptionKey: "app.vpn.valueprop",
    category: "security",
    visibility: "visible",
    purchasable: true,
    name: "AkurAI VPN",
    available: true,
    url: "https://akurai-vpn.olibuijr.com",
    buildRepo: "AkurAI-VPN",
    bundle: [],
    fulfillment: "provisioned",
  },
  {
    id: "passvault",
    nameKey: "app.passvault.name",
    kindKey: "app.passvault.kind",
    descriptionKey: "app.passvault.valueprop",
    category: "security",
    visibility: "visible",
    purchasable: true,
    name: "AkurAI PassVault",
    available: true,
    url: "https://akurai-passvault.olibuijr.com",
    buildRepo: "AkurAI-PassVault",
    bundle: [],
    fulfillment: "provisioned",
  },

  // ── Storage ──
  {
    id: "drive",
    nameKey: "app.drive.name",
    kindKey: "app.drive.kind",
    descriptionKey: "app.drive.valueprop",
    category: "storage",
    visibility: "visible",
    purchasable: true,
    name: "AkurAI Drive",
    available: true,
    url: "https://akurai-drive.olibuijr.com",
    buildRepo: "AkurAI-Drive",
    bundle: [],
    fulfillment: "provisioned",
  },

  // ── Knowledge ──
  {
    id: "notes",
    nameKey: "app.notes.name",
    kindKey: "app.notes.kind",
    descriptionKey: "app.notes.valueprop",
    category: "knowledge",
    visibility: "visible",
    purchasable: true,
    name: "AkurAI Notes",
    available: true,
    url: "https://akurai-notes.olibuijr.com",
    buildRepo: "AkurAI-Notes",
    bundle: [],
    fulfillment: "provisioned",
  },

  // ── Business ──
  {
    id: "crm",
    nameKey: "app.crm.name",
    kindKey: "app.crm.kind",
    descriptionKey: "app.crm.valueprop",
    category: "business",
    visibility: "dev",
    purchasable: false,
    name: "AkurAI CRM",
    available: false,
    url: null,
    buildRepo: "AkurAI-CRM",
    bundle: [],
    fulfillment: null,
  },
  {
    id: "tasks",
    nameKey: "app.tasks.name",
    kindKey: "app.tasks.kind",
    descriptionKey: "app.tasks.valueprop",
    category: "business",
    visibility: "visible",
    purchasable: true,
    name: "AkurAI Tasks",
    available: true,
    url: "https://akurai-tasks.olibuijr.com",
    buildRepo: "akurai-tasks",
    bundle: [],
    fulfillment: "provisioned",
  },

  // ── Infrastructure ──
  {
    id: "router",
    nameKey: "app.router.name",
    kindKey: "app.router.kind",
    descriptionKey: "app.router.valueprop",
    category: "infrastructure",
    visibility: "visible",
    purchasable: true,
    name: "AkurAI Router",
    available: true,
    url: "https://akurai-router.olibuijr.com",
    buildRepo: "AkurAI-Router",
    bundle: [],
    fulfillment: "provisioned",
  },
  {
    id: "monitor",
    nameKey: "app.monitor.name",
    kindKey: "app.monitor.kind",
    descriptionKey: "app.monitor.valueprop",
    category: "infrastructure",
    visibility: "visible",
    purchasable: true,
    name: "AkurAI Monitor",
    available: true,
    url: "https://monitor.olibuijr.com",
    buildRepo: "AkurAI-Monitor",
    bundle: [],
    fulfillment: "provisioned",
  },
  {
    id: "dns",
    nameKey: "app.dns.name",
    kindKey: "app.dns.kind",
    descriptionKey: "app.dns.valueprop",
    category: "infrastructure",
    visibility: "visible",
    purchasable: true,
    name: "AkurAI DNS",
    available: true,
    url: null,
    buildRepo: "AkurAI-DNS",
    bundle: [],
    fulfillment: "provisioned",
  },
  {
    id: "build",
    nameKey: "app.build.name",
    kindKey: "app.build.kind",
    descriptionKey: "app.build.valueprop",
    category: "infrastructure",
    visibility: "visible",
    purchasable: true,
    name: "AkurAI Build",
    available: true,
    url: "https://akurai-build.olibuijr.com",
    buildRepo: "AkurAI-Build",
    bundle: [],
    fulfillment: "provisioned",
  },

  // ── Developer ──
  {
    id: "framework",
    nameKey: "app.framework.name",
    kindKey: "app.framework.kind",
    descriptionKey: "app.framework.valueprop",
    category: "developer",
    visibility: "visible",
    purchasable: true,
    name: "AkurAI Framework",
    available: true,
    url: "https://akurai-framework.olibuijr.com",
    buildRepo: "AkurAI-Framework",
    bundle: [],
    fulfillment: "provisioned",
  },
  {
    id: "platform",
    nameKey: "app.platform.name",
    kindKey: "app.platform.kind",
    descriptionKey: "app.platform.valueprop",
    category: "developer",
    visibility: "visible",
    purchasable: true,
    name: "AkurAI Platform",
    available: true,
    url: "https://platform.olibuijr.com",
    buildRepo: "AkurAI-Platform",
    bundle: [],
    fulfillment: "provisioned",
  },
  {
    id: "gateway",
    nameKey: "app.gateway.name",
    kindKey: "app.gateway.kind",
    descriptionKey: "app.gateway.valueprop",
    category: "developer",
    visibility: "hidden",
    purchasable: false,
    name: "Agent Gateway",
    available: false,
    url: null,
    buildRepo: "akurai-gateway",
    bundle: [],
    fulfillment: null,
  },
];

export const catalog: Catalog = {
  currency: "EUR",
  monthly_unit_cents: 900,
  annual_discount_percent: 15,
  products,
};

// ── Validation ─────────────────────────────────────────────────────────────

export interface CatalogError {
  productId: string;
  field: string;
  message: string;
}

/**
 * Validate catalog integrity at build time:
 *  1. No duplicate product ids.
 *  2. Purchasable products MUST have a non-null fulfillment target.
 *  3. All locale keys (nameKey, kindKey, descriptionKey) exist in
 *     both en.json and is.json.
 *  4. Dev/hidden products are flagged as informational (not errors).
 */
export function validateCatalogIntegrity(localeDir?: string): CatalogError[] {
  const errors: CatalogError[] = [];

  // Duplicate check
  const ids = new Set<string>();
  for (const p of products) {
    if (ids.has(p.id)) {
      errors.push({ productId: p.id, field: "id", message: `Duplicate product id: ${p.id}` });
    }
    ids.add(p.id);
  }

  // Purchasable must have fulfillment
  for (const p of products) {
    if (p.purchasable && !p.fulfillment) {
      errors.push({
        productId: p.id,
        field: "fulfillment",
        message: `Purchasable product "${p.id}" must have a fulfillment target`,
      });
    }
  }

  // Visibility flags (informational)
  for (const p of products) {
    if (p.visibility === "dev") {
      errors.push({
        productId: p.id,
        field: "visibility",
        message: `Product "${p.id}" is in development — not yet available`,
      });
    }
    if (p.visibility === "hidden") {
      errors.push({
        productId: p.id,
        field: "visibility",
        message: `Product "${p.id}" is hidden from the suite tiles`,
      });
    }
  }

  // Locale key validation
  const dir = localeDir ?? resolve(dirname(fileURLToPath(import.meta.url)), "locales");
  const enPath = resolve(dir, "en.json");
  const isPath = resolve(dir, "is.json");

  if (!existsSync(enPath) || !existsSync(isPath)) {
    errors.push({
      productId: "(syste'm)",
      field: "locale",
      message: `Locale files not found: en.json exists=${existsSync(enPath)}, is.json exists=${existsSync(isPath)}`,
    });
    return errors;
  }

  const enKeys = new Set(Object.keys(JSON.parse(readFileSync(enPath, "utf8"))));
  const isKeys = new Set(Object.keys(JSON.parse(readFileSync(isPath, "utf8"))));

  for (const p of products) {
    if (p.visibility === "hidden") continue; // hidden products have no UI, no locale keys needed
    for (const [field, key] of [["nameKey", p.nameKey], ["kindKey", p.kindKey], ["descriptionKey", p.descriptionKey]] as const) {
      if (!enKeys.has(key)) {
        errors.push({
          productId: p.id,
          field,
          message: `Locale key "${key}" missing from en.json`,
        });
      }
      if (!isKeys.has(key)) {
        errors.push({
          productId: p.id,
          field,
          message: `Locale key "${key}" missing from is.json`,
        });
      }
    }
  }

  return errors;
}

// ── CLI: run on `bun run backend/catalog.ts` ──────────────────────────────
// Prints all errors and exits with code 1 if any are found.
if (import.meta.main) {
  const localeDir = process.argv[2];
  const errors = validateCatalogIntegrity(localeDir);

  if (errors.length === 0) {
    console.log("✓ Product catalog validation: passed");
    process.exit(0);
  }

  let fail = false;
  for (const err of errors) {
    const isInfo = err.field === "visibility";
    console.log(`${isInfo ? "ℹ" : "✗"} [${err.productId}] ${err.message}`);
    if (!isInfo) fail = true;
  }

  if (fail) {
    console.log("\nProduct catalog validation: FAILED — some issues must be fixed");
    process.exit(1);
  } else {
    console.log("\n✓ Product catalog validation: passed (visibility flags are informational)");
    process.exit(0);
  }
}