@l4yercak3/cli 1.3.2 → 2.0.0-alpha.1

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 (357) hide show
  1. package/README.md +10 -220
  2. package/dist/api/client.d.ts +12 -0
  3. package/dist/api/client.d.ts.map +1 -0
  4. package/dist/api/client.js +37 -0
  5. package/dist/api/client.js.map +1 -0
  6. package/dist/api/platform.d.ts +161 -0
  7. package/dist/api/platform.d.ts.map +1 -0
  8. package/dist/api/platform.js +70 -0
  9. package/dist/api/platform.js.map +1 -0
  10. package/dist/bin/sevenlayers.d.ts +3 -0
  11. package/dist/bin/sevenlayers.d.ts.map +1 -0
  12. package/dist/bin/sevenlayers.js +198 -0
  13. package/dist/bin/sevenlayers.js.map +1 -0
  14. package/dist/commands/agent/catalog.d.ts +5 -0
  15. package/dist/commands/agent/catalog.d.ts.map +1 -0
  16. package/dist/commands/agent/catalog.js +142 -0
  17. package/dist/commands/agent/catalog.js.map +1 -0
  18. package/dist/commands/agent/drift.d.ts +5 -0
  19. package/dist/commands/agent/drift.d.ts.map +1 -0
  20. package/dist/commands/agent/drift.js +113 -0
  21. package/dist/commands/agent/drift.js.map +1 -0
  22. package/dist/commands/agent/init.d.ts +5 -0
  23. package/dist/commands/agent/init.d.ts.map +1 -0
  24. package/dist/commands/agent/init.js +75 -0
  25. package/dist/commands/agent/init.js.map +1 -0
  26. package/dist/commands/agent/permissions.d.ts +5 -0
  27. package/dist/commands/agent/permissions.d.ts.map +1 -0
  28. package/dist/commands/agent/permissions.js +88 -0
  29. package/dist/commands/agent/permissions.js.map +1 -0
  30. package/dist/commands/agent/runner.d.ts +14 -0
  31. package/dist/commands/agent/runner.d.ts.map +1 -0
  32. package/dist/commands/agent/runner.js +59 -0
  33. package/dist/commands/agent/runner.js.map +1 -0
  34. package/dist/commands/agent/shared.d.ts +13 -0
  35. package/dist/commands/agent/shared.d.ts.map +1 -0
  36. package/dist/commands/agent/shared.js +31 -0
  37. package/dist/commands/agent/shared.js.map +1 -0
  38. package/dist/commands/agent/template.d.ts +5 -0
  39. package/dist/commands/agent/template.d.ts.map +1 -0
  40. package/dist/commands/agent/template.js +104 -0
  41. package/dist/commands/agent/template.js.map +1 -0
  42. package/dist/commands/app/connect.d.ts +7 -0
  43. package/dist/commands/app/connect.d.ts.map +1 -0
  44. package/dist/commands/app/connect.js +12 -0
  45. package/dist/commands/app/connect.js.map +1 -0
  46. package/dist/commands/app/init.d.ts +7 -0
  47. package/dist/commands/app/init.d.ts.map +1 -0
  48. package/dist/commands/app/init.js +12 -0
  49. package/dist/commands/app/init.js.map +1 -0
  50. package/dist/commands/app/link.d.ts +3 -0
  51. package/dist/commands/app/link.d.ts.map +1 -0
  52. package/dist/commands/app/link.js +92 -0
  53. package/dist/commands/app/link.js.map +1 -0
  54. package/dist/commands/app/pages.d.ts +15 -0
  55. package/dist/commands/app/pages.d.ts.map +1 -0
  56. package/dist/commands/app/pages.js +180 -0
  57. package/dist/commands/app/pages.js.map +1 -0
  58. package/dist/commands/app/register.d.ts +3 -0
  59. package/dist/commands/app/register.d.ts.map +1 -0
  60. package/dist/commands/app/register.js +120 -0
  61. package/dist/commands/app/register.js.map +1 -0
  62. package/dist/commands/app/remote.d.ts +14 -0
  63. package/dist/commands/app/remote.d.ts.map +1 -0
  64. package/dist/commands/app/remote.js +44 -0
  65. package/dist/commands/app/remote.js.map +1 -0
  66. package/dist/commands/app/setup.d.ts +3 -0
  67. package/dist/commands/app/setup.d.ts.map +1 -0
  68. package/dist/commands/app/setup.js +299 -0
  69. package/dist/commands/app/setup.js.map +1 -0
  70. package/dist/commands/app/shared.d.ts +9 -0
  71. package/dist/commands/app/shared.d.ts.map +1 -0
  72. package/dist/commands/app/shared.js +122 -0
  73. package/dist/commands/app/shared.js.map +1 -0
  74. package/dist/commands/app/sync.d.ts +7 -0
  75. package/dist/commands/app/sync.d.ts.map +1 -0
  76. package/dist/commands/app/sync.js +107 -0
  77. package/dist/commands/app/sync.js.map +1 -0
  78. package/dist/commands/booking/check.d.ts +3 -0
  79. package/dist/commands/booking/check.d.ts.map +1 -0
  80. package/dist/commands/booking/check.js +68 -0
  81. package/dist/commands/booking/check.js.map +1 -0
  82. package/dist/commands/booking/setup.d.ts +3 -0
  83. package/dist/commands/booking/setup.d.ts.map +1 -0
  84. package/dist/commands/booking/setup.js +95 -0
  85. package/dist/commands/booking/setup.js.map +1 -0
  86. package/dist/commands/booking/shared.d.ts +31 -0
  87. package/dist/commands/booking/shared.d.ts.map +1 -0
  88. package/dist/commands/booking/shared.js +112 -0
  89. package/dist/commands/booking/shared.js.map +1 -0
  90. package/dist/commands/booking/smoke.d.ts +3 -0
  91. package/dist/commands/booking/smoke.d.ts.map +1 -0
  92. package/dist/commands/booking/smoke.js +101 -0
  93. package/dist/commands/booking/smoke.js.map +1 -0
  94. package/dist/commands/cms/bind.d.ts +3 -0
  95. package/dist/commands/cms/bind.d.ts.map +1 -0
  96. package/dist/commands/cms/bind.js +212 -0
  97. package/dist/commands/cms/bind.js.map +1 -0
  98. package/dist/commands/cms/content.d.ts +40 -0
  99. package/dist/commands/cms/content.d.ts.map +1 -0
  100. package/dist/commands/cms/content.js +169 -0
  101. package/dist/commands/cms/content.js.map +1 -0
  102. package/dist/commands/cms/doctor.d.ts +3 -0
  103. package/dist/commands/cms/doctor.d.ts.map +1 -0
  104. package/dist/commands/cms/doctor.js +69 -0
  105. package/dist/commands/cms/doctor.js.map +1 -0
  106. package/dist/commands/cms/migrate.d.ts +3 -0
  107. package/dist/commands/cms/migrate.d.ts.map +1 -0
  108. package/dist/commands/cms/migrate.js +78 -0
  109. package/dist/commands/cms/migrate.js.map +1 -0
  110. package/dist/commands/cms/registry.d.ts +3 -0
  111. package/dist/commands/cms/registry.d.ts.map +1 -0
  112. package/dist/commands/cms/registry.js +161 -0
  113. package/dist/commands/cms/registry.js.map +1 -0
  114. package/dist/commands/cms/seed.d.ts +3 -0
  115. package/dist/commands/cms/seed.d.ts.map +1 -0
  116. package/dist/commands/cms/seed.js +102 -0
  117. package/dist/commands/cms/seed.js.map +1 -0
  118. package/dist/commands/cms/shared.d.ts +22 -0
  119. package/dist/commands/cms/shared.d.ts.map +1 -0
  120. package/dist/commands/cms/shared.js +82 -0
  121. package/dist/commands/cms/shared.js.map +1 -0
  122. package/dist/commands/doctor/target.d.ts +3 -0
  123. package/dist/commands/doctor/target.d.ts.map +1 -0
  124. package/dist/commands/doctor/target.js +46 -0
  125. package/dist/commands/doctor/target.js.map +1 -0
  126. package/dist/commands/env/list.d.ts +3 -0
  127. package/dist/commands/env/list.d.ts.map +1 -0
  128. package/dist/commands/env/list.js +28 -0
  129. package/dist/commands/env/list.js.map +1 -0
  130. package/dist/commands/env/set.d.ts +3 -0
  131. package/dist/commands/env/set.d.ts.map +1 -0
  132. package/dist/commands/env/set.js +36 -0
  133. package/dist/commands/env/set.js.map +1 -0
  134. package/dist/commands/env/use.d.ts +3 -0
  135. package/dist/commands/env/use.d.ts.map +1 -0
  136. package/dist/commands/env/use.js +15 -0
  137. package/dist/commands/env/use.js.map +1 -0
  138. package/dist/commands/legacy/connect.d.ts +3 -0
  139. package/dist/commands/legacy/connect.d.ts.map +1 -0
  140. package/dist/commands/legacy/connect.js +8 -0
  141. package/dist/commands/legacy/connect.js.map +1 -0
  142. package/dist/commands/legacy/pages.d.ts +3 -0
  143. package/dist/commands/legacy/pages.d.ts.map +1 -0
  144. package/dist/commands/legacy/pages.js +16 -0
  145. package/dist/commands/legacy/pages.js.map +1 -0
  146. package/dist/commands/legacy/spread.d.ts +3 -0
  147. package/dist/commands/legacy/spread.d.ts.map +1 -0
  148. package/dist/commands/legacy/spread.js +8 -0
  149. package/dist/commands/legacy/spread.js.map +1 -0
  150. package/dist/commands/legacy/sync.d.ts +3 -0
  151. package/dist/commands/legacy/sync.d.ts.map +1 -0
  152. package/dist/commands/legacy/sync.js +8 -0
  153. package/dist/commands/legacy/sync.js.map +1 -0
  154. package/dist/config/env-diff.d.ts +10 -0
  155. package/dist/config/env-diff.d.ts.map +1 -0
  156. package/dist/config/env-diff.js +24 -0
  157. package/dist/config/env-diff.js.map +1 -0
  158. package/dist/config/env-parser.d.ts +20 -0
  159. package/dist/config/env-parser.d.ts.map +1 -0
  160. package/dist/config/env-parser.js +70 -0
  161. package/dist/config/env-parser.js.map +1 -0
  162. package/dist/config/env-writer.d.ts +22 -0
  163. package/dist/config/env-writer.d.ts.map +1 -0
  164. package/dist/config/env-writer.js +172 -0
  165. package/dist/config/env-writer.js.map +1 -0
  166. package/dist/config/profile-store.d.ts +29 -0
  167. package/dist/config/profile-store.d.ts.map +1 -0
  168. package/dist/config/profile-store.js +257 -0
  169. package/dist/config/profile-store.js.map +1 -0
  170. package/dist/core/args.d.ts +11 -0
  171. package/dist/core/args.d.ts.map +1 -0
  172. package/dist/core/args.js +106 -0
  173. package/dist/core/args.js.map +1 -0
  174. package/dist/core/colors.d.ts +6 -0
  175. package/dist/core/colors.d.ts.map +1 -0
  176. package/dist/core/colors.js +29 -0
  177. package/dist/core/colors.js.map +1 -0
  178. package/dist/safety/target-guard.d.ts +16 -0
  179. package/dist/safety/target-guard.d.ts.map +1 -0
  180. package/dist/safety/target-guard.js +55 -0
  181. package/dist/safety/target-guard.js.map +1 -0
  182. package/dist/testing/booking-smoke.d.ts +17 -0
  183. package/dist/testing/booking-smoke.d.ts.map +1 -0
  184. package/dist/testing/booking-smoke.js +43 -0
  185. package/dist/testing/booking-smoke.js.map +1 -0
  186. package/dist/tests/agent-commands.test.d.ts +2 -0
  187. package/dist/tests/agent-commands.test.d.ts.map +1 -0
  188. package/dist/tests/agent-commands.test.js +180 -0
  189. package/dist/tests/agent-commands.test.js.map +1 -0
  190. package/dist/tests/agent-governance.test.d.ts +2 -0
  191. package/dist/tests/agent-governance.test.d.ts.map +1 -0
  192. package/dist/tests/agent-governance.test.js +233 -0
  193. package/dist/tests/agent-governance.test.js.map +1 -0
  194. package/dist/tests/app-commands.test.d.ts +2 -0
  195. package/dist/tests/app-commands.test.d.ts.map +1 -0
  196. package/dist/tests/app-commands.test.js +462 -0
  197. package/dist/tests/app-commands.test.js.map +1 -0
  198. package/dist/tests/booking-commands.test.d.ts +2 -0
  199. package/dist/tests/booking-commands.test.d.ts.map +1 -0
  200. package/dist/tests/booking-commands.test.js +204 -0
  201. package/dist/tests/booking-commands.test.js.map +1 -0
  202. package/dist/tests/booking-smoke.test.d.ts +2 -0
  203. package/dist/tests/booking-smoke.test.d.ts.map +1 -0
  204. package/dist/tests/booking-smoke.test.js +183 -0
  205. package/dist/tests/booking-smoke.test.js.map +1 -0
  206. package/dist/tests/cms-commands.test.d.ts +2 -0
  207. package/dist/tests/cms-commands.test.d.ts.map +1 -0
  208. package/dist/tests/cms-commands.test.js +254 -0
  209. package/dist/tests/cms-commands.test.js.map +1 -0
  210. package/dist/tests/cms-ops.test.d.ts +2 -0
  211. package/dist/tests/cms-ops.test.d.ts.map +1 -0
  212. package/dist/tests/cms-ops.test.js +125 -0
  213. package/dist/tests/cms-ops.test.js.map +1 -0
  214. package/dist/tests/env-writer.test.d.ts +2 -0
  215. package/dist/tests/env-writer.test.d.ts.map +1 -0
  216. package/dist/tests/env-writer.test.js +90 -0
  217. package/dist/tests/env-writer.test.js.map +1 -0
  218. package/dist/tests/profile-store.test.d.ts +2 -0
  219. package/dist/tests/profile-store.test.d.ts.map +1 -0
  220. package/dist/tests/profile-store.test.js +88 -0
  221. package/dist/tests/profile-store.test.js.map +1 -0
  222. package/dist/tests/target-guard.test.d.ts +2 -0
  223. package/dist/tests/target-guard.test.d.ts.map +1 -0
  224. package/dist/tests/target-guard.test.js +132 -0
  225. package/dist/tests/target-guard.test.js.map +1 -0
  226. package/dist/ui/logo.d.ts +2 -0
  227. package/dist/ui/logo.d.ts.map +1 -0
  228. package/dist/ui/logo.js +22 -0
  229. package/dist/ui/logo.js.map +1 -0
  230. package/package.json +17 -53
  231. package/.claude/settings.local.json +0 -36
  232. package/.cursor/rules.md +0 -203
  233. package/.eslintrc.js +0 -31
  234. package/CLAUDE.md +0 -100
  235. package/bin/cli.js +0 -116
  236. package/docs/ADDING_FRAMEWORK_DETECTORS.md +0 -391
  237. package/docs/ADDING_NEW_PROJECT_TYPE.md +0 -156
  238. package/docs/ARCHITECTURE_RELATIONSHIPS.md +0 -411
  239. package/docs/CLI_AUTHENTICATION.md +0 -214
  240. package/docs/CLI_PAGE_DETECTION_REQUIREMENTS.md +0 -519
  241. package/docs/CRM-PIPELINES-SEQUENCES-SPEC.md +0 -429
  242. package/docs/DETECTOR_ARCHITECTURE.md +0 -326
  243. package/docs/DEVELOPMENT.md +0 -194
  244. package/docs/IMPLEMENTATION_PHASES.md +0 -468
  245. package/docs/INTEGRATION_PATHS_ARCHITECTURE.md +0 -1543
  246. package/docs/OAUTH_CLARIFICATION.md +0 -258
  247. package/docs/OAUTH_SETUP_GUIDE_TEMPLATE.md +0 -211
  248. package/docs/PHASE_0_PROGRESS.md +0 -120
  249. package/docs/PHASE_1_COMPLETE.md +0 -366
  250. package/docs/PHASE_SUMMARY.md +0 -149
  251. package/docs/PLAN.md +0 -511
  252. package/docs/README.md +0 -56
  253. package/docs/STRIPE_INTEGRATION.md +0 -447
  254. package/docs/SUMMARY.md +0 -230
  255. package/docs/UPDATED_PLAN.md +0 -447
  256. package/docs/mcp_server/MCP_EXTENSION_GUIDE.md +0 -1313
  257. package/docs/mcp_server/MCP_SERVER_ARCHITECTURE.md +0 -1481
  258. package/docs/mcp_server/applicationOntology.ts +0 -817
  259. package/docs/mcp_server/cliApplications.ts +0 -639
  260. package/docs/mcp_server/crmOntology.ts +0 -1063
  261. package/docs/mcp_server/eventOntology.ts +0 -1183
  262. package/docs/mcp_server/formsOntology.ts +0 -1401
  263. package/docs/mcp_server/ontologySchemas.ts +0 -185
  264. package/docs/mcp_server/schema.ts +0 -250
  265. package/docs/microsass_production_machine/CLI_API_REFERENCE.md +0 -1197
  266. package/docs/microsass_production_machine/CLI_PRODUCT_VISION.md +0 -676
  267. package/docs/microsass_production_machine/CLI_REQUIREMENTS.md +0 -606
  268. package/docs/microsass_production_machine/CONNECTED_APPLICATIONS_SPEC.md +0 -390
  269. package/docs/microsass_production_machine/IMPLEMENTATION_ROADMAP.md +0 -725
  270. package/docs/microsass_production_machine/OBJECT_MAPPINGS.md +0 -808
  271. package/docs/microsass_production_machine/REFERENCE_IMPLEMENTATION.md +0 -532
  272. package/src/api/backend-client.js +0 -449
  273. package/src/commands/api-keys.js +0 -119
  274. package/src/commands/connect.js +0 -253
  275. package/src/commands/login.js +0 -332
  276. package/src/commands/logout.js +0 -30
  277. package/src/commands/mcp-server.js +0 -85
  278. package/src/commands/mcp-setup.js +0 -686
  279. package/src/commands/pages.js +0 -317
  280. package/src/commands/scaffold.js +0 -409
  281. package/src/commands/spread.js +0 -861
  282. package/src/commands/status.js +0 -62
  283. package/src/commands/sync.js +0 -169
  284. package/src/commands/upgrade.js +0 -48
  285. package/src/config/config-manager.js +0 -206
  286. package/src/detectors/api-client-detector.js +0 -85
  287. package/src/detectors/base-detector.js +0 -77
  288. package/src/detectors/database-detector.js +0 -245
  289. package/src/detectors/expo-detector.js +0 -166
  290. package/src/detectors/github-detector.js +0 -74
  291. package/src/detectors/index.js +0 -106
  292. package/src/detectors/mapping-suggestor.js +0 -119
  293. package/src/detectors/model-detector.js +0 -318
  294. package/src/detectors/nextjs-detector.js +0 -139
  295. package/src/detectors/oauth-detector.js +0 -122
  296. package/src/detectors/page-detector.js +0 -480
  297. package/src/detectors/registry.js +0 -121
  298. package/src/generators/api-client-generator.js +0 -223
  299. package/src/generators/api-only/client.js +0 -683
  300. package/src/generators/api-only/index.js +0 -96
  301. package/src/generators/api-only/types.js +0 -618
  302. package/src/generators/api-only/webhooks.js +0 -377
  303. package/src/generators/env-generator.js +0 -191
  304. package/src/generators/expo-auth-generator.js +0 -1009
  305. package/src/generators/gitignore-generator.js +0 -92
  306. package/src/generators/index.js +0 -166
  307. package/src/generators/manifest-generator.js +0 -154
  308. package/src/generators/mcp-guide-generator.js +0 -256
  309. package/src/generators/nextauth-generator.js +0 -247
  310. package/src/generators/oauth-guide-generator.js +0 -277
  311. package/src/generators/quickstart/components/index.js +0 -1699
  312. package/src/generators/quickstart/components-mobile/index.js +0 -1440
  313. package/src/generators/quickstart/database/convex.js +0 -1257
  314. package/src/generators/quickstart/database/index.js +0 -34
  315. package/src/generators/quickstart/database/supabase.js +0 -1132
  316. package/src/generators/quickstart/hooks/index.js +0 -1065
  317. package/src/generators/quickstart/index.js +0 -191
  318. package/src/generators/quickstart/pages/index.js +0 -1466
  319. package/src/generators/quickstart/screens/index.js +0 -1498
  320. package/src/logo.js +0 -116
  321. package/src/mcp/auth.js +0 -127
  322. package/src/mcp/registry/domains/applications.js +0 -516
  323. package/src/mcp/registry/domains/benefits.js +0 -798
  324. package/src/mcp/registry/domains/codegen.js +0 -894
  325. package/src/mcp/registry/domains/core.js +0 -324
  326. package/src/mcp/registry/domains/crm.js +0 -591
  327. package/src/mcp/registry/domains/events.js +0 -649
  328. package/src/mcp/registry/domains/forms.js +0 -696
  329. package/src/mcp/registry/index.js +0 -164
  330. package/src/mcp/server.js +0 -116
  331. package/src/utils/file-utils.js +0 -117
  332. package/src/utils/init-helpers.js +0 -243
  333. package/src/utils/prompt-utils.js +0 -195
  334. package/templates/CLAUDE.md +0 -86
  335. package/tests/api-client-detector.test.js +0 -214
  336. package/tests/api-client-generator.test.js +0 -176
  337. package/tests/backend-client.test.js +0 -640
  338. package/tests/base-detector.test.js +0 -101
  339. package/tests/commands/login.test.js +0 -143
  340. package/tests/commands/logout.test.js +0 -84
  341. package/tests/commands/status.test.js +0 -167
  342. package/tests/config-manager.test.js +0 -321
  343. package/tests/database-detector.test.js +0 -221
  344. package/tests/detector-index.test.js +0 -209
  345. package/tests/detector-registry.test.js +0 -93
  346. package/tests/env-generator.test.js +0 -278
  347. package/tests/expo-detector.test.js +0 -263
  348. package/tests/file-utils.test.js +0 -194
  349. package/tests/generators-index.test.js +0 -454
  350. package/tests/github-detector.test.js +0 -145
  351. package/tests/gitignore-generator.test.js +0 -109
  352. package/tests/logo.test.js +0 -96
  353. package/tests/nextauth-generator.test.js +0 -255
  354. package/tests/nextjs-detector.test.js +0 -235
  355. package/tests/oauth-detector.test.js +0 -264
  356. package/tests/oauth-guide-generator.test.js +0 -273
  357. package/tests/page-detector.test.js +0 -371
