@equilateral_ai/mindmeld 3.5.3 → 4.0.2

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 (138) hide show
  1. package/hooks/session-start.js +312 -85
  2. package/package.json +21 -13
  3. package/scripts/init-project.js +9 -23
  4. package/scripts/repo-analyzer.js +118 -2
  5. package/src/client/dbShim.js +16 -0
  6. package/src/core/AuthManager.js +3 -2
  7. package/src/handlers/helpers/dbOperations.js +9 -46
  8. package/src/index.js +2 -217
  9. package/src/utils/piiMask.js +16 -0
  10. package/scripts/inject.js +0 -409
  11. package/scripts/mcp-bridge.js +0 -220
  12. package/scripts/standards.js +0 -285
  13. package/src/collaboration/CollaborationPrompt.js +0 -460
  14. package/src/core/AlertEngine.js +0 -813
  15. package/src/core/AlertNotifier.js +0 -363
  16. package/src/core/CorrelationAnalyzer.js +0 -931
  17. package/src/core/CrossReferenceEngine.js +0 -624
  18. package/src/core/CurationEngine.js +0 -688
  19. package/src/core/DeprecationScheduler.js +0 -183
  20. package/src/core/LoadBearingDetector.js +0 -242
  21. package/src/core/NotificationService.js +0 -1032
  22. package/src/core/RapportOrchestrator.js +0 -632
  23. package/src/core/RelevanceDetector.js +0 -694
  24. package/src/core/StandardLifecycle.js +0 -244
  25. package/src/core/StandardsIngestion.js +0 -991
  26. package/src/core/TeamLoadBearingDetector.js +0 -431
  27. package/src/core/parsers/adrParser.js +0 -479
  28. package/src/core/parsers/cursorRulesParser.js +0 -564
  29. package/src/core/parsers/eslintParser.js +0 -439
  30. package/src/database/dbOperations.js +0 -105
  31. package/src/handlers/activity/activityGetMe.js +0 -98
  32. package/src/handlers/activity/activityGetTeam.js +0 -175
  33. package/src/handlers/admin/adminSetup.js +0 -216
  34. package/src/handlers/alerts/alertsAcknowledge.js +0 -92
  35. package/src/handlers/alerts/alertsGet.js +0 -250
  36. package/src/handlers/analytics/activitySummaryGet.js +0 -234
  37. package/src/handlers/analytics/coachingGet.js +0 -361
  38. package/src/handlers/analytics/convergenceGet.js +0 -236
  39. package/src/handlers/analytics/developerScoreGet.js +0 -137
  40. package/src/handlers/collaborators/collaboratorAdd.js +0 -200
  41. package/src/handlers/collaborators/collaboratorInvite.js +0 -219
  42. package/src/handlers/collaborators/collaboratorList.js +0 -82
  43. package/src/handlers/collaborators/collaboratorRemove.js +0 -128
  44. package/src/handlers/collaborators/inviteAccept.js +0 -122
  45. package/src/handlers/company/companyUsersDelete.js +0 -141
  46. package/src/handlers/company/companyUsersGet.js +0 -90
  47. package/src/handlers/company/companyUsersPost.js +0 -267
  48. package/src/handlers/company/companyUsersPut.js +0 -76
  49. package/src/handlers/context/contextGet.js +0 -57
  50. package/src/handlers/context/invariantsGet.js +0 -74
  51. package/src/handlers/context/loopsGet.js +0 -82
  52. package/src/handlers/context/notesCreate.js +0 -74
  53. package/src/handlers/context/purposeGet.js +0 -78
  54. package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
  55. package/src/handlers/correlations/correlationsGet.js +0 -93
  56. package/src/handlers/correlations/correlationsProjectGet.js +0 -153
  57. package/src/handlers/enterprise/controlTowerGet.js +0 -224
  58. package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
  59. package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
  60. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
  61. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
  62. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
  63. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
  64. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
  65. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
  66. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
  67. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
  68. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
  69. package/src/handlers/github/githubConnectionStatus.js +0 -49
  70. package/src/handlers/github/githubDiscoverPatterns.js +0 -621
  71. package/src/handlers/github/githubOAuthCallback.js +0 -178
  72. package/src/handlers/github/githubOAuthStart.js +0 -59
  73. package/src/handlers/github/githubPatternsReview.js +0 -76
  74. package/src/handlers/github/githubReposList.js +0 -105
  75. package/src/handlers/health/healthGet.js +0 -55
  76. package/src/handlers/helpers/auditLogger.js +0 -201
  77. package/src/handlers/helpers/checkSuperAdmin.js +0 -84
  78. package/src/handlers/helpers/decisionFrames.js +0 -29
  79. package/src/handlers/helpers/errorHandler.js +0 -49
  80. package/src/handlers/helpers/index.js +0 -138
  81. package/src/handlers/helpers/lambdaWrapper.js +0 -60
  82. package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
  83. package/src/handlers/helpers/predictiveCache.js +0 -51
  84. package/src/handlers/helpers/projectAccess.js +0 -88
  85. package/src/handlers/helpers/responseUtil.js +0 -55
  86. package/src/handlers/helpers/subscriptionTiers.js +0 -1168
  87. package/src/handlers/mcp/mcpHandler.js +0 -569
  88. package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
  89. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
  90. package/src/handlers/notifications/getPreferences.js +0 -84
  91. package/src/handlers/notifications/sendNotification.js +0 -170
  92. package/src/handlers/notifications/updatePreferences.js +0 -316
  93. package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
  94. package/src/handlers/patterns/patternUsagePost.js +0 -182
  95. package/src/handlers/patterns/patternViolationPost.js +0 -185
  96. package/src/handlers/projects/projectCreate.js +0 -248
  97. package/src/handlers/projects/projectDelete.js +0 -82
  98. package/src/handlers/projects/projectGet.js +0 -95
  99. package/src/handlers/projects/projectUpdate.js +0 -117
  100. package/src/handlers/reports/aiLeverage.js +0 -210
  101. package/src/handlers/reports/engineeringInvestment.js +0 -132
  102. package/src/handlers/reports/riskForecast.js +0 -206
  103. package/src/handlers/reports/standardsRoi.js +0 -254
  104. package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
  105. package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
  106. package/src/handlers/scheduled/generateAlerts.js +0 -135
  107. package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
  108. package/src/handlers/scheduled/refreshActivity.js +0 -21
  109. package/src/handlers/scheduled/scanCompliance.js +0 -334
  110. package/src/handlers/sessions/sessionEndPost.js +0 -180
  111. package/src/handlers/sessions/sessionStandardsPost.js +0 -171
  112. package/src/handlers/standards/catalogGet.js +0 -185
  113. package/src/handlers/standards/catalogSync.js +0 -120
  114. package/src/handlers/standards/discoveriesGet.js +0 -89
  115. package/src/handlers/standards/projectStandardsGet.js +0 -129
  116. package/src/handlers/standards/projectStandardsPut.js +0 -151
  117. package/src/handlers/standards/standardsAuditGet.js +0 -65
  118. package/src/handlers/standards/standardsParseUpload.js +0 -149
  119. package/src/handlers/standards/standardsRelevantPost.js +0 -405
  120. package/src/handlers/standards/standardsTransition.js +0 -161
  121. package/src/handlers/stripe/addonManagePost.js +0 -240
  122. package/src/handlers/stripe/billingPortalPost.js +0 -93
  123. package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
  124. package/src/handlers/stripe/seatsUpdatePost.js +0 -185
  125. package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
  126. package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
  127. package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
  128. package/src/handlers/stripe/webhookPost.js +0 -482
  129. package/src/handlers/user/apiTokenCreate.js +0 -71
  130. package/src/handlers/user/apiTokenList.js +0 -64
  131. package/src/handlers/user/userSplashAck.js +0 -91
  132. package/src/handlers/user/userSplashGet.js +0 -211
  133. package/src/handlers/users/cognitoPostConfirmation.js +0 -186
  134. package/src/handlers/users/cognitoPreSignUp.js +0 -114
  135. package/src/handlers/users/userEntitlementsGet.js +0 -89
  136. package/src/handlers/users/userGet.js +0 -118
  137. package/src/handlers/users/userProfilePut.js +0 -77
  138. package/src/handlers/webhooks/githubWebhook.js +0 -215
