@fayz-ai/plugin-courses 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AA+B5E,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,MAAM,CAAC,EAAE;QACP,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,OAAO,CAAC,EAAE,MAAM,CAAA;KACjB,CAAA;IACD,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE;QACR,WAAW,CAAC,EAAE,OAAO,CAAA;QACrB,KAAK,CAAC,EAAE,OAAO,CAAA;QACf,aAAa,CAAC,EAAE,OAAO,CAAA;QACvB,SAAS,CAAC,EAAE,OAAO,CAAA;QACnB,OAAO,CAAC,EAAE,OAAO,CAAA;KAClB,CAAA;IACD;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QACP,mFAAmF;QACnF,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,qDAAqD;QACrD,aAAa,CAAC,EAAE,MAAM,CAAA;KACvB,CAAA;IACD;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AA6BD,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,cAAc,CA6GlF;AAOD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAgC5E,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,MAAM,CAAC,EAAE;QACP,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,OAAO,CAAC,EAAE,MAAM,CAAA;KACjB,CAAA;IACD,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE;QACR,WAAW,CAAC,EAAE,OAAO,CAAA;QACrB,KAAK,CAAC,EAAE,OAAO,CAAA;QACf,aAAa,CAAC,EAAE,OAAO,CAAA;QACvB,SAAS,CAAC,EAAE,OAAO,CAAA;QACnB,OAAO,CAAC,EAAE,OAAO,CAAA;KAClB,CAAA;IACD;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QACP,mFAAmF;QACnF,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,qDAAqD;QACrD,aAAa,CAAC,EAAE,MAAM,CAAA;KACvB,CAAA;IACD;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AA6BD,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,cAAc,CAoKlF;AAOD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA"}
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import * as React3 from 'react';
2
- import { hashRouterAdapter, registerPluginFactory, registerTranslations, useTranslation, getSupabaseClientOptional } from '@fayz-ai/core';
3
- import { setCoursesTenantResolver, getCoursesProviderOptional, setCoursesProvider, createMockCoursesProvider, getCoursesProvider } from '@fayz-ai/courses';
2
+ import { useState, useEffect, useMemo } from 'react';
3
+ import { hashRouterAdapter, createConnectionStore, connectionSettings, connectionStatus, settingsStatus, registerPluginFactory, registerTranslations, useTranslation, getSupabaseClientOptional, connectorRuntimeToken, CONNECTOR_RUNTIME_TOKEN_HEADER } from '@fayz-ai/core';
4
+ import { setCoursesTenantResolver, getCoursesProviderOptional, setCoursesProvider, createMockCoursesProvider, getCoursesTenantId, COURSES_TABLES, getCoursesProvider } from '@fayz-ai/courses';
4
5
  import { useOrganizationStore, useLimitGuard, usePermissionOptional, PermissionGate, OrderListView, OrderDetailSkeleton, PersonLink, OrderDetailView, invalidateLimit, PluginSettingsPanel, SettingsGroup, SelectRow, ToggleRow } from '@fayz-ai/admin';
5
- import { defineKpiWidget, useModuleLayout, PageHeaderActions, Button, Badge, useSaveBar, PageHeader, toast, Separator, Tabs, TabsList, TabsTrigger, TabsContent, Input, SegmentedControl, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Checkbox, ConfirmDialog, KpiCard, Skeleton, HeaderTabs, Card, DataTable } from '@fayz-ai/ui';
6
+ import { Button, DatePicker, defineKpiWidget, toast, useModuleLayout, PageHeaderActions, Badge, useSaveBar, PageHeader, Separator, Tabs, TabsList, TabsTrigger, TabsContent, Input, SegmentedControl, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Checkbox, ConfirmDialog, KpiCard, Skeleton, HeaderTabs, Card, DataTable } from '@fayz-ai/ui';
6
7
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
- import { ImagePlus, Users, ShoppingCart, Layers, Tag, Loader2, UploadCloud, Info, Plus, Trash2, Clock, Lock, Check, GripVertical, ChevronDown, Video, FileText, PlayCircle } from 'lucide-react';
8
+ import { Loader2, Plug, Link2, Download, History, CheckCircle2, AlertCircle, ImagePlus, Users, ShoppingCart, Layers, Tag, UploadCloud, Info, Plus, Trash2, Clock, Lock, Check, GripVertical, ChevronDown, Video, FileText, PlayCircle } from 'lucide-react';
8
9
  import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter } from '@dnd-kit/core';
9
10
  import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, arrayMove, useSortable } from '@dnd-kit/sortable';
