@soulbatical/tetra-core 0.10.5 → 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 (245) hide show
  1. package/README.md +32 -6
  2. package/dist/core/createApp.d.ts +1 -1
  3. package/dist/core/createApp.d.ts.map +1 -1
  4. package/dist/generators/rls-checker.d.ts +1 -1
  5. package/dist/generators/rls-checker.js +1 -1
  6. package/dist/generators/rls-exec-sql.d.ts +1 -1
  7. package/dist/generators/rls-exec-sql.js +1 -1
  8. package/dist/generators/rpc/index.d.ts +1 -1
  9. package/dist/generators/rpc/index.js +1 -1
  10. package/dist/index.d.ts +0 -6
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +3 -9
  13. package/dist/index.js.map +1 -1
  14. package/dist/middleware/securityMiddleware.d.ts +1 -1
  15. package/dist/middleware/securityMiddleware.d.ts.map +1 -1
  16. package/dist/utils/logger.d.ts.map +1 -1
  17. package/dist/utils/logger.js +12 -1
  18. package/dist/utils/logger.js.map +1 -1
  19. package/package.json +30 -44
  20. package/dist/affiliate.d.ts +0 -11
  21. package/dist/affiliate.d.ts.map +0 -1
  22. package/dist/affiliate.js +0 -10
  23. package/dist/affiliate.js.map +0 -1
  24. package/dist/billing.d.ts +0 -8
  25. package/dist/billing.d.ts.map +0 -1
  26. package/dist/billing.js +0 -7
  27. package/dist/billing.js.map +0 -1
  28. package/dist/email.d.ts +0 -9
  29. package/dist/email.d.ts.map +0 -1
  30. package/dist/email.js +0 -8
  31. package/dist/email.js.map +0 -1
  32. package/dist/generators/rls-exec-sql.sql +0 -57
  33. package/dist/generators.d.ts +0 -9
  34. package/dist/generators.d.ts.map +0 -1
  35. package/dist/generators.js +0 -8
  36. package/dist/generators.js.map +0 -1
  37. package/dist/mcp.d.ts +0 -8
  38. package/dist/mcp.d.ts.map +0 -1
  39. package/dist/mcp.js +0 -7
  40. package/dist/mcp.js.map +0 -1
  41. package/dist/planner.d.ts +0 -8
  42. package/dist/planner.d.ts.map +0 -1
  43. package/dist/planner.js +0 -7
  44. package/dist/planner.js.map +0 -1
  45. package/dist/shared/affiliate/AffiliateAttributionService.d.ts +0 -47
  46. package/dist/shared/affiliate/AffiliateAttributionService.d.ts.map +0 -1
  47. package/dist/shared/affiliate/AffiliateAttributionService.js +0 -308
  48. package/dist/shared/affiliate/AffiliateAttributionService.js.map +0 -1
  49. package/dist/shared/affiliate/AffiliateClickService.d.ts +0 -35
  50. package/dist/shared/affiliate/AffiliateClickService.d.ts.map +0 -1
  51. package/dist/shared/affiliate/AffiliateClickService.js +0 -87
  52. package/dist/shared/affiliate/AffiliateClickService.js.map +0 -1
  53. package/dist/shared/affiliate/affiliateFeatureConfig.d.ts +0 -11
  54. package/dist/shared/affiliate/affiliateFeatureConfig.d.ts.map +0 -1
  55. package/dist/shared/affiliate/affiliateFeatureConfig.js +0 -242
  56. package/dist/shared/affiliate/affiliateFeatureConfig.js.map +0 -1
  57. package/dist/shared/affiliate/index.d.ts +0 -11
  58. package/dist/shared/affiliate/index.d.ts.map +0 -1
  59. package/dist/shared/affiliate/index.js +0 -13
  60. package/dist/shared/affiliate/index.js.map +0 -1
  61. package/dist/shared/affiliate/routes.d.ts +0 -87
  62. package/dist/shared/affiliate/routes.d.ts.map +0 -1
  63. package/dist/shared/affiliate/routes.js +0 -404
  64. package/dist/shared/affiliate/routes.js.map +0 -1
  65. package/dist/shared/affiliate/types.d.ts +0 -170
  66. package/dist/shared/affiliate/types.d.ts.map +0 -1
  67. package/dist/shared/affiliate/types.js +0 -11
  68. package/dist/shared/affiliate/types.js.map +0 -1
  69. package/dist/shared/billing/BillingService.d.ts +0 -56
  70. package/dist/shared/billing/BillingService.d.ts.map +0 -1
  71. package/dist/shared/billing/BillingService.js +0 -588
  72. package/dist/shared/billing/BillingService.js.map +0 -1
  73. package/dist/shared/billing/SeatBillingService.d.ts +0 -106
  74. package/dist/shared/billing/SeatBillingService.d.ts.map +0 -1
  75. package/dist/shared/billing/SeatBillingService.js +0 -292
  76. package/dist/shared/billing/SeatBillingService.js.map +0 -1
  77. package/dist/shared/billing/index.d.ts +0 -30
  78. package/dist/shared/billing/index.d.ts.map +0 -1
  79. package/dist/shared/billing/index.js +0 -27
  80. package/dist/shared/billing/index.js.map +0 -1
  81. package/dist/shared/billing/routes.d.ts +0 -45
  82. package/dist/shared/billing/routes.d.ts.map +0 -1
  83. package/dist/shared/billing/routes.js +0 -184
  84. package/dist/shared/billing/routes.js.map +0 -1
  85. package/dist/shared/billing/seat-pricing.d.ts +0 -53
  86. package/dist/shared/billing/seat-pricing.d.ts.map +0 -1
  87. package/dist/shared/billing/seat-pricing.js +0 -81
  88. package/dist/shared/billing/seat-pricing.js.map +0 -1
  89. package/dist/shared/billing/types.d.ts +0 -109
  90. package/dist/shared/billing/types.d.ts.map +0 -1
  91. package/dist/shared/billing/types.js +0 -8
  92. package/dist/shared/billing/types.js.map +0 -1
  93. package/dist/shared/email/EmailService.d.ts +0 -64
  94. package/dist/shared/email/EmailService.d.ts.map +0 -1
  95. package/dist/shared/email/EmailService.js +0 -300
  96. package/dist/shared/email/EmailService.js.map +0 -1
  97. package/dist/shared/email/adminRoutes.d.ts +0 -30
  98. package/dist/shared/email/adminRoutes.d.ts.map +0 -1
  99. package/dist/shared/email/adminRoutes.js +0 -227
  100. package/dist/shared/email/adminRoutes.js.map +0 -1
  101. package/dist/shared/email/gmail.d.ts +0 -208
  102. package/dist/shared/email/gmail.d.ts.map +0 -1
  103. package/dist/shared/email/gmail.js +0 -626
  104. package/dist/shared/email/gmail.js.map +0 -1
  105. package/dist/shared/email/index.d.ts +0 -15
  106. package/dist/shared/email/index.d.ts.map +0 -1
  107. package/dist/shared/email/index.js +0 -18
  108. package/dist/shared/email/index.js.map +0 -1
  109. package/dist/shared/email/mailgun.d.ts +0 -18
  110. package/dist/shared/email/mailgun.d.ts.map +0 -1
  111. package/dist/shared/email/mailgun.js +0 -76
  112. package/dist/shared/email/mailgun.js.map +0 -1
  113. package/dist/shared/email/sanitize.d.ts +0 -25
  114. package/dist/shared/email/sanitize.d.ts.map +0 -1
  115. package/dist/shared/email/sanitize.js +0 -39
  116. package/dist/shared/email/sanitize.js.map +0 -1
  117. package/dist/shared/email/smtp.d.ts +0 -20
  118. package/dist/shared/email/smtp.d.ts.map +0 -1
  119. package/dist/shared/email/smtp.js +0 -53
  120. package/dist/shared/email/smtp.js.map +0 -1
  121. package/dist/shared/email/types.d.ts +0 -113
  122. package/dist/shared/email/types.d.ts.map +0 -1
  123. package/dist/shared/email/types.js +0 -7
  124. package/dist/shared/email/types.js.map +0 -1
  125. package/dist/shared/email/webhookRoutes.d.ts +0 -29
  126. package/dist/shared/email/webhookRoutes.d.ts.map +0 -1
  127. package/dist/shared/email/webhookRoutes.js +0 -125
  128. package/dist/shared/email/webhookRoutes.js.map +0 -1
  129. package/dist/shared/mcp/index.d.ts +0 -51
  130. package/dist/shared/mcp/index.d.ts.map +0 -1
  131. package/dist/shared/mcp/index.js +0 -51
  132. package/dist/shared/mcp/index.js.map +0 -1
  133. package/dist/shared/mcp/mcp-auth-routes.d.ts +0 -26
  134. package/dist/shared/mcp/mcp-auth-routes.d.ts.map +0 -1
  135. package/dist/shared/mcp/mcp-auth-routes.js +0 -141
  136. package/dist/shared/mcp/mcp-auth-routes.js.map +0 -1
  137. package/dist/shared/mcp/mcp-db.d.ts +0 -99
  138. package/dist/shared/mcp/mcp-db.d.ts.map +0 -1
  139. package/dist/shared/mcp/mcp-db.js +0 -106
  140. package/dist/shared/mcp/mcp-db.js.map +0 -1
  141. package/dist/shared/mcp/mcp-routes.d.ts +0 -29
  142. package/dist/shared/mcp/mcp-routes.d.ts.map +0 -1
  143. package/dist/shared/mcp/mcp-routes.js +0 -171
  144. package/dist/shared/mcp/mcp-routes.js.map +0 -1
  145. package/dist/shared/mcp/mcp-tokens-routes.d.ts +0 -35
  146. package/dist/shared/mcp/mcp-tokens-routes.d.ts.map +0 -1
  147. package/dist/shared/mcp/mcp-tokens-routes.js +0 -94
  148. package/dist/shared/mcp/mcp-tokens-routes.js.map +0 -1
  149. package/dist/shared/mcp/mcp-usage-routes.d.ts +0 -17
  150. package/dist/shared/mcp/mcp-usage-routes.d.ts.map +0 -1
  151. package/dist/shared/mcp/mcp-usage-routes.js +0 -81
  152. package/dist/shared/mcp/mcp-usage-routes.js.map +0 -1
  153. package/dist/shared/mcp/tenant-context.d.ts +0 -59
  154. package/dist/shared/mcp/tenant-context.d.ts.map +0 -1
  155. package/dist/shared/mcp/tenant-context.js +0 -136
  156. package/dist/shared/mcp/tenant-context.js.map +0 -1
  157. package/dist/shared/mcp/types.d.ts +0 -74
  158. package/dist/shared/mcp/types.d.ts.map +0 -1
  159. package/dist/shared/mcp/types.js +0 -7
  160. package/dist/shared/mcp/types.js.map +0 -1
  161. package/dist/shared/planner/GoogleCalendarService.d.ts +0 -137
  162. package/dist/shared/planner/GoogleCalendarService.d.ts.map +0 -1
  163. package/dist/shared/planner/GoogleCalendarService.js +0 -525
  164. package/dist/shared/planner/GoogleCalendarService.js.map +0 -1
  165. package/dist/shared/planner/PlannerService.d.ts +0 -264
  166. package/dist/shared/planner/PlannerService.d.ts.map +0 -1
  167. package/dist/shared/planner/PlannerService.js +0 -1393
  168. package/dist/shared/planner/PlannerService.js.map +0 -1
  169. package/dist/shared/planner/index.d.ts +0 -37
  170. package/dist/shared/planner/index.d.ts.map +0 -1
  171. package/dist/shared/planner/index.js +0 -35
  172. package/dist/shared/planner/index.js.map +0 -1
  173. package/dist/shared/planner/intervals.d.ts +0 -60
  174. package/dist/shared/planner/intervals.d.ts.map +0 -1
  175. package/dist/shared/planner/intervals.js +0 -141
  176. package/dist/shared/planner/intervals.js.map +0 -1
  177. package/dist/shared/planner/routes.d.ts +0 -69
  178. package/dist/shared/planner/routes.d.ts.map +0 -1
  179. package/dist/shared/planner/routes.js +0 -770
  180. package/dist/shared/planner/routes.js.map +0 -1
  181. package/dist/shared/planner/types.d.ts +0 -328
  182. package/dist/shared/planner/types.d.ts.map +0 -1
  183. package/dist/shared/planner/types.js +0 -9
  184. package/dist/shared/planner/types.js.map +0 -1
  185. package/dist/shared/storage/ImageProcessingService.d.ts +0 -32
  186. package/dist/shared/storage/ImageProcessingService.d.ts.map +0 -1
  187. package/dist/shared/storage/ImageProcessingService.js +0 -127
  188. package/dist/shared/storage/ImageProcessingService.js.map +0 -1
  189. package/dist/shared/storage/StorageProxyService.d.ts +0 -47
  190. package/dist/shared/storage/StorageProxyService.d.ts.map +0 -1
  191. package/dist/shared/storage/StorageProxyService.js +0 -196
  192. package/dist/shared/storage/StorageProxyService.js.map +0 -1
  193. package/dist/shared/storage/StorageUploadService.d.ts +0 -126
  194. package/dist/shared/storage/StorageUploadService.d.ts.map +0 -1
  195. package/dist/shared/storage/StorageUploadService.js +0 -206
  196. package/dist/shared/storage/StorageUploadService.js.map +0 -1
  197. package/dist/shared/storage/creative-urls.d.ts +0 -14
  198. package/dist/shared/storage/creative-urls.d.ts.map +0 -1
  199. package/dist/shared/storage/creative-urls.js +0 -30
  200. package/dist/shared/storage/creative-urls.js.map +0 -1
  201. package/dist/shared/storage/index.d.ts +0 -28
  202. package/dist/shared/storage/index.d.ts.map +0 -1
  203. package/dist/shared/storage/index.js +0 -27
  204. package/dist/shared/storage/index.js.map +0 -1
  205. package/dist/shared/storage/routes.d.ts +0 -42
  206. package/dist/shared/storage/routes.d.ts.map +0 -1
  207. package/dist/shared/storage/routes.js +0 -160
  208. package/dist/shared/storage/routes.js.map +0 -1
  209. package/dist/shared/storage/types.d.ts +0 -53
  210. package/dist/shared/storage/types.d.ts.map +0 -1
  211. package/dist/shared/storage/types.js +0 -2
  212. package/dist/shared/storage/types.js.map +0 -1
  213. package/dist/shared/telegram/index.d.ts +0 -4
  214. package/dist/shared/telegram/index.d.ts.map +0 -1
  215. package/dist/shared/telegram/index.js +0 -3
  216. package/dist/shared/telegram/index.js.map +0 -1
  217. package/dist/shared/telegram/routes.d.ts +0 -48
  218. package/dist/shared/telegram/routes.d.ts.map +0 -1
  219. package/dist/shared/telegram/routes.js +0 -918
  220. package/dist/shared/telegram/routes.js.map +0 -1
  221. package/dist/shared/telegram/types.d.ts +0 -186
  222. package/dist/shared/telegram/types.d.ts.map +0 -1
  223. package/dist/shared/telegram/types.js +0 -7
  224. package/dist/shared/telegram/types.js.map +0 -1
  225. package/dist/shared/telegram/utils.d.ts +0 -44
  226. package/dist/shared/telegram/utils.d.ts.map +0 -1
  227. package/dist/shared/telegram/utils.js +0 -121
  228. package/dist/shared/telegram/utils.js.map +0 -1
  229. package/dist/storage.d.ts +0 -9
  230. package/dist/storage.d.ts.map +0 -1
  231. package/dist/storage.js +0 -8
  232. package/dist/storage.js.map +0 -1
  233. package/dist/telemetry.d.ts +0 -9
  234. package/dist/telemetry.d.ts.map +0 -1
  235. package/dist/telemetry.js +0 -8
  236. package/dist/telemetry.js.map +0 -1
  237. package/scripts/postinstall.js +0 -79
  238. package/src/shared/affiliate/migrations/001_create_affiliates.sql +0 -49
  239. package/src/shared/affiliate/migrations/002_create_affiliate_commissions.sql +0 -31
  240. package/src/shared/affiliate/migrations/003_create_affiliate_clicks.sql +0 -26
  241. package/src/shared/affiliate/migrations/004_create_affiliate_payments.sql +0 -34
  242. package/src/shared/affiliate/migrations/005_create_affiliate_tier_history.sql +0 -19
  243. package/src/shared/affiliate/migrations/006_create_affiliate_rpc_functions.sql +0 -209
  244. package/src/shared/affiliate/migrations/007_create_affiliate_rls_policies.sql +0 -123
  245. package/src/shared/billing/migrations/00000000000001_billing.sql +0 -114
@@ -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