@@ -1,163 +0,0 @@
1
- /**
2
- * Subscription Update Handler
3
- * Upgrades or downgrades subscription tier
4
- *
5
- * PUT /api/stripe/subscription/update
6
- * Body: { newTier, billingPeriod }
7
- * Auth: Cognito JWT required
8
- */
9
-
10
- const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError, getTierConfig, getStripePriceId } = require('./helpers');
11
-
12
- // Stripe is optional
13
- let stripe = null;
14
- try {
15
- if (process.env.STRIPE_SECRET_KEY) {
16
- stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
17
- }
18
- } catch (e) {
19
- console.warn('Stripe not available:', e.message);
20
- }
21
-
22
- /**
23
- * Update subscription tier
24
- */
25
- async function updateSubscription({ body: requestBody = {}, requestContext }) {
26
- try {
27
- const Request_ID = requestContext.requestId;
28
- // REST API: requestContext.authorizer.claims.email
29
- // HTTP API: requestContext.authorizer.jwt.claims.email
30
- const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
31
-
32
- if (!email) {
33
- return createErrorResponse(401, 'Authentication required');
34
- }
35
-
36
- const { newTier, billingPeriod = 'monthly' } = requestBody;
37
-
38
- // Validate new tier
39
- const newTierConfig = getTierConfig(newTier);
40
- if (!newTierConfig || newTier === 'free') {
41
- return createErrorResponse(400, 'Invalid tier. Use cancel endpoint for downgrade to free.', {
42
- valid_tiers: ['team', 'professional', 'enterprise']
43
- });
44
- }
45
-
46
- if (!stripe) {
47
- return createErrorResponse(503, 'Payment system not configured');
48
- }
49
-
50
- // Get user's current subscription
51
- const userQuery = `
52
- SELECT
53
- u.client_id,
54
- c.stripe_customer_id,
55
- c.subscription_tier,
56
- c.subscription_status
57
- FROM rapport.users u
58
- JOIN rapport.clients c ON u.client_id = c.client_id
59
- WHERE u.email_address = $1 AND u.active = true
60
- `;
61
- const userResult = await executeQuery(userQuery, [email]);
62
-
63
- if (userResult.rowCount === 0) {
64
- return createErrorResponse(404, 'User not found');
65
- }
66
-
67
- const user = userResult.rows[0];
68
- const currentTierConfig = getTierConfig(user.subscription_tier);
69
-
70
- if (!user.stripe_customer_id || user.subscription_status !== 'active') {
71
- return createErrorResponse(400, 'No active subscription. Use create endpoint first.');
72
- }
73
-
74
- // Get active subscription from Stripe
75
- const subscriptions = await stripe.subscriptions.list({
76
- customer: user.stripe_customer_id,
77
- status: 'active',
78
- limit: 1
79
- });
80
-
81
- if (subscriptions.data.length === 0) {
82
- return createErrorResponse(400, 'No active subscription found in Stripe');
83
- }
84
-
85
- const subscription = subscriptions.data[0];
86
- const newPriceId = getStripePriceId(newTier, billingPeriod);
87
-
88
- if (!newPriceId) {
89
- return createErrorResponse(500, 'Price not configured for tier');
90
- }
91
-
92
- // Count users for per-seat pricing
93
- const usageQuery = `
94
- SELECT COUNT(*) as user_count
95
- FROM rapport.user_entitlements
96
- WHERE client_id = $1
97
- `;
98
- const usageResult = await executeQuery(usageQuery, [user.client_id]);
99
- const userCount = parseInt(usageResult.rows[0].user_count) || 1;
100
-
101
- // Determine if upgrade or downgrade
102
- const isUpgrade = (newTierConfig.priceMonthly || 0) > (currentTierConfig?.priceMonthly || 0);
103
-
104
- // Update subscription in Stripe
105
- const updatedSubscription = await stripe.subscriptions.update(subscription.id, {
106
- items: [{
107
- id: subscription.items.data[0].id,
108
- price: newPriceId,
109
- quantity: newTierConfig.perUser ? userCount : 1
110
- }],
111
- proration_behavior: 'create_prorations',
112
- metadata: {
113
- client_id: user.client_id,
114
- tier: newTier
115
- }
116
- });
117
-
118
- // Update local database
119
- await executeQuery(`
120
- UPDATE rapport.clients
121
- SET subscription_tier = $2,
122
- last_updated = CURRENT_TIMESTAMP
123
- WHERE client_id = $1
124
- `, [user.client_id, newTier]);
125
-
126
- // Calculate proration info
127
- const currentPeriodEnd = new Date(subscription.current_period_end * 1000);
128
- const daysRemaining = Math.ceil((currentPeriodEnd - new Date()) / (1000 * 60 * 60 * 24));
129
-
130
- return createSuccessResponse(
131
- {
132
- Records: [{
133
- previous_tier: user.subscription_tier,
134
- new_tier: newTier,
135
- new_tier_name: newTierConfig.displayName,
136
- is_upgrade: isUpgrade,
137
- billing_period: billingPeriod,
138
- user_count: newTierConfig.perUser ? userCount : null,
139
- monthly_cost: newTierConfig.perUser
140
- ? newTierConfig.priceMonthly * userCount
141
- : newTierConfig.priceMonthly,
142
- proration: {
143
- applied: true,
144
- days_remaining: daysRemaining,
145
- current_period_end: currentPeriodEnd.toISOString()
146
- }
147
- }]
148
- },
149
- isUpgrade ? 'Subscription upgraded' : 'Subscription changed',
150
- {
151
- Total_Records: 1,
152
- Request_ID,
153
- Timestamp: new Date().toISOString()
154
- }
155
- );
156
-
157
- } catch (error) {
158
- console.error('Handler Error:', error);
159
- return handleError(error);
160
- }
161
- }
162
-
163
- exports.handler = wrapHandler(updateSubscription);
@@ -1,482 +0,0 @@
1
- /**
2
- * Stripe Webhook Handler
3
- * Processes Stripe webhook events for subscription lifecycle
4
- *
5
- * POST /api/stripe/webhook
6
- * Auth: Stripe signature verification (no Cognito)
7
- *
8
- * Following HoneyDo webhookPost.js pattern
9
- */
10
-
11
- const { executeQuery } = require('./helpers');
12
- const crypto = require('crypto');
13
-
14
- // Stripe is optional
15
- let stripe = null;
16
- try {
17
- if (process.env.STRIPE_SECRET_KEY) {
18
- stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
19
- }
20
- } catch (e) {
21
- console.warn('Stripe not available:', e.message);
22
- }
23
-
24
- /**
25
- * Verify Stripe webhook signature
26
- * Uses timing-safe comparison to prevent timing attacks
27
- */
28
- function verifySignature(payload, signature, secret) {
29
- const parts = signature.split(',').reduce((acc, part) => {
30
- const [key, value] = part.split('=');
31
- acc[key] = value;
32
- return acc;
33
- }, {});
34
-
35
- const timestamp = parts.t;
36
- const expectedSig = parts.v1;
37
-
38
- if (!timestamp || !expectedSig) {
39
- return false;
40
- }
41
-
42
- // Check timestamp is within 5 minutes
43
- const now = Math.floor(Date.now() / 1000);
44
- if (Math.abs(now - parseInt(timestamp)) > 300) {
45
- console.warn('Webhook timestamp too old');
46
- return false;
47
- }
48
-
49
- // Compute expected signature
50
- const signedPayload = `${timestamp}.${payload}`;
51
- const computedSig = crypto
52
- .createHmac('sha256', secret)
53
- .update(signedPayload)
54
- .digest('hex');
55
-
56
- // Timing-safe comparison
57
- try {
58
- return crypto.timingSafeEqual(
59
- Buffer.from(expectedSig),
60
- Buffer.from(computedSig)
61
- );
62
- } catch {
63
- return false;
64
- }
65
- }
66
-
67
- /**
68
- * Handle checkout.session.completed
69
- * New subscription created
70
- */
71
- async function handleCheckoutCompleted(session) {
72
- const { client_id, tier, user_email, enterprise_package, seat_count, addons } = session.metadata || {};
73
-
74
- if (!client_id) {
75
- console.error('[webhookPost] CRITICAL: Missing client_id in checkout session metadata — subscription will not activate');
76
- throw new Error('Missing client_id in checkout session metadata');
77
- }
78
-
79
- // Parse enterprise metadata
80
- const isEnterprise = tier === 'enterprise';
81
- const seatCountNum = seat_count ? parseInt(seat_count, 10) : 1;
82
- let addonsList = [];
83
- if (addons) {
84
- try { addonsList = JSON.parse(addons); }
85
- catch { console.warn('Failed to parse addons metadata:', addons); }
86
- }
87
-
88
- // Update client with subscription
89
- if (isEnterprise) {
90
- const updateResult = await executeQuery(`
91
- UPDATE rapport.clients
92
- SET stripe_customer_id = $2,
93
- stripe_subscription_id = $3,
94
- subscription_tier = 'enterprise',
95
- subscription_status = 'active',
96
- enterprise_package = $4,
97
- seat_count = $5,
98
- subscribed_addons = $6,
99
- last_updated = CURRENT_TIMESTAMP
100
- WHERE client_id = $1
101
- `, [client_id, session.customer, session.subscription, enterprise_package, seatCountNum, JSON.stringify(addonsList)]);
102
-
103
- if (updateResult.rowCount === 0) {
104
- console.error(`[webhookPost] CRITICAL: Client ${client_id} not found in DB — subscription paid but not activated`);
105
- throw new Error(`Client ${client_id} not found — subscription activation failed`);
106
- }
107
-
108
- // Create addon entitlements if any
109
- if (addonsList.length > 0) {
110
- for (const addonId of addonsList) {
111
- await executeQuery(`
112
- INSERT INTO rapport.addon_entitlements (client_id, addon_id, seat_count, status)
113
- VALUES ($1, $2, $3, 'active')
114
- ON CONFLICT (client_id, addon_id) DO UPDATE SET
115
- seat_count = EXCLUDED.seat_count,
116
- status = 'active',
117
- updated_at = CURRENT_TIMESTAMP
118
- `, [client_id, addonId, seatCountNum]);
119
- }
120
- }
121
-
122
- console.log('Enterprise checkout completed:', { client_id, enterprise_package, seat_count: seatCountNum, addons: addonsList });
123
- } else {
124
- const updateResult = await executeQuery(`
125
- UPDATE rapport.clients
126
- SET stripe_customer_id = $2,
127
- stripe_subscription_id = $3,
128
- subscription_tier = $4,
129
- subscription_status = 'active',
130
- last_updated = CURRENT_TIMESTAMP
131
- WHERE client_id = $1
132
- `, [client_id, session.customer, session.subscription, tier || 'team']);
133
-
134
- if (updateResult.rowCount === 0) {
135
- console.error(`[webhookPost] CRITICAL: Client ${client_id} not found in DB — subscription paid but not activated`);
136
- throw new Error(`Client ${client_id} not found — subscription activation failed`);
137
- }
138
-
139
- console.log('Checkout completed:', { client_id, tier, customer: session.customer });
140
- }
141
-
142
- // Update checkout session record
143
- await executeQuery(`
144
- UPDATE rapport.stripe_checkout_sessions
145
- SET status = 'completed',
146
- stripe_customer_id = $2,
147
- stripe_subscription_id = $3,
148
- completed_at = CURRENT_TIMESTAMP
149
- WHERE session_id = $1
150
- `, [session.id, session.customer, session.subscription]);
151
- }
152
-
153
- /**
154
- * Handle customer.subscription.updated
155
- * Subscription tier/status changed, seat count changed, addons changed
156
- */
157
- async function handleSubscriptionUpdated(subscription) {
158
- const { client_id, tier, enterprise_package, seat_count, addons } = subscription.metadata || {};
159
-
160
- // Find client by customer ID if not in metadata
161
- let clientId = client_id;
162
- if (!clientId) {
163
- const result = await executeQuery(`
164
- SELECT client_id FROM rapport.clients WHERE stripe_customer_id = $1
165
- `, [subscription.customer]);
166
-
167
- if (result.rowCount === 0) {
168
- console.warn('Client not found for subscription update');
169
- return;
170
- }
171
- clientId = result.rows[0].client_id;
172
- }
173
-
174
- // Parse enterprise metadata
175
- const isEnterprise = tier === 'enterprise';
176
- const seatCountNum = seat_count ? parseInt(seat_count, 10) : null;
177
- let addonsList = null;
178
- if (addons) {
179
- try { addonsList = JSON.parse(addons); }
180
- catch { console.warn('Failed to parse addons metadata in subscription update:', addons); }
181
- }
182
-
183
- // Build update query based on what changed
184
- if (isEnterprise) {
185
- // Calculate seat count from subscription items quantity
186
- let calculatedSeats = seatCountNum;
187
- if (!calculatedSeats && subscription.items?.data?.length > 0) {
188
- // Each item quantity represents seat packs (1 pack = 25 seats)
189
- const seatPacks = subscription.items.data[0].quantity || 1;
190
- calculatedSeats = seatPacks * 25;
191
- }
192
-
193
- await executeQuery(`
194
- UPDATE rapport.clients
195
- SET subscription_status = $2,
196
- subscription_tier = 'enterprise',
197
- enterprise_package = COALESCE($3, enterprise_package),
198
- seat_count = COALESCE($4, seat_count),
199
- subscribed_addons = COALESCE($5, subscribed_addons),
200
- stripe_subscription_id = $6,
201
- last_updated = CURRENT_TIMESTAMP
202
- WHERE client_id = $1
203
- `, [
204
- clientId,
205
- subscription.status,
206
- enterprise_package,
207
- calculatedSeats,
208
- addonsList ? JSON.stringify(addonsList) : null,
209
- subscription.id
210
- ]);
211
-
212
- console.log('Enterprise subscription updated:', {
213
- client_id: clientId,
214
- status: subscription.status,
215
- seat_count: calculatedSeats
216
- });
217
- } else {
218
- await executeQuery(`
219
- UPDATE rapport.clients
220
- SET subscription_tier = COALESCE($2, subscription_tier),
221
- subscription_status = $3,
222
- stripe_subscription_id = $4,
223
- last_updated = CURRENT_TIMESTAMP
224
- WHERE client_id = $1
225
- `, [clientId, tier, subscription.status, subscription.id]);
226
-
227
- console.log('Subscription updated:', { client_id: clientId, status: subscription.status });
228
- }
229
- }
230
-
231
- /**
232
- * Handle customer.subscription.deleted
233
- * Subscription canceled
234
- */
235
- async function handleSubscriptionDeleted(subscription) {
236
- // Find client by customer ID
237
- const result = await executeQuery(`
238
- SELECT client_id FROM rapport.clients WHERE stripe_customer_id = $1
239
- `, [subscription.customer]);
240
-
241
- if (result.rowCount === 0) {
242
- console.warn('Client not found for subscription deletion');
243
- return;
244
- }
245
-
246
- const clientId = result.rows[0].client_id;
247
-
248
- await executeQuery(`
249
- UPDATE rapport.clients
250
- SET subscription_tier = 'free',
251
- subscription_status = 'canceled',
252
- subscription_ends_at = CURRENT_TIMESTAMP,
253
- last_updated = CURRENT_TIMESTAMP
254
- WHERE client_id = $1
255
- `, [clientId]);
256
-
257
- console.log('Subscription deleted, reverted to free:', { client_id: clientId });
258
- }
259
-
260
- /**
261
- * Handle invoice.payment_succeeded
262
- * Record successful payment
263
- */
264
- async function handlePaymentSucceeded(invoice) {
265
- // Find client by customer
266
- const result = await executeQuery(`
267
- SELECT client_id FROM rapport.clients WHERE stripe_customer_id = $1
268
- `, [invoice.customer]);
269
-
270
- if (result.rowCount === 0) {
271
- console.warn('Client not found for payment');
272
- return;
273
- }
274
-
275
- const clientId = result.rows[0].client_id;
276
-
277
- await executeQuery(`
278
- INSERT INTO rapport.payment_history (
279
- stripe_invoice_id,
280
- stripe_subscription_id,
281
- client_id,
282
- amount_cents,
283
- currency,
284
- status,
285
- paid_at
286
- )
287
- VALUES ($1, $2, $3, $4, $5, 'succeeded', $6)
288
- ON CONFLICT (stripe_invoice_id) DO UPDATE SET
289
- status = 'succeeded',
290
- paid_at = EXCLUDED.paid_at
291
- `, [
292
- invoice.id,
293
- invoice.subscription,
294
- clientId,
295
- invoice.amount_paid,
296
- invoice.currency,
297
- new Date(invoice.status_transitions?.paid_at * 1000 || Date.now())
298
- ]);
299
-
300
- console.log('Payment succeeded:', { invoice: invoice.id, amount: invoice.amount_paid });
301
- }
302
-
303
- /**
304
- * Handle invoice.payment_failed
305
- * Record failed payment
306
- */
307
- async function handlePaymentFailed(invoice) {
308
- const result = await executeQuery(`
309
- SELECT client_id FROM rapport.clients WHERE stripe_customer_id = $1
310
- `, [invoice.customer]);
311
-
312
- if (result.rowCount === 0) {
313
- return;
314
- }
315
-
316
- const clientId = result.rows[0].client_id;
317
-
318
- // Record failed payment
319
- await executeQuery(`
320
- INSERT INTO rapport.payment_history (
321
- stripe_invoice_id,
322
- stripe_subscription_id,
323
- client_id,
324
- amount_cents,
325
- currency,
326
- status,
327
- failure_message
328
- )
329
- VALUES ($1, $2, $3, $4, $5, 'failed', $6)
330
- ON CONFLICT (stripe_invoice_id) DO UPDATE SET
331
- status = 'failed',
332
- failure_message = EXCLUDED.failure_message
333
- `, [
334
- invoice.id,
335
- invoice.subscription,
336
- clientId,
337
- invoice.amount_due,
338
- invoice.currency,
339
- invoice.last_finalization_error?.message || 'Payment failed'
340
- ]);
341
-
342
- // Update subscription status
343
- await executeQuery(`
344
- UPDATE rapport.clients
345
- SET subscription_status = 'past_due',
346
- last_updated = CURRENT_TIMESTAMP
347
- WHERE client_id = $1
348
- `, [clientId]);
349
-
350
- console.log('Payment failed:', { invoice: invoice.id, client_id: clientId });
351
- }
352
-
353
- /**
354
- * Main webhook handler
355
- */
356
- async function handler(event, context) {
357
- try {
358
- const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
359
-
360
- if (!webhookSecret) {
361
- console.error('STRIPE_WEBHOOK_SECRET not configured');
362
- return {
363
- statusCode: 500,
364
- headers: { 'Content-Type': 'application/json' },
365
- body: JSON.stringify({ error: 'Webhook not configured' })
366
- };
367
- }
368
-
369
- const signature = event.headers['stripe-signature'] || event.headers['Stripe-Signature'];
370
- const payload = event.body;
371
-
372
- if (!signature || !payload) {
373
- return {
374
- statusCode: 400,
375
- headers: { 'Content-Type': 'application/json' },
376
- body: JSON.stringify({ error: 'Missing signature or payload' })
377
- };
378
- }
379
-
380
- // Verify signature
381
- if (!verifySignature(payload, signature, webhookSecret)) {
382
- console.warn('Invalid webhook signature');
383
- return {
384
- statusCode: 400,
385
- headers: { 'Content-Type': 'application/json' },
386
- body: JSON.stringify({ error: 'Invalid signature' })
387
- };
388
- }
389
-
390
- const stripeEvent = JSON.parse(payload);
391
-
392
- // Check for duplicate (idempotency)
393
- const dupCheck = await executeQuery(`
394
- SELECT event_id FROM rapport.stripe_webhook_events WHERE event_id = $1
395
- `, [stripeEvent.id]);
396
-
397
- if (dupCheck.rowCount > 0) {
398
- console.log('Duplicate event, skipping:', stripeEvent.id);
399
- return {
400
- statusCode: 200,
401
- headers: { 'Content-Type': 'application/json' },
402
- body: JSON.stringify({ received: true, duplicate: true })
403
- };
404
- }
405
-
406
- // Record event before processing
407
- await executeQuery(`
408
- INSERT INTO rapport.stripe_webhook_events (event_id, event_type, handled)
409
- VALUES ($1, $2, false)
410
- `, [stripeEvent.id, stripeEvent.type]);
411
-
412
- // Process by event type
413
- try {
414
- switch (stripeEvent.type) {
415
- case 'checkout.session.completed':
416
- await handleCheckoutCompleted(stripeEvent.data.object);
417
- break;
418
- case 'customer.subscription.updated':
419
- await handleSubscriptionUpdated(stripeEvent.data.object);
420
- break;
421
- case 'customer.subscription.deleted':
422
- await handleSubscriptionDeleted(stripeEvent.data.object);
423
- break;
424
- case 'invoice.payment_succeeded':
425
- await handlePaymentSucceeded(stripeEvent.data.object);
426
- break;
427
- case 'invoice.payment_failed':
428
- await handlePaymentFailed(stripeEvent.data.object);
429
- break;
430
- default:
431
- console.log('Unhandled event type:', stripeEvent.type);
432
- }
433
-
434
- // Mark as handled
435
- await executeQuery(`
436
- UPDATE rapport.stripe_webhook_events
437
- SET handled = true, processed_at = CURRENT_TIMESTAMP
438
- WHERE event_id = $1
439
- `, [stripeEvent.id]);
440
-
441
- } catch (processError) {
442
- console.error('Error processing webhook:', processError);
443
-
444
- // Record error
445
- try {
446
- await executeQuery(`
447
- UPDATE rapport.stripe_webhook_events
448
- SET handled = false,
449
- error = $2,
450
- processed_at = CURRENT_TIMESTAMP
451
- WHERE event_id = $1
452
- `, [stripeEvent.id, processError.message]);
453
- } catch (recordErr) {
454
- console.error('Failed to record webhook error:', recordErr.message);
455
- }
456
-
457
- // Return 500 for critical failures so Stripe retries
458
- // (missing client, DB connection errors, subscription not activated)
459
- return {
460
- statusCode: 500,
461
- headers: { 'Content-Type': 'application/json' },
462
- body: JSON.stringify({ error: processError.message })
463
- };
464
- }
465
-
466
- return {
467
- statusCode: 200,
468
- headers: { 'Content-Type': 'application/json' },
469
- body: JSON.stringify({ received: true })
470
- };
471
-
472
- } catch (error) {
473
- console.error('Webhook handler error:', error);
474
- return {
475
- statusCode: 500,
476
- headers: { 'Content-Type': 'application/json' },
477
- body: JSON.stringify({ error: 'Internal error' })
478
- };
479
- }
480
- }
481
-
482
- module.exports = { handler };