@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.
- package/README.md +78 -38
- package/dist/core/createApp.d.ts +1 -1
- package/dist/core/createApp.d.ts.map +1 -1
- package/dist/core/createApp.js +77 -2
- package/dist/core/createApp.js.map +1 -1
- package/dist/core/dualWriteProxy.d.ts +7 -2
- package/dist/core/dualWriteProxy.d.ts.map +1 -1
- package/dist/core/dualWriteProxy.js +16 -5
- package/dist/core/dualWriteProxy.js.map +1 -1
- package/dist/core/routeContext.d.ts +24 -0
- package/dist/core/routeContext.d.ts.map +1 -1
- package/dist/core/routeContext.js +31 -4
- package/dist/core/routeContext.js.map +1 -1
- package/dist/core/systemDb.d.ts +2 -2
- package/dist/core/systemDb.js +2 -2
- package/dist/generators/rls-checker.d.ts +1 -1
- package/dist/generators/rls-checker.js +1 -1
- package/dist/generators/rls-exec-sql.d.ts +1 -1
- package/dist/generators/rls-exec-sql.js +1 -1
- package/dist/generators/rpc/index.d.ts +1 -1
- package/dist/generators/rpc/index.js +1 -1
- package/dist/index.d.ts +3 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -32
- package/dist/index.js.map +1 -1
- package/dist/middleware/securityMiddleware.d.ts +1 -1
- package/dist/middleware/securityMiddleware.d.ts.map +1 -1
- package/dist/middleware/validateBody.d.ts.map +1 -1
- package/dist/middleware/validateBody.js +51 -8
- package/dist/middleware/validateBody.js.map +1 -1
- package/dist/shared/rfc7807ErrorResponse.d.ts +7 -0
- package/dist/shared/rfc7807ErrorResponse.d.ts.map +1 -1
- package/dist/shared/rfc7807ErrorResponse.js +19 -5
- package/dist/shared/rfc7807ErrorResponse.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +16 -1
- package/dist/utils/logger.js.map +1 -1
- package/package.json +33 -77
- package/dist/affiliate.d.ts +0 -11
- package/dist/affiliate.d.ts.map +0 -1
- package/dist/affiliate.js +0 -10
- package/dist/affiliate.js.map +0 -1
- package/dist/billing.d.ts +0 -8
- package/dist/billing.d.ts.map +0 -1
- package/dist/billing.js +0 -7
- package/dist/billing.js.map +0 -1
- package/dist/email.d.ts +0 -9
- package/dist/email.d.ts.map +0 -1
- package/dist/email.js +0 -8
- package/dist/email.js.map +0 -1
- package/dist/generators/rls-exec-sql.sql +0 -57
- package/dist/generators.d.ts +0 -15
- package/dist/generators.d.ts.map +0 -1
- package/dist/generators.js +0 -12
- package/dist/generators.js.map +0 -1
- package/dist/mcp.d.ts +0 -8
- package/dist/mcp.d.ts.map +0 -1
- package/dist/mcp.js +0 -7
- package/dist/mcp.js.map +0 -1
- package/dist/planner.d.ts +0 -8
- package/dist/planner.d.ts.map +0 -1
- package/dist/planner.js +0 -7
- package/dist/planner.js.map +0 -1
- package/dist/shared/affiliate/AffiliateAttributionService.d.ts +0 -47
- package/dist/shared/affiliate/AffiliateAttributionService.d.ts.map +0 -1
- package/dist/shared/affiliate/AffiliateAttributionService.js +0 -308
- package/dist/shared/affiliate/AffiliateAttributionService.js.map +0 -1
- package/dist/shared/affiliate/AffiliateClickService.d.ts +0 -35
- package/dist/shared/affiliate/AffiliateClickService.d.ts.map +0 -1
- package/dist/shared/affiliate/AffiliateClickService.js +0 -87
- package/dist/shared/affiliate/AffiliateClickService.js.map +0 -1
- package/dist/shared/affiliate/affiliateFeatureConfig.d.ts +0 -11
- package/dist/shared/affiliate/affiliateFeatureConfig.d.ts.map +0 -1
- package/dist/shared/affiliate/affiliateFeatureConfig.js +0 -242
- package/dist/shared/affiliate/affiliateFeatureConfig.js.map +0 -1
- package/dist/shared/affiliate/index.d.ts +0 -11
- package/dist/shared/affiliate/index.d.ts.map +0 -1
- package/dist/shared/affiliate/index.js +0 -13
- package/dist/shared/affiliate/index.js.map +0 -1
- package/dist/shared/affiliate/routes.d.ts +0 -87
- package/dist/shared/affiliate/routes.d.ts.map +0 -1
- package/dist/shared/affiliate/routes.js +0 -404
- package/dist/shared/affiliate/routes.js.map +0 -1
- package/dist/shared/affiliate/types.d.ts +0 -170
- package/dist/shared/affiliate/types.d.ts.map +0 -1
- package/dist/shared/affiliate/types.js +0 -11
- package/dist/shared/affiliate/types.js.map +0 -1
- package/dist/shared/billing/BillingService.d.ts +0 -56
- package/dist/shared/billing/BillingService.d.ts.map +0 -1
- package/dist/shared/billing/BillingService.js +0 -588
- package/dist/shared/billing/BillingService.js.map +0 -1
- package/dist/shared/billing/SeatBillingService.d.ts +0 -106
- package/dist/shared/billing/SeatBillingService.d.ts.map +0 -1
- package/dist/shared/billing/SeatBillingService.js +0 -292
- package/dist/shared/billing/SeatBillingService.js.map +0 -1
- package/dist/shared/billing/index.d.ts +0 -30
- package/dist/shared/billing/index.d.ts.map +0 -1
- package/dist/shared/billing/index.js +0 -27
- package/dist/shared/billing/index.js.map +0 -1
- package/dist/shared/billing/routes.d.ts +0 -45
- package/dist/shared/billing/routes.d.ts.map +0 -1
- package/dist/shared/billing/routes.js +0 -184
- package/dist/shared/billing/routes.js.map +0 -1
- package/dist/shared/billing/seat-pricing.d.ts +0 -53
- package/dist/shared/billing/seat-pricing.d.ts.map +0 -1
- package/dist/shared/billing/seat-pricing.js +0 -81
- package/dist/shared/billing/seat-pricing.js.map +0 -1
- package/dist/shared/billing/types.d.ts +0 -109
- package/dist/shared/billing/types.d.ts.map +0 -1
- package/dist/shared/billing/types.js +0 -8
- package/dist/shared/billing/types.js.map +0 -1
- package/dist/shared/email/EmailService.d.ts +0 -64
- package/dist/shared/email/EmailService.d.ts.map +0 -1
- package/dist/shared/email/EmailService.js +0 -300
- package/dist/shared/email/EmailService.js.map +0 -1
- package/dist/shared/email/adminRoutes.d.ts +0 -30
- package/dist/shared/email/adminRoutes.d.ts.map +0 -1
- package/dist/shared/email/adminRoutes.js +0 -227
- package/dist/shared/email/adminRoutes.js.map +0 -1
- package/dist/shared/email/gmail.d.ts +0 -208
- package/dist/shared/email/gmail.d.ts.map +0 -1
- package/dist/shared/email/gmail.js +0 -626
- package/dist/shared/email/gmail.js.map +0 -1
- package/dist/shared/email/index.d.ts +0 -15
- package/dist/shared/email/index.d.ts.map +0 -1
- package/dist/shared/email/index.js +0 -18
- package/dist/shared/email/index.js.map +0 -1
- package/dist/shared/email/mailgun.d.ts +0 -18
- package/dist/shared/email/mailgun.d.ts.map +0 -1
- package/dist/shared/email/mailgun.js +0 -76
- package/dist/shared/email/mailgun.js.map +0 -1
- package/dist/shared/email/sanitize.d.ts +0 -25
- package/dist/shared/email/sanitize.d.ts.map +0 -1
- package/dist/shared/email/sanitize.js +0 -39
- package/dist/shared/email/sanitize.js.map +0 -1
- package/dist/shared/email/smtp.d.ts +0 -20
- package/dist/shared/email/smtp.d.ts.map +0 -1
- package/dist/shared/email/smtp.js +0 -53
- package/dist/shared/email/smtp.js.map +0 -1
- package/dist/shared/email/types.d.ts +0 -113
- package/dist/shared/email/types.d.ts.map +0 -1
- package/dist/shared/email/types.js +0 -7
- package/dist/shared/email/types.js.map +0 -1
- package/dist/shared/email/webhookRoutes.d.ts +0 -29
- package/dist/shared/email/webhookRoutes.d.ts.map +0 -1
- package/dist/shared/email/webhookRoutes.js +0 -125
- package/dist/shared/email/webhookRoutes.js.map +0 -1
- package/dist/shared/mcp/index.d.ts +0 -51
- package/dist/shared/mcp/index.d.ts.map +0 -1
- package/dist/shared/mcp/index.js +0 -51
- package/dist/shared/mcp/index.js.map +0 -1
- package/dist/shared/mcp/mcp-auth-routes.d.ts +0 -26
- package/dist/shared/mcp/mcp-auth-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-auth-routes.js +0 -141
- package/dist/shared/mcp/mcp-auth-routes.js.map +0 -1
- package/dist/shared/mcp/mcp-db.d.ts +0 -99
- package/dist/shared/mcp/mcp-db.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-db.js +0 -106
- package/dist/shared/mcp/mcp-db.js.map +0 -1
- package/dist/shared/mcp/mcp-routes.d.ts +0 -29
- package/dist/shared/mcp/mcp-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-routes.js +0 -171
- package/dist/shared/mcp/mcp-routes.js.map +0 -1
- package/dist/shared/mcp/mcp-tokens-routes.d.ts +0 -35
- package/dist/shared/mcp/mcp-tokens-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-tokens-routes.js +0 -94
- package/dist/shared/mcp/mcp-tokens-routes.js.map +0 -1
- package/dist/shared/mcp/mcp-usage-routes.d.ts +0 -17
- package/dist/shared/mcp/mcp-usage-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-usage-routes.js +0 -81
- package/dist/shared/mcp/mcp-usage-routes.js.map +0 -1
- package/dist/shared/mcp/tenant-context.d.ts +0 -59
- package/dist/shared/mcp/tenant-context.d.ts.map +0 -1
- package/dist/shared/mcp/tenant-context.js +0 -136
- package/dist/shared/mcp/tenant-context.js.map +0 -1
- package/dist/shared/mcp/types.d.ts +0 -74
- package/dist/shared/mcp/types.d.ts.map +0 -1
- package/dist/shared/mcp/types.js +0 -7
- package/dist/shared/mcp/types.js.map +0 -1
- package/dist/shared/planner/GoogleCalendarService.d.ts +0 -137
- package/dist/shared/planner/GoogleCalendarService.d.ts.map +0 -1
- package/dist/shared/planner/GoogleCalendarService.js +0 -525
- package/dist/shared/planner/GoogleCalendarService.js.map +0 -1
- package/dist/shared/planner/PlannerService.d.ts +0 -264
- package/dist/shared/planner/PlannerService.d.ts.map +0 -1
- package/dist/shared/planner/PlannerService.js +0 -1393
- package/dist/shared/planner/PlannerService.js.map +0 -1
- package/dist/shared/planner/index.d.ts +0 -37
- package/dist/shared/planner/index.d.ts.map +0 -1
- package/dist/shared/planner/index.js +0 -35
- package/dist/shared/planner/index.js.map +0 -1
- package/dist/shared/planner/intervals.d.ts +0 -60
- package/dist/shared/planner/intervals.d.ts.map +0 -1
- package/dist/shared/planner/intervals.js +0 -141
- package/dist/shared/planner/intervals.js.map +0 -1
- package/dist/shared/planner/routes.d.ts +0 -69
- package/dist/shared/planner/routes.d.ts.map +0 -1
- package/dist/shared/planner/routes.js +0 -770
- package/dist/shared/planner/routes.js.map +0 -1
- package/dist/shared/planner/types.d.ts +0 -328
- package/dist/shared/planner/types.d.ts.map +0 -1
- package/dist/shared/planner/types.js +0 -9
- package/dist/shared/planner/types.js.map +0 -1
- package/dist/shared/storage/ImageProcessingService.d.ts +0 -32
- package/dist/shared/storage/ImageProcessingService.d.ts.map +0 -1
- package/dist/shared/storage/ImageProcessingService.js +0 -127
- package/dist/shared/storage/ImageProcessingService.js.map +0 -1
- package/dist/shared/storage/StorageProxyService.d.ts +0 -47
- package/dist/shared/storage/StorageProxyService.d.ts.map +0 -1
- package/dist/shared/storage/StorageProxyService.js +0 -196
- package/dist/shared/storage/StorageProxyService.js.map +0 -1
- package/dist/shared/storage/StorageUploadService.d.ts +0 -126
- package/dist/shared/storage/StorageUploadService.d.ts.map +0 -1
- package/dist/shared/storage/StorageUploadService.js +0 -206
- package/dist/shared/storage/StorageUploadService.js.map +0 -1
- package/dist/shared/storage/creative-urls.d.ts +0 -14
- package/dist/shared/storage/creative-urls.d.ts.map +0 -1
- package/dist/shared/storage/creative-urls.js +0 -30
- package/dist/shared/storage/creative-urls.js.map +0 -1
- package/dist/shared/storage/index.d.ts +0 -28
- package/dist/shared/storage/index.d.ts.map +0 -1
- package/dist/shared/storage/index.js +0 -27
- package/dist/shared/storage/index.js.map +0 -1
- package/dist/shared/storage/routes.d.ts +0 -42
- package/dist/shared/storage/routes.d.ts.map +0 -1
- package/dist/shared/storage/routes.js +0 -160
- package/dist/shared/storage/routes.js.map +0 -1
- package/dist/shared/storage/types.d.ts +0 -53
- package/dist/shared/storage/types.d.ts.map +0 -1
- package/dist/shared/storage/types.js +0 -2
- package/dist/shared/storage/types.js.map +0 -1
- package/dist/shared/telegram/index.d.ts +0 -4
- package/dist/shared/telegram/index.d.ts.map +0 -1
- package/dist/shared/telegram/index.js +0 -3
- package/dist/shared/telegram/index.js.map +0 -1
- package/dist/shared/telegram/routes.d.ts +0 -43
- package/dist/shared/telegram/routes.d.ts.map +0 -1
- package/dist/shared/telegram/routes.js +0 -868
- package/dist/shared/telegram/routes.js.map +0 -1
- package/dist/shared/telegram/types.d.ts +0 -168
- package/dist/shared/telegram/types.d.ts.map +0 -1
- package/dist/shared/telegram/types.js +0 -7
- package/dist/shared/telegram/types.js.map +0 -1
- package/dist/shared/telegram/utils.d.ts +0 -44
- package/dist/shared/telegram/utils.d.ts.map +0 -1
- package/dist/shared/telegram/utils.js +0 -121
- package/dist/shared/telegram/utils.js.map +0 -1
- package/dist/storage.d.ts +0 -9
- package/dist/storage.d.ts.map +0 -1
- package/dist/storage.js +0 -8
- package/dist/storage.js.map +0 -1
- package/dist/telemetry.d.ts +0 -9
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/telemetry.js +0 -8
- package/dist/telemetry.js.map +0 -1
- package/scripts/postinstall.js +0 -79
- package/src/shared/affiliate/migrations/001_create_affiliates.sql +0 -49
- package/src/shared/affiliate/migrations/002_create_affiliate_commissions.sql +0 -31
- package/src/shared/affiliate/migrations/003_create_affiliate_clicks.sql +0 -26
- package/src/shared/affiliate/migrations/004_create_affiliate_payments.sql +0 -34
- package/src/shared/affiliate/migrations/005_create_affiliate_tier_history.sql +0 -19
- package/src/shared/affiliate/migrations/006_create_affiliate_rpc_functions.sql +0 -209
- package/src/shared/affiliate/migrations/007_create_affiliate_rls_policies.sql +0 -123
- package/src/shared/billing/migrations/00000000000001_billing.sql +0 -114
- package/src/shared/email/migrations/000_create_email_logs.sql +0 -27
- package/src/shared/email/migrations/001_create_email_templates.sql +0 -27
- package/src/shared/email/migrations/002_add_rls_baseline_policies.sql +0 -37
- package/src/shared/email/migrations/003_create_gmail_accounts.sql +0 -82
- package/src/shared/email/migrations/004_add_email_logs_tracking_columns.sql +0 -15
- package/src/shared/mcp/migrations/001_mcp_api_tokens.sql +0 -21
- 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
|