From 88f24d84a11531dca8bddced79b46e2644334b36 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sat, 26 Sep 2026 15:48:18 +0900 Subject: [PATCH] refactor(core): keep plan prices, cloud quotas and public URLs in one contract (WS-A) Prices, quotas and site URLs were copied by hand into the edge functions, admin, desktop and the landing site, and the copies disagreed (Payple billed 9,900/29,900 KRW, admin labels said 12,900/24,900 KRW and $9.9/$19.9, the site said 2,900/8,900 KRW). - packages/core/src/plan-catalog.ts is the single source for PLAN_PRICE_KRW (Free 0 / Pro 2,900 / Pro+ 8,900 a month) and PLAN_QUOTA. - packages/core/src/web-urls.ts is the single source for the public origin, the /app web-app base path, SITE_URLS and billingUrl(). - Deno cannot bundle packages/core, so scripts/ci/sync-core-contract.mjs generates _shared/core-contract.generated.ts; `npm run contract:check` fails on drift (same pattern as version:sync). - Payple checkout, renewal and webhook amount checks now bill the catalog price, so existing subscribers move to the new price at their next renewal. quota.ts, team-contract.ts and the tests read the generated values. - Admin MRR/ARR is computed in KRW from the catalog; license labels, the release link and desktop PREMIUM_LLM limits derive from core; the site imports prices and quotas directly. Policy: docs/REFACTOR_POLICY.md Wave 3, W3-1 and W3-2. --- apps/admin/src/app/(admin)/page.tsx | 7 +- apps/admin/src/app/(admin)/releases/page.tsx | 3 +- .../src/components/license-issuer-dialog.tsx | 9 +- apps/admin/src/lib/subscription-metrics.ts | 24 +-- .../src/main/services/LicenseService.ts | 27 +-- package.json | 2 + packages/core/package.json | 8 + packages/core/src/constants.ts | 50 +++--- packages/core/src/index.ts | 2 + packages/core/src/plan-catalog.ts | 82 +++++++++ packages/core/src/web-urls.ts | 43 +++++ scripts/ci/sync-core-contract.mjs | 170 ++++++++++++++++++ .../functions/_shared/billing-catalog.test.ts | 8 +- .../_shared/core-contract.generated.ts | 134 ++++++++++++++ .../supabase/functions/_shared/payple.test.ts | 14 +- server/supabase/functions/_shared/payple.ts | 7 +- server/supabase/functions/_shared/quota.ts | 65 ++----- .../functions/_shared/team-contract.test.ts | 9 +- .../functions/_shared/team-contract.ts | 5 +- site/src/pricing.ts | 41 ++--- site/src/sections/Pricing.tsx | 10 +- 21 files changed, 568 insertions(+), 152 deletions(-) create mode 100644 packages/core/src/plan-catalog.ts create mode 100644 packages/core/src/web-urls.ts create mode 100644 scripts/ci/sync-core-contract.mjs create mode 100644 server/supabase/functions/_shared/core-contract.generated.ts diff --git a/apps/admin/src/app/(admin)/page.tsx b/apps/admin/src/app/(admin)/page.tsx index 00df8c2..03070b5 100644 --- a/apps/admin/src/app/(admin)/page.tsx +++ b/apps/admin/src/app/(admin)/page.tsx @@ -22,11 +22,12 @@ export default async function AdminOverviewPage(): Promise { } } + const krw = new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW', maximumFractionDigits: 0 }) const revenueCards = revenue ? [ { title: 'Annual Recurring Revenue', - value: `$${revenue.arrUsd.toLocaleString()}`, + value: krw.format(revenue.arrKrw), subtext: 'Active subscriptions × 12 months', color: 'green' as const, badge: 'ARR', @@ -34,7 +35,7 @@ export default async function AdminOverviewPage(): Promise { }, { title: 'Monthly Recurring Revenue', - value: `$${revenue.mrrUsd.toLocaleString()}`, + value: krw.format(revenue.mrrKrw), subtext: `Pro ${revenue.tierBreakdown.pro.toLocaleString()} · Pro+ ${revenue.tierBreakdown.pro_plus.toLocaleString()}`, color: 'blue' as const, badge: 'MRR', @@ -208,7 +209,7 @@ export default async function AdminOverviewPage(): Promise { > - $ + ₩ {card.badge} diff --git a/apps/admin/src/app/(admin)/releases/page.tsx b/apps/admin/src/app/(admin)/releases/page.tsx index f377684..500d4bb 100644 --- a/apps/admin/src/app/(admin)/releases/page.tsx +++ b/apps/admin/src/app/(admin)/releases/page.tsx @@ -3,6 +3,7 @@ import { Box, Typography, Button } from '@mui/material' import { DoubleBezelCard, TactileBadge } from '@d3ro/ui/components/ds' +import { SITE_URLS } from '@d3ro/core/web-urls' import { C, FONT_SANS, FONT_MONO, panelSx, tableSx, statusBadgeSx } from '@/lib/console-theme' import { requireManager } from '@/lib/admin-guard' import { fetchReleaseHub, type ReleaseAssetPlatform, type ReleaseHub } from '@/lib/forgejo-releases' @@ -107,7 +108,7 @@ function HeaderBar({ feedLive, repoHtmlUrl }: { feedLive: boolean; repoHtmlUrl: Forgejo Releases ↗