@burdenoff/microfe-subscriberbot 2026.1030.3 → 2026.1030.5

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.
Files changed (40) hide show
  1. package/dist/subscriberbot/SubscriberBotRoutes.d.ts.map +1 -1
  2. package/dist/subscriberbot/SubscriberBotRoutes.js +12 -8
  3. package/dist/subscriberbot/SubscriberBotRoutes.js.map +1 -1
  4. package/dist/subscriberbot/components/decisions/DecisionCard.d.ts.map +1 -1
  5. package/dist/subscriberbot/components/decisions/DecisionCard.js +1 -0
  6. package/dist/subscriberbot/components/decisions/DecisionCard.js.map +1 -1
  7. package/dist/subscriberbot/components/decisions/decisionModel.d.ts +3 -2
  8. package/dist/subscriberbot/components/decisions/decisionModel.d.ts.map +1 -1
  9. package/dist/subscriberbot/components/decisions/decisionModel.js +18 -5
  10. package/dist/subscriberbot/components/decisions/decisionModel.js.map +1 -1
  11. package/dist/subscriberbot/components/savings/SavingsCards.d.ts +11 -0
  12. package/dist/subscriberbot/components/savings/SavingsCards.d.ts.map +1 -0
  13. package/dist/subscriberbot/components/savings/SavingsCards.js +448 -0
  14. package/dist/subscriberbot/components/savings/SavingsCards.js.map +1 -0
  15. package/dist/subscriberbot/components/savings/index.d.ts +8 -0
  16. package/dist/subscriberbot/components/savings/index.d.ts.map +1 -0
  17. package/dist/subscriberbot/components/savings/index.js +2 -0
  18. package/dist/subscriberbot/components/savings/savingsModel.d.ts +53 -0
  19. package/dist/subscriberbot/components/savings/savingsModel.d.ts.map +1 -0
  20. package/dist/subscriberbot/components/savings/savingsModel.js +105 -0
  21. package/dist/subscriberbot/components/savings/savingsModel.js.map +1 -0
  22. package/dist/subscriberbot/hooks/index.d.ts +1 -1
  23. package/dist/subscriberbot/hooks/index.d.ts.map +1 -1
  24. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts +8 -1
  25. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts.map +1 -1
  26. package/dist/subscriberbot/hooks/useSubscriberBotApi.js +45 -15
  27. package/dist/subscriberbot/hooks/useSubscriberBotApi.js.map +1 -1
  28. package/dist/subscriberbot/pages/InsightsPage.d.ts.map +1 -1
  29. package/dist/subscriberbot/pages/InsightsPage.js +121 -110
  30. package/dist/subscriberbot/pages/InsightsPage.js.map +1 -1
  31. package/dist/subscriberbot/pages/SavingsPage.d.ts +4 -0
  32. package/dist/subscriberbot/pages/SavingsPage.d.ts.map +1 -0
  33. package/dist/subscriberbot/pages/SavingsPage.js +205 -0
  34. package/dist/subscriberbot/pages/SavingsPage.js.map +1 -0
  35. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.d.ts.map +1 -1
  36. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js +292 -301
  37. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js.map +1 -1
  38. package/dist/subscriberbot/types/index.d.ts +62 -2
  39. package/dist/subscriberbot/types/index.d.ts.map +1 -1
  40. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"SubscriberBotRoutes.d.ts","sourceRoot":"","sources":["../../src/subscriberbot/SubscriberBotRoutes.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,EAAkB,MAAM,OAAO,CAAC;AA8EhD,eAAO,MAAM,mBAAmB,EAAE,EAiEjC,CAAC"}
