@soulbatical/tetra-core 0.10.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/README.md +78 -38
  2. package/dist/core/createApp.d.ts +1 -1
  3. package/dist/core/createApp.d.ts.map +1 -1
  4. package/dist/core/createApp.js +77 -2
  5. package/dist/core/createApp.js.map +1 -1
  6. package/dist/core/dualWriteProxy.d.ts +7 -2
  7. package/dist/core/dualWriteProxy.d.ts.map +1 -1
  8. package/dist/core/dualWriteProxy.js +16 -5
  9. package/dist/core/dualWriteProxy.js.map +1 -1
  10. package/dist/core/routeContext.d.ts +24 -0
  11. package/dist/core/routeContext.d.ts.map +1 -1
  12. package/dist/core/routeContext.js +31 -4
  13. package/dist/core/routeContext.js.map +1 -1
  14. package/dist/core/systemDb.d.ts +2 -2
  15. package/dist/core/systemDb.js +2 -2
  16. package/dist/generators/rls-checker.d.ts +1 -1
  17. package/dist/generators/rls-checker.js +1 -1
  18. package/dist/generators/rls-exec-sql.d.ts +1 -1
  19. package/dist/generators/rls-exec-sql.js +1 -1
  20. package/dist/generators/rpc/index.d.ts +1 -1
  21. package/dist/generators/rpc/index.js +1 -1
  22. package/dist/index.d.ts +3 -31
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -32
  25. package/dist/index.js.map +1 -1
  26. package/dist/middleware/securityMiddleware.d.ts +1 -1
  27. package/dist/middleware/securityMiddleware.d.ts.map +1 -1
  28. package/dist/middleware/validateBody.d.ts.map +1 -1
  29. package/dist/middleware/validateBody.js +51 -8
  30. package/dist/middleware/validateBody.js.map +1 -1
  31. package/dist/shared/rfc7807ErrorResponse.d.ts +7 -0
  32. package/dist/shared/rfc7807ErrorResponse.d.ts.map +1 -1
  33. package/dist/shared/rfc7807ErrorResponse.js +19 -5
  34. package/dist/shared/rfc7807ErrorResponse.js.map +1 -1
  35. package/dist/utils/logger.d.ts.map +1 -1
  36. package/dist/utils/logger.js +16 -1
  37. package/dist/utils/logger.js.map +1 -1
  38. package/package.json +33 -77
  39. package/dist/affiliate.d.ts +0 -11
  40. package/dist/affiliate.d.ts.map +0 -1
  41. package/dist/affiliate.js +0 -10
  42. package/dist/affiliate.js.map +0 -1
  43. package/dist/billing.d.ts +0 -8
  44. package/dist/billing.d.ts.map +0 -1
  45. package/dist/billing.js +0 -7
  46. package/dist/billing.js.map +0 -1
  47. package/dist/email.d.ts +0 -9
  48. package/dist/email.d.ts.map +0 -1
  49. package/dist/email.js +0 -8
  50. package/dist/email.js.map +0 -1
  51. package/dist/generators/rls-exec-sql.sql +0 -57
  52. package/dist/generators.d.ts +0 -15
  53. package/dist/generators.d.ts.map +0 -1
  54. package/dist/generators.js +0 -12
  55. package/dist/generators.js.map +0 -1
  56. package/dist/mcp.d.ts +0 -8
  57. package/dist/mcp.d.ts.map +0 -1
  58. package/dist/mcp.js +0 -7
  59. package/dist/mcp.js.map +0 -1
  60. package/dist/planner.d.ts +0 -8
  61. package/dist/planner.d.ts.map +0 -1
  62. package/dist/planner.js +0 -7
  63. package/dist/planner.js.map +0 -1
  64. package/dist/shared/affiliate/AffiliateAttributionService.d.ts +0 -47
  65. package/dist/shared/affiliate/AffiliateAttributionService.d.ts.map +0 -1
  66. package/dist/shared/affiliate/AffiliateAttributionService.js +0 -308
  67. package/dist/shared/affiliate/AffiliateAttributionService.js.map +0 -1
  68. package/dist/shared/affiliate/AffiliateClickService.d.ts +0 -35
  69. package/dist/shared/affiliate/AffiliateClickService.d.ts.map +0 -1
  70. package/dist/shared/affiliate/AffiliateClickService.js +0 -87
  71. package/dist/shared/affiliate/AffiliateClickService.js.map +0 -1
  72. package/dist/shared/affiliate/affiliateFeatureConfig.d.ts +0 -11
  73. package/dist/shared/affiliate/affiliateFeatureConfig.d.ts.map +0 -1
  74. package/dist/shared/affiliate/affiliateFeatureConfig.js +0 -242
  75. package/dist/shared/affiliate/affiliateFeatureConfig.js.map +0 -1
  76. package/dist/shared/affiliate/index.d.ts +0 -11
  77. package/dist/shared/affiliate/index.d.ts.map +0 -1
  78. package/dist/shared/affiliate/index.js +0 -13
  79. package/dist/shared/affiliate/index.js.map +0 -1
  80. package/dist/shared/affiliate/routes.d.ts +0 -87
  81. package/dist/shared/affiliate/routes.d.ts.map +0 -1
  82. package/dist/shared/affiliate/routes.js +0 -404
  83. package/dist/shared/affiliate/routes.js.map +0 -1
  84. package/dist/shared/affiliate/types.d.ts +0 -170
  85. package/dist/shared/affiliate/types.d.ts.map +0 -1
  86. package/dist/shared/affiliate/types.js +0 -11
  87. package/dist/shared/affiliate/types.js.map +0 -1
  88. package/dist/shared/billing/BillingService.d.ts +0 -56
  89. package/dist/shared/billing/BillingService.d.ts.map +0 -1
  90. package/dist/shared/billing/BillingService.js +0 -588
  91. package/dist/shared/billing/BillingService.js.map +0 -1
  92. package/dist/shared/billing/SeatBillingService.d.ts +0 -106
  93. package/dist/shared/billing/SeatBillingService.d.ts.map +0 -1
  94. package/dist/shared/billing/SeatBillingService.js +0 -292
  95. package/dist/shared/billing/SeatBillingService.js.map +0 -1
  96. package/dist/shared/billing/index.d.ts +0 -30
  97. package/dist/shared/billing/index.d.ts.map +0 -1
  98. package/dist/shared/billing/index.js +0 -27
  99. package/dist/shared/billing/index.js.map +0 -1
  100. package/dist/shared/billing/routes.d.ts +0 -45
  101. package/dist/shared/billing/routes.d.ts.map +0 -1
  102. package/dist/shared/billing/routes.js +0 -184
  103. package/dist/shared/billing/routes.js.map +0 -1
  104. package/dist/shared/billing/seat-pricing.d.ts +0 -53
  105. package/dist/shared/billing/seat-pricing.d.ts.map +0 -1
  106. package/dist/shared/billing/seat-pricing.js +0 -81
  107. package/dist/shared/billing/seat-pricing.js.map +0 -1
  108. package/dist/shared/billing/types.d.ts +0 -109
  109. package/dist/shared/billing/types.d.ts.map +0 -1
  110. package/dist/shared/billing/types.js +0 -8
  111. package/dist/shared/billing/types.js.map +0 -1
  112. package/dist/shared/email/EmailService.d.ts +0 -64
  113. package/dist/shared/email/EmailService.d.ts.map +0 -1
  114. package/dist/shared/email/EmailService.js +0 -300
  115. package/dist/shared/email/EmailService.js.map +0 -1
  116. package/dist/shared/email/adminRoutes.d.ts +0 -30
  117. package/dist/shared/email/adminRoutes.d.ts.map +0 -1
  118. package/dist/shared/email/adminRoutes.js +0 -227
  119. package/dist/shared/email/adminRoutes.js.map +0 -1
  120. package/dist/shared/email/gmail.d.ts +0 -208
  121. package/dist/shared/email/gmail.d.ts.map +0 -1
  122. package/dist/shared/email/gmail.js +0 -626
  123. package/dist/shared/email/gmail.js.map +0 -1
  124. package/dist/shared/email/index.d.ts +0 -15
  125. package/dist/shared/email/index.d.ts.map +0 -1
  126. package/dist/shared/email/index.js +0 -18
  127. package/dist/shared/email/index.js.map +0 -1
  128. package/dist/shared/email/mailgun.d.ts +0 -18
  129. package/dist/shared/email/mailgun.d.ts.map +0 -1
  130. package/dist/shared/email/mailgun.js +0 -76
  131. package/dist/shared/email/mailgun.js.map +0 -1
  132. package/dist/shared/email/sanitize.d.ts +0 -25
  133. package/dist/shared/email/sanitize.d.ts.map +0 -1
  134. package/dist/shared/email/sanitize.js +0 -39
  135. package/dist/shared/email/sanitize.js.map +0 -1
  136. package/dist/shared/email/smtp.d.ts +0 -20
  137. package/dist/shared/email/smtp.d.ts.map +0 -1
  138. package/dist/shared/email/smtp.js +0 -53
  139. package/dist/shared/email/smtp.js.map +0 -1
  140. package/dist/shared/email/types.d.ts +0 -113
  141. package/dist/shared/email/types.d.ts.map +0 -1
  142. package/dist/shared/email/types.js +0 -7
  143. package/dist/shared/email/types.js.map +0 -1
  144. package/dist/shared/email/webhookRoutes.d.ts +0 -29
  145. package/dist/shared/email/webhookRoutes.d.ts.map +0 -1
  146. package/dist/shared/email/webhookRoutes.js +0 -125
  147. package/dist/shared/email/webhookRoutes.js.map +0 -1
  148. package/dist/shared/mcp/index.d.ts +0 -51
  149. package/dist/shared/mcp/index.d.ts.map +0 -1
  150. package/dist/shared/mcp/index.js +0 -51
  151. package/dist/shared/mcp/index.js.map +0 -1
  152. package/dist/shared/mcp/mcp-auth-routes.d.ts +0 -26
  153. package/dist/shared/mcp/mcp-auth-routes.d.ts.map +0 -1
  154. package/dist/shared/mcp/mcp-auth-routes.js +0 -141
  155. package/dist/shared/mcp/mcp-auth-routes.js.map +0 -1
  156. package/dist/shared/mcp/mcp-db.d.ts +0 -99
  157. package/dist/shared/mcp/mcp-db.d.ts.map +0 -1
  158. package/dist/shared/mcp/mcp-db.js +0 -106
  159. package/dist/shared/mcp/mcp-db.js.map +0 -1
  160. package/dist/shared/mcp/mcp-routes.d.ts +0 -29
  161. package/dist/shared/mcp/mcp-routes.d.ts.map +0 -1
  162. package/dist/shared/mcp/mcp-routes.js +0 -171
  163. package/dist/shared/mcp/mcp-routes.js.map +0 -1
  164. package/dist/shared/mcp/mcp-tokens-routes.d.ts +0 -35
  165. package/dist/shared/mcp/mcp-tokens-routes.d.ts.map +0 -1
  166. package/dist/shared/mcp/mcp-tokens-routes.js +0 -94
  167. package/dist/shared/mcp/mcp-tokens-routes.js.map +0 -1
  168. package/dist/shared/mcp/mcp-usage-routes.d.ts +0 -17
  169. package/dist/shared/mcp/mcp-usage-routes.d.ts.map +0 -1
  170. package/dist/shared/mcp/mcp-usage-routes.js +0 -81
  171. package/dist/shared/mcp/mcp-usage-routes.js.map +0 -1
  172. package/dist/shared/mcp/tenant-context.d.ts +0 -59
  173. package/dist/shared/mcp/tenant-context.d.ts.map +0 -1
  174. package/dist/shared/mcp/tenant-context.js +0 -136
  175. package/dist/shared/mcp/tenant-context.js.map +0 -1
  176. package/dist/shared/mcp/types.d.ts +0 -74
  177. package/dist/shared/mcp/types.d.ts.map +0 -1
  178. package/dist/shared/mcp/types.js +0 -7
  179. package/dist/shared/mcp/types.js.map +0 -1
  180. package/dist/shared/planner/GoogleCalendarService.d.ts +0 -137
  181. package/dist/shared/planner/GoogleCalendarService.d.ts.map +0 -1
  182. package/dist/shared/planner/GoogleCalendarService.js +0 -525
  183. package/dist/shared/planner/GoogleCalendarService.js.map +0 -1
  184. package/dist/shared/planner/PlannerService.d.ts +0 -264
  185. package/dist/shared/planner/PlannerService.d.ts.map +0 -1
  186. package/dist/shared/planner/PlannerService.js +0 -1393
  187. package/dist/shared/planner/PlannerService.js.map +0 -1
  188. package/dist/shared/planner/index.d.ts +0 -37
  189. package/dist/shared/planner/index.d.ts.map +0 -1
  190. package/dist/shared/planner/index.js +0 -35
  191. package/dist/shared/planner/index.js.map +0 -1
  192. package/dist/shared/planner/intervals.d.ts +0 -60
  193. package/dist/shared/planner/intervals.d.ts.map +0 -1
  194. package/dist/shared/planner/intervals.js +0 -141
  195. package/dist/shared/planner/intervals.js.map +0 -1
  196. package/dist/shared/planner/routes.d.ts +0 -69
  197. package/dist/shared/planner/routes.d.ts.map +0 -1
  198. package/dist/shared/planner/routes.js +0 -770
  199. package/dist/shared/planner/routes.js.map +0 -1
  200. package/dist/shared/planner/types.d.ts +0 -328
  201. package/dist/shared/planner/types.d.ts.map +0 -1
  202. package/dist/shared/planner/types.js +0 -9
  203. package/dist/shared/planner/types.js.map +0 -1
  204. package/dist/shared/storage/ImageProcessingService.d.ts +0 -32
  205. package/dist/shared/storage/ImageProcessingService.d.ts.map +0 -1
  206. package/dist/shared/storage/ImageProcessingService.js +0 -127
  207. package/dist/shared/storage/ImageProcessingService.js.map +0 -1
  208. package/dist/shared/storage/StorageProxyService.d.ts +0 -47
  209. package/dist/shared/storage/StorageProxyService.d.ts.map +0 -1
  210. package/dist/shared/storage/StorageProxyService.js +0 -196
  211. package/dist/shared/storage/StorageProxyService.js.map +0 -1
  212. package/dist/shared/storage/StorageUploadService.d.ts +0 -126
  213. package/dist/shared/storage/StorageUploadService.d.ts.map +0 -1
  214. package/dist/shared/storage/StorageUploadService.js +0 -206
  215. package/dist/shared/storage/StorageUploadService.js.map +0 -1
  216. package/dist/shared/storage/creative-urls.d.ts +0 -14
  217. package/dist/shared/storage/creative-urls.d.ts.map +0 -1
  218. package/dist/shared/storage/creative-urls.js +0 -30
  219. package/dist/shared/storage/creative-urls.js.map +0 -1
  220. package/dist/shared/storage/index.d.ts +0 -28
  221. package/dist/shared/storage/index.d.ts.map +0 -1
  222. package/dist/shared/storage/index.js +0 -27
  223. package/dist/shared/storage/index.js.map +0 -1
  224. package/dist/shared/storage/routes.d.ts +0 -42
  225. package/dist/shared/storage/routes.d.ts.map +0 -1
  226. package/dist/shared/storage/routes.js +0 -160
  227. package/dist/shared/storage/routes.js.map +0 -1
  228. package/dist/shared/storage/types.d.ts +0 -53
  229. package/dist/shared/storage/types.d.ts.map +0 -1
  230. package/dist/shared/storage/types.js +0 -2
  231. package/dist/shared/storage/types.js.map +0 -1
  232. package/dist/shared/telegram/index.d.ts +0 -4
  233. package/dist/shared/telegram/index.d.ts.map +0 -1
  234. package/dist/shared/telegram/index.js +0 -3
  235. package/dist/shared/telegram/index.js.map +0 -1
  236. package/dist/shared/telegram/routes.d.ts +0 -43
  237. package/dist/shared/telegram/routes.d.ts.map +0 -1
  238. package/dist/shared/telegram/routes.js +0 -868
  239. package/dist/shared/telegram/routes.js.map +0 -1
  240. package/dist/shared/telegram/types.d.ts +0 -168
  241. package/dist/shared/telegram/types.d.ts.map +0 -1
  242. package/dist/shared/telegram/types.js +0 -7
  243. package/dist/shared/telegram/types.js.map +0 -1
  244. package/dist/shared/telegram/utils.d.ts +0 -44
  245. package/dist/shared/telegram/utils.d.ts.map +0 -1
  246. package/dist/shared/telegram/utils.js +0 -121
  247. package/dist/shared/telegram/utils.js.map +0 -1
  248. package/dist/storage.d.ts +0 -9
  249. package/dist/storage.d.ts.map +0 -1
  250. package/dist/storage.js +0 -8
  251. package/dist/storage.js.map +0 -1
  252. package/dist/telemetry.d.ts +0 -9
  253. package/dist/telemetry.d.ts.map +0 -1
  254. package/dist/telemetry.js +0 -8
  255. package/dist/telemetry.js.map +0 -1
  256. package/scripts/postinstall.js +0 -79
  257. package/src/shared/affiliate/migrations/001_create_affiliates.sql +0 -49
  258. package/src/shared/affiliate/migrations/002_create_affiliate_commissions.sql +0 -31
  259. package/src/shared/affiliate/migrations/003_create_affiliate_clicks.sql +0 -26
  260. package/src/shared/affiliate/migrations/004_create_affiliate_payments.sql +0 -34
  261. package/src/shared/affiliate/migrations/005_create_affiliate_tier_history.sql +0 -19
  262. package/src/shared/affiliate/migrations/006_create_affiliate_rpc_functions.sql +0 -209
  263. package/src/shared/affiliate/migrations/007_create_affiliate_rls_policies.sql +0 -123
  264. package/src/shared/billing/migrations/00000000000001_billing.sql +0 -114
  265. package/src/shared/email/migrations/000_create_email_logs.sql +0 -27
  266. package/src/shared/email/migrations/001_create_email_templates.sql +0 -27
  267. package/src/shared/email/migrations/002_add_rls_baseline_policies.sql +0 -37
  268. package/src/shared/email/migrations/003_create_gmail_accounts.sql +0 -82
  269. package/src/shared/email/migrations/004_add_email_logs_tracking_columns.sql +0 -15
  270. package/src/shared/mcp/migrations/001_mcp_api_tokens.sql +0 -21
  271. package/src/shared/mcp/migrations/002_mcp_audit_log.sql +0 -16