10
11
  import { CSS } from '@dnd-kit/utilities';
@@ -179,11 +180,6 @@ var en = {
179
180
  "courses.financial.date": "Date",
180
181
  "courses.financial.amount": "Amount",
181
182
  "courses.financial.status": "Status",
182
- "courses.financial.connect": "Connect Stripe",
183
- "courses.financial.connected": "Stripe connected",
184
- "courses.financial.notConnected": "Connect your Stripe account to receive payouts. The platform fee is applied automatically on each sale.",
185
- "courses.financial.platformFee": "Platform fee",
186
- "courses.financial.connectSoon": "Stripe Connect onboarding wires up in Stage 3.",
187
183
  "courses.payoutStatus.paid": "Paid",
188
184
  "courses.payoutStatus.pending": "Pending",
189
185
  "courses.payoutStatus.failed": "Failed",
@@ -386,11 +382,6 @@ var ptBR = {
386
382
  "courses.financial.date": "Data",
387
383
  "courses.financial.amount": "Valor",
388
384
  "courses.financial.status": "Status",
389
- "courses.financial.connect": "Conectar Stripe",
390
- "courses.financial.connected": "Stripe conectado",
391
- "courses.financial.notConnected": "Conecte sua conta Stripe para receber os saques. A taxa da plataforma \xE9 aplicada automaticamente em cada venda.",
392
- "courses.financial.platformFee": "Taxa da plataforma",
393
- "courses.financial.connectSoon": "O onboarding do Stripe Connect \xE9 ligado na Etapa 3.",
394
385
  "courses.payoutStatus.paid": "Conclu\xEDdo",
395
386
  "courses.payoutStatus.pending": "Pendente",
396
387
  "courses.payoutStatus.failed": "Falhou",
@@ -1549,10 +1540,10 @@ function SalesPage({ navTabs, activeNavTab, ordersRoute = "/courses/sales" } = {
1549
1540
  void Promise.all([
1550
1541
  p.listCoreSales().catch(() => []),
1551
1542
  p.listOrders().catch(() => [])
1552
- ]).then(([core, store]) => {
1543
+ ]).then(([core, store2]) => {
1553
1544
  if (!alive) return;
1554
1545
  const byId = /* @__PURE__ */ new Map();
1555
- for (const o of store) byId.set(o.id, o);
1546
+ for (const o of store2) byId.set(o.id, o);
1556
1547
  for (const o of core) byId.set(o.id, o);
1557
1548
  const list = [...byId.values()].sort((a, b) => a.createdAt < b.createdAt ? 1 : -1);
1558
1549
  salesCache = list;
@@ -1774,18 +1765,12 @@ function FinancialPage() {
1774
1765
  const t = useTranslation();
1775
1766
  const [summary, setSummary] = React3.useState(null);
1776
1767
  const [payouts, setPayouts] = React3.useState(null);
1777
- const [account, setAccount] = React3.useState(null);
1778
1768
  React3.useEffect(() => {
1779
1769
  const p = getCoursesProvider();
1780
1770
  void p.getFinancialSummary().then(setSummary);
1781
1771
  void p.listPayouts().then(setPayouts);
1782
- void p.getCreatorAccount().then(setAccount);
1783
1772
  }, []);
1784
1773
  const currency = summary?.currency ?? "BRL";
1785
- const connected = !!account?.stripeAccountId && account.chargesEnabled;
1786
- function connectStripe() {
1787
- toast.info(t("courses.financial.connectSoon") || "Stripe Connect onboarding wires up in Stage 3.");
1788
- }
1789
1774
  const columns = [
1790
1775
  { key: "date", header: t("courses.financial.date") || "Date", render: (p) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatDate(p.createdAt) }) },
1791
1776
  { key: "amount", header: t("courses.financial.amount") || "Amount", align: "right", render: (p) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatMoney(p.amount, p.currency) }) },
@@ -1795,19 +1780,8 @@ function FinancialPage() {
1795
1780
  PageContainer,
1796
1781
  {
1797
1782
  title: t("courses.financial.title") || "Financial settings",
1798
- subtitle: t("courses.financial.subtitle") || "Balance, payouts and your Stripe connection.",
1799
- actions: /* @__PURE__ */ jsx(Button, { variant: connected ? "outline" : "default", onClick: connectStripe, children: connected ? t("courses.financial.connected") || "Stripe connected" : t("courses.financial.connect") || "Connect Stripe" }),
1783
+ subtitle: t("courses.financial.subtitle") || "Balance and payouts.",
1800
1784
  children: [
1801
- !connected && /* @__PURE__ */ jsxs("div", { className: "mb-6 rounded-xl border border-amber-500/40 bg-amber-500/10 p-4 text-sm text-foreground", children: [
1802
- t("courses.financial.notConnected") || "Connect your Stripe account to receive payouts. The platform fee is applied automatically on each sale.",
1803
- account && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-muted-foreground", children: [
1804
- "(",
1805
- t("courses.financial.platformFee") || "Platform fee",
1806
- ": ",
1807
- (account.platformFeeBps / 100).toFixed(2),
1808
- "%)"
1809
- ] })
1810
- ] }),
1811
1785
  /* @__PURE__ */ jsxs(StatGrid, { children: [
1812
1786
  /* @__PURE__ */ jsx(StatCard, { label: t("courses.financial.available") || "Available balance", value: formatMoney(summary?.availableBalance ?? 0, currency) }),
1813
1787
  /* @__PURE__ */ jsx(StatCard, { label: t("courses.financial.pending") || "Pending balance", value: formatMoney(summary?.pendingBalance ?? 0, currency) }),
@@ -1952,31 +1926,378 @@ function createCoursesDashboardWidgets(modules = {}) {
1952
1926
  ...modules.financial ? [defineKpiWidget({ id: "courses.kpi.fees", title: "courses.kpi.fees", domain: "courses", defaultOrder: 3, component: FeesKpi })] : []
1953
1927
  ];
1954
1928
  }
1955
- function createStripeConnector() {
1956
- return {
1957
- id: "stripe-connect",
1958
- hostPluginId: "courses",
1959
- name: "Stripe",
1960
- description: "Receba os pagamentos dos alunos; a taxa da plataforma \xE9 aplicada automaticamente.",
1961
- icon: "CreditCard",
1962
- authKind: "oauth",
1963
- async getStatus() {
1964
- try {
1965
- const account = await getCoursesProvider().getCreatorAccount();
1966
- const fee = (account.platformFeeBps / 100).toFixed(2);
1967
- return {
1968
- connected: !!account.stripeAccountId && account.chargesEnabled,
1969
- detail: `Taxa da plataforma: ${fee}%`
1970
- };
1971
- } catch {
1972
- return { connected: false };
1973
- }
1974
- },
1975
- async startOAuth(_redirectTo) {
1976
- throw new Error("O onboarding do Stripe Connect \xE9 ligado na Etapa 3.");
1929
+ var KIWIFY_FUNCTION = "kiwify-courses-sync";
1930
+ var KIWIFY_SOURCE = "kiwify";
1931
+ function client() {
1932
+ const supabase = getSupabaseClientOptional();
1933
+ if (!supabase) throw new Error("Sem conex\xE3o com o banco para falar com a Kiwify.");
1934
+ return supabase;
1935
+ }
1936
+ async function invoke(body) {
1937
+ const tenantId = getCoursesTenantId();
1938
+ const runtimeToken = await connectorRuntimeToken({ tenantId });
1939
+ const { data, error } = await client().functions.invoke(KIWIFY_FUNCTION, {
1940
+ body: { ...body, tenantId },
1941
+ headers: { [CONNECTOR_RUNTIME_TOKEN_HEADER]: runtimeToken }
1942
+ });
1943
+ if (error) throw new Error(error.message);
1944
+ return data;
1945
+ }
1946
+ function checkAccount() {
1947
+ return invoke({ action: "test" });
1948
+ }
1949
+ function listProducts() {
1950
+ return invoke({ action: "products" }).then((data) => data.products ?? []);
1951
+ }
1952
+ function importSales(input) {
1953
+ return invoke({ action: "sync_sales", from: input.from, to: input.to });
1954
+ }
1955
+ async function listMappings() {
1956
+ const tenantId = getCoursesTenantId();
1957
+ let query = client().from(COURSES_TABLES.courses).select("id,title,external_id,external_source").order("title");
1958
+ if (tenantId) query = query.eq("tenant_id", tenantId);
1959
+ const { data, error } = await query;
1960
+ if (error) throw new Error(error.message);
1961
+ return (data ?? []).map((row) => ({
1962
+ courseId: String(row.id),
1963
+ title: String(row.title ?? ""),
1964
+ // A course mapped to another provider is unmapped as far as Kiwify is
1965
+ // concerned — showing Hotmart's id in Kiwify's column would invite someone
1966
+ // to overwrite it.
1967
+ externalId: row.external_source === KIWIFY_SOURCE ? row.external_id ?? null : null
1968
+ }));
1969
+ }
1970
+ async function saveMapping(courseId, externalId) {
1971
+ const value = externalId?.trim() ? externalId.trim() : null;
1972
+ const { error } = await client().from(COURSES_TABLES.courses).update({
1973
+ external_id: value,
1974
+ // Cleared together: a source with no id is a row that claims an origin it
1975
+ // cannot name, and the partial unique index would stop enforcing anything.
1976
+ external_source: value ? KIWIFY_SOURCE : null
1977
+ }).eq("id", courseId);
1978
+ if (error) throw new Error(error.message);
1979
+ }
1980
+ var KIWIFY_CONNECTOR_ID = "kiwify";
1981
+ var KIWIFY_FIELDS = [
1982
+ { key: "accountId", label: "Account ID", type: "text", placeholder: "Kiwify \u2192 Apps \u2192 API" },
1983
+ { key: "clientId", label: "Client ID", type: "text", secret: true, placeholder: "Kiwify \u2192 Apps \u2192 API" },
1984
+ { key: "clientSecret", label: "Client Secret", type: "password", placeholder: "Mostrado junto com o Client ID" }
1985
+ ];
1986
+ var KIWIFY_MAX_WINDOW_DAYS = 90;
1987
+ var KIWIFY_SETTINGS = [
1988
+ {
1989
+ key: "importStatuses",
1990
+ label: "Quais vendas importar",
1991
+ description: "Kiwify tem onze situa\xE7\xF5es de venda; estas s\xE3o as que viram pedido aqui.",
1992
+ type: "multi-select",
1993
+ options: [
1994
+ { value: "paid", label: "Pagas" },
1995
+ { value: "pending", label: "Aguardando pagamento" },
1996
+ { value: "refunded", label: "Reembolsadas" },
1997
+ { value: "chargeback", label: "Chargeback" }
1998
+ ],
1999
+ // Required to OPERATE and not to connect: a feed with nothing selected has
2000
+ // nothing to bring, so the connection is real and reads as "conectado,
2001
+ // falta configurar" (`plg_connections.status = 'pending'`).
2002
+ requiredToOperate: true,
2003
+ defaultValue: ["paid", "refunded", "chargeback"]
2004
+ },
2005
+ {
2006
+ // The difference between "a venda entrou" and "o aluno entrou" is a
2007
+ // decision. A school that reviews enrolments by hand needs the sale
2008
+ // recorded and the door still shut.
2009
+ key: "grantAccess",
2010
+ label: "Liberar o acesso automaticamente",
2011
+ description: "Venda paga vira matr\xEDcula na hora. Desligado, o pedido entra e a matr\xEDcula \xE9 sua.",
2012
+ type: "toggle",
2013
+ defaultValue: true
2014
+ },
2015
+ {
2016
+ key: "windowDays",
2017
+ label: "Per\xEDodo de cada sincroniza\xE7\xE3o",
2018
+ description: "Quantos dias para tr\xE1s buscar quando a sincroniza\xE7\xE3o roda.",
2019
+ type: "text",
2020
+ placeholder: "30",
2021
+ // 1–90. Kiwify refuses a wider window than 90 days, so a number that reads
2022
+ // fine here and fails at the provider is not expressible.
2023
+ pattern: "[1-9]|[1-8]\\d|90",
2024
+ patternHint: "De 1 a 90 dias \u2014 o limite \xE9 da pr\xF3pria Kiwify.",
2025
+ defaultValue: "30"
2026
+ }
2027
+ ];
2028
+ var store = createConnectionStore(KIWIFY_CONNECTOR_ID, {
2029
+ fields: KIWIFY_FIELDS,
2030
+ settings: KIWIFY_SETTINGS
2031
+ });
2032
+ var STATUS_LABELS = {
2033
+ paid: "Pagas",
2034
+ pending: "Aguardando",
2035
+ refunded: "Reembolsadas",
2036
+ chargeback: "Chargeback"
2037
+ };
2038
+ function statusesDetail(value) {
2039
+ const list = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
2040
+ const labels = list.map((v) => STATUS_LABELS[String(v).trim()] ?? String(v).trim()).filter(Boolean);
2041
+ return labels.length > 0 ? labels.join(", ") : void 0;
2042
+ }
2043
+ function daysAgo(days) {
2044
+ const d = /* @__PURE__ */ new Date();
2045
+ d.setDate(d.getDate() - days);
2046
+ return d.toISOString().slice(0, 10);
2047
+ }
2048
+ function KiwifyExtraPanel() {
2049
+ const [connected, setConnected] = useState(false);
2050
+ const [loaded, setLoaded] = useState(false);
2051
+ const [products, setProducts] = useState(null);
2052
+ const [mappings, setMappings] = useState([]);
2053
+ const [from, setFrom] = useState(() => daysAgo(30));
2054
+ const [to, setTo] = useState(() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
2055
+ const [importing, setImporting] = useState(false);
2056
+ const [checking, setChecking] = useState(false);
2057
+ const [runs, setRuns] = useState([]);
2058
+ useEffect(() => {
2059
+ let cancelled = false;
2060
+ void store.get().then((c) => {
2061
+ if (!cancelled) setConnected(!!c?.active);
2062
+ }).catch(() => {
2063
+ }).finally(() => {
2064
+ if (!cancelled) setLoaded(true);
2065
+ });
2066
+ return () => {
2067
+ cancelled = true;
2068
+ };
2069
+ }, []);
2070
+ useEffect(() => {
2071
+ if (!connected) return;
2072
+ void listMappings().then(setMappings).catch(() => {
2073
+ });
2074
+ void store.listRuns({ limit: 10 }).then(setRuns).catch(() => {
2075
+ });
2076
+ void listProducts().then(setProducts).catch(() => setProducts([]));
2077
+ }, [connected]);
2078
+ const mappedCount = useMemo(() => mappings.filter((m) => m.externalId).length, [mappings]);
2079
+ if (!loaded || !connected) return null;
2080
+ async function handleMap(courseId, externalId) {
2081
+ const previous = mappings;
2082
+ setMappings((rows) => rows.map((r) => r.courseId === courseId ? { ...r, externalId: externalId || null } : r));
2083
+ try {
2084
+ await saveMapping(courseId, externalId || null);
2085
+ } catch (e) {
2086
+ setMappings(previous);
2087
+ toast.error(e?.message ?? "N\xE3o deu para salvar o v\xEDnculo");
1977
2088
  }
1978
- };
2089
+ }
2090
+ async function handleImport() {
2091
+ setImporting(true);
2092
+ try {
2093
+ const result = await importSales({ from, to });
2094
+ const parts = [`${result.imported} novas`];
2095
+ if (result.updated) parts.push(`${result.updated} atualizadas`);
2096
+ if (result.enrolled) parts.push(`${result.enrolled} matr\xEDculas`);
2097
+ if (result.unmapped) parts.push(`${result.unmapped} sem curso vinculado`);
2098
+ toast.success(parts.join(" \xB7 "));
2099
+ void store.listRuns({ limit: 10 }).then(setRuns);
2100
+ } catch (e) {
2101
+ toast.error(e?.message ?? "Erro ao importar as vendas");
2102
+ } finally {
2103
+ setImporting(false);
2104
+ }
2105
+ }
2106
+ async function handleCheck() {
2107
+ setChecking(true);
2108
+ try {
2109
+ const result = await checkAccount();
2110
+ if (!result.ok) throw new Error(result.message ?? "A Kiwify n\xE3o reconheceu essa credencial");
2111
+ toast.success(result.message ?? "Credencial v\xE1lida");
2112
+ } catch (e) {
2113
+ toast.error(e?.message ?? "N\xE3o deu para falar com a Kiwify");
2114
+ } finally {
2115
+ setChecking(false);
2116
+ }
2117
+ }
2118
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-5 border-t pt-4", children: [
2119
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: handleCheck, disabled: checking, children: [
2120
+ checking ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Plug, { className: "h-3.5 w-3.5" }),
2121
+ " ",
2122
+ "Testar credencial"
2123
+ ] }),
2124
+ /* @__PURE__ */ jsxs("div", { children: [
2125
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2126
+ /* @__PURE__ */ jsx(Link2, { className: "h-4 w-4 text-muted-foreground" }),
2127
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Qual produto \xE9 qual curso" })
2128
+ ] }),
2129
+ /* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: [
2130
+ "Uma venda s\xF3 entra se o produto vendido na Kiwify apontar para um curso daqui.",
2131
+ " ",
2132
+ mappedCount,
2133
+ " de ",
2134
+ mappings.length,
2135
+ " vinculados."
2136
+ ] }),
2137
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 max-h-72 divide-y overflow-auto rounded-md border", children: [
2138
+ mappings.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-3 py-4 text-xs text-muted-foreground", children: "Nenhum curso cadastrado ainda." }),
2139
+ mappings.map((m) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-3 px-3 py-2 text-sm", children: [
2140
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: m.title }),
2141
+ /* @__PURE__ */ jsxs(
2142
+ "select",
2143
+ {
2144
+ className: "h-8 w-56 shrink-0 rounded-md border border-border bg-background px-2 text-xs",
2145
+ value: m.externalId ?? "",
2146
+ onChange: (e) => void handleMap(m.courseId, e.target.value),
2147
+ children: [
2148
+ /* @__PURE__ */ jsx("option", { value: "", children: "\u2014 sem v\xEDnculo \u2014" }),
2149
+ m.externalId && !(products ?? []).some((p) => p.id === m.externalId) && /* @__PURE__ */ jsx("option", { value: m.externalId, children: m.externalId }),
2150
+ (products ?? []).map((p) => /* @__PURE__ */ jsx("option", { value: p.id, children: p.name }, p.id))
2151
+ ]
2152
+ }
2153
+ )
2154
+ ] }, m.courseId))
2155
+ ] }),
2156
+ products?.length === 0 && /* @__PURE__ */ jsx("p", { className: "mt-2 text-[11px] text-muted-foreground", children: "N\xE3o deu para ler o cat\xE1logo da Kiwify agora. Voc\xEA ainda pode colar o id do produto." })
2157
+ ] }),
2158
+ /* @__PURE__ */ jsxs("div", { children: [
2159
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2160
+ /* @__PURE__ */ jsx(Download, { className: "h-4 w-4 text-muted-foreground" }),
2161
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Importar vendas" })
2162
+ ] }),
2163
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-end gap-3", children: [
2164
+ /* @__PURE__ */ jsxs("div", { children: [
2165
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "De" }),
2166
+ /* @__PURE__ */ jsx(DatePicker, { value: from, onChange: setFrom, className: "mt-1" })
2167
+ ] }),
2168
+ /* @__PURE__ */ jsxs("div", { children: [
2169
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "At\xE9" }),
2170
+ /* @__PURE__ */ jsx(DatePicker, { value: to, onChange: setTo, className: "mt-1" })
2171
+ ] }),
2172
+ /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: handleImport, disabled: importing, children: [
2173
+ importing ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Download, { className: "h-3.5 w-3.5" }),
2174
+ " ",
2175
+ "Importar"
2176
+ ] })
2177
+ ] }),
2178
+ /* @__PURE__ */ jsxs("p", { className: "mt-2 text-[11px] text-muted-foreground", children: [
2179
+ "A Kiwify responde no m\xE1ximo ",
2180
+ KIWIFY_MAX_WINDOW_DAYS,
2181
+ " dias por consulta. Reimportar o mesmo per\xEDodo n\xE3o duplica: uma venda j\xE1 importada \xE9 reconhecida pelo id da Kiwify e s\xF3 tem a situa\xE7\xE3o atualizada."
2182
+ ] })
2183
+ ] }),
2184
+ runs.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2185
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2186
+ /* @__PURE__ */ jsx(History, { className: "h-4 w-4 text-muted-foreground" }),
2187
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Hist\xF3rico" })
2188
+ ] }),
2189
+ /* @__PURE__ */ jsx("div", { className: "divide-y rounded-md border text-sm", children: runs.map((r) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-3 py-2", title: r.error ?? void 0, children: [
2190
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: r.startedAt ? new Date(r.startedAt).toLocaleString("pt-BR") : "\u2014" }),
2191
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto text-xs", children: [
2192
+ r.written,
2193
+ "/",
2194
+ r.fetched,
2195
+ " importadas",
2196
+ typeof r.stats?.unmapped === "number" && r.stats.unmapped > 0 ? ` \xB7 ${r.stats.unmapped} sem v\xEDnculo` : ""
2197
+ ] }),
2198
+ r.status === "success" ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsx(AlertCircle, { className: "h-3.5 w-3.5 text-warning" })
2199
+ ] }, r.id)) })
2200
+ ] }),
2201
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: "O segredo da API fica guardado fora deste aplicativo. Quem fala com a Kiwify \xE9 o servidor, que pede a credencial na hora da consulta." })
2202
+ ] });
1979
2203
  }