@@ -1,1063 +0,0 @@
1
- /**
2
- * CRM ONTOLOGY
3
- *
4
- * Manages CRM contacts and organizations for customer relationship management.
5
- * Uses the universal ontology system (objects table).
6
- *
7
- * Object Types:
8
- * - crm_contact: Individual contacts (customers, leads, prospects)
9
- * - crm_organization: Companies/organizations (customer companies)
10
- *
11
- * Contact Types (subtype):
12
- * - "customer" - Paying customers
13
- * - "lead" - Potential customers
14
- * - "prospect" - Qualified leads
15
- *
16
- * Organization Types (subtype):
17
- * - "customer" - Customer companies
18
- * - "prospect" - Potential customer companies
19
- * - "partner" - Partner organizations
20
- *
21
- * Status Workflow:
22
- * - "active" - Active contact/org
23
- * - "inactive" - Temporarily inactive
24
- * - "unsubscribed" - Opted out (contacts only)
25
- * - "archived" - Archived/deleted
26
- *
27
- * GRAVEL ROAD APPROACH:
28
- * - Start simple: name, email, phone, basic info
29
- * - Add fields via customProperties as needed
30
- * - Use objectLinks for relationships (contact → organization)
31
- * - Use objectActions for audit trail
32
- *
33
- * ADDRESS SUPPORT:
34
- * - Support multiple addresses per contact/organization
35
- * - Address types: billing, shipping, mailing, physical, warehouse, other
36
- * - One primary address per type
37
- * - Backward compatible with single address field
38
- */
39
-
40
- import { query, mutation, internalQuery } from "./_generated/server";
41
- import { v } from "convex/values";
42
- import { requireAuthenticatedUser } from "./rbacHelpers";
43
- import { checkResourceLimit } from "./licensing/helpers";
44
- import { internal } from "./_generated/api";
45
-
46
- // ============================================================================
47
- // ADDRESS VALIDATORS
48
- // ============================================================================
49
-
50
- /**
51
- * Address type enum
52
- */
53
- export const addressTypes = ["billing", "shipping", "mailing", "physical", "warehouse", "other"] as const;
54
-
55
- /**
56
- * Single address validator
57
- */
58
- export const addressValidator = v.object({
59
- type: v.union(
60
- v.literal("billing"),
61
- v.literal("shipping"),
62
- v.literal("mailing"),
63
- v.literal("physical"),
64
- v.literal("warehouse"),
65
- v.literal("other")
66
- ),
67
- isPrimary: v.boolean(),
68
- label: v.optional(v.string()), // e.g., "Corporate HQ", "Warehouse 1"
69
- street: v.optional(v.string()),
70
- street2: v.optional(v.string()), // Additional address line
71
- city: v.optional(v.string()),
72
- state: v.optional(v.string()),
73
- postalCode: v.optional(v.string()),
74
- country: v.optional(v.string()),
75
- });
76
-
77
- /**
78
- * Addresses array validator
79
- */
80
- export const addressesValidator = v.array(addressValidator);
81
-
82
- // ============================================================================
83
- // CRM CONTACT OPERATIONS
84
- // ============================================================================
85
-
86
- /**
87
- * GET CONTACTS
88
- * Returns all contacts for an organization
89
- */
90
- export const getContacts = query({
91
- args: {
92
- sessionId: v.string(),
93
- organizationId: v.id("organizations"),
94
- subtype: v.optional(v.string()), // Filter by contact type
95
- status: v.optional(v.string()), // Filter by status
96
- },
97
- handler: async (ctx, args) => {
98
- await requireAuthenticatedUser(ctx, args.sessionId);
99
-
100
- const q = ctx.db
101
- .query("objects")
102
- .withIndex("by_org_type", (q) =>
103
- q.eq("organizationId", args.organizationId).eq("type", "crm_contact")
104
- );
105
-
106
- let contacts = await q.collect();
107
-
108
- // Apply filters
109
- if (args.subtype) {
110
- contacts = contacts.filter((c) => c.subtype === args.subtype);
111
- }
112
-
113
- if (args.status) {
114
- contacts = contacts.filter((c) => c.status === args.status);
115
- }
116
-
117
- return contacts;
118
- },
119
- });
120
-
121
- /**
122
- * GET CONTACT
123
- * Get a single contact by ID
124
- */
125
- export const getContact = query({
126
- args: {
127
- sessionId: v.string(),
128
- contactId: v.id("objects"),
129
- },
130
- handler: async (ctx, args) => {
131
- await requireAuthenticatedUser(ctx, args.sessionId);
132
-
133
- const contact = await ctx.db.get(args.contactId);
134
-
135
- if (!contact || contact.type !== "crm_contact") {
136
- throw new Error("Contact not found");
137
- }
138
-
139
- return contact;
140
- },
141
- });
142
-
143
- /**
144
- * INTERNAL: Get CRM Contact by ID
145
- * Used by internal systems (e.g., PDF generation) to fetch contact data
146
- */
147
- export const getContactInternal = internalQuery({
148
- args: {
149
- contactId: v.id("objects"),
150
- },
151
- handler: async (ctx, args) => {
152
- const contact = await ctx.db.get(args.contactId);
153
-
154
- if (!contact || contact.type !== "crm_contact") {
155
- return null;
156
- }
157
-
158
- return contact;
159
- },
160
- });
161
-
162
- /**
163
- * CREATE CONTACT
164
- * Create a new CRM contact
165
- *
166
- * NOTE: When implementing bulk contact import/export features, add:
167
- * - checkFeatureAccess(ctx, organizationId, "contactImportExportEnabled")
168
- * This requires Starter+ tier.
169
- */
170
- export const createContact = mutation({
171
- args: {
172
- sessionId: v.string(),
173
- organizationId: v.id("organizations"),
174
- subtype: v.string(), // "customer" | "lead" | "prospect"
175
- firstName: v.string(),
176
- lastName: v.string(),
177
- email: v.string(),
178
- phone: v.optional(v.string()),
179
- jobTitle: v.optional(v.string()),
180
- company: v.optional(v.string()),
181
- // BACKWARD COMPATIBLE: Support old single address field
182
- address: v.optional(v.object({
183
- street: v.optional(v.string()),
184
- city: v.optional(v.string()),
185
- state: v.optional(v.string()),
186
- postalCode: v.optional(v.string()),
187
- country: v.optional(v.string()),
188
- })),
189
- // NEW: Support multiple addresses
190
- addresses: v.optional(addressesValidator),
191
- source: v.optional(v.string()), // "manual" | "checkout" | "event" | "import"
192
- sourceRef: v.optional(v.string()), // Reference to source (checkout ID, event ID, etc.)
193
- tags: v.optional(v.array(v.string())),
194
- notes: v.optional(v.string()),
195
- customFields: v.optional(v.any()), // Additional custom fields
196
- },
197
- handler: async (ctx, args) => {
198
- const session = await ctx.db
199
- .query("sessions")
200
- .filter((q) => q.eq(q.field("_id"), args.sessionId))
201
- .first();
202
-
203
- if (!session) throw new Error("Invalid session");
204
-
205
- // CHECK LICENSE LIMIT: Enforce contact limit for organization's tier
206
- // Free: 100, Starter: 1,000, Pro: 5,000, Agency: 10,000, Enterprise: Unlimited
207
- await checkResourceLimit(ctx, args.organizationId, "crm_contact", "maxContacts");
208
-
209
- // Handle addresses: convert old address format to new format if needed
210
- let addresses = args.addresses;
211
- if (!addresses && args.address) {
212
- // Backward compatibility: convert single address to addresses array
213
- addresses = [{
214
- type: "mailing" as const,
215
- isPrimary: true,
216
- label: "Primary Address",
217
- ...args.address,
218
- }];
219
- }
220
-
221
- const contactId = await ctx.db.insert("objects", {
222
- organizationId: args.organizationId,
223
- type: "crm_contact",
224
- subtype: args.subtype,
225
- name: `${args.firstName} ${args.lastName}`,
226
- description: args.jobTitle || "Contact",
227
- status: "active",
228
- customProperties: {
229
- firstName: args.firstName,
230
- lastName: args.lastName,
231
- email: args.email,
232
- phone: args.phone,
233
- jobTitle: args.jobTitle,
234
- company: args.company,
235
- // Keep old address for backward compatibility
236
- address: args.address,
237
- // Add new addresses array
238
- addresses: addresses,
239
- source: args.source || "manual",
240
- sourceRef: args.sourceRef,
241
- tags: args.tags || [],
242
- notes: args.notes,
243
- ...args.customFields,
244
- },
245
- createdBy: session.userId,
246
- createdAt: Date.now(),
247
- updatedAt: Date.now(),
248
- });
249
-
250
- // Log creation action
251
- await ctx.db.insert("objectActions", {
252
- organizationId: args.organizationId,
253
- objectId: contactId,
254
- actionType: "created",
255
- actionData: {
256
- source: args.source || "manual",
257
- subtype: args.subtype,
258
- },
259
- performedBy: session.userId,
260
- performedAt: Date.now(),
261
- });
262
-
263
- return contactId;
264
- },
265
- });
266
-
267
- /**
268
- * UPDATE CONTACT
269
- * Update an existing contact
270
- */
271
- export const updateContact = mutation({
272
- args: {
273
- sessionId: v.string(),
274
- contactId: v.id("objects"),
275
- updates: v.object({
276
- firstName: v.optional(v.string()),
277
- lastName: v.optional(v.string()),
278
- email: v.optional(v.string()),
279
- phone: v.optional(v.string()),
280
- jobTitle: v.optional(v.string()),
281
- company: v.optional(v.string()),
282
- // BACKWARD COMPATIBLE: Support old single address field
283
- address: v.optional(v.any()),
284
- // NEW: Support multiple addresses
285
- addresses: v.optional(addressesValidator),
286
- status: v.optional(v.string()),
287
- subtype: v.optional(v.string()), // Lifecycle stage: "lead" | "prospect" | "customer" | "partner"
288
- tags: v.optional(v.array(v.string())),
289
- notes: v.optional(v.string()),
290
- customFields: v.optional(v.any()),
291
- }),
292
- },
293
- handler: async (ctx, args) => {
294
- const session = await ctx.db
295
- .query("sessions")
296
- .filter((q) => q.eq(q.field("_id"), args.sessionId))
297
- .first();
298
-
299
- if (!session) throw new Error("Invalid session");
300
-
301
- const contact = await ctx.db.get(args.contactId);
302
- if (!contact || contact.type !== "crm_contact") {
303
- throw new Error("Contact not found");
304
- }
305
-
306
- // Update name if first/last name changed
307
- let newName = contact.name;
308
- if (args.updates.firstName || args.updates.lastName) {
309
- const firstName = args.updates.firstName || contact.customProperties?.firstName;
310
- const lastName = args.updates.lastName || contact.customProperties?.lastName;
311
- newName = `${firstName} ${lastName}`;
312
- }
313
-
314
- await ctx.db.patch(args.contactId, {
315
- name: newName,
316
- status: args.updates.status || contact.status,
317
- subtype: args.updates.subtype || contact.subtype, // Update lifecycle stage
318
- customProperties: {
319
- ...contact.customProperties,
320
- ...args.updates,
321
- ...args.updates.customFields,
322
- },
323
- updatedAt: Date.now(),
324
- });
325
-
326
- // Log update action
327
- await ctx.db.insert("objectActions", {
328
- organizationId: contact.organizationId,
329
- objectId: args.contactId,
330
- actionType: "updated",
331
- actionData: {
332
- updatedFields: Object.keys(args.updates),
333
- },
334
- performedBy: session.userId,
335
- performedAt: Date.now(),
336
- });
337
- },
338
- });
339
-
340
- /**
341
- * DELETE CONTACT
342
- * Permanently delete a contact and all associated links
343
- */
344
- export const deleteContact = mutation({
345
- args: {
346
- sessionId: v.string(),
347
- contactId: v.id("objects"),
348
- },
349
- handler: async (ctx, args) => {
350
- const session = await ctx.db
351
- .query("sessions")
352
- .filter((q) => q.eq(q.field("_id"), args.sessionId))
353
- .first();
354
-
355
- if (!session) throw new Error("Invalid session");
356
-
357
- const contact = await ctx.db.get(args.contactId);
358
- if (!contact || contact.type !== "crm_contact") {
359
- throw new Error("Contact not found");
360
- }
361
-
362
- // Log deletion action BEFORE deleting (so we have the data)
363
- await ctx.db.insert("objectActions", {
364
- organizationId: contact.organizationId,
365
- objectId: args.contactId,
366
- actionType: "deleted",
367
- actionData: {
368
- contactName: contact.name,
369
- email: contact.customProperties?.email,
370
- deletedBy: session.userId,
371
- },
372
- performedBy: session.userId,
373
- performedAt: Date.now(),
374
- });
375
-
376
- // Delete all links involving this contact
377
- const linksFrom = await ctx.db
378
- .query("objectLinks")
379
- .withIndex("by_from_object", (q) => q.eq("fromObjectId", args.contactId))
380
- .collect();
381
-
382
- const linksTo = await ctx.db
383
- .query("objectLinks")
384
- .withIndex("by_to_object", (q) => q.eq("toObjectId", args.contactId))
385
- .collect();
386
-
387
- // Delete all links
388
- for (const link of [...linksFrom, ...linksTo]) {
389
- await ctx.db.delete(link._id);
390
- }
391
-
392
- // Permanently delete the contact
393
- await ctx.db.delete(args.contactId);
394
- },
395
- });
396
-
397
- // ============================================================================
398
- // CRM ORGANIZATION OPERATIONS
399
- // ============================================================================
400
-
401
- /**
402
- * GET CRM ORGANIZATIONS
403
- * Returns all CRM organizations for an organization
404
- */
405
- export const getCrmOrganizations = query({
406
- args: {
407
- sessionId: v.string(),
408
- organizationId: v.id("organizations"),
409
- subtype: v.optional(v.string()), // Filter by org type
410
- status: v.optional(v.string()), // Filter by status
411
- },
412
- handler: async (ctx, args) => {
413
- await requireAuthenticatedUser(ctx, args.sessionId);
414
-
415
- const q = ctx.db
416
- .query("objects")
417
- .withIndex("by_org_type", (q) =>
418
- q.eq("organizationId", args.organizationId).eq("type", "crm_organization")
419
- );
420
-
421
- let orgs = await q.collect();
422
-
423
- // Apply filters
424
- if (args.subtype) {
425
- orgs = orgs.filter((o) => o.subtype === args.subtype);
426
- }
427
-
428
- if (args.status) {
429
- orgs = orgs.filter((o) => o.status === args.status);
430
- }
431
-
432
- return orgs;
433
- },
434
- });
435
-
436
- /**
437
- * GET CRM ORGANIZATION
438
- * Get a single CRM organization by ID
439
- */
440
- export const getCrmOrganization = query({
441
- args: {
442
- sessionId: v.string(),
443
- crmOrganizationId: v.id("objects"),
444
- },
445
- handler: async (ctx, args) => {
446
- await requireAuthenticatedUser(ctx, args.sessionId);
447
-
448
- const org = await ctx.db.get(args.crmOrganizationId);
449
-
450
- if (!org || org.type !== "crm_organization") {
451
- throw new Error("CRM organization not found");
452
- }
453
-
454
- return org;
455
- },
456
- });
457
-
458
- /**
459
- * GET PUBLIC CRM ORGANIZATION BILLING INFO
460
- * Public query for checkout - returns limited billing information only
461
- * Used during checkout to pre-fill employer billing addresses
462
- */
463
- export const getPublicCrmOrganizationBilling = query({
464
- args: {
465
- crmOrganizationId: v.id("objects"),
466
- },
467
- handler: async (ctx, args) => {
468
- const org = await ctx.db.get(args.crmOrganizationId);
469
-
470
- if (!org || org.type !== "crm_organization") {
471
- return null;
472
- }
473
-
474
- // Return only public billing information (no sensitive data)
475
- return {
476
- _id: org._id,
477
- name: org.name,
478
- customProperties: {
479
- address: (org.customProperties as { address?: unknown })?.address,
480
- taxId: (org.customProperties as { taxId?: unknown })?.taxId,
481
- vatNumber: (org.customProperties as { vatNumber?: unknown })?.vatNumber,
482
- billingEmail: (org.customProperties as { billingEmail?: unknown })?.billingEmail,
483
- billingContact: (org.customProperties as { billingContact?: unknown })?.billingContact,
484
- billingAddress: (org.customProperties as { billingAddress?: unknown })?.billingAddress,
485
- phone: (org.customProperties as { phone?: unknown })?.phone,
486
- website: (org.customProperties as { website?: unknown })?.website,
487
- },
488
- };
489
- },
490
- });
491
-
492
- /**
493
- * CREATE CRM ORGANIZATION
494
- * Create a new CRM organization
495
- */
496
- export const createCrmOrganization = mutation({
497
- args: {
498
- sessionId: v.string(),
499
- organizationId: v.id("organizations"),
500
- subtype: v.string(), // "customer" | "prospect" | "partner" | "sponsor"
501
- name: v.string(),
502
- website: v.optional(v.string()),
503
- industry: v.optional(v.string()),
504
- size: v.optional(v.string()), // "1-10" | "11-50" | "51-200" | "201-500" | "501+"
505
- // BACKWARD COMPATIBLE: Support old single address field
506
- address: v.optional(v.object({
507
- street: v.optional(v.string()),
508
- city: v.optional(v.string()),
509
- state: v.optional(v.string()),
510
- postalCode: v.optional(v.string()),
511
- country: v.optional(v.string()),
512
- })),
513
- // NEW: Support multiple addresses
514
- addresses: v.optional(addressesValidator),
515
- // Basic contact info
516
- taxId: v.optional(v.string()),
517
- billingEmail: v.optional(v.string()),
518
- phone: v.optional(v.string()),
519
- tags: v.optional(v.array(v.string())),
520
- notes: v.optional(v.string()),
521
- // B2B Billing fields (DEPRECATED - use addresses array with type="billing")
522
- billingAddress: v.optional(v.object({
523
- street: v.optional(v.string()),
524
- city: v.optional(v.string()),
525
- state: v.optional(v.string()),
526
- postalCode: v.optional(v.string()),
527
- country: v.optional(v.string()),
528
- })),
529
- legalEntityType: v.optional(v.string()), // "corporation", "llc", "partnership", "sole_proprietorship", "nonprofit"
530
- registrationNumber: v.optional(v.string()), // Company registration number
531
- vatNumber: v.optional(v.string()), // VAT/GST number
532
- taxExempt: v.optional(v.boolean()),
533
- paymentTerms: v.optional(v.string()), // "due_on_receipt", "net15", "net30", "net60", "net90"
534
- creditLimit: v.optional(v.number()),
535
- preferredPaymentMethod: v.optional(v.string()), // "invoice", "bank_transfer", "credit_card", "check"
536
- accountingReference: v.optional(v.string()), // External accounting system reference
537
- costCenter: v.optional(v.string()),
538
- purchaseOrderRequired: v.optional(v.boolean()),
539
- billingContact: v.optional(v.string()), // Name of billing contact
540
- billingContactEmail: v.optional(v.string()),
541
- billingContactPhone: v.optional(v.string()),
542
- customFields: v.optional(v.any()),
543
- },
544
- handler: async (ctx, args) => {
545
- const session = await ctx.db
546
- .query("sessions")
547
- .filter((q) => q.eq(q.field("_id"), args.sessionId))
548
- .first();
549
-
550
- if (!session) throw new Error("Invalid session");
551
-
552
- // CHECK LICENSE LIMIT: Enforce CRM organization limit for organization's tier
553
- // Free: 10, Starter: 50, Pro: 200, Agency: 500, Enterprise: Unlimited
554
- await checkResourceLimit(ctx, args.organizationId, "crm_organization", "maxOrganizations");
555
-
556
- // Handle addresses: convert old address/billingAddress format to new format if needed
557
- let addresses = args.addresses;
558
- if (!addresses) {
559
- addresses = [];
560
- // Convert old address field to mailing address
561
- if (args.address) {
562
- addresses.push({
563
- type: "mailing" as const,
564
- isPrimary: true,
565
- label: "Primary Address",
566
- ...args.address,
567
- });
568
- }
569
- // Convert old billingAddress field to billing address
570
- if (args.billingAddress) {
571
- addresses.push({
572
- type: "billing" as const,
573
- isPrimary: true,
574
- label: "Billing Address",
575
- ...args.billingAddress,
576
- });
577
- }
578
- }
579
-
580
- const orgId = await ctx.db.insert("objects", {
581
- organizationId: args.organizationId,
582
- type: "crm_organization",
583
- subtype: args.subtype,
584
- name: args.name,
585
- description: `${args.industry || "Company"} organization`,
586
- status: "active",
587
- customProperties: {
588
- // Basic info
589
- website: args.website,
590
- industry: args.industry,
591
- size: args.size,
592
- // Keep old fields for backward compatibility
593
- address: args.address,
594
- billingAddress: args.billingAddress,
595
- // Add new addresses array
596
- addresses: addresses,
597
- phone: args.phone,
598
- tags: args.tags || [],
599
- notes: args.notes,
600
- // Basic billing
601
- taxId: args.taxId,
602
- billingEmail: args.billingEmail,
603
- // B2B Billing
604
- legalEntityType: args.legalEntityType,
605
- registrationNumber: args.registrationNumber,
606
- vatNumber: args.vatNumber,
607
- taxExempt: args.taxExempt || false,
608
- paymentTerms: args.paymentTerms || "net30",
609
- creditLimit: args.creditLimit,
610
- preferredPaymentMethod: args.preferredPaymentMethod,
611
- accountingReference: args.accountingReference,
612
- costCenter: args.costCenter,
613
- purchaseOrderRequired: args.purchaseOrderRequired || false,
614
- billingContact: args.billingContact,
615
- billingContactEmail: args.billingContactEmail,
616
- billingContactPhone: args.billingContactPhone,
617
- ...args.customFields,
618
- },
619
- createdBy: session.userId,
620
- createdAt: Date.now(),
621
- updatedAt: Date.now(),
622
- });
623
-
624
- // Log creation action
625
- await ctx.db.insert("objectActions", {
626
- organizationId: args.organizationId,
627
- objectId: orgId,
628
- actionType: "created",
629
- actionData: {
630
- subtype: args.subtype,
631
- },
632
- performedBy: session.userId,
633
- performedAt: Date.now(),
634
- });
635
-
636
- return orgId;
637
- },
638
- });
639
-
640
- /**
641
- * UPDATE CRM ORGANIZATION
642
- * Update an existing CRM organization (including subtype/org type)
643
- */
644
- export const updateCrmOrganization = mutation({
645
- args: {
646
- sessionId: v.string(),
647
- crmOrganizationId: v.id("objects"),
648
- updates: v.object({
649
- name: v.optional(v.string()),
650
- subtype: v.optional(v.string()), // "customer" | "prospect" | "partner" | "sponsor"
651
- website: v.optional(v.string()),
652
- industry: v.optional(v.string()),
653
- size: v.optional(v.string()),
654
- // BACKWARD COMPATIBLE: Support old single address field
655
- address: v.optional(v.any()),
656
- // NEW: Support multiple addresses
657
- addresses: v.optional(addressesValidator),
658
- phone: v.optional(v.string()),
659
- status: v.optional(v.string()),
660
- tags: v.optional(v.array(v.string())),
661
- notes: v.optional(v.string()),
662
- // Basic billing
663
- taxId: v.optional(v.string()),
664
- billingEmail: v.optional(v.string()),
665
- // B2B Billing fields (DEPRECATED - use addresses array)
666
- billingAddress: v.optional(v.any()),
667
- legalEntityType: v.optional(v.string()),
668
- registrationNumber: v.optional(v.string()),
669
- vatNumber: v.optional(v.string()),
670
- taxExempt: v.optional(v.boolean()),
671
- paymentTerms: v.optional(v.string()),
672
- creditLimit: v.optional(v.number()),
673
- preferredPaymentMethod: v.optional(v.string()),
674
- accountingReference: v.optional(v.string()),
675
- costCenter: v.optional(v.string()),
676
- purchaseOrderRequired: v.optional(v.boolean()),
677
- billingContact: v.optional(v.string()),
678
- billingContactEmail: v.optional(v.string()),
679
- billingContactPhone: v.optional(v.string()),
680
- customFields: v.optional(v.any()),
681
- }),
682
- },
683
- handler: async (ctx, args) => {
684
- const session = await ctx.db
685
- .query("sessions")
686
- .filter((q) => q.eq(q.field("_id"), args.sessionId))
687
- .first();
688
-
689
- if (!session) throw new Error("Invalid session");
690
-
691
- const org = await ctx.db.get(args.crmOrganizationId);
692
- if (!org || org.type !== "crm_organization") {
693
- throw new Error("CRM organization not found");
694
- }
695
-
696
- await ctx.db.patch(args.crmOrganizationId, {
697
- name: args.updates.name || org.name,
698
- subtype: args.updates.subtype || org.subtype,
699
- status: args.updates.status || org.status,
700
- customProperties: {
701
- ...org.customProperties,
702
- ...args.updates,
703
- ...args.updates.customFields,
704
- },
705
- updatedAt: Date.now(),
706
- });
707
-
708
- // Log update action
709
- await ctx.db.insert("objectActions", {
710
- organizationId: org.organizationId,
711
- objectId: args.crmOrganizationId,
712
- actionType: "updated",
713
- actionData: {
714
- updatedFields: Object.keys(args.updates),
715
- },
716
- performedBy: session.userId,
717
- performedAt: Date.now(),
718
- });
719
- },
720
- });
721
-
722
- /**
723
- * DELETE CRM ORGANIZATION
724
- * Permanently delete a CRM organization and all associated links
725
- */
726
- export const deleteCrmOrganization = mutation({
727
- args: {
728
- sessionId: v.string(),
729
- crmOrganizationId: v.id("objects"),
730
- },
731
- handler: async (ctx, args) => {
732
- const session = await ctx.db
733
- .query("sessions")
734
- .filter((q) => q.eq(q.field("_id"), args.sessionId))
735
- .first();
736
-
737
- if (!session) throw new Error("Invalid session");
738
-
739
- const org = await ctx.db.get(args.crmOrganizationId);
740
- if (!org || org.type !== "crm_organization") {
741
- throw new Error("CRM organization not found");
742
- }
743
-
744
- // Log deletion action BEFORE deleting (so we have the data)
745
- await ctx.db.insert("objectActions", {
746
- organizationId: org.organizationId,
747
- objectId: args.crmOrganizationId,
748
- actionType: "deleted",
749
- actionData: {
750
- organizationName: org.name,
751
- deletedBy: session.userId,
752
- },
753
- performedBy: session.userId,
754
- performedAt: Date.now(),
755
- });
756
-
757
- // Delete all links involving this organization
758
- const linksFrom = await ctx.db
759
- .query("objectLinks")
760
- .withIndex("by_from_object", (q) => q.eq("fromObjectId", args.crmOrganizationId))
761
- .collect();
762
-
763
- const linksTo = await ctx.db
764
- .query("objectLinks")
765
- .withIndex("by_to_object", (q) => q.eq("toObjectId", args.crmOrganizationId))
766
- .collect();
767
-
768
- // Delete all links
769
- for (const link of [...linksFrom, ...linksTo]) {
770
- await ctx.db.delete(link._id);
771
- }
772
-
773
- // Permanently delete the organization
774
- await ctx.db.delete(args.crmOrganizationId);
775
- },
776
- });
777
-
778
- // ============================================================================
779
- // RELATIONSHIP OPERATIONS
780
- // ============================================================================
781
-
782
- /**
783
- * LINK CONTACT TO ORGANIZATION
784
- * Create a relationship between a contact and a CRM organization
785
- */
786
- export const linkContactToOrganization = mutation({
787
- args: {
788
- sessionId: v.string(),
789
- contactId: v.id("objects"),
790
- crmOrganizationId: v.id("objects"),
791
- jobTitle: v.optional(v.string()),
792
- isPrimaryContact: v.optional(v.boolean()),
793
- department: v.optional(v.string()),
794
- },
795
- handler: async (ctx, args) => {
796
- const session = await ctx.db
797
- .query("sessions")
798
- .filter((q) => q.eq(q.field("_id"), args.sessionId))
799
- .first();
800
-
801
- if (!session) throw new Error("Invalid session");
802
-
803
- // Validate objects exist
804
- const contact = await ctx.db.get(args.contactId);
805
- const org = await ctx.db.get(args.crmOrganizationId);
806
-
807
- if (!contact || contact.type !== "crm_contact") {
808
- throw new Error("Invalid contact");
809
- }
810
-
811
- if (!org || org.type !== "crm_organization") {
812
- throw new Error("Invalid CRM organization");
813
- }
814
-
815
- // Create link
816
- const linkId = await ctx.db.insert("objectLinks", {
817
- organizationId: contact.organizationId,
818
- fromObjectId: args.contactId,
819
- toObjectId: args.crmOrganizationId,
820
- linkType: "works_at",
821
- properties: {
822
- jobTitle: args.jobTitle,
823
- isPrimaryContact: args.isPrimaryContact ?? false,
824
- department: args.department,
825
- },
826
- createdBy: session.userId,
827
- createdAt: Date.now(),
828
- });
829
-
830
- return linkId;
831
- },
832
- });
833
-
834
- /**
835
- * GET ORGANIZATION CONTACTS
836
- * Get all contacts for a CRM organization
837
- */
838
- export const getOrganizationContacts = query({
839
- args: {
840
- sessionId: v.string(),
841
- crmOrganizationId: v.id("objects"),
842
- },
843
- handler: async (ctx, args) => {
844
- await requireAuthenticatedUser(ctx, args.sessionId);
845
-
846
- // Get all links where toObjectId = organization
847
- const links = await ctx.db
848
- .query("objectLinks")
849
- .withIndex("by_to_object", (q) => q.eq("toObjectId", args.crmOrganizationId))
850
- .collect();
851
-
852
- const worksAtLinks = links.filter((l) => l.linkType === "works_at");
853
-
854
- // Get contact objects
855
- const contacts = await Promise.all(
856
- worksAtLinks.map(async (link) => {
857
- const contact = await ctx.db.get(link.fromObjectId);
858
- return {
859
- ...contact,
860
- relationship: link.properties,
861
- linkId: link._id,
862
- };
863
- })
864
- );
865
-
866
- return contacts;
867
- },
868
- });
869
-
870
- /**
871
- * GET CONTACT ORGANIZATIONS
872
- * Get all organizations for a contact
873
- */
874
- export const getContactOrganizations = query({
875
- args: {
876
- sessionId: v.string(),
877
- contactId: v.id("objects"),
878
- },
879
- handler: async (ctx, args) => {
880
- await requireAuthenticatedUser(ctx, args.sessionId);
881
-
882
- // Get all links where fromObjectId = contact
883
- const links = await ctx.db
884
- .query("objectLinks")
885
- .withIndex("by_from_object", (q) => q.eq("fromObjectId", args.contactId))
886
- .collect();
887
-
888
- const worksAtLinks = links.filter((l) => l.linkType === "works_at");
889
-
890
- // Get organization objects
891
- const organizations = await Promise.all(
892
- worksAtLinks.map(async (link) => {
893
- const org = await ctx.db.get(link.toObjectId);
894
- return {
895
- ...org,
896
- relationship: link.properties,
897
- linkId: link._id,
898
- };
899
- })
900
- );
901
-
902
- return organizations;
903
- },
904
- });
905
-
906
- // ============================================================================
907
- // PORTAL INVITATION CONVENIENCE METHODS
908
- // ============================================================================
909
-
910
- /**
911
- * INVITE CONTACT TO PORTAL
912
- *
913
- * Convenience method to invite a CRM contact to an external portal.
914
- * Wrapper around portalInvitations.createPortalInvitation.
915
- *
916
- * Example:
917
- * - Invite freelancer to project portal
918
- * - Invite client to dashboard
919
- * - Invite vendor to supplier portal
920
- */
921
- export const inviteContactToPortal = mutation({
922
- args: {
923
- sessionId: v.string(),
924
- contactId: v.id("objects"),
925
- portalType: v.union(
926
- v.literal("freelancer_portal"),
927
- v.literal("client_portal"),
928
- v.literal("vendor_portal"),
929
- v.literal("custom_portal")
930
- ),
931
- portalUrl: v.string(),
932
- authMethod: v.optional(v.union(
933
- v.literal("oauth"),
934
- v.literal("magic_link"),
935
- v.literal("both")
936
- )),
937
- expiresInDays: v.optional(v.number()),
938
- customMessage: v.optional(v.string()),
939
- },
940
- handler: async (ctx, args) => {
941
- const session = await ctx.db
942
- .query("sessions")
943
- .filter((q) => q.eq(q.field("_id"), args.sessionId))
944
- .first();
945
-
946
- if (!session) throw new Error("Invalid session");
947
-
948
- // Get contact to verify it exists and get organizationId
949
- const contact = await ctx.db.get(args.contactId);
950
- if (!contact || contact.type !== "crm_contact") {
951
- throw new Error("Contact not found");
952
- }
953
-
954
- // Generate unique invitation token
955
- const invitationToken = crypto.randomUUID();
956
-
957
- // Calculate expiration
958
- const expiresInMs = (args.expiresInDays || 7) * 24 * 60 * 60 * 1000;
959
- const expiresAt = Date.now() + expiresInMs;
960
-
961
- const contactEmail = contact.customProperties?.email as string;
962
- if (!contactEmail) {
963
- throw new Error("Contact must have an email address");
964
- }
965
-
966
- // Create portal_invitation object
967
- const invitationId = await ctx.db.insert("objects", {
968
- organizationId: contact.organizationId,
969
- type: "portal_invitation",
970
- subtype: args.portalType,
971
- name: `Portal Invitation - ${contact.name}`,
972
- description: `Invitation to ${args.portalType} for ${contact.name}`,
973
- status: "pending",
974
- customProperties: {
975
- contactId: args.contactId,
976
- contactEmail: contactEmail,
977
- portalType: args.portalType,
978
- portalUrl: args.portalUrl,
979
- authMethod: args.authMethod || "both",
980
- invitationToken: invitationToken,
981
- expiresAt: expiresAt,
982
- customMessage: args.customMessage,
983
- permissions: [],
984
- sentAt: Date.now(),
985
- acceptedAt: null,
986
- lastAccessedAt: null,
987
- accessCount: 0,
988
- },
989
- createdBy: session.userId,
990
- createdAt: Date.now(),
991
- updatedAt: Date.now(),
992
- });
993
-
994
- // Link invitation to contact
995
- await ctx.db.insert("objectLinks", {
996
- organizationId: contact.organizationId,
997
- fromObjectId: invitationId,
998
- toObjectId: args.contactId,
999
- linkType: "invites",
1000
- properties: {
1001
- portalType: args.portalType,
1002
- invitedAt: Date.now(),
1003
- },
1004
- createdBy: session.userId,
1005
- createdAt: Date.now(),
1006
- });
1007
-
1008
- // Schedule invitation email
1009
- await ctx.scheduler.runAfter(0, internal.portalInvitations.sendInvitationEmail, {
1010
- invitationId,
1011
- contactEmail,
1012
- portalUrl: args.portalUrl,
1013
- authMethod: args.authMethod || "both",
1014
- invitationToken,
1015
- customMessage: args.customMessage,
1016
- organizationId: contact.organizationId,
1017
- });
1018
-
1019
- return {
1020
- invitationId,
1021
- invitationToken,
1022
- expiresAt,
1023
- };
1024
- },
1025
- });
1026
-
1027
- /**
1028
- * GET CONTACT PORTAL ACCESS
1029
- *
1030
- * Returns all portal invitations for a contact (active, pending, expired).
1031
- */
1032
- export const getContactPortalAccess = query({
1033
- args: {
1034
- sessionId: v.string(),
1035
- contactId: v.id("objects"),
1036
- },
1037
- handler: async (ctx, args) => {
1038
- await requireAuthenticatedUser(ctx, args.sessionId);
1039
-
1040
- // Get all portal invitations linked to this contact
1041
- const links = await ctx.db
1042
- .query("objectLinks")
1043
- .withIndex("by_to_object", (q) => q.eq("toObjectId", args.contactId))
1044
- .filter((q) => q.eq(q.field("linkType"), "invites"))
1045
- .collect();
1046
-
1047
- // Fetch invitation objects
1048
- const invitations = await Promise.all(
1049
- links.map(async (link) => {
1050
- const invitation = await ctx.db.get(link.fromObjectId);
1051
- if (invitation && invitation.type === "portal_invitation") {
1052
- return {
1053
- ...invitation,
1054
- linkId: link._id,
1055
- };
1056
- }
1057
- return null;
1058
- })
1059
- );
1060
-
1061
- return invitations.filter((inv) => inv !== null);
1062
- },
1063
- });