@@ -1,770 +0,0 @@
1
- /**
2
- * Planner Route Factories — standard scheduling endpoints
3
- *
4
- * Usage:
5
- * ```typescript
6
- * import { addPlannerRoutes, addPlannerPublicRoutes, addPlannerCalendarRoutes } from '@soulbatical/tetra-core';
7
- *
8
- * // Authenticated (behind auth middleware)
9
- * addPlannerRoutes(router, { config });
10
- *
11
- * // Public booking (no auth)
12
- * addPlannerPublicRoutes(publicRouter, { config, getSystemDB });
13
- *
14
- * // Google Calendar OAuth (behind auth middleware)
15
- * addPlannerCalendarRoutes(calendarRouter, { google: {...} });
16
- * ```
17
- */
18
- import { PlannerService } from './PlannerService.js';
19
- import { GoogleCalendarService } from './GoogleCalendarService.js';
20
- import { RFC7807ErrorResponse } from '../rfc7807ErrorResponse.js';
21
- import { createLogger } from '../../utils/logger.js';
22
- const logger = createLogger('planner:routes');
23
- const TIME_REGEX = /^\d{2}:\d{2}(:\d{2})?$/;
24
- const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
25
- // ─── Authenticated Planner Routes ───────────────────────────
26
- /**
27
- * Add authenticated planner routes to a router.
28
- *
29
- * Adds:
30
- * - GET /availability — Get own availability
31
- * - PUT /availability — Set own availability
32
- * - GET /owners/:ownerId/availability — Get an owner's availability
33
- * - GET /owners/:ownerId/available-slots — Get bookable slots for a date
34
- * - POST /owners/:ownerId/book — Book a slot with an owner
35
- * - GET /vacations — List own vacations
36
- * - POST /vacations — Create vacation
37
- * - DELETE /vacations/:id — Delete vacation
38
- * - GET /scheduler-settings — Get own scheduler settings
39
- * - PUT /scheduler-settings — Update own scheduler settings
40
- * - GET /appointments — List own appointments
41
- * - POST /appointments — Create appointment
42
- * - PUT /appointments/:id — Update appointment
43
- * - DELETE /appointments/:id — Cancel appointment
44
- * - POST /appointments/:id/complete — Complete appointment
45
- * - GET /team-availability — Team availability for a date (admin)
46
- *
47
- * Returns the PlannerService instance for external use.
48
- */
49
- export function addPlannerRoutes(router, options) {
50
- const service = new PlannerService(options.config);
51
- // ── Availability ────────────────────────────────────────
52
- // GET /availability — own
53
- router.get('/availability', async (req, res) => {
54
- try {
55
- const user = req.user;
56
- if (!user?.id)
57
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
58
- const { adminDB } = await import('../../core/adminDb.js');
59
- const db = await adminDB(req);
60
- const slots = await service.getAvailability(db, user.id);
61
- res.json({ success: true, data: slots });
62
- }
63
- catch (err) {
64
- logger.error({ error: err }, 'Get availability error');
65
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
66
- }
67
- });
68
- // PUT /availability — set own
69
- router.put('/availability', async (req, res) => {
70
- try {
71
- const user = req.user;
72
- if (!user?.id || !user?.organizationId)
73
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
74
- const { slots } = req.body;
75
- if (!Array.isArray(slots))
76
- return RFC7807ErrorResponse.badRequest(res, 'slots must be an array');
77
- const { adminDB } = await import('../../core/adminDb.js');
78
- const db = await adminDB(req);
79
- const result = await service.setAvailability(db, user.id, user.organizationId, slots);
80
- res.json({ success: true, data: result });
81
- }
82
- catch (err) {
83
- logger.error({ error: err }, 'Set availability error');
84
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
85
- }
86
- });
87
- // GET /owners/:ownerId/availability
88
- router.get('/owners/:ownerId/availability', async (req, res) => {
89
- try {
90
- const user = req.user;
91
- if (!user?.id)
92
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
93
- const { adminDB } = await import('../../core/adminDb.js');
94
- const db = await adminDB(req);
95
- const slots = await service.getAvailability(db, req.params.ownerId);
96
- res.json({ success: true, data: slots });
97
- }
98
- catch (err) {
99
- logger.error({ error: err }, 'Get owner availability error');
100
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
101
- }
102
- });
103
- // GET /owners/:ownerId/available-slots?date=YYYY-MM-DD
104
- router.get('/owners/:ownerId/available-slots', async (req, res) => {
105
- try {
106
- const user = req.user;
107
- if (!user?.id)
108
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
109
- const date = req.query.date;
110
- if (!date || !DATE_REGEX.test(date))
111
- return RFC7807ErrorResponse.badRequest(res, 'date query parameter required (YYYY-MM-DD)');
112
- const { adminDB } = await import('../../core/adminDb.js');
113
- const db = await adminDB(req);
114
- const result = await service.getAvailableSlots(db, req.params.ownerId, date);
115
- res.json({ success: true, data: result });
116
- }
117
- catch (err) {
118
- logger.error({ error: err }, 'Get available slots error');
119
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
120
- }
121
- });
122
- // POST /owners/:ownerId/book
123
- router.post('/owners/:ownerId/book', async (req, res) => {
124
- try {
125
- const user = req.user;
126
- if (!user?.id || !user?.organizationId)
127
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
128
- const { parentId, date, startTime, endTime, title } = req.body;
129
- if (!date || !startTime || !endTime)
130
- return RFC7807ErrorResponse.badRequest(res, 'date, startTime, and endTime are required');
131
- const startISO = `${date}T${startTime.length === 5 ? startTime + ':00' : startTime}Z`;
132
- const endISO = `${date}T${endTime.length === 5 ? endTime + ':00' : endTime}Z`;
133
- const { adminDB } = await import('../../core/adminDb.js');
134
- const db = await adminDB(req);
135
- const appointment = await service.createAppointment(db, user.id, user.organizationId, {
136
- title: title || 'Session',
137
- parentId,
138
- startTime: startISO,
139
- endTime: endISO,
140
- });
141
- res.status(201).json({ success: true, data: appointment });
142
- }
143
- catch (err) {
144
- logger.error({ error: err }, 'Book slot error');
145
- if (err.message.includes('conflict') || err.message.includes('outside')) {
146
- RFC7807ErrorResponse.conflict(res, err.message);
147
- }
148
- else {
149
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
150
- }
151
- }
152
- });
153
- // ── Vacations ───────────────────────────────────────────
154
- router.get('/vacations', async (req, res) => {
155
- try {
156
- const user = req.user;
157
- if (!user?.id)
158
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
159
- const { adminDB } = await import('../../core/adminDb.js');
160
- const db = await adminDB(req);
161
- const vacations = await service.getVacations(db, user.id);
162
- res.json({ success: true, data: vacations });
163
- }
164
- catch (err) {
165
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
166
- }
167
- });
168
- router.post('/vacations', async (req, res) => {
169
- try {
170
- const user = req.user;
171
- if (!user?.id || !user?.organizationId)
172
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
173
- const { startDate, endDate, description } = req.body;
174
- if (!startDate || !endDate)
175
- return RFC7807ErrorResponse.badRequest(res, 'startDate and endDate are required');
176
- const { adminDB } = await import('../../core/adminDb.js');
177
- const db = await adminDB(req);
178
- const vacation = await service.createVacation(db, user.id, user.organizationId, startDate, endDate, description);
179
- res.status(201).json({ success: true, data: vacation });
180
- }
181
- catch (err) {
182
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
183
- }
184
- });
185
- router.delete('/vacations/:id', async (req, res) => {
186
- try {
187
- const user = req.user;
188
- if (!user?.id)
189
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
190
- const { adminDB } = await import('../../core/adminDb.js');
191
- const db = await adminDB(req);
192
- await service.deleteVacation(db, req.params.id);
193
- res.json({ success: true });
194
- }
195
- catch (err) {
196
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
197
- }
198
- });
199
- // ── Scheduler Settings ──────────────────────────────────
200
- router.get('/scheduler-settings', async (req, res) => {
201
- try {
202
- const user = req.user;
203
- if (!user?.id)
204
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
205
- const { adminDB } = await import('../../core/adminDb.js');
206
- const db = await adminDB(req);
207
- const settings = await service.getSettings(db, user.id);
208
- res.json({ success: true, data: settings });
209
- }
210
- catch (err) {
211
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
212
- }
213
- });
214
- router.put('/scheduler-settings', async (req, res) => {
215
- try {
216
- const user = req.user;
217
- if (!user?.id || !user?.organizationId)
218
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
219
- const { adminDB } = await import('../../core/adminDb.js');
220
- const db = await adminDB(req);
221
- const result = await service.updateSettings(db, user.id, user.organizationId, req.body);
222
- res.json({ success: true, data: result });
223
- }
224
- catch (err) {
225
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
226
- }
227
- });
228
- // ── Appointments ────────────────────────────────────────
229
- router.get('/appointments', async (req, res) => {
230
- try {
231
- const user = req.user;
232
- if (!user?.id)
233
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
234
- const { adminDB } = await import('../../core/adminDb.js');
235
- const db = await adminDB(req);
236
- const appointments = await service.listAppointments(db, user.id, {
237
- status: req.query.status,
238
- });
239
- res.json({ success: true, data: appointments });
240
- }
241
- catch (err) {
242
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
243
- }
244
- });
245
- router.post('/appointments', async (req, res) => {
246
- try {
247
- const user = req.user;
248
- if (!user?.id || !user?.organizationId)
249
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
250
- const { title, parentId, description, startTime, endTime, location, type, attendeeIds } = req.body;
251
- if (!title || !startTime || !endTime)
252
- return RFC7807ErrorResponse.badRequest(res, 'title, startTime, and endTime are required');
253
- const { adminDB } = await import('../../core/adminDb.js');
254
- const db = await adminDB(req);
255
- const appointment = await service.createAppointment(db, user.id, user.organizationId, {
256
- title, parentId, description, startTime, endTime, location, type, attendeeIds,
257
- });
258
- res.status(201).json({ success: true, data: appointment });
259
- }
260
- catch (err) {
261
- if (err.message.includes('Invalid'))
262
- RFC7807ErrorResponse.badRequest(res, err.message);
263
- else
264
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
265
- }
266
- });
267
- router.put('/appointments/:id', async (req, res) => {
268
- try {
269
- const user = req.user;
270
- if (!user?.id)
271
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
272
- const { adminDB } = await import('../../core/adminDb.js');
273
- const db = await adminDB(req);
274
- const appointment = await service.updateAppointment(db, req.params.id, req.body);
275
- res.json({ success: true, data: appointment });
276
- }
277
- catch (err) {
278
- if (err.message === 'Appointment not found')
279
- RFC7807ErrorResponse.notFound(res, 'Appointment');
280
- else
281
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
282
- }
283
- });
284
- router.delete('/appointments/:id', async (req, res) => {
285
- try {
286
- const user = req.user;
287
- if (!user?.id || !user?.organizationId)
288
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
289
- const { adminDB } = await import('../../core/adminDb.js');
290
- const db = await adminDB(req);
291
- const appointment = await service.cancelAppointment(db, req.params.id, user.organizationId);
292
- res.json({ success: true, data: appointment });
293
- }
294
- catch (err) {
295
- if (err.message === 'Appointment not found')
296
- RFC7807ErrorResponse.notFound(res, 'Appointment');
297
- else
298
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
299
- }
300
- });
301
- router.post('/appointments/:id/complete', async (req, res) => {
302
- try {
303
- const user = req.user;
304
- if (!user?.id || !user?.organizationId)
305
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
306
- const { adminDB } = await import('../../core/adminDb.js');
307
- const db = await adminDB(req);
308
- const appointment = await service.completeAppointment(db, req.params.id, user.organizationId);
309
- res.json({ success: true, data: appointment });
310
- }
311
- catch (err) {
312
- if (err.message === 'Appointment not found')
313
- RFC7807ErrorResponse.notFound(res, 'Appointment');
314
- else
315
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
316
- }
317
- });
318
- // ── Team Availability ───────────────────────────────────
319
- router.get('/team-availability', async (req, res) => {
320
- try {
321
- const user = req.user;
322
- if (!user?.id || !user?.organizationId)
323
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
324
- const date = req.query.date;
325
- if (!date || !DATE_REGEX.test(date))
326
- return RFC7807ErrorResponse.badRequest(res, 'date query parameter required (YYYY-MM-DD)');
327
- const { adminDB } = await import('../../core/adminDb.js');
328
- const db = await adminDB(req);
329
- const result = await service.getTeamAvailability(db, user.organizationId, date);
330
- res.json({ success: true, data: result });
331
- }
332
- catch (err) {
333
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
334
- }
335
- });
336
- // ── Computed Availability (Phase 2) ───────────────────────
337
- // GET /computed-availability/:ownerId?dateFrom=YYYY-MM-DD&dateTo=YYYY-MM-DD
338
- router.get('/computed-availability/:ownerId', async (req, res) => {
339
- try {
340
- const user = req.user;
341
- if (!user?.id)
342
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
343
- const dateFrom = req.query.dateFrom;
344
- const dateTo = req.query.dateTo;
345
- if (!dateFrom || !dateTo || !DATE_REGEX.test(dateFrom) || !DATE_REGEX.test(dateTo)) {
346
- return RFC7807ErrorResponse.badRequest(res, 'dateFrom and dateTo (YYYY-MM-DD) are required');
347
- }
348
- const { adminDB } = await import('../../core/adminDb.js');
349
- const db = await adminDB(req);
350
- const result = await service.getComputedAvailability(db, req.params.ownerId, dateFrom, dateTo);
351
- res.json({ success: true, data: result });
352
- }
353
- catch (err) {
354
- logger.error({ error: err }, 'Computed availability error');
355
- if (err.message?.includes('YYYY-MM-DD') || err.message?.includes('on or before')) {
356
- return RFC7807ErrorResponse.badRequest(res, err.message);
357
- }
358
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
359
- }
360
- });
361
- // ── Slot Reservations (Phase 3) ───────────────────────────
362
- // POST /reserve-slot { ownerId, slotStart, slotEnd, ttlSeconds? }
363
- router.post('/reserve-slot', async (req, res) => {
364
- try {
365
- const user = req.user;
366
- if (!user?.id || !user?.organizationId)
367
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
368
- const { ownerId, slotStart, slotEnd, ttlSeconds } = req.body;
369
- if (!ownerId || !slotStart || !slotEnd) {
370
- return RFC7807ErrorResponse.badRequest(res, 'ownerId, slotStart and slotEnd are required');
371
- }
372
- const { adminDB } = await import('../../core/adminDb.js');
373
- const db = await adminDB(req);
374
- const reservation = await service.reserveSlot(db, ownerId, user.organizationId, slotStart, slotEnd, ttlSeconds || 300);
375
- res.status(201).json({ success: true, data: reservation });
376
- }
377
- catch (err) {
378
- if (err.message?.includes('already reserved') || err.message?.includes('Slot')) {
379
- return RFC7807ErrorResponse.conflict(res, err.message);
380
- }
381
- logger.error({ error: err }, 'Reserve slot error');
382
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
383
- }
384
- });
385
- // DELETE /reserve-slot/:id
386
- router.delete('/reserve-slot/:id', async (req, res) => {
387
- try {
388
- const user = req.user;
389
- if (!user?.id)
390
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
391
- const { adminDB } = await import('../../core/adminDb.js');
392
- const db = await adminDB(req);
393
- await service.releaseSlot(db, req.params.id);
394
- res.json({ success: true });
395
- }
396
- catch (err) {
397
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
398
- }
399
- });
400
- // ── Coach Auto-Assignment (Phase 3) ───────────────────────
401
- // GET /coach-assignment-config — list coaches with config + active counts
402
- router.get('/coach-assignment-config', async (req, res) => {
403
- try {
404
- const user = req.user;
405
- if (!user?.id || !user?.organizationId)
406
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
407
- const { adminDB } = await import('../../core/adminDb.js');
408
- const db = await adminDB(req);
409
- const result = await service.getCoachAssignmentConfig(db, user.organizationId);
410
- res.json({ success: true, data: result });
411
- }
412
- catch (err) {
413
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
414
- }
415
- });
416
- // PUT /coach-assignment-config/:coachId
417
- router.put('/coach-assignment-config/:coachId', async (req, res) => {
418
- try {
419
- const user = req.user;
420
- if (!user?.id || !user?.organizationId)
421
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
422
- // Use systemDB to bypass RLS — admin-only endpoint, org_id is enforced at code level
423
- const { systemDB } = await import('../../core/systemDb.js');
424
- const db = await systemDB('planner:update-coach-assignment-config');
425
- await service.updateCoachAssignmentConfig(db, user.organizationId, req.params.coachId, req.body);
426
- res.json({ success: true });
427
- }
428
- catch (err) {
429
- logger.error({ error: err }, 'Update coach assignment config error');
430
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
431
- }
432
- });
433
- // POST /auto-assign-coach
434
- router.post('/auto-assign-coach', async (req, res) => {
435
- try {
436
- const user = req.user;
437
- if (!user?.id || !user?.organizationId)
438
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
439
- // systemDB needed because findBestCoach writes last_assigned_at
440
- const { systemDB } = await import('../../core/systemDb.js');
441
- const db = await systemDB('planner:auto-assign-coach');
442
- const result = await service.findBestCoach(db, user.organizationId);
443
- res.json({ success: true, data: result });
444
- }
445
- catch (err) {
446
- if (err.message?.includes('No available') || err.message?.includes('maximum')) {
447
- return RFC7807ErrorResponse.conflict(res, err.message);
448
- }
449
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
450
- }
451
- });
452
- // GET /team-appointments — all org appointments (for team planner view)
453
- router.get('/team-appointments', async (req, res) => {
454
- try {
455
- const user = req.user;
456
- if (!user?.id || !user?.organizationId)
457
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
458
- const { adminDB } = await import('../../core/adminDb.js');
459
- const db = await adminDB(req);
460
- const appointments = await service.listTeamAppointments(db, user.organizationId, {
461
- status: req.query.status,
462
- dateFrom: req.query.dateFrom,
463
- dateTo: req.query.dateTo,
464
- });
465
- res.json({ success: true, data: appointments });
466
- }
467
- catch (err) {
468
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
469
- }
470
- });
471
- return service;
472
- }
473
- // ─── Public Planner Routes (no auth) ────────────────────────
474
- /**
475
- * Add public booking routes (Calendly-style, no authentication).
476
- *
477
- * Adds:
478
- * - GET /scheduler/:ownerId — Scheduler page data
479
- * - GET /scheduler/:ownerId/slots — Available slots for a date
480
- * - POST /scheduler/:ownerId/book — Book a slot
481
- */
482
- export function addPlannerPublicRoutes(router, options) {
483
- const service = new PlannerService(options.config);
484
- const getDB = async () => {
485
- if (options.getSystemDB)
486
- return options.getSystemDB();
487
- const { systemDB } = await import('../../core/systemDb.js');
488
- return systemDB('planner:public');
489
- };
490
- // GET /scheduler/:ownerId
491
- router.get('/scheduler/:ownerId', async (req, res) => {
492
- try {
493
- const db = await getDB();
494
- const data = await service.getPublicSchedulerData(db, req.params.ownerId);
495
- if (!data)
496
- return RFC7807ErrorResponse.notFound(res, 'Owner');
497
- res.json({ success: true, data });
498
- }
499
- catch (err) {
500
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
501
- }
502
- });
503
- // GET /scheduler/:ownerId/slots?date=YYYY-MM-DD
504
- router.get('/scheduler/:ownerId/slots', async (req, res) => {
505
- try {
506
- const date = req.query.date;
507
- if (!date || !DATE_REGEX.test(date))
508
- return RFC7807ErrorResponse.badRequest(res, 'date required (YYYY-MM-DD)');
509
- const db = await getDB();
510
- const result = await service.getAvailableSlots(db, req.params.ownerId, date);
511
- res.json({ success: true, data: result });
512
- }
513
- catch (err) {
514
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
515
- }
516
- });
517
- // POST /scheduler/:ownerId/book
518
- router.post('/scheduler/:ownerId/book', async (req, res) => {
519
- try {
520
- const db = await getDB();
521
- const appointment = await service.publicBook(db, req.params.ownerId, req.body);
522
- res.status(201).json({ success: true, data: appointment });
523
- }
524
- catch (err) {
525
- if (err.message.includes('outside') || err.message.includes('no longer') || err.message.includes('vacation')) {
526
- RFC7807ErrorResponse.conflict(res, err.message);
527
- }
528
- else if (err.message.includes('required') || err.message.includes('Invalid')) {
529
- RFC7807ErrorResponse.badRequest(res, err.message);
530
- }
531
- else {
532
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
533
- }
534
- }
535
- });
536
- return service;
537
- }
538
- // ─── Google Calendar Routes ─────────────────────────────────
539
- /**
540
- * Add Google Calendar OAuth routes.
541
- *
542
- * Adds:
543
- * - GET /auth-url — Generate OAuth URL
544
- * - GET /callback — Handle OAuth callback
545
- * - GET /status — Connection status
546
- * - GET /calendars — List available calendars
547
- * - PUT /calendar-id — Select single calendar (legacy)
548
- * - GET /selected-calendars — List calendars with multi-select state
549
- * - PUT /selected-calendars — Multi-select calendars for conflict checking
550
- * - POST /disconnect — Remove connection
551
- */
552
- export function addPlannerCalendarRoutes(router, config) {
553
- const calService = new GoogleCalendarService(config);
554
- // GET /auth-url
555
- router.get('/auth-url', async (req, res) => {
556
- try {
557
- const user = req.user;
558
- if (!user?.id)
559
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
560
- const url = await calService.getAuthUrl(user.id);
561
- res.json({ success: true, data: { url } });
562
- }
563
- catch (err) {
564
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
565
- }
566
- });
567
- // GET /callback
568
- router.get('/callback', async (req, res) => {
569
- try {
570
- const code = req.query.code;
571
- const stateRaw = req.query.state;
572
- if (!code || !stateRaw)
573
- return RFC7807ErrorResponse.badRequest(res, 'Missing code or state');
574
- let state;
575
- try {
576
- state = JSON.parse(Buffer.from(stateRaw, 'base64').toString());
577
- }
578
- catch {
579
- return RFC7807ErrorResponse.badRequest(res, 'Invalid state');
580
- }
581
- // Look up org for user
582
- const { systemDB } = await import('../../core/systemDb.js');
583
- const db = await systemDB('planner:calendar-callback');
584
- const { data: userData } = await db
585
- .from('users_public')
586
- .select('active_organization_id')
587
- .eq('id', state.userId)
588
- .single();
589
- if (!userData)
590
- return RFC7807ErrorResponse.notFound(res, 'User');
591
- await calService.handleCallback(code, state.userId, userData.active_organization_id);
592
- // Redirect to frontend settings
593
- const frontendUrl = process.env.FRONTEND_URL || '';
594
- res.redirect(`${frontendUrl}/settings?calendar=connected`);
595
- }
596
- catch (err) {
597
- logger.error({ error: err }, 'Calendar callback error');
598
- const frontendUrl = process.env.FRONTEND_URL || '';
599
- res.redirect(`${frontendUrl}/settings?calendar=error&reason=exchange_failed`);
600
- }
601
- });
602
- // GET /status
603
- router.get('/status', async (req, res) => {
604
- try {
605
- const user = req.user;
606
- if (!user?.id)
607
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
608
- const connected = await calService.isConnected(user.id);
609
- res.json({ success: true, data: { connected } });
610
- }
611
- catch (err) {
612
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
613
- }
614
- });
615
- // GET /calendars
616
- router.get('/calendars', async (req, res) => {
617
- try {
618
- const user = req.user;
619
- if (!user?.id)
620
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
621
- const calendars = await calService.listCalendars(user.id);
622
- res.json({ success: true, data: calendars || [] });
623
- }
624
- catch (err) {
625
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
626
- }
627
- });
628
- // PUT /calendar-id
629
- router.put('/calendar-id', async (req, res) => {
630
- try {
631
- const user = req.user;
632
- if (!user?.id)
633
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
634
- const { calendarId } = req.body;
635
- if (!calendarId)
636
- return RFC7807ErrorResponse.badRequest(res, 'calendarId is required');
637
- const success = await calService.selectCalendar(user.id, calendarId);
638
- if (!success)
639
- return RFC7807ErrorResponse.internalError(res, 'Failed to select calendar');
640
- res.json({ success: true });
641
- }
642
- catch (err) {
643
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
644
- }
645
- });
646
- // GET /selected-calendars — List calendars with multi-select state
647
- router.get('/selected-calendars', async (req, res) => {
648
- try {
649
- const user = req.user;
650
- if (!user?.id)
651
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
652
- const calendars = await calService.listCalendars(user.id);
653
- if (!calendars)
654
- return res.json({ success: true, data: [] });
655
- // Map to the format the frontend expects
656
- res.json({
657
- success: true,
658
- data: calendars.map((cal) => ({
659
- id: cal.id,
660
- externalCalendarId: cal.id,
661
- calendarName: cal.name,
662
- calendarColor: cal.color,
663
- isPrimary: cal.isPrimary,
664
- isSelected: cal.isSelected,
665
- })),
666
- });
667
- }
668
- catch (err) {
669
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
670
- }
671
- });
672
- // PUT /selected-calendars — Multi-select calendars for conflict checking
673
- router.put('/selected-calendars', async (req, res) => {
674
- try {
675
- const user = req.user;
676
- if (!user?.id)
677
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
678
- const { calendars } = req.body;
679
- if (!Array.isArray(calendars)) {
680
- return RFC7807ErrorResponse.badRequest(res, 'calendars must be an array');
681
- }
682
- const selectedIds = calendars
683
- .filter((c) => c.isSelected)
684
- .map((c) => c.externalCalendarId || c.id);
685
- if (selectedIds.length === 0) {
686
- return RFC7807ErrorResponse.badRequest(res, 'At least one calendar must be selected');
687
- }
688
- const success = await calService.selectCalendars(user.id, selectedIds);
689
- if (!success)
690
- return RFC7807ErrorResponse.internalError(res, 'Failed to save selection');
691
- res.json({ success: true });
692
- }
693
- catch (err) {
694
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
695
- }
696
- });
697
- // POST /disconnect
698
- router.post('/disconnect', async (req, res) => {
699
- try {
700
- const user = req.user;
701
- if (!user?.id)
702
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
703
- await calService.disconnect(user.id);
704
- res.json({ success: true });
705
- }
706
- catch (err) {
707
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
708
- }
709
- });
710
- // GET /busy-slots?dateFrom=YYYY-MM-DD&dateTo=YYYY-MM-DD
711
- // Returns busy slots from ALL selected calendars (Phase 1).
712
- router.get('/busy-slots', async (req, res) => {
713
- try {
714
- const user = req.user;
715
- if (!user?.id)
716
- return RFC7807ErrorResponse.unauthorized(res, 'Authentication required');
717
- const dateFrom = req.query.dateFrom;
718
- const dateTo = req.query.dateTo;
719
- if (!dateFrom || !dateTo || !DATE_REGEX.test(dateFrom) || !DATE_REGEX.test(dateTo)) {
720
- return RFC7807ErrorResponse.badRequest(res, 'dateFrom and dateTo (YYYY-MM-DD) are required');
721
- }
722
- const slots = await calService.getBusySlots(user.id, dateFrom, dateTo);
723
- res.json({ success: true, data: slots });
724
- }
725
- catch (err) {
726
- logger.error({ error: err }, 'Get busy slots error');
727
- RFC7807ErrorResponse.internalError(res, err.message || 'Failed');
728
- }
729
- });
730
- // POST /webhook — Google Calendar push notification receiver (Phase 4)
731
- // Google sends a POST with X-Goog-* headers when any event changes.
732
- // We don't trust the body — we look up the channel and refetch.
733
- router.post('/webhook', async (req, res) => {
734
- try {
735
- const channelId = req.headers['x-goog-channel-id'];
736
- const resourceId = req.headers['x-goog-resource-id'];
737
- const resourceState = req.headers['x-goog-resource-state'];
738
- if (!channelId || !resourceId) {
739
- return res.status(400).json({ success: false, error: 'Missing channel headers' });
740
- }
741
- logger.info({ channelId, resourceId, resourceState }, 'Calendar webhook received');
742
- // Acknowledge fast — actual cache refresh happens async via cron or queue
743
- const { systemDB } = await import('../../core/systemDb.js');
744
- const db = await systemDB('planner:webhook');
745
- // Mark cache stale by deleting synced_at marker for this user/calendar
746
- // (a follow-up cron job will refetch)
747
- const { data: channel } = await db
748
- .from('planner_calendar_channels')
749
- .select('user_id, external_calendar_id')
750
- .eq('channel_id', channelId)
751
- .single();
752
- if (channel) {
753
- // Touch the cache so the next read knows to refresh
754
- await db
755
- .from('planner_calendar_cache')
756
- .update({ synced_at: new Date(0).toISOString() })
757
- .eq('user_id', channel.user_id)
758
- .eq('external_calendar_id', channel.external_calendar_id);
759
- }
760
- res.status(200).end();
761
- }
762
- catch (err) {
763
- logger.error({ error: err }, 'Calendar webhook error');
764
- // Always 200 to Google to avoid retries
765
- res.status(200).end();
766
- }
767
- });
768
- return calService;
769
- }
770
- //# sourceMappingURL=routes.js.map