2204
+ var kiwifyConnectorDef = {
2205
+ id: KIWIFY_CONNECTOR_ID,
2206
+ hostPluginId: "courses",
2207
+ name: "Kiwify",
2208
+ description: "Traga as vendas feitas na Kiwify e libere o acesso do aluno na \xE1rea de membros.",
2209
+ icon: "GraduationCap",
2210
+ category: "courses",
2211
+ authKind: "api-key",
2212
+ // Inbound because WE reach Kiwify: it has never heard of Fayz, which is what
2213
+ // makes this a pull connector and why it ships an edge function at all.
2214
+ direction: "inbound",
2215
+ // Sync only. The sale has to exist INSIDE Fayz to enrol the student, appear in
2216
+ // Pedidos and be reported on — it is not a question the assistant can answer
2217
+ // by asking Kiwify at the moment it is asked.
2218
+ planes: ["sync"],
2219
+ docsUrl: "https://docs.kiwify.com.br/api-reference/general",
2220
+ // Kiwify documents 100 calls per minute for one account, and the budget is
2221
+ // expressed per SECOND — 1/s rather than 1.66/s because the scheduler rounds
2222
+ // its spacing up to whole milliseconds anyway and a pull of a few hundred
2223
+ // sales has no reason to run at the edge of a 429.
2224
+ rateLimit: { perSecond: 1 },
2225
+ // Grounded in what 0010's `courses_import_external_sales` actually does, and
2226
+ // padded with nothing. It reads courses to find which one was sold; it writes
2227
+ // the order, the student and the enrolment. It reads no order back, so there
2228
+ // is no `order: read` here.
2229
+ permissions: [
2230
+ {
2231
+ kind: "data",
2232
+ resource: "course",
2233
+ actions: ["read"],
2234
+ reason: "Descobrir qual curso daqui corresponde ao produto vendido na Kiwify. Nenhum curso \xE9 alterado."
2235
+ },
2236
+ {
2237
+ kind: "data",
2238
+ resource: "order",
2239
+ actions: ["write"],
2240
+ reason: "Registrar a venda: valor, forma de pagamento, situa\xE7\xE3o e os dados de quem comprou. Uma venda j\xE1 importada \xE9 atualizada, nunca duplicada.",
2241
+ sensitive: true
2242
+ },
2243
+ {
2244
+ kind: "data",
2245
+ resource: "customer",
2246
+ actions: ["write"],
2247
+ reason: "Cadastrar o aluno que comprou, com nome e e-mail, quando ele ainda n\xE3o existe aqui.",
2248
+ sensitive: true
2249
+ },
2250
+ {
2251
+ kind: "data",
2252
+ resource: "enrollment",
2253
+ actions: ["write"],
2254
+ reason: "Dar acesso ao curso quando a venda est\xE1 paga \u2014 e tirar o acesso quando ela \xE9 reembolsada ou vira chargeback."
2255
+ }
2256
+ ],
2257
+ // Manual only, and that is the whole list today. A scheduled pull belongs to
2258
+ // the Fayz schedule worker (FAY-1347) and a webhook needs an endpoint Kiwify
2259
+ // can reach; declaring either here would promise a moment that never arrives.
2260
+ invokedAt: [
2261
+ {
2262
+ key: "courses.sales.import",
2263
+ label: "Voc\xEA escolhe um per\xEDodo aqui e as vendas da Kiwify s\xE3o importadas",
2264
+ plane: "sync",
2265
+ trigger: "manual",
2266
+ touches: ["course", "order", "customer", "enrollment"]
2267
+ }
2268
+ ],
2269
+ fields: KIWIFY_FIELDS,
2270
+ settings: KIWIFY_SETTINGS,
2271
+ // Read from the row the list already batched, so a screen with N connectors
2272
+ // costs one plg_connections request rather than N.
2273
+ ...connectionStatus(
2274
+ KIWIFY_CONNECTOR_ID,
2275
+ (connection) => ({
2276
+ connected: !!connection?.active,
2277
+ detail: statusesDetail(connection?.settings?.importStatuses),
2278
+ plane: "sync",
2279
+ ...settingsStatus(KIWIFY_SETTINGS, connection)
2280
+ }),
2281
+ { store: () => store }
2282
+ ),
2283
+ ...connectionSettings(store),
2284
+ // Reached only AFTER the credential sink accepted the pair — that ordering is
2285
+ // what makes `active = true` mean "a credential was placed for this tenant"
2286
+ // rather than "somebody pressed a button". Neither `clientId` nor
2287
+ // `clientSecret` arrives here; the hub split both out before calling this, so
2288
+ // the account id is the whole of what this function sees.
2289
+ saveConnection: async (values) => {
2290
+ await store.save({
2291
+ active: true,
2292
+ // The provider's own id for the account: what the row shows an operator,
2293
+ // and what the edge function spends the redeemed token against.
2294
+ providerAccountKey: values.accountId ?? null,
2295
+ settings: { accountId: values.accountId }
2296
+ });
2297
+ },
2298
+ disconnect: () => store.disconnect(),
2299
+ ExtraPanel: KiwifyExtraPanel
2300
+ };
1980
2301
 