1
+ {"version":3,"file":"SubscriberBotRoutes.d.ts","sourceRoot":"","sources":["../../src/subscriberbot/SubscriberBotRoutes.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,EAAkB,MAAM,OAAO,CAAC;AAmFhD,eAAO,MAAM,mBAAmB,EAAE,EAkEjC,CAAC"}
@@ -5,15 +5,15 @@ import { Navigate as i, Route as a, Routes as o, useLocation as s } from "react-
5
5
  import { jsx as c, jsxs as l } from "react/jsx-runtime";
6
6
  import { Spinner as u } from "@burdenoff/fe-libs/ui";
7
7
  //#region src/subscriberbot/SubscriberBotRoutes.tsx
8
- var d = r(() => import("./pages/relationships/RelationshipsListPage.js").then((e) => ({ default: e.RelationshipsListPage }))), f = r(() => import("./pages/relationships/RelationshipNewPage.js").then((e) => ({ default: e.RelationshipNewPage }))), p = r(() => import("./pages/relationships/RelationshipDetailPage.js").then((e) => ({ default: e.RelationshipDetailPage }))), m = r(() => import("./pages/providers/ProvidersListPage.js").then((e) => ({ default: e.ProvidersListPage }))), h = r(() => import("./pages/providers/ProviderNewPage.js").then((e) => ({ default: e.ProviderNewPage }))), g = r(() => import("./pages/providers/ProviderDetailPage.js").then((e) => ({ default: e.ProviderDetailPage }))), _ = r(() => import("./pages/InboxPage.js").then((e) => ({ default: e.InboxPage }))), v = r(() => import("./pages/InvoicesPage.js").then((e) => ({ default: e.InvoicesPage }))), y = r(() => import("./pages/WorkflowsPage.js").then((e) => ({ default: e.WorkflowsPage }))), b = r(() => import("./pages/agents/AgentsListPage.js").then((e) => ({ default: e.AgentsListPage }))), x = r(() => import("./pages/agents/AgentNewPage.js").then((e) => ({ default: e.AgentNewPage }))), S = r(() => import("./pages/agents/AgentDetailPage.js").then((e) => ({ default: e.AgentDetailPage }))), C = r(() => import("./pages/decisions/DecisionsPage.js").then((e) => ({ default: e.DecisionsPage }))), w = r(() => import("./pages/decisions/DecisionDetailPage.js").then((e) => ({ default: e.DecisionDetailPage }))), T = r(() => import("./pages/InsightsPage.js").then((e) => ({ default: e.InsightsPage }))), E = r(() => import("./pages/AssetVaultPage.js").then((e) => ({ default: e.AssetVaultPage }))), D = () => /* @__PURE__ */ c("div", {
8
+ var d = r(() => import("./pages/relationships/RelationshipsListPage.js").then((e) => ({ default: e.RelationshipsListPage }))), f = r(() => import("./pages/relationships/RelationshipNewPage.js").then((e) => ({ default: e.RelationshipNewPage }))), p = r(() => import("./pages/relationships/RelationshipDetailPage.js").then((e) => ({ default: e.RelationshipDetailPage }))), m = r(() => import("./pages/providers/ProvidersListPage.js").then((e) => ({ default: e.ProvidersListPage }))), h = r(() => import("./pages/providers/ProviderNewPage.js").then((e) => ({ default: e.ProviderNewPage }))), g = r(() => import("./pages/providers/ProviderDetailPage.js").then((e) => ({ default: e.ProviderDetailPage }))), _ = r(() => import("./pages/InboxPage.js").then((e) => ({ default: e.InboxPage }))), v = r(() => import("./pages/InvoicesPage.js").then((e) => ({ default: e.InvoicesPage }))), y = r(() => import("./pages/WorkflowsPage.js").then((e) => ({ default: e.WorkflowsPage }))), b = r(() => import("./pages/agents/AgentsListPage.js").then((e) => ({ default: e.AgentsListPage }))), x = r(() => import("./pages/agents/AgentNewPage.js").then((e) => ({ default: e.AgentNewPage }))), S = r(() => import("./pages/agents/AgentDetailPage.js").then((e) => ({ default: e.AgentDetailPage }))), C = r(() => import("./pages/decisions/DecisionsPage.js").then((e) => ({ default: e.DecisionsPage }))), w = r(() => import("./pages/decisions/DecisionDetailPage.js").then((e) => ({ default: e.DecisionDetailPage }))), T = r(() => import("./pages/SavingsPage.js").then((e) => ({ default: e.SavingsPage }))), E = r(() => import("./pages/InsightsPage.js").then((e) => ({ default: e.InsightsPage }))), D = r(() => import("./pages/AssetVaultPage.js").then((e) => ({ default: e.AssetVaultPage }))), O = () => /* @__PURE__ */ c("div", {
9
9
  className: "flex min-h-[400px] items-center justify-center",
10
10
  children: /* @__PURE__ */ c(u, { size: "lg" })
11
- }), O = () => {
12
- let { basePath: r, isStandalone: u } = e(), O = s(), k = r === "/" ? "" : (r ?? "").replace(/\/$/, "");
13
- return k && !u && !(O.pathname === k || O.pathname.startsWith(k + "/")) ? null : /* @__PURE__ */ c("div", {
11
+ }), k = () => {
12
+ let { basePath: r, isStandalone: u } = e(), k = s(), A = r === "/" ? "" : (r ?? "").replace(/\/$/, "");
13
+ return A && !u && !(k.pathname === A || k.pathname.startsWith(A + "/")) ? null : /* @__PURE__ */ c("div", {
14
14
  className: "flex min-h-full flex-col",
15
15
  children: /* @__PURE__ */ c(n, {
16
- fallback: /* @__PURE__ */ c(D, {}),
16
+ fallback: /* @__PURE__ */ c(O, {}),
17
17
  children: /* @__PURE__ */ l(o, { children: [
18
18
  /* @__PURE__ */ c(a, {
19
19
  index: !0,
@@ -53,11 +53,11 @@ var d = r(() => import("./pages/relationships/RelationshipsListPage.js").then((e
53
53
  }),
54
54
  /* @__PURE__ */ c(a, {
55
55
  path: "insights",
56
- element: /* @__PURE__ */ c(T, {})
56
+ element: /* @__PURE__ */ c(E, {})
57
57
  }),
58
58
  /* @__PURE__ */ c(a, {
59
59
  path: "vault",
60
- element: /* @__PURE__ */ c(E, {})
60
+ element: /* @__PURE__ */ c(D, {})
61
61
  }),
62
62
  /* @__PURE__ */ c(a, {
63
63
  path: "agents",
@@ -75,6 +75,10 @@ var d = r(() => import("./pages/relationships/RelationshipsListPage.js").then((e
75
75
  path: "decisions",
76
76
  element: /* @__PURE__ */ c(C, {})
77
77
  }),
78
+ /* @__PURE__ */ c(a, {
79
+ path: "savings",
80
+ element: /* @__PURE__ */ c(T, {})
81
+ }),
78
82
  /* @__PURE__ */ c(a, {
79
83
  path: "decisions/:id",
80
84
  element: /* @__PURE__ */ c(w, {})
@@ -95,6 +99,6 @@ var d = r(() => import("./pages/relationships/RelationshipsListPage.js").then((e
95
99
  });
96
100
  };
97
101
  //#endregion
98
- export { O as SubscriberBotRoutes };
102
+ export { k as SubscriberBotRoutes };
99
103
 
100
104
  //# sourceMappingURL=SubscriberBotRoutes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SubscriberBotRoutes.js","names":[],"sources":["../../src/subscriberbot/SubscriberBotRoutes.tsx"],"sourcesContent":["/**\n * SubscriberBot Routes\n * @module subscriberbot\n *\n * Domain routes for the Subscriber Bot product. The dashboard is eager-loaded\n * (primary landing); all other pages are lazy-loaded.\n */\n\nimport { type FC, lazy, Suspense } from 'react';\nimport { Routes, Route, Navigate, useLocation } from 'react-router';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\nimport { useSubscriberBot } from './context/SubscriberBotContext';\n\n// Eager — primary landing\nimport { DashboardPage } from './pages/DashboardPage';\n\n// Relationships\nconst RelationshipsListPage = lazy(() =>\n import('./pages/relationships/RelationshipsListPage').then((m) => ({\n default: m.RelationshipsListPage,\n }))\n);\nconst RelationshipNewPage = lazy(() =>\n import('./pages/relationships/RelationshipNewPage').then((m) => ({\n default: m.RelationshipNewPage,\n }))\n);\nconst RelationshipDetailPage = lazy(() =>\n import('./pages/relationships/RelationshipDetailPage').then((m) => ({\n default: m.RelationshipDetailPage,\n }))\n);\n\n// Providers\nconst ProvidersListPage = lazy(() =>\n import('./pages/providers/ProvidersListPage').then((m) => ({ default: m.ProvidersListPage }))\n);\nconst ProviderNewPage = lazy(() =>\n import('./pages/providers/ProviderNewPage').then((m) => ({ default: m.ProviderNewPage }))\n);\nconst ProviderDetailPage = lazy(() =>\n import('./pages/providers/ProviderDetailPage').then((m) => ({ default: m.ProviderDetailPage }))\n);\n\n// Inbox / Invoices / Workflows\nconst InboxPage = lazy(() => import('./pages/InboxPage').then((m) => ({ default: m.InboxPage })));\nconst InvoicesPage = lazy(() =>\n import('./pages/InvoicesPage').then((m) => ({ default: m.InvoicesPage }))\n);\nconst WorkflowsPage = lazy(() =>\n import('./pages/WorkflowsPage').then((m) => ({ default: m.WorkflowsPage }))\n);\n\n// Agents\nconst AgentsListPage = lazy(() =>\n import('./pages/agents/AgentsListPage').then((m) => ({ default: m.AgentsListPage }))\n);\nconst AgentNewPage = lazy(() =>\n import('./pages/agents/AgentNewPage').then((m) => ({ default: m.AgentNewPage }))\n);\nconst AgentDetailPage = lazy(() =>\n import('./pages/agents/AgentDetailPage').then((m) => ({ default: m.AgentDetailPage }))\n);\n\n// AI decision queue (BOFF-3663)\nconst DecisionsPage = lazy(() =>\n import('./pages/decisions/DecisionsPage').then((m) => ({ default: m.DecisionsPage }))\n);\nconst DecisionDetailPage = lazy(() =>\n import('./pages/decisions/DecisionDetailPage').then((m) => ({ default: m.DecisionDetailPage }))\n);\n\n// Insights + Asset Vault (aggregated real data)\nconst InsightsPage = lazy(() =>\n import('./pages/InsightsPage').then((m) => ({ default: m.InsightsPage }))\n);\nconst AssetVaultPage = lazy(() =>\n import('./pages/AssetVaultPage').then((m) => ({ default: m.AssetVaultPage }))\n);\n\nconst PageLoader: FC = () => (\n <div className=\"flex min-h-[400px] items-center justify-center\">\n <Spinner size=\"lg\" />\n </div>\n);\n\nexport const SubscriberBotRoutes: FC = () => {\n const { basePath, isStandalone } = useSubscriberBot();\n const location = useLocation();\n\n // Normalize base path\n const normalizedBasePath = basePath === '/' ? '' : (basePath ?? '').replace(/\\/$/, '');\n\n // Guard: only render if the current path matches basePath (integrated mode).\n if (normalizedBasePath && !isStandalone) {\n const pathMatches =\n location.pathname === normalizedBasePath ||\n location.pathname.startsWith(normalizedBasePath + '/');\n if (!pathMatches) {\n return null;\n }\n }\n\n return (\n <div className=\"flex min-h-full flex-col\">\n <Suspense fallback={<PageLoader />}>\n <Routes>\n {/* Overview / dashboard */}\n <Route index element={<DashboardPage />} />\n\n {/* Relationships (the core domain) */}\n <Route path=\"relationships\" element={<RelationshipsListPage />} />\n <Route path=\"relationships/new\" element={<RelationshipNewPage />} />\n <Route path=\"relationships/:id\" element={<RelationshipDetailPage />} />\n\n {/* Providers (registry) */}\n <Route path=\"providers\" element={<ProvidersListPage />} />\n <Route path=\"providers/new\" element={<ProviderNewPage />} />\n <Route path=\"providers/:id\" element={<ProviderDetailPage />} />\n\n {/* Universal Inbox */}\n {/* One optional-segment route so selecting a message never remounts the page */}\n <Route path=\"inbox/:selectedId?\" element={<InboxPage />} />\n\n {/* Invoices */}\n <Route path=\"invoices\" element={<InvoicesPage />} />\n\n {/* Insights (spend analytics + renewal forecast) */}\n <Route path=\"insights\" element={<InsightsPage />} />\n\n {/* Digital Asset Vault */}\n <Route path=\"vault\" element={<AssetVaultPage />} />\n\n {/* AI Agents */}\n <Route path=\"agents\" element={<AgentsListPage />} />\n <Route path=\"agents/new\" element={<AgentNewPage />} />\n <Route path=\"agents/:id\" element={<AgentDetailPage />} />\n\n {/* AI decision queue */}\n <Route path=\"decisions\" element={<DecisionsPage />} />\n <Route path=\"decisions/:id\" element={<DecisionDetailPage />} />\n\n {/* Workflows */}\n <Route path=\"workflows/:selectedId?\" element={<WorkflowsPage />} />\n\n {/* Fallback */}\n <Route path=\"*\" element={<Navigate to=\"\" replace />} />\n </Routes>\n </Suspense>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"SubscriberBotRoutes.js","names":[],"sources":["../../src/subscriberbot/SubscriberBotRoutes.tsx"],"sourcesContent":["/**\n * SubscriberBot Routes\n * @module subscriberbot\n *\n * Domain routes for the Subscriber Bot product. The dashboard is eager-loaded\n * (primary landing); all other pages are lazy-loaded.\n */\n\nimport { type FC, lazy, Suspense } from 'react';\nimport { Routes, Route, Navigate, useLocation } from 'react-router';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\nimport { useSubscriberBot } from './context/SubscriberBotContext';\n\n// Eager — primary landing\nimport { DashboardPage } from './pages/DashboardPage';\n\n// Relationships\nconst RelationshipsListPage = lazy(() =>\n import('./pages/relationships/RelationshipsListPage').then((m) => ({\n default: m.RelationshipsListPage,\n }))\n);\nconst RelationshipNewPage = lazy(() =>\n import('./pages/relationships/RelationshipNewPage').then((m) => ({\n default: m.RelationshipNewPage,\n }))\n);\nconst RelationshipDetailPage = lazy(() =>\n import('./pages/relationships/RelationshipDetailPage').then((m) => ({\n default: m.RelationshipDetailPage,\n }))\n);\n\n// Providers\nconst ProvidersListPage = lazy(() =>\n import('./pages/providers/ProvidersListPage').then((m) => ({ default: m.ProvidersListPage }))\n);\nconst ProviderNewPage = lazy(() =>\n import('./pages/providers/ProviderNewPage').then((m) => ({ default: m.ProviderNewPage }))\n);\nconst ProviderDetailPage = lazy(() =>\n import('./pages/providers/ProviderDetailPage').then((m) => ({ default: m.ProviderDetailPage }))\n);\n\n// Inbox / Invoices / Workflows\nconst InboxPage = lazy(() => import('./pages/InboxPage').then((m) => ({ default: m.InboxPage })));\nconst InvoicesPage = lazy(() =>\n import('./pages/InvoicesPage').then((m) => ({ default: m.InvoicesPage }))\n);\nconst WorkflowsPage = lazy(() =>\n import('./pages/WorkflowsPage').then((m) => ({ default: m.WorkflowsPage }))\n);\n\n// Agents\nconst AgentsListPage = lazy(() =>\n import('./pages/agents/AgentsListPage').then((m) => ({ default: m.AgentsListPage }))\n);\nconst AgentNewPage = lazy(() =>\n import('./pages/agents/AgentNewPage').then((m) => ({ default: m.AgentNewPage }))\n);\nconst AgentDetailPage = lazy(() =>\n import('./pages/agents/AgentDetailPage').then((m) => ({ default: m.AgentDetailPage }))\n);\n\n// AI decision queue (BOFF-3663)\nconst DecisionsPage = lazy(() =>\n import('./pages/decisions/DecisionsPage').then((m) => ({ default: m.DecisionsPage }))\n);\nconst DecisionDetailPage = lazy(() =>\n import('./pages/decisions/DecisionDetailPage').then((m) => ({ default: m.DecisionDetailPage }))\n);\n\n// Cost optimization dashboard (BOFF-4001)\nconst SavingsPage = lazy(() =>\n import('./pages/SavingsPage').then((m) => ({ default: m.SavingsPage }))\n);\n\n// Insights + Asset Vault (aggregated real data)\nconst InsightsPage = lazy(() =>\n import('./pages/InsightsPage').then((m) => ({ default: m.InsightsPage }))\n);\nconst AssetVaultPage = lazy(() =>\n import('./pages/AssetVaultPage').then((m) => ({ default: m.AssetVaultPage }))\n);\n\nconst PageLoader: FC = () => (\n <div className=\"flex min-h-[400px] items-center justify-center\">\n <Spinner size=\"lg\" />\n </div>\n);\n\nexport const SubscriberBotRoutes: FC = () => {\n const { basePath, isStandalone } = useSubscriberBot();\n const location = useLocation();\n\n // Normalize base path\n const normalizedBasePath = basePath === '/' ? '' : (basePath ?? '').replace(/\\/$/, '');\n\n // Guard: only render if the current path matches basePath (integrated mode).\n if (normalizedBasePath && !isStandalone) {\n const pathMatches =\n location.pathname === normalizedBasePath ||\n location.pathname.startsWith(normalizedBasePath + '/');\n if (!pathMatches) {\n return null;\n }\n }\n\n return (\n <div className=\"flex min-h-full flex-col\">\n <Suspense fallback={<PageLoader />}>\n <Routes>\n {/* Overview / dashboard */}\n <Route index element={<DashboardPage />} />\n\n {/* Relationships (the core domain) */}\n <Route path=\"relationships\" element={<RelationshipsListPage />} />\n <Route path=\"relationships/new\" element={<RelationshipNewPage />} />\n <Route path=\"relationships/:id\" element={<RelationshipDetailPage />} />\n\n {/* Providers (registry) */}\n <Route path=\"providers\" element={<ProvidersListPage />} />\n <Route path=\"providers/new\" element={<ProviderNewPage />} />\n <Route path=\"providers/:id\" element={<ProviderDetailPage />} />\n\n {/* Universal Inbox */}\n {/* One optional-segment route so selecting a message never remounts the page */}\n <Route path=\"inbox/:selectedId?\" element={<InboxPage />} />\n\n {/* Invoices */}\n <Route path=\"invoices\" element={<InvoicesPage />} />\n\n {/* Insights (spend analytics + renewal forecast) */}\n <Route path=\"insights\" element={<InsightsPage />} />\n\n {/* Digital Asset Vault */}\n <Route path=\"vault\" element={<AssetVaultPage />} />\n\n {/* AI Agents */}\n <Route path=\"agents\" element={<AgentsListPage />} />\n <Route path=\"agents/new\" element={<AgentNewPage />} />\n <Route path=\"agents/:id\" element={<AgentDetailPage />} />\n\n {/* AI decision queue */}\n <Route path=\"decisions\" element={<DecisionsPage />} />\n <Route path=\"savings\" element={<SavingsPage />} />\n <Route path=\"decisions/:id\" element={<DecisionDetailPage />} />\n\n {/* Workflows */}\n <Route path=\"workflows/:selectedId?\" element={<WorkflowsPage />} />\n\n {/* Fallback */}\n <Route path=\"*\" element={<Navigate to=\"\" replace />} />\n </Routes>\n </Suspense>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"DecisionCard.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/components/decisions/DecisionCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAY,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAU1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA6B/C,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC;IACnC,QAAQ,EAAE,WAAW,CAAC;IACtB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC7C,CA0GA,CAAC;AAMF,eAAO,MAAM,qBAAqB,EAAE,EAAE,CAAC;IAAE,QAAQ,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CA4EpF,CAAC;AAMF,eAAO,MAAM,eAAe,EAAE,EAAE,CAAC;IAAE,QAAQ,EAAE,WAAW,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA4CzE,CAAC;AAMF,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC;IAC5B,QAAQ,EAAE,WAAW,CAAC;IACtB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAoHA,CAAC"}
1
+ {"version":3,"file":"DecisionCard.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/components/decisions/DecisionCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAY,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAU1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA6B/C,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC;IACnC,QAAQ,EAAE,WAAW,CAAC;IACtB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC7C,CA0GA,CAAC;AAMF,eAAO,MAAM,qBAAqB,EAAE,EAAE,CAAC;IAAE,QAAQ,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CA4EpF,CAAC;AAMF,eAAO,MAAM,eAAe,EAAE,EAAE,CAAC;IAAE,QAAQ,EAAE,WAAW,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA6CzE,CAAC;AAMF,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC;IAC5B,QAAQ,EAAE,WAAW,CAAC;IACtB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAoHA,CAAC"}
@@ -154,6 +154,7 @@ var k = ({ decision: e, onClose: n, onApplied: r }) => {
154
154
  }, j = ({ decision: t, limit: n }) => {
155
155
  let r = s(), { navigateTo: i } = e(), a = p(t), o = n ? a.slice(0, n) : a, c = {
156
156
  relationship: r("decisions.sources.relationship", "Subscription"),
157
+ keptRelationship: r("decisions.sources.keptRelationship", "Keep instead"),
157
158
  plan: r("decisions.sources.plan", "Current plan"),
158
159
  alternativePlan: r("decisions.sources.alternativePlan", "Suggested plan"),
159
160
  renewalEvent: r("decisions.sources.renewalEvent", "Renewal event"),
@@ -1 +1 @@
1
- {"version":3,"file":"DecisionCard.js","names":[],"sources":["../../../../src/subscriberbot/components/decisions/DecisionCard.tsx"],"sourcesContent":["/**\n * Decision card + apply / dismiss dialogs\n * @module subscriberbot/components/decisions\n *\n * Renders one `SubDecision` from the decision engine: the recommendation, the\n * rationale, confidence, estimated annual saving, the relationship it concerns\n * and the source rows it reasoned over. Apply and dismiss go through\n * `subApplyDecision` / `subDismissDecision`.\n *\n * Apply always opens a confirmation dialog that names the relationship and its\n * amount. For a destructive recommendation (cancel) the Apply button stays\n * disabled until the user ticks an acknowledgement that names the same\n * relationship and amount, so a cancellation can never happen on a misclick.\n */\n\nimport { useState, type FC } from 'react';\nimport { CheckCircle2, ExternalLink, Gauge, PiggyBank, ShieldAlert, XCircle } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { Modal } from '../Modal';\nimport { StatusBadge } from '../StatusBadge';\nimport { useTr } from '../../hooks/useTr';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useApplyDecision, useDismissDecision } from '../../hooks/useSubscriberBotApi';\nimport { formatCurrency, formatEnumLabel, formatRelativeTime } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { SubDecision } from '../../types';\nimport {\n DECISION_KIND_LABEL,\n RECOMMENDATION_LABEL,\n confidencePercent,\n evidenceSources,\n isActionable,\n isDestructive,\n relationshipSnapshot,\n} from './decisionModel';\n\nfunction errorText(err: unknown, fallback: string): string {\n return err instanceof Error && err.message ? err.message : fallback;\n}\n\n/** \"Netflix Premium · 15.49 USD / monthly\" — the identity a confirmation must name. */\nfunction useRelationshipIdentity(decision: SubDecision): { label: string; amount: string | null } {\n const snap = relationshipSnapshot(decision);\n const amount =\n snap.amount !== null\n ? `${formatCurrency(snap.amount, snap.currency ?? decision.currency)}${snap.interval ? ` / ${formatEnumLabel(snap.interval).toLowerCase()}` : ''}`\n : null;\n return { label: snap.label ?? decision.relationshipId, amount };\n}\n\n// ---------------------------------------------------------------------------\n// Apply confirmation\n// ---------------------------------------------------------------------------\n\nexport const ApplyDecisionDialog: FC<{\n decision: SubDecision;\n onClose: () => void;\n onApplied?: (decision: SubDecision) => void;\n}> = ({ decision, onClose, onApplied }) => {\n const tr = useTr();\n const apply = useApplyDecision();\n const [acknowledged, setAcknowledged] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const destructive = isDestructive(decision.recommendation);\n const identity = useRelationshipIdentity(decision);\n const rec = RECOMMENDATION_LABEL[decision.recommendation];\n const canSubmit = !apply.isPending && (!destructive || acknowledged);\n\n const submit = () => {\n if (!canSubmit) return;\n setError(null);\n apply.mutate(decision.id, {\n onSuccess: (result) => {\n onApplied?.(result.decision);\n onClose();\n },\n onError: (err) =>\n setError(errorText(err, tr('decisions.apply.error', 'Could not apply this decision.'))),\n });\n };\n\n return (\n <Modal\n title={tr('decisions.apply.title', 'Apply: {{action}}', {\n action: tr(rec.key, rec.fallback),\n })}\n description={tr(\n 'decisions.apply.description',\n 'This runs the same change you could make by hand on the relationship page.'\n )}\n onClose={onClose}\n >\n <div className=\"space-y-4\" data-testid=\"apply-decision-dialog\">\n <div className=\"rounded-lg border border-border-subtle bg-bg-sunken px-4 py-3\">\n <p className=\"text-xs text-text-secondary\">\n {tr('decisions.apply.relationship', 'Subscription')}\n </p>\n <p className=\"text-sm font-semibold text-text-primary\" data-testid=\"confirm-label\">\n {identity.label}\n </p>\n {identity.amount && (\n <p className=\"text-sm tabular-nums text-text-primary\" data-testid=\"confirm-amount\">\n {identity.amount}\n </p>\n )}\n </div>\n <p className=\"text-sm text-text-secondary\">{decision.rationale}</p>\n\n {destructive && (\n <label className=\"flex items-start gap-2.5 rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-3 text-sm text-status-error-text\">\n <input\n type=\"checkbox\"\n checked={acknowledged}\n onChange={(e) => setAcknowledged(e.target.checked)}\n className=\"mt-0.5 size-4 shrink-0 rounded border-border-subtle\"\n data-testid=\"confirm-destructive\"\n />\n <span>\n {tr(\n 'decisions.apply.acknowledgeCancel',\n 'I want to cancel \"{{label}}\"{{amount}}. I understand the subscription will stop.',\n {\n label: identity.label,\n amount: identity.amount ? ` (${identity.amount})` : '',\n }\n )}\n </span>\n </label>\n )}\n\n {error && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {error}\n </div>\n )}\n\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n {tr('decisions.apply.back', 'Not now')}\n </button>\n <button\n type=\"button\"\n onClick={submit}\n disabled={!canSubmit}\n data-testid=\"confirm-apply\"\n className={cn(\n 'rounded-lg px-4 py-2 text-sm font-medium hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50',\n destructive\n ? 'bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bg-hover'\n : 'bg-action-primary-bg text-action-primary-text'\n )}\n >\n {apply.isPending\n ? tr('decisions.apply.applying', 'Applying…')\n : tr('decisions.apply.submit', 'Apply')}\n </button>\n </div>\n </div>\n </Modal>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Dismiss\n// ---------------------------------------------------------------------------\n\nexport const DismissDecisionDialog: FC<{ decision: SubDecision; onClose: () => void }> = ({\n decision,\n onClose,\n}) => {\n const tr = useTr();\n const dismiss = useDismissDecision();\n const [reason, setReason] = useState('');\n const [error, setError] = useState<string | null>(null);\n const identity = useRelationshipIdentity(decision);\n\n const submit = (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n dismiss.mutate(\n { id: decision.id, reason },\n {\n onSuccess: () => onClose(),\n onError: (err) =>\n setError(\n errorText(err, tr('decisions.dismiss.error', 'Could not dismiss this decision.'))\n ),\n }\n );\n };\n\n return (\n <Modal\n title={tr('decisions.dismiss.title', 'Dismiss recommendation')}\n description={identity.label}\n onClose={onClose}\n >\n <form onSubmit={submit} className=\"space-y-4\" data-testid=\"dismiss-decision-dialog\">\n <div>\n <label\n htmlFor={`dismiss-reason-${decision.id}`}\n className=\"mb-1 block text-xs font-medium text-text-secondary\"\n >\n {tr('decisions.dismiss.reason', 'Reason (optional)')}\n </label>\n <textarea\n id={`dismiss-reason-${decision.id}`}\n value={reason}\n onChange={(e) => setReason(e.target.value)}\n rows={3}\n maxLength={500}\n placeholder={tr('decisions.dismiss.placeholder', 'e.g. We still use this every week')}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n {error && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {error}\n </div>\n )}\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n {tr('decisions.dismiss.back', 'Back')}\n </button>\n <button\n type=\"submit\"\n disabled={dismiss.isPending}\n data-testid=\"confirm-dismiss\"\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {dismiss.isPending\n ? tr('decisions.dismiss.dismissing', 'Dismissing…')\n : tr('decisions.dismiss.submit', 'Dismiss')}\n </button>\n </div>\n </form>\n </Modal>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Source rows\n// ---------------------------------------------------------------------------\n\nexport const DecisionSources: FC<{ decision: SubDecision; limit?: number }> = ({\n decision,\n limit,\n}) => {\n const tr = useTr();\n const { navigateTo } = useSubscriberBot();\n const all = evidenceSources(decision);\n const shown = limit ? all.slice(0, limit) : all;\n const kindLabel: Record<string, string> = {\n relationship: tr('decisions.sources.relationship', 'Subscription'),\n plan: tr('decisions.sources.plan', 'Current plan'),\n alternativePlan: tr('decisions.sources.alternativePlan', 'Suggested plan'),\n renewalEvent: tr('decisions.sources.renewalEvent', 'Renewal event'),\n entitlement: tr('decisions.sources.entitlement', 'Entitlement usage'),\n };\n return (\n <div>\n <p className=\"text-[10px] font-medium uppercase tracking-wide text-text-secondary\">\n {tr('decisions.sources.title', 'Based on')}\n </p>\n <ul className=\"mt-1.5 flex flex-wrap gap-1.5\" data-testid=\"decision-sources\">\n {shown.map((source) => (\n <li key={`${source.kind}-${source.id}`}>\n <button\n type=\"button\"\n onClick={() => navigateTo(source.path)}\n data-source-kind={source.kind}\n className=\"inline-flex max-w-full items-center gap-1.5 rounded-md border border-border-subtle bg-bg-sunken px-2 py-0.5 text-left text-[11px] text-text-secondary hover:text-text-primary\"\n >\n <span className=\"font-medium text-text-primary\">{kindLabel[source.kind]}</span>\n <span className=\"truncate\">{source.title}</span>\n {source.detail && <span className=\"tabular-nums\">{source.detail}</span>}\n <ExternalLink className=\"h-3 w-3 shrink-0\" aria-hidden />\n </button>\n </li>\n ))}\n {limit && all.length > limit && (\n <li className=\"px-1 py-0.5 text-[11px] text-text-secondary\">\n {tr('decisions.sources.more', '+{{count}} more', { count: all.length - limit })}\n </li>\n )}\n </ul>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Card\n// ---------------------------------------------------------------------------\n\nexport const DecisionCard: FC<{\n decision: SubDecision;\n /** Hide the agent name (e.g. on the agent's own page). */\n hideAgent?: boolean;\n}> = ({ decision, hideAgent = false }) => {\n const tr = useTr();\n const { navigateTo } = useSubscriberBot();\n const [dialog, setDialog] = useState<'apply' | 'dismiss' | null>(null);\n const kind = DECISION_KIND_LABEL[decision.kind];\n const rec = RECOMMENDATION_LABEL[decision.recommendation];\n const destructive = isDestructive(decision.recommendation);\n const actionable = isActionable(decision);\n const identity = useRelationshipIdentity(decision);\n\n return (\n <GlassCard\n treatment=\"metric\"\n className=\"flex flex-col gap-3 p-4\"\n data-testid=\"decision-card\"\n data-decision-id={decision.id}\n data-recommendation={decision.recommendation}\n data-status={decision.status}\n >\n <div className=\"flex flex-wrap items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n {tr(kind.key, kind.fallback)}\n {!hideAgent && decision.agent ? ` · ${decision.agent.name}` : ''}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/decisions/${decision.id}`)}\n className=\"mt-0.5 flex items-center gap-1.5 text-left text-sm font-semibold text-text-primary hover:underline\"\n >\n {destructive && (\n <ShieldAlert className=\"h-4 w-4 shrink-0 text-status-error-text\" aria-hidden />\n )}\n {tr(rec.key, rec.fallback)}: {identity.label}\n </button>\n {identity.amount && (\n <p className=\"text-xs tabular-nums text-text-secondary\">{identity.amount}</p>\n )}\n </div>\n <StatusBadge status={decision.status} />\n </div>\n\n <p className=\"text-sm text-text-secondary\">{decision.rationale}</p>\n\n <div className=\"flex flex-wrap gap-2 text-xs\">\n <span className=\"inline-flex items-center gap-1.5 rounded-md bg-bg-sunken px-2 py-0.5 text-text-secondary\">\n <Gauge className=\"h-3.5 w-3.5\" aria-hidden />\n {tr('decisions.card.confidence', 'Confidence {{pct}}%', {\n pct: confidencePercent(decision.confidence),\n })}\n </span>\n {decision.estimatedSavings > 0 && (\n <span className=\"inline-flex items-center gap-1.5 rounded-md bg-status-success-bg-subtle px-2 py-0.5 font-medium text-status-success-text tabular-nums\">\n <PiggyBank className=\"h-3.5 w-3.5\" aria-hidden />\n {tr('decisions.card.savings', 'Saves {{amount}} a year', {\n amount: formatCurrency(decision.estimatedSavings, decision.currency),\n })}\n </span>\n )}\n <span className=\"inline-flex items-center rounded-md bg-bg-sunken px-2 py-0.5 text-text-secondary\">\n {formatRelativeTime(decision.createdAt)}\n </span>\n </div>\n\n <DecisionSources decision={decision} limit={4} />\n\n {decision.status === 'FLAGGED' && decision.validationErrors.length > 0 && (\n <p className=\"rounded-md bg-status-warning-bg-subtle px-3 py-2 text-xs text-status-warning-text\">\n {tr('decisions.card.flagged', 'Held back: failed validation')} —{' '}\n {decision.validationErrors.join('; ')}\n </p>\n )}\n {decision.status === 'DISMISSED' && decision.dismissedReason && (\n <p className=\"text-xs text-text-secondary\">\n {tr('decisions.card.dismissedReason', 'Dismissed: {{reason}}', {\n reason: decision.dismissedReason,\n })}\n </p>\n )}\n\n {actionable && (\n <div className=\"flex flex-wrap justify-end gap-2 border-t border-border-subtle pt-3\">\n <button\n type=\"button\"\n onClick={() => setDialog('dismiss')}\n data-testid=\"dismiss-decision\"\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <XCircle className=\"h-3.5 w-3.5\" aria-hidden />\n {tr('decisions.card.dismiss', 'Dismiss')}\n </button>\n <button\n type=\"button\"\n onClick={() => setDialog('apply')}\n data-testid=\"apply-decision\"\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium hover:opacity-90',\n destructive\n ? 'border border-status-error-text/40 bg-status-error-bg-subtle text-status-error-text'\n : 'bg-action-primary-bg text-action-primary-text'\n )}\n >\n <CheckCircle2 className=\"h-3.5 w-3.5\" aria-hidden />\n {tr('decisions.card.apply', 'Review and apply')}\n </button>\n </div>\n )}\n\n {dialog === 'apply' && (\n <ApplyDecisionDialog decision={decision} onClose={() => setDialog(null)} />\n )}\n {dialog === 'dismiss' && (\n <DismissDecisionDialog decision={decision} onClose={() => setDialog(null)} />\n )}\n </GlassCard>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"DecisionCard.js","names":[],"sources":["../../../../src/subscriberbot/components/decisions/DecisionCard.tsx"],"sourcesContent":["/**\n * Decision card + apply / dismiss dialogs\n * @module subscriberbot/components/decisions\n *\n * Renders one `SubDecision` from the decision engine: the recommendation, the\n * rationale, confidence, estimated annual saving, the relationship it concerns\n * and the source rows it reasoned over. Apply and dismiss go through\n * `subApplyDecision` / `subDismissDecision`.\n *\n * Apply always opens a confirmation dialog that names the relationship and its\n * amount. For a destructive recommendation (cancel) the Apply button stays\n * disabled until the user ticks an acknowledgement that names the same\n * relationship and amount, so a cancellation can never happen on a misclick.\n */\n\nimport { useState, type FC } from 'react';\nimport { CheckCircle2, ExternalLink, Gauge, PiggyBank, ShieldAlert, XCircle } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { Modal } from '../Modal';\nimport { StatusBadge } from '../StatusBadge';\nimport { useTr } from '../../hooks/useTr';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useApplyDecision, useDismissDecision } from '../../hooks/useSubscriberBotApi';\nimport { formatCurrency, formatEnumLabel, formatRelativeTime } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { SubDecision } from '../../types';\nimport {\n DECISION_KIND_LABEL,\n RECOMMENDATION_LABEL,\n confidencePercent,\n evidenceSources,\n isActionable,\n isDestructive,\n relationshipSnapshot,\n} from './decisionModel';\n\nfunction errorText(err: unknown, fallback: string): string {\n return err instanceof Error && err.message ? err.message : fallback;\n}\n\n/** \"Netflix Premium · 15.49 USD / monthly\" — the identity a confirmation must name. */\nfunction useRelationshipIdentity(decision: SubDecision): { label: string; amount: string | null } {\n const snap = relationshipSnapshot(decision);\n const amount =\n snap.amount !== null\n ? `${formatCurrency(snap.amount, snap.currency ?? decision.currency)}${snap.interval ? ` / ${formatEnumLabel(snap.interval).toLowerCase()}` : ''}`\n : null;\n return { label: snap.label ?? decision.relationshipId, amount };\n}\n\n// ---------------------------------------------------------------------------\n// Apply confirmation\n// ---------------------------------------------------------------------------\n\nexport const ApplyDecisionDialog: FC<{\n decision: SubDecision;\n onClose: () => void;\n onApplied?: (decision: SubDecision) => void;\n}> = ({ decision, onClose, onApplied }) => {\n const tr = useTr();\n const apply = useApplyDecision();\n const [acknowledged, setAcknowledged] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const destructive = isDestructive(decision.recommendation);\n const identity = useRelationshipIdentity(decision);\n const rec = RECOMMENDATION_LABEL[decision.recommendation];\n const canSubmit = !apply.isPending && (!destructive || acknowledged);\n\n const submit = () => {\n if (!canSubmit) return;\n setError(null);\n apply.mutate(decision.id, {\n onSuccess: (result) => {\n onApplied?.(result.decision);\n onClose();\n },\n onError: (err) =>\n setError(errorText(err, tr('decisions.apply.error', 'Could not apply this decision.'))),\n });\n };\n\n return (\n <Modal\n title={tr('decisions.apply.title', 'Apply: {{action}}', {\n action: tr(rec.key, rec.fallback),\n })}\n description={tr(\n 'decisions.apply.description',\n 'This runs the same change you could make by hand on the relationship page.'\n )}\n onClose={onClose}\n >\n <div className=\"space-y-4\" data-testid=\"apply-decision-dialog\">\n <div className=\"rounded-lg border border-border-subtle bg-bg-sunken px-4 py-3\">\n <p className=\"text-xs text-text-secondary\">\n {tr('decisions.apply.relationship', 'Subscription')}\n </p>\n <p className=\"text-sm font-semibold text-text-primary\" data-testid=\"confirm-label\">\n {identity.label}\n </p>\n {identity.amount && (\n <p className=\"text-sm tabular-nums text-text-primary\" data-testid=\"confirm-amount\">\n {identity.amount}\n </p>\n )}\n </div>\n <p className=\"text-sm text-text-secondary\">{decision.rationale}</p>\n\n {destructive && (\n <label className=\"flex items-start gap-2.5 rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-3 text-sm text-status-error-text\">\n <input\n type=\"checkbox\"\n checked={acknowledged}\n onChange={(e) => setAcknowledged(e.target.checked)}\n className=\"mt-0.5 size-4 shrink-0 rounded border-border-subtle\"\n data-testid=\"confirm-destructive\"\n />\n <span>\n {tr(\n 'decisions.apply.acknowledgeCancel',\n 'I want to cancel \"{{label}}\"{{amount}}. I understand the subscription will stop.',\n {\n label: identity.label,\n amount: identity.amount ? ` (${identity.amount})` : '',\n }\n )}\n </span>\n </label>\n )}\n\n {error && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {error}\n </div>\n )}\n\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n {tr('decisions.apply.back', 'Not now')}\n </button>\n <button\n type=\"button\"\n onClick={submit}\n disabled={!canSubmit}\n data-testid=\"confirm-apply\"\n className={cn(\n 'rounded-lg px-4 py-2 text-sm font-medium hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50',\n destructive\n ? 'bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bg-hover'\n : 'bg-action-primary-bg text-action-primary-text'\n )}\n >\n {apply.isPending\n ? tr('decisions.apply.applying', 'Applying…')\n : tr('decisions.apply.submit', 'Apply')}\n </button>\n </div>\n </div>\n </Modal>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Dismiss\n// ---------------------------------------------------------------------------\n\nexport const DismissDecisionDialog: FC<{ decision: SubDecision; onClose: () => void }> = ({\n decision,\n onClose,\n}) => {\n const tr = useTr();\n const dismiss = useDismissDecision();\n const [reason, setReason] = useState('');\n const [error, setError] = useState<string | null>(null);\n const identity = useRelationshipIdentity(decision);\n\n const submit = (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n dismiss.mutate(\n { id: decision.id, reason },\n {\n onSuccess: () => onClose(),\n onError: (err) =>\n setError(\n errorText(err, tr('decisions.dismiss.error', 'Could not dismiss this decision.'))\n ),\n }\n );\n };\n\n return (\n <Modal\n title={tr('decisions.dismiss.title', 'Dismiss recommendation')}\n description={identity.label}\n onClose={onClose}\n >\n <form onSubmit={submit} className=\"space-y-4\" data-testid=\"dismiss-decision-dialog\">\n <div>\n <label\n htmlFor={`dismiss-reason-${decision.id}`}\n className=\"mb-1 block text-xs font-medium text-text-secondary\"\n >\n {tr('decisions.dismiss.reason', 'Reason (optional)')}\n </label>\n <textarea\n id={`dismiss-reason-${decision.id}`}\n value={reason}\n onChange={(e) => setReason(e.target.value)}\n rows={3}\n maxLength={500}\n placeholder={tr('decisions.dismiss.placeholder', 'e.g. We still use this every week')}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n {error && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {error}\n </div>\n )}\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n {tr('decisions.dismiss.back', 'Back')}\n </button>\n <button\n type=\"submit\"\n disabled={dismiss.isPending}\n data-testid=\"confirm-dismiss\"\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {dismiss.isPending\n ? tr('decisions.dismiss.dismissing', 'Dismissing…')\n : tr('decisions.dismiss.submit', 'Dismiss')}\n </button>\n </div>\n </form>\n </Modal>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Source rows\n// ---------------------------------------------------------------------------\n\nexport const DecisionSources: FC<{ decision: SubDecision; limit?: number }> = ({\n decision,\n limit,\n}) => {\n const tr = useTr();\n const { navigateTo } = useSubscriberBot();\n const all = evidenceSources(decision);\n const shown = limit ? all.slice(0, limit) : all;\n const kindLabel: Record<string, string> = {\n relationship: tr('decisions.sources.relationship', 'Subscription'),\n keptRelationship: tr('decisions.sources.keptRelationship', 'Keep instead'),\n plan: tr('decisions.sources.plan', 'Current plan'),\n alternativePlan: tr('decisions.sources.alternativePlan', 'Suggested plan'),\n renewalEvent: tr('decisions.sources.renewalEvent', 'Renewal event'),\n entitlement: tr('decisions.sources.entitlement', 'Entitlement usage'),\n };\n return (\n <div>\n <p className=\"text-[10px] font-medium uppercase tracking-wide text-text-secondary\">\n {tr('decisions.sources.title', 'Based on')}\n </p>\n <ul className=\"mt-1.5 flex flex-wrap gap-1.5\" data-testid=\"decision-sources\">\n {shown.map((source) => (\n <li key={`${source.kind}-${source.id}`}>\n <button\n type=\"button\"\n onClick={() => navigateTo(source.path)}\n data-source-kind={source.kind}\n className=\"inline-flex max-w-full items-center gap-1.5 rounded-md border border-border-subtle bg-bg-sunken px-2 py-0.5 text-left text-[11px] text-text-secondary hover:text-text-primary\"\n >\n <span className=\"font-medium text-text-primary\">{kindLabel[source.kind]}</span>\n <span className=\"truncate\">{source.title}</span>\n {source.detail && <span className=\"tabular-nums\">{source.detail}</span>}\n <ExternalLink className=\"h-3 w-3 shrink-0\" aria-hidden />\n </button>\n </li>\n ))}\n {limit && all.length > limit && (\n <li className=\"px-1 py-0.5 text-[11px] text-text-secondary\">\n {tr('decisions.sources.more', '+{{count}} more', { count: all.length - limit })}\n </li>\n )}\n </ul>\n </div>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Card\n// ---------------------------------------------------------------------------\n\nexport const DecisionCard: FC<{\n decision: SubDecision;\n /** Hide the agent name (e.g. on the agent's own page). */\n hideAgent?: boolean;\n}> = ({ decision, hideAgent = false }) => {\n const tr = useTr();\n const { navigateTo } = useSubscriberBot();\n const [dialog, setDialog] = useState<'apply' | 'dismiss' | null>(null);\n const kind = DECISION_KIND_LABEL[decision.kind];\n const rec = RECOMMENDATION_LABEL[decision.recommendation];\n const destructive = isDestructive(decision.recommendation);\n const actionable = isActionable(decision);\n const identity = useRelationshipIdentity(decision);\n\n return (\n <GlassCard\n treatment=\"metric\"\n className=\"flex flex-col gap-3 p-4\"\n data-testid=\"decision-card\"\n data-decision-id={decision.id}\n data-recommendation={decision.recommendation}\n data-status={decision.status}\n >\n <div className=\"flex flex-wrap items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n {tr(kind.key, kind.fallback)}\n {!hideAgent && decision.agent ? ` · ${decision.agent.name}` : ''}\n </p>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/decisions/${decision.id}`)}\n className=\"mt-0.5 flex items-center gap-1.5 text-left text-sm font-semibold text-text-primary hover:underline\"\n >\n {destructive && (\n <ShieldAlert className=\"h-4 w-4 shrink-0 text-status-error-text\" aria-hidden />\n )}\n {tr(rec.key, rec.fallback)}: {identity.label}\n </button>\n {identity.amount && (\n <p className=\"text-xs tabular-nums text-text-secondary\">{identity.amount}</p>\n )}\n </div>\n <StatusBadge status={decision.status} />\n </div>\n\n <p className=\"text-sm text-text-secondary\">{decision.rationale}</p>\n\n <div className=\"flex flex-wrap gap-2 text-xs\">\n <span className=\"inline-flex items-center gap-1.5 rounded-md bg-bg-sunken px-2 py-0.5 text-text-secondary\">\n <Gauge className=\"h-3.5 w-3.5\" aria-hidden />\n {tr('decisions.card.confidence', 'Confidence {{pct}}%', {\n pct: confidencePercent(decision.confidence),\n })}\n </span>\n {decision.estimatedSavings > 0 && (\n <span className=\"inline-flex items-center gap-1.5 rounded-md bg-status-success-bg-subtle px-2 py-0.5 font-medium text-status-success-text tabular-nums\">\n <PiggyBank className=\"h-3.5 w-3.5\" aria-hidden />\n {tr('decisions.card.savings', 'Saves {{amount}} a year', {\n amount: formatCurrency(decision.estimatedSavings, decision.currency),\n })}\n </span>\n )}\n <span className=\"inline-flex items-center rounded-md bg-bg-sunken px-2 py-0.5 text-text-secondary\">\n {formatRelativeTime(decision.createdAt)}\n </span>\n </div>\n\n <DecisionSources decision={decision} limit={4} />\n\n {decision.status === 'FLAGGED' && decision.validationErrors.length > 0 && (\n <p className=\"rounded-md bg-status-warning-bg-subtle px-3 py-2 text-xs text-status-warning-text\">\n {tr('decisions.card.flagged', 'Held back: failed validation')} —{' '}\n {decision.validationErrors.join('; ')}\n </p>\n )}\n {decision.status === 'DISMISSED' && decision.dismissedReason && (\n <p className=\"text-xs text-text-secondary\">\n {tr('decisions.card.dismissedReason', 'Dismissed: {{reason}}', {\n reason: decision.dismissedReason,\n })}\n </p>\n )}\n\n {actionable && (\n <div className=\"flex flex-wrap justify-end gap-2 border-t border-border-subtle pt-3\">\n <button\n type=\"button\"\n onClick={() => setDialog('dismiss')}\n data-testid=\"dismiss-decision\"\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <XCircle className=\"h-3.5 w-3.5\" aria-hidden />\n {tr('decisions.card.dismiss', 'Dismiss')}\n </button>\n <button\n type=\"button\"\n onClick={() => setDialog('apply')}\n data-testid=\"apply-decision\"\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium hover:opacity-90',\n destructive\n ? 'border border-status-error-text/40 bg-status-error-bg-subtle text-status-error-text'\n : 'bg-action-primary-bg text-action-primary-text'\n )}\n >\n <CheckCircle2 className=\"h-3.5 w-3.5\" aria-hidden />\n {tr('decisions.card.apply', 'Review and apply')}\n </button>\n </div>\n )}\n\n {dialog === 'apply' && (\n <ApplyDecisionDialog decision={decision} onClose={() => setDialog(null)} />\n )}\n {dialog === 'dismiss' && (\n <DismissDecisionDialog decision={decision} onClose={() => setDialog(null)} />\n )}\n </GlassCard>\n );\n};\n"],"mappings":"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"}
@@ -35,7 +35,7 @@ export interface RelationshipSnapshot {
35
35
  export declare function relationshipSnapshot(decision: SubDecision): RelationshipSnapshot;
36
36
  /** One source row a decision reasoned over, with the in-app page that shows it. */
37
37
  export interface EvidenceSource {
38
- kind: 'relationship' | 'plan' | 'alternativePlan' | 'renewalEvent' | 'entitlement';
38
+ kind: 'relationship' | 'keptRelationship' | 'plan' | 'alternativePlan' | 'renewalEvent' | 'entitlement';
39
39
  id: string;
40
40
  title: string;
41
41
  detail: string | null;
@@ -44,7 +44,8 @@ export interface EvidenceSource {
44
44
  }
45
45
  /**
46
46
  * Every source row in a decision's evidence, in reading order: the
47
- * relationship, its current plan, the alternative plan (if any), then the
47
+ * relationship, the relationship to keep instead (overlap consolidation,
48
+ * BOFF-3703), its current plan, the alternative plan (if any), then the
48
49
  * renewal events and entitlements the strategy inspected.
49
50
  */
50
51
  export declare function evidenceSources(decision: SubDecision): EvidenceSource[];
@@ -1 +1 @@
1
- {"version":3,"file":"decisionModel.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/components/decisions/decisionModel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,eAAe,EACf,yBAAyB,EACzB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAErB,oFAAoF;AACpF,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,YAAY,CAKzD,CAAC;AAEH,eAAO,MAAM,cAAc,EAAE,eAAe,EAK3C,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,iBAAiB,EAOhD,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,eAAe,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAK1F,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,MAAM,CACvC,yBAAyB,EACzB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAWlC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,cAAc,EAAE,yBAAyB,GAAG,OAAO,CAEhF;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,OAAO,CAE3E;AAED,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAG5D;AAgBD,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,WAAW,GAAG,oBAAoB,CAahF;AAED,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,cAAc,GAAG,MAAM,GAAG,iBAAiB,GAAG,cAAc,GAAG,aAAa,CAAC;IACnF,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;CACd;AA4BD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,GAAG,cAAc,EAAE,CAsEvE"}
1
+ {"version":3,"file":"decisionModel.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/components/decisions/decisionModel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,eAAe,EACf,yBAAyB,EACzB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAErB,oFAAoF;AACpF,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,YAAY,CAKzD,CAAC;AAEH,eAAO,MAAM,cAAc,EAAE,eAAe,EAM3C,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,iBAAiB,EAOhD,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,eAAe,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAS1F,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,MAAM,CACvC,yBAAyB,EACzB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAWlC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,cAAc,EAAE,yBAAyB,GAAG,OAAO,CAEhF;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,OAAO,CAE3E;AAED,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAG5D;AAgBD,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,WAAW,GAAG,oBAAoB,CAahF;AAED,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,IAAI,EACA,cAAc,GACd,kBAAkB,GAClB,MAAM,GACN,iBAAiB,GACjB,cAAc,GACd,aAAa,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;CACd;AA4BD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,GAAG,cAAc,EAAE,CAiFvE"}
@@ -8,7 +8,8 @@ var e = new Set([
8
8
  "CANCEL_CANDIDATE",
9
9
  "RENEWAL_TRIAGE",
10
10
  "PLAN_RIGHT_SIZE",
11
- "TRIAL_EXPIRY"
11
+ "TRIAL_EXPIRY",
12
+ "CONSOLIDATE_OVERLAP"
12
13
  ], n = {
13
14
  CANCEL_CANDIDATE: {
14
15
  key: "decisions.kinds.cancelCandidate",
@@ -25,6 +26,10 @@ var e = new Set([
25
26
  TRIAL_EXPIRY: {
26
27
  key: "decisions.kinds.trialExpiry",
27
28
  fallback: "Trial expiry"
29
+ },
30
+ CONSOLIDATE_OVERLAP: {
31
+ key: "decisions.kinds.consolidateOverlap",
32
+ fallback: "Overlap consolidation"
28
33
  }
29
34
  }, r = {
30
35
  CANCEL: {
@@ -104,10 +109,18 @@ function f(e) {
104
109
  path: r
105
110
  }];
106
111
  if (!t) return i;
107
- let a = d("plan", t.plan, n.providerId, r);
108
- a && i.push(a);
109
- let o = d("alternativePlan", t.alternativePlan, n.providerId, r);
110
- if (o && i.push(o), Array.isArray(t.renewalEvents)) for (let e of t.renewalEvents) {
112
+ let a = c(t.keepRelationshipId);
113
+ a && a !== n.id && i.push({
114
+ kind: "keptRelationship",
115
+ id: a,
116
+ title: c(t.keepLabel) ?? a,
117
+ detail: null,
118
+ path: `/relationships/${a}`
119
+ });
120
+ let o = d("plan", t.plan, n.providerId, r);
121
+ o && i.push(o);
122
+ let f = d("alternativePlan", t.alternativePlan, n.providerId, r);
123
+ if (f && i.push(f), Array.isArray(t.renewalEvents)) for (let e of t.renewalEvents) {
111
124
  let t = s(e), n = c(t?.id);
112
125
  if (!t || !n) continue;
113
126
  let a = l(t.amount), o = c(t.occursAt);
@@ -1 +1 @@
1
- {"version":3,"file":"decisionModel.js","names":[],"sources":["../../../../src/subscriberbot/components/decisions/decisionModel.ts"],"sourcesContent":["/**\n * Decision view-model helpers\n * @module subscriberbot/components/decisions\n *\n * Pure functions that turn a `SubDecision` (as returned by the BOFF-3634\n * decision engine) into what the decision cards render: whether applying it is\n * destructive, which source rows it reasoned over, and the labels for its\n * enums. `evidence` is `JSON!` on the svc contract, so every read here is\n * defensive: a missing or malformed field yields nothing rather than a guess.\n */\n\nimport type {\n SubAgentKind,\n SubDecision,\n SubDecisionKind,\n SubDecisionRecommendation,\n SubDecisionStatus,\n} from '../../types';\n\n/** Agent kinds the decision engine implements; the rest are refused server-side. */\nexport const RUNNABLE_AGENT_KINDS: ReadonlySet<SubAgentKind> = new Set<SubAgentKind>([\n 'COST',\n 'RENEWAL',\n 'VENDOR',\n 'DISCOVERY',\n]);\n\nexport const DECISION_KINDS: SubDecisionKind[] = [\n 'CANCEL_CANDIDATE',\n 'RENEWAL_TRIAGE',\n 'PLAN_RIGHT_SIZE',\n 'TRIAL_EXPIRY',\n];\n\nexport const DECISION_STATUSES: SubDecisionStatus[] = [\n 'PROPOSED',\n 'APPLIED',\n 'DISMISSED',\n 'STALE',\n 'FLAGGED',\n 'APPLYING',\n];\n\nexport const DECISION_KIND_LABEL: Record<SubDecisionKind, { key: string; fallback: string }> = {\n CANCEL_CANDIDATE: { key: 'decisions.kinds.cancelCandidate', fallback: 'Cancel candidate' },\n RENEWAL_TRIAGE: { key: 'decisions.kinds.renewalTriage', fallback: 'Renewal triage' },\n PLAN_RIGHT_SIZE: { key: 'decisions.kinds.planRightSize', fallback: 'Plan right-sizing' },\n TRIAL_EXPIRY: { key: 'decisions.kinds.trialExpiry', fallback: 'Trial expiry' },\n};\n\nexport const RECOMMENDATION_LABEL: Record<\n SubDecisionRecommendation,\n { key: string; fallback: string }\n> = {\n CANCEL: { key: 'decisions.recommendations.cancel', fallback: 'Cancel subscription' },\n PAUSE: { key: 'decisions.recommendations.pause', fallback: 'Pause subscription' },\n RENEW: { key: 'decisions.recommendations.renew', fallback: 'Let it renew' },\n DOWNGRADE: { key: 'decisions.recommendations.downgrade', fallback: 'Downgrade plan' },\n SWITCH_TO_ANNUAL: {\n key: 'decisions.recommendations.switchToAnnual',\n fallback: 'Switch to annual billing',\n },\n CONVERT_TRIAL: { key: 'decisions.recommendations.convertTrial', fallback: 'Keep after trial' },\n};\n\n/**\n * Recommendations whose apply ends a paid relationship. These need an\n * explicit, acknowledged confirmation that names the relationship and amount.\n */\nexport function isDestructive(recommendation: SubDecisionRecommendation): boolean {\n return recommendation === 'CANCEL';\n}\n\n/** Only PROPOSED decisions can be applied or dismissed. */\nexport function isActionable(decision: Pick<SubDecision, 'status'>): boolean {\n return decision.status === 'PROPOSED';\n}\n\n/** Confidence (0..1) as a whole percentage, clamped. */\nexport function confidencePercent(confidence: number): number {\n if (!Number.isFinite(confidence)) return 0;\n return Math.round(Math.min(1, Math.max(0, confidence)) * 100);\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction asString(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n\nfunction asNumber(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n\n/** The relationship facts a decision was made against (its evidence snapshot). */\nexport interface RelationshipSnapshot {\n id: string;\n label: string | null;\n status: string | null;\n amount: number | null;\n currency: string | null;\n interval: string | null;\n providerId: string | null;\n planId: string | null;\n}\n\nexport function relationshipSnapshot(decision: SubDecision): RelationshipSnapshot {\n const snap = asRecord(asRecord(decision.evidence)?.relationship);\n const live = decision.relationship ?? null;\n return {\n id: asString(snap?.id) ?? decision.relationshipId,\n label: asString(snap?.label) ?? live?.label ?? null,\n status: asString(snap?.status) ?? live?.status ?? null,\n amount: asNumber(snap?.amount) ?? live?.amount ?? null,\n currency: asString(snap?.currency) ?? live?.currency ?? decision.currency,\n interval: asString(snap?.interval) ?? live?.interval ?? null,\n providerId: asString(snap?.providerId) ?? live?.providerId ?? null,\n planId: asString(snap?.planId) ?? live?.planId ?? null,\n };\n}\n\n/** One source row a decision reasoned over, with the in-app page that shows it. */\nexport interface EvidenceSource {\n kind: 'relationship' | 'plan' | 'alternativePlan' | 'renewalEvent' | 'entitlement';\n id: string;\n title: string;\n detail: string | null;\n /** MFE-relative path (`navigateTo`) of the page where this row is visible. */\n path: string;\n}\n\nfunction planSource(\n kind: 'plan' | 'alternativePlan',\n raw: unknown,\n providerId: string | null,\n relationshipPath: string\n): EvidenceSource | null {\n const plan = asRecord(raw);\n const id = asString(plan?.id);\n if (!plan || !id) return null;\n const amount = asNumber(plan.amount);\n const currency = asString(plan.currency);\n const interval = asString(plan.interval);\n return {\n kind,\n id,\n title: asString(plan.name) ?? id,\n detail:\n amount !== null\n ? `${amount.toFixed(2)}${currency ? ` ${currency}` : ''}${interval ? ` / ${interval.toLowerCase()}` : ''}`\n : null,\n // Plans are listed on their provider's page; without a provider the\n // relationship page still shows the plan the subscription is on.\n path: providerId ? `/providers/${providerId}` : relationshipPath,\n };\n}\n\n/**\n * Every source row in a decision's evidence, in reading order: the\n * relationship, its current plan, the alternative plan (if any), then the\n * renewal events and entitlements the strategy inspected.\n */\nexport function evidenceSources(decision: SubDecision): EvidenceSource[] {\n const evidence = asRecord(decision.evidence);\n const snap = relationshipSnapshot(decision);\n const relationshipPath = `/relationships/${snap.id}`;\n const sources: EvidenceSource[] = [\n {\n kind: 'relationship',\n id: snap.id,\n title: snap.label ?? snap.id,\n detail:\n snap.amount !== null\n ? `${snap.amount.toFixed(2)}${snap.currency ? ` ${snap.currency}` : ''}${snap.interval ? ` / ${snap.interval.toLowerCase()}` : ''}`\n : null,\n path: relationshipPath,\n },\n ];\n if (!evidence) return sources;\n\n const plan = planSource('plan', evidence.plan, snap.providerId, relationshipPath);\n if (plan) sources.push(plan);\n const alternative = planSource(\n 'alternativePlan',\n evidence.alternativePlan,\n snap.providerId,\n relationshipPath\n );\n if (alternative) sources.push(alternative);\n\n if (Array.isArray(evidence.renewalEvents)) {\n for (const raw of evidence.renewalEvents) {\n const ev = asRecord(raw);\n const id = asString(ev?.id);\n if (!ev || !id) continue;\n const amount = asNumber(ev.amount);\n const occursAt = asString(ev.occursAt);\n sources.push({\n kind: 'renewalEvent',\n id,\n title: asString(ev.eventType) ?? id,\n detail:\n [\n occursAt ? occursAt.slice(0, 10) : null,\n amount !== null\n ? `${amount.toFixed(2)}${asString(ev.currency) ? ` ${String(ev.currency)}` : ''}`\n : null,\n ]\n .filter(Boolean)\n .join(' · ') || null,\n path: relationshipPath,\n });\n }\n }\n\n if (Array.isArray(evidence.entitlements)) {\n for (const raw of evidence.entitlements) {\n const ent = asRecord(raw);\n const id = asString(ent?.id);\n if (!ent || !id) continue;\n const used = asNumber(ent.used);\n const quota = asNumber(ent.quota);\n sources.push({\n kind: 'entitlement',\n id,\n title: asString(ent.name) ?? id,\n detail: used !== null ? `${used}${quota !== null ? ` / ${quota}` : ''} used` : null,\n path: relationshipPath,\n });\n }\n }\n return sources;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"decisionModel.js","names":[],"sources":["../../../../src/subscriberbot/components/decisions/decisionModel.ts"],"sourcesContent":["/**\n * Decision view-model helpers\n * @module subscriberbot/components/decisions\n *\n * Pure functions that turn a `SubDecision` (as returned by the BOFF-3634\n * decision engine) into what the decision cards render: whether applying it is\n * destructive, which source rows it reasoned over, and the labels for its\n * enums. `evidence` is `JSON!` on the svc contract, so every read here is\n * defensive: a missing or malformed field yields nothing rather than a guess.\n */\n\nimport type {\n SubAgentKind,\n SubDecision,\n SubDecisionKind,\n SubDecisionRecommendation,\n SubDecisionStatus,\n} from '../../types';\n\n/** Agent kinds the decision engine implements; the rest are refused server-side. */\nexport const RUNNABLE_AGENT_KINDS: ReadonlySet<SubAgentKind> = new Set<SubAgentKind>([\n 'COST',\n 'RENEWAL',\n 'VENDOR',\n 'DISCOVERY',\n]);\n\nexport const DECISION_KINDS: SubDecisionKind[] = [\n 'CANCEL_CANDIDATE',\n 'RENEWAL_TRIAGE',\n 'PLAN_RIGHT_SIZE',\n 'TRIAL_EXPIRY',\n 'CONSOLIDATE_OVERLAP',\n];\n\nexport const DECISION_STATUSES: SubDecisionStatus[] = [\n 'PROPOSED',\n 'APPLIED',\n 'DISMISSED',\n 'STALE',\n 'FLAGGED',\n 'APPLYING',\n];\n\nexport const DECISION_KIND_LABEL: Record<SubDecisionKind, { key: string; fallback: string }> = {\n CANCEL_CANDIDATE: { key: 'decisions.kinds.cancelCandidate', fallback: 'Cancel candidate' },\n RENEWAL_TRIAGE: { key: 'decisions.kinds.renewalTriage', fallback: 'Renewal triage' },\n PLAN_RIGHT_SIZE: { key: 'decisions.kinds.planRightSize', fallback: 'Plan right-sizing' },\n TRIAL_EXPIRY: { key: 'decisions.kinds.trialExpiry', fallback: 'Trial expiry' },\n CONSOLIDATE_OVERLAP: {\n key: 'decisions.kinds.consolidateOverlap',\n fallback: 'Overlap consolidation',\n },\n};\n\nexport const RECOMMENDATION_LABEL: Record<\n SubDecisionRecommendation,\n { key: string; fallback: string }\n> = {\n CANCEL: { key: 'decisions.recommendations.cancel', fallback: 'Cancel subscription' },\n PAUSE: { key: 'decisions.recommendations.pause', fallback: 'Pause subscription' },\n RENEW: { key: 'decisions.recommendations.renew', fallback: 'Let it renew' },\n DOWNGRADE: { key: 'decisions.recommendations.downgrade', fallback: 'Downgrade plan' },\n SWITCH_TO_ANNUAL: {\n key: 'decisions.recommendations.switchToAnnual',\n fallback: 'Switch to annual billing',\n },\n CONVERT_TRIAL: { key: 'decisions.recommendations.convertTrial', fallback: 'Keep after trial' },\n};\n\n/**\n * Recommendations whose apply ends a paid relationship. These need an\n * explicit, acknowledged confirmation that names the relationship and amount.\n */\nexport function isDestructive(recommendation: SubDecisionRecommendation): boolean {\n return recommendation === 'CANCEL';\n}\n\n/** Only PROPOSED decisions can be applied or dismissed. */\nexport function isActionable(decision: Pick<SubDecision, 'status'>): boolean {\n return decision.status === 'PROPOSED';\n}\n\n/** Confidence (0..1) as a whole percentage, clamped. */\nexport function confidencePercent(confidence: number): number {\n if (!Number.isFinite(confidence)) return 0;\n return Math.round(Math.min(1, Math.max(0, confidence)) * 100);\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n}\n\nfunction asString(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n\nfunction asNumber(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n\n/** The relationship facts a decision was made against (its evidence snapshot). */\nexport interface RelationshipSnapshot {\n id: string;\n label: string | null;\n status: string | null;\n amount: number | null;\n currency: string | null;\n interval: string | null;\n providerId: string | null;\n planId: string | null;\n}\n\nexport function relationshipSnapshot(decision: SubDecision): RelationshipSnapshot {\n const snap = asRecord(asRecord(decision.evidence)?.relationship);\n const live = decision.relationship ?? null;\n return {\n id: asString(snap?.id) ?? decision.relationshipId,\n label: asString(snap?.label) ?? live?.label ?? null,\n status: asString(snap?.status) ?? live?.status ?? null,\n amount: asNumber(snap?.amount) ?? live?.amount ?? null,\n currency: asString(snap?.currency) ?? live?.currency ?? decision.currency,\n interval: asString(snap?.interval) ?? live?.interval ?? null,\n providerId: asString(snap?.providerId) ?? live?.providerId ?? null,\n planId: asString(snap?.planId) ?? live?.planId ?? null,\n };\n}\n\n/** One source row a decision reasoned over, with the in-app page that shows it. */\nexport interface EvidenceSource {\n kind:\n | 'relationship'\n | 'keptRelationship'\n | 'plan'\n | 'alternativePlan'\n | 'renewalEvent'\n | 'entitlement';\n id: string;\n title: string;\n detail: string | null;\n /** MFE-relative path (`navigateTo`) of the page where this row is visible. */\n path: string;\n}\n\nfunction planSource(\n kind: 'plan' | 'alternativePlan',\n raw: unknown,\n providerId: string | null,\n relationshipPath: string\n): EvidenceSource | null {\n const plan = asRecord(raw);\n const id = asString(plan?.id);\n if (!plan || !id) return null;\n const amount = asNumber(plan.amount);\n const currency = asString(plan.currency);\n const interval = asString(plan.interval);\n return {\n kind,\n id,\n title: asString(plan.name) ?? id,\n detail:\n amount !== null\n ? `${amount.toFixed(2)}${currency ? ` ${currency}` : ''}${interval ? ` / ${interval.toLowerCase()}` : ''}`\n : null,\n // Plans are listed on their provider's page; without a provider the\n // relationship page still shows the plan the subscription is on.\n path: providerId ? `/providers/${providerId}` : relationshipPath,\n };\n}\n\n/**\n * Every source row in a decision's evidence, in reading order: the\n * relationship, the relationship to keep instead (overlap consolidation,\n * BOFF-3703), its current plan, the alternative plan (if any), then the\n * renewal events and entitlements the strategy inspected.\n */\nexport function evidenceSources(decision: SubDecision): EvidenceSource[] {\n const evidence = asRecord(decision.evidence);\n const snap = relationshipSnapshot(decision);\n const relationshipPath = `/relationships/${snap.id}`;\n const sources: EvidenceSource[] = [\n {\n kind: 'relationship',\n id: snap.id,\n title: snap.label ?? snap.id,\n detail:\n snap.amount !== null\n ? `${snap.amount.toFixed(2)}${snap.currency ? ` ${snap.currency}` : ''}${snap.interval ? ` / ${snap.interval.toLowerCase()}` : ''}`\n : null,\n path: relationshipPath,\n },\n ];\n if (!evidence) return sources;\n\n const keepId = asString(evidence.keepRelationshipId);\n if (keepId && keepId !== snap.id) {\n sources.push({\n kind: 'keptRelationship',\n id: keepId,\n title: asString(evidence.keepLabel) ?? keepId,\n detail: null,\n path: `/relationships/${keepId}`,\n });\n }\n\n const plan = planSource('plan', evidence.plan, snap.providerId, relationshipPath);\n if (plan) sources.push(plan);\n const alternative = planSource(\n 'alternativePlan',\n evidence.alternativePlan,\n snap.providerId,\n relationshipPath\n );\n if (alternative) sources.push(alternative);\n\n if (Array.isArray(evidence.renewalEvents)) {\n for (const raw of evidence.renewalEvents) {\n const ev = asRecord(raw);\n const id = asString(ev?.id);\n if (!ev || !id) continue;\n const amount = asNumber(ev.amount);\n const occursAt = asString(ev.occursAt);\n sources.push({\n kind: 'renewalEvent',\n id,\n title: asString(ev.eventType) ?? id,\n detail:\n [\n occursAt ? occursAt.slice(0, 10) : null,\n amount !== null\n ? `${amount.toFixed(2)}${asString(ev.currency) ? ` ${String(ev.currency)}` : ''}`\n : null,\n ]\n .filter(Boolean)\n .join(' · ') || null,\n path: relationshipPath,\n });\n }\n }\n\n if (Array.isArray(evidence.entitlements)) {\n for (const raw of evidence.entitlements) {\n const ent = asRecord(raw);\n const id = asString(ent?.id);\n if (!ent || !id) continue;\n const used = asNumber(ent.used);\n const quota = asNumber(ent.quota);\n sources.push({\n kind: 'entitlement',\n id,\n title: asString(ent.name) ?? id,\n detail: used !== null ? `${used}${quota !== null ? ` / ${quota}` : ''} used` : null,\n path: relationshipPath,\n });\n }\n }\n return sources;\n}\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { FC } from 'react';
2
+ import { SubBundleRecommendation, SubConsolidationResult, SubDecision } from '../../types';
3
+
4
+ export declare const SavingsCard: FC<{
5
+ decision: SubDecision;
6
+ }>;
7
+ export declare const BundleCard: FC<{
8
+ bundle: SubBundleRecommendation;
9
+ onConsolidated?: (result: SubConsolidationResult) => void;
10
+ }>;
11
+ //# sourceMappingURL=SavingsCards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SavingsCards.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/components/savings/SavingsCards.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAY,KAAK,EAAE,EAAkB,MAAM,OAAO,CAAC;AA6B1D,OAAO,KAAK,EAEV,uBAAuB,EACvB,sBAAsB,EACtB,WAAW,EACZ,MAAM,aAAa,CAAC;AA8BrB,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC;IAAE,QAAQ,EAAE,WAAW,CAAA;CAAE,CA0HrD,CAAC;AAgRF,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC;IAC1B,MAAM,EAAE,uBAAuB,CAAC;IAChC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;CAC3D,CAkIA,CAAC"}