1981
2302
  // src/index.ts
1982
2303
  var asComp = (c) => c;
@@ -2062,7 +2383,11 @@ function createCoursesPlugin(options) {
2062
2383
  // saas/admin Panel world rather than the ecommerce storefront.
2063
2384
  scaffolds: ["saas"],
2064
2385
  defaultEnabled: true,
2065
- dependencies: [],
2386
+ // Cursos does not collect money. It produces the reason to charge — a
2387
+ // matrícula, a mensalidade — and Financeiro is what turns that into a
2388
+ // receivable and receives it. Stating the dependency is what keeps the two
2389
+ // from overlapping: the module that charges is one, and it is not this one.
2390
+ dependencies: ["financial"],
2066
2391
  declaredFeatures: [
2067
2392
  { id: "courses", label: "Courses", group: "Courses" },
2068
2393
  { id: "courses.sales", label: "Sales", group: "Courses" },
@@ -2077,10 +2402,60 @@ function createCoursesPlugin(options) {
2077
2402
  // Commerce KPIs on the shared 'home' surface follow the same opt-in rule:
2078
2403
  // no module enabled → no widgets broadcast into a host's dashboard.
2079
2404
  dashboardWidgets: createCoursesDashboardWidgets(modules),
2080
- connectors: [createStripeConnector()],
2081
2405
  settings: [
2082
2406
  { id: "courses", label: "Courses", icon: "BookOpen", component: asComp(CoursesSettingsTab), order: 20, permission: read("courses") }
2083
2407
  ],
2408
+ // No payment connector here, and no payment slug on the shelf either.
2409
+ // `stripe` and `mercadopago` used to sit in both lists, which put a second
2410
+ // Stripe row on the connectors screen of an app that has one Stripe account
2411
+ // and one place to charge from. Money-in is Financeiro's, whoever raised the
2412
+ // invoice — see `dependencies` above.
2413
+ //
2414
+ // Hotmart and Kiwify stay: the catalogue's `courses` shelf is literally
2415
+ // "courses sold by a third party", which is a different relationship — the
2416
+ // sale happens over there and we learn about it.
2417
+ integrations: ["mailchimp", "hotmart", "kiwify"],
2418
+ // Kiwify is that shelf's first BUILT one. It takes no payment: the student
2419
+ // paid on Kiwify's checkout into the creator's Kiwify account, and what is
2420
+ // imported is the fact of the sale, so the members area can let them in.
2421
+ // The slug stays on `integrations` above too — the catalogue row joins on
2422
+ // the union of the two lists.
2423
+ connectors: [kiwifyConnectorDef],
2424
+ functions: [
2425
+ {
2426
+ slug: KIWIFY_FUNCTION,
2427
+ // Invoked from the browser by an authenticated operator picking a
2428
+ // period, so Supabase must check the caller's JWT. Nothing outside
2429
+ // reaches it: Kiwify does not call us, and no Fayz ingress wakes a pull
2430
+ // connector.
2431
+ ingress: "none",
2432
+ verifyJwt: true,
2433
+ // NAMES only. The tenant's Kiwify pair is NOT here and could not be: a
2434
+ // Supabase secret is per-POOL and a pool serves N schools with N Kiwify
2435
+ // accounts. The pair is redeemed per call from the platform vault, which
2436
+ // is what the last three names are for.
2437
+ secrets: [
2438
+ "SUPABASE_URL",
2439
+ "SUPABASE_SERVICE_ROLE_KEY",
2440
+ "FAYZ_API_URL",
2441
+ "FAYZ_PROJECT_ID",
2442
+ "CONNECTOR_REDEMPTION_SECRET"
2443
+ ],
2444
+ // What this connector's credential may touch. Direct table access is
2445
+ // not on the list on purpose (2026-08-11 ADR): the import goes through
2446
+ // the RPC, which takes the tenant from the connection row instead of
2447
+ // from the request body.
2448
+ grants: ["courses_import_external_sales"]
2449
+ }
2450
+ ],
2451
+ declaredRpcs: [
2452
+ {
2453
+ name: "courses_import_external_sales",
2454
+ kind: "write",
2455
+ description: "Import third-party course sales as orders, students and enrolments. Idempotent on (tenant, external source, external id); the tenant comes from the connection row, never from the payload. A sale whose product maps to no course here is counted and skipped.",
2456
+ audits: false
2457
+ }
2458
+ ],
2084
2459
  locales: coursesLocales
2085
2460
  };
2086
2461
  }