@equilateral_ai/mindmeld 3.0.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 +300 -0
- package/hooks/README.md +494 -0
- package/hooks/pre-compact.js +392 -0
- package/hooks/session-start.js +264 -0
- package/package.json +90 -0
- package/scripts/harvest.js +561 -0
- package/scripts/init-project.js +437 -0
- package/scripts/inject.js +388 -0
- package/src/collaboration/CollaborationPrompt.js +460 -0
- package/src/core/AlertEngine.js +813 -0
- package/src/core/AlertNotifier.js +363 -0
- package/src/core/CorrelationAnalyzer.js +774 -0
- package/src/core/CurationEngine.js +688 -0
- package/src/core/LLMPatternDetector.js +508 -0
- package/src/core/LoadBearingDetector.js +242 -0
- package/src/core/NotificationService.js +1032 -0
- package/src/core/PatternValidator.js +355 -0
- package/src/core/README.md +160 -0
- package/src/core/RapportOrchestrator.js +446 -0
- package/src/core/RelevanceDetector.js +577 -0
- package/src/core/StandardsIngestion.js +575 -0
- package/src/core/TeamLoadBearingDetector.js +431 -0
- package/src/database/dbOperations.js +105 -0
- package/src/handlers/activity/activityGetMe.js +98 -0
- package/src/handlers/activity/activityGetTeam.js +130 -0
- package/src/handlers/alerts/alertsAcknowledge.js +91 -0
- package/src/handlers/alerts/alertsGet.js +250 -0
- package/src/handlers/collaborators/collaboratorAdd.js +201 -0
- package/src/handlers/collaborators/collaboratorInvite.js +218 -0
- package/src/handlers/collaborators/collaboratorList.js +88 -0
- package/src/handlers/collaborators/collaboratorRemove.js +127 -0
- package/src/handlers/collaborators/inviteAccept.js +122 -0
- package/src/handlers/context/contextGet.js +57 -0
- package/src/handlers/context/invariantsGet.js +74 -0
- package/src/handlers/context/loopsGet.js +82 -0
- package/src/handlers/context/notesCreate.js +74 -0
- package/src/handlers/context/purposeGet.js +78 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
- package/src/handlers/correlations/correlationsGet.js +93 -0
- package/src/handlers/correlations/correlationsProjectGet.js +161 -0
- package/src/handlers/github/githubConnectionStatus.js +49 -0
- package/src/handlers/github/githubDiscoverPatterns.js +364 -0
- package/src/handlers/github/githubOAuthCallback.js +166 -0
- package/src/handlers/github/githubOAuthStart.js +59 -0
- package/src/handlers/github/githubPatternsReview.js +109 -0
- package/src/handlers/github/githubReposList.js +105 -0
- package/src/handlers/helpers/checkSuperAdmin.js +85 -0
- package/src/handlers/helpers/dbOperations.js +53 -0
- package/src/handlers/helpers/errorHandler.js +49 -0
- package/src/handlers/helpers/index.js +106 -0
- package/src/handlers/helpers/lambdaWrapper.js +60 -0
- package/src/handlers/helpers/responseUtil.js +55 -0
- package/src/handlers/helpers/subscriptionTiers.js +1168 -0
- package/src/handlers/notifications/getPreferences.js +84 -0
- package/src/handlers/notifications/sendNotification.js +170 -0
- package/src/handlers/notifications/updatePreferences.js +316 -0
- package/src/handlers/patterns/patternUsagePost.js +182 -0
- package/src/handlers/patterns/patternViolationPost.js +185 -0
- package/src/handlers/projects/projectCreate.js +107 -0
- package/src/handlers/projects/projectDelete.js +82 -0
- package/src/handlers/projects/projectGet.js +95 -0
- package/src/handlers/projects/projectUpdate.js +118 -0
- package/src/handlers/reports/aiLeverage.js +206 -0
- package/src/handlers/reports/engineeringInvestment.js +132 -0
- package/src/handlers/reports/riskForecast.js +186 -0
- package/src/handlers/reports/standardsRoi.js +162 -0
- package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
- package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
- package/src/handlers/scheduled/generateAlerts.js +135 -0
- package/src/handlers/scheduled/refreshActivity.js +21 -0
- package/src/handlers/scheduled/scanCompliance.js +334 -0
- package/src/handlers/sessions/sessionEndPost.js +180 -0
- package/src/handlers/sessions/sessionStandardsPost.js +135 -0
- package/src/handlers/stripe/addonManagePost.js +240 -0
- package/src/handlers/stripe/billingPortalPost.js +93 -0
- package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
- package/src/handlers/stripe/seatsUpdatePost.js +185 -0
- package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
- package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
- package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
- package/src/handlers/stripe/webhookPost.js +454 -0
- package/src/handlers/users/cognitoPostConfirmation.js +150 -0
- package/src/handlers/users/userEntitlementsGet.js +89 -0
- package/src/handlers/users/userGet.js +114 -0
- package/src/handlers/webhooks/githubWebhook.js +223 -0
- package/src/index.js +969 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add-on Management Handler
|
|
3
|
+
* Add or remove enterprise add-ons
|
|
4
|
+
*
|
|
5
|
+
* POST /api/stripe/subscription/addons
|
|
6
|
+
* Body: { action: 'add'|'remove', addonId: 'engineering_intelligence' }
|
|
7
|
+
* Auth: Cognito JWT required
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
wrapHandler,
|
|
12
|
+
executeQuery,
|
|
13
|
+
createSuccessResponse,
|
|
14
|
+
createErrorResponse,
|
|
15
|
+
handleError,
|
|
16
|
+
getEnterpriseAddon,
|
|
17
|
+
getAddonPriceId,
|
|
18
|
+
calculateEnterpriseCost
|
|
19
|
+
} = require('./helpers');
|
|
20
|
+
|
|
21
|
+
// Stripe is optional
|
|
22
|
+
let stripe = null;
|
|
23
|
+
try {
|
|
24
|
+
if (process.env.STRIPE_SECRET_KEY) {
|
|
25
|
+
stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.warn('Stripe not available:', e.message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Add or remove enterprise add-on
|
|
33
|
+
*/
|
|
34
|
+
async function manageAddon({ body: requestBody = {}, requestContext }) {
|
|
35
|
+
try {
|
|
36
|
+
const Request_ID = requestContext.requestId;
|
|
37
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
38
|
+
|
|
39
|
+
if (!email) {
|
|
40
|
+
return createErrorResponse(401, 'Authentication required');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { action, addonId } = requestBody;
|
|
44
|
+
|
|
45
|
+
// Validate action
|
|
46
|
+
if (!['add', 'remove'].includes(action)) {
|
|
47
|
+
return createErrorResponse(400, 'Invalid action', {
|
|
48
|
+
valid_actions: ['add', 'remove']
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Validate add-on ID
|
|
53
|
+
const addon = getEnterpriseAddon(addonId);
|
|
54
|
+
if (!addon) {
|
|
55
|
+
return createErrorResponse(400, `Invalid add-on: ${addonId}`, {
|
|
56
|
+
valid_addons: ['engineering_intelligence', 'knowledge_continuity']
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check Stripe is configured
|
|
61
|
+
if (!stripe) {
|
|
62
|
+
return createErrorResponse(503, 'Payment system not configured');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Get client with enterprise subscription
|
|
66
|
+
const clientQuery = `
|
|
67
|
+
SELECT c.client_id, c.subscription_tier, c.subscription_status,
|
|
68
|
+
c.stripe_subscription_id, c.seat_count, c.enterprise_package,
|
|
69
|
+
c.subscribed_addons, c.stripe_customer_id
|
|
70
|
+
FROM rapport.users u
|
|
71
|
+
JOIN rapport.clients c ON u.client_id = c.client_id
|
|
72
|
+
WHERE u.email_address = $1 AND u.active = true
|
|
73
|
+
`;
|
|
74
|
+
const clientResult = await executeQuery(clientQuery, [email]);
|
|
75
|
+
|
|
76
|
+
if (clientResult.rowCount === 0) {
|
|
77
|
+
return createErrorResponse(404, 'User not found');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const client = clientResult.rows[0];
|
|
81
|
+
|
|
82
|
+
// Verify enterprise subscription
|
|
83
|
+
if (client.subscription_tier !== 'enterprise') {
|
|
84
|
+
return createErrorResponse(400, 'Add-ons only available for enterprise subscriptions');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!client.stripe_subscription_id) {
|
|
88
|
+
return createErrorResponse(400, 'No active Stripe subscription found');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if using Full Platform (already includes all add-ons)
|
|
92
|
+
if (client.enterprise_package === 'full') {
|
|
93
|
+
return createErrorResponse(400, 'Full Platform package already includes all add-ons', {
|
|
94
|
+
tip: 'Downgrade to base package to manage add-ons separately'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const currentAddons = client.subscribed_addons || [];
|
|
99
|
+
const hasAddon = currentAddons.includes(addonId);
|
|
100
|
+
const seatCount = client.seat_count || 25;
|
|
101
|
+
const seatPacks = seatCount / 25;
|
|
102
|
+
|
|
103
|
+
if (action === 'add') {
|
|
104
|
+
if (hasAddon) {
|
|
105
|
+
return createErrorResponse(400, `Add-on ${addon.name} is already active`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Get subscription to add item
|
|
109
|
+
const subscription = await stripe.subscriptions.retrieve(client.stripe_subscription_id);
|
|
110
|
+
|
|
111
|
+
// Determine billing interval from existing subscription
|
|
112
|
+
const existingItem = subscription.items.data[0];
|
|
113
|
+
const billingInterval = existingItem?.price?.recurring?.interval || 'month';
|
|
114
|
+
const priceId = getAddonPriceId(addonId, billingInterval === 'year' ? 'annual' : 'monthly');
|
|
115
|
+
|
|
116
|
+
if (!priceId) {
|
|
117
|
+
return createErrorResponse(500, 'Add-on price not configured');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Add subscription item
|
|
121
|
+
const newItem = await stripe.subscriptionItems.create({
|
|
122
|
+
subscription: client.stripe_subscription_id,
|
|
123
|
+
price: priceId,
|
|
124
|
+
quantity: seatPacks,
|
|
125
|
+
proration_behavior: 'create_prorations'
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Update client record
|
|
129
|
+
const updatedAddons = [...currentAddons, addonId];
|
|
130
|
+
await executeQuery(`
|
|
131
|
+
UPDATE rapport.clients
|
|
132
|
+
SET subscribed_addons = $2, last_updated = CURRENT_TIMESTAMP
|
|
133
|
+
WHERE client_id = $1
|
|
134
|
+
`, [client.client_id, JSON.stringify(updatedAddons)]);
|
|
135
|
+
|
|
136
|
+
// Record in addon_entitlements
|
|
137
|
+
await executeQuery(`
|
|
138
|
+
INSERT INTO rapport.addon_entitlements (
|
|
139
|
+
client_id, addon_id, stripe_subscription_item_id, seat_count, status
|
|
140
|
+
)
|
|
141
|
+
VALUES ($1, $2, $3, $4, 'active')
|
|
142
|
+
ON CONFLICT (client_id, addon_id) DO UPDATE SET
|
|
143
|
+
stripe_subscription_item_id = EXCLUDED.stripe_subscription_item_id,
|
|
144
|
+
seat_count = EXCLUDED.seat_count,
|
|
145
|
+
status = 'active',
|
|
146
|
+
updated_at = CURRENT_TIMESTAMP
|
|
147
|
+
`, [client.client_id, addonId, newItem.id, seatCount]);
|
|
148
|
+
|
|
149
|
+
// Calculate new cost
|
|
150
|
+
const newCost = calculateEnterpriseCost(
|
|
151
|
+
client.enterprise_package,
|
|
152
|
+
seatCount,
|
|
153
|
+
updatedAddons,
|
|
154
|
+
'monthly'
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return createSuccessResponse(
|
|
158
|
+
{
|
|
159
|
+
Records: [{
|
|
160
|
+
action: 'added',
|
|
161
|
+
addon: addon.name,
|
|
162
|
+
addonId,
|
|
163
|
+
seatCount,
|
|
164
|
+
monthlyAddonCost: addon.pricePerSeat * seatCount,
|
|
165
|
+
newMonthlyTotal: newCost.total,
|
|
166
|
+
prorated: true
|
|
167
|
+
}]
|
|
168
|
+
},
|
|
169
|
+
`Added ${addon.name} add-on`,
|
|
170
|
+
{ Request_ID, Timestamp: new Date().toISOString() }
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
} else if (action === 'remove') {
|
|
174
|
+
if (!hasAddon) {
|
|
175
|
+
return createErrorResponse(400, `Add-on ${addon.name} is not active`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Find the subscription item for this add-on
|
|
179
|
+
const entitlementQuery = `
|
|
180
|
+
SELECT stripe_subscription_item_id
|
|
181
|
+
FROM rapport.addon_entitlements
|
|
182
|
+
WHERE client_id = $1 AND addon_id = $2 AND status = 'active'
|
|
183
|
+
`;
|
|
184
|
+
const entitlementResult = await executeQuery(entitlementQuery, [client.client_id, addonId]);
|
|
185
|
+
|
|
186
|
+
if (entitlementResult.rowCount > 0 && entitlementResult.rows[0].stripe_subscription_item_id) {
|
|
187
|
+
// Remove subscription item from Stripe
|
|
188
|
+
await stripe.subscriptionItems.del(
|
|
189
|
+
entitlementResult.rows[0].stripe_subscription_item_id,
|
|
190
|
+
{ proration_behavior: 'create_prorations' }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Update client record
|
|
195
|
+
const updatedAddons = currentAddons.filter(id => id !== addonId);
|
|
196
|
+
await executeQuery(`
|
|
197
|
+
UPDATE rapport.clients
|
|
198
|
+
SET subscribed_addons = $2, last_updated = CURRENT_TIMESTAMP
|
|
199
|
+
WHERE client_id = $1
|
|
200
|
+
`, [client.client_id, JSON.stringify(updatedAddons)]);
|
|
201
|
+
|
|
202
|
+
// Update addon_entitlements
|
|
203
|
+
await executeQuery(`
|
|
204
|
+
UPDATE rapport.addon_entitlements
|
|
205
|
+
SET status = 'canceled', updated_at = CURRENT_TIMESTAMP
|
|
206
|
+
WHERE client_id = $1 AND addon_id = $2
|
|
207
|
+
`, [client.client_id, addonId]);
|
|
208
|
+
|
|
209
|
+
// Calculate new cost
|
|
210
|
+
const newCost = calculateEnterpriseCost(
|
|
211
|
+
client.enterprise_package,
|
|
212
|
+
seatCount,
|
|
213
|
+
updatedAddons,
|
|
214
|
+
'monthly'
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return createSuccessResponse(
|
|
218
|
+
{
|
|
219
|
+
Records: [{
|
|
220
|
+
action: 'removed',
|
|
221
|
+
addon: addon.name,
|
|
222
|
+
addonId,
|
|
223
|
+
monthlyAddonCost: addon.pricePerSeat * seatCount,
|
|
224
|
+
newMonthlyTotal: newCost.total,
|
|
225
|
+
prorated: true,
|
|
226
|
+
creditIssued: true
|
|
227
|
+
}]
|
|
228
|
+
},
|
|
229
|
+
`Removed ${addon.name} add-on`,
|
|
230
|
+
{ Request_ID, Timestamp: new Date().toISOString() }
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('Handler Error:', error);
|
|
236
|
+
return handleError(error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
exports.handler = wrapHandler(manageAddon);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Portal Handler
|
|
3
|
+
* Creates Stripe Customer Portal session for self-service subscription management
|
|
4
|
+
*
|
|
5
|
+
* POST /api/stripe/billing-portal
|
|
6
|
+
* Body: { returnUrl?: string }
|
|
7
|
+
* Auth: Cognito JWT required
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
wrapHandler,
|
|
12
|
+
executeQuery,
|
|
13
|
+
createSuccessResponse,
|
|
14
|
+
createErrorResponse,
|
|
15
|
+
handleError
|
|
16
|
+
} = require('./helpers');
|
|
17
|
+
|
|
18
|
+
// Stripe is optional
|
|
19
|
+
let stripe = null;
|
|
20
|
+
try {
|
|
21
|
+
if (process.env.STRIPE_SECRET_KEY) {
|
|
22
|
+
stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
|
23
|
+
}
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.warn('Stripe not available:', e.message);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create billing portal session
|
|
30
|
+
*/
|
|
31
|
+
async function createBillingPortal({ body: requestBody = {}, requestContext }) {
|
|
32
|
+
try {
|
|
33
|
+
const Request_ID = requestContext.requestId;
|
|
34
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
35
|
+
|
|
36
|
+
if (!email) {
|
|
37
|
+
return createErrorResponse(401, 'Authentication required');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { returnUrl } = requestBody;
|
|
41
|
+
|
|
42
|
+
// Check Stripe is configured
|
|
43
|
+
if (!stripe) {
|
|
44
|
+
return createErrorResponse(503, 'Payment system not configured');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get client with Stripe customer ID
|
|
48
|
+
const clientQuery = `
|
|
49
|
+
SELECT c.client_id, c.stripe_customer_id, c.subscription_tier,
|
|
50
|
+
c.subscription_status
|
|
51
|
+
FROM rapport.users u
|
|
52
|
+
JOIN rapport.clients c ON u.client_id = c.client_id
|
|
53
|
+
WHERE u.email_address = $1 AND u.active = true
|
|
54
|
+
`;
|
|
55
|
+
const clientResult = await executeQuery(clientQuery, [email]);
|
|
56
|
+
|
|
57
|
+
if (clientResult.rowCount === 0) {
|
|
58
|
+
return createErrorResponse(404, 'User not found');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const client = clientResult.rows[0];
|
|
62
|
+
|
|
63
|
+
if (!client.stripe_customer_id) {
|
|
64
|
+
return createErrorResponse(400, 'No billing account found. Subscribe to a plan first.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create billing portal session
|
|
68
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
69
|
+
customer: client.stripe_customer_id,
|
|
70
|
+
return_url: returnUrl || `${process.env.APP_URL || 'https://mindmeld.dev'}/settings/billing`
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return createSuccessResponse(
|
|
74
|
+
{
|
|
75
|
+
Records: [{
|
|
76
|
+
portalUrl: session.url,
|
|
77
|
+
returnUrl: session.return_url
|
|
78
|
+
}]
|
|
79
|
+
},
|
|
80
|
+
'Billing portal session created',
|
|
81
|
+
{
|
|
82
|
+
Request_ID,
|
|
83
|
+
Timestamp: new Date().toISOString()
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Handler Error:', error);
|
|
89
|
+
return handleError(error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
exports.handler = wrapHandler(createBillingPortal);
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise Checkout Handler
|
|
3
|
+
* Creates Stripe Checkout session for enterprise subscriptions
|
|
4
|
+
*
|
|
5
|
+
* POST /api/stripe/enterprise/checkout
|
|
6
|
+
* Body: { package: 'base'|'full', seatCount: 25, addons: [], billingPeriod: 'monthly'|'annual' }
|
|
7
|
+
* Auth: Cognito JWT required
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
wrapHandler,
|
|
12
|
+
executeQuery,
|
|
13
|
+
createSuccessResponse,
|
|
14
|
+
createErrorResponse,
|
|
15
|
+
handleError,
|
|
16
|
+
validateSeatCount,
|
|
17
|
+
calculateEnterpriseCost,
|
|
18
|
+
getEnterprisePackage,
|
|
19
|
+
getEnterpriseAddon,
|
|
20
|
+
getEnterprisePriceId,
|
|
21
|
+
getAddonPriceId,
|
|
22
|
+
getVolumeDiscount
|
|
23
|
+
} = require('./helpers');
|
|
24
|
+
|
|
25
|
+
// Stripe is optional - will be null if not configured
|
|
26
|
+
let stripe = null;
|
|
27
|
+
try {
|
|
28
|
+
if (process.env.STRIPE_SECRET_KEY) {
|
|
29
|
+
stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.warn('Stripe not available:', e.message);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create enterprise checkout session
|
|
37
|
+
*/
|
|
38
|
+
async function createEnterpriseCheckout({ body: requestBody = {}, requestContext }) {
|
|
39
|
+
try {
|
|
40
|
+
const Request_ID = requestContext.requestId;
|
|
41
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
42
|
+
|
|
43
|
+
if (!email) {
|
|
44
|
+
return createErrorResponse(401, 'Authentication required');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
package: packageType = 'base',
|
|
49
|
+
seatCount = 25,
|
|
50
|
+
addons = [],
|
|
51
|
+
billingPeriod = 'monthly'
|
|
52
|
+
} = requestBody;
|
|
53
|
+
|
|
54
|
+
// Validate package type
|
|
55
|
+
const pkgConfig = getEnterprisePackage(packageType);
|
|
56
|
+
if (!pkgConfig) {
|
|
57
|
+
return createErrorResponse(400, 'Invalid enterprise package', {
|
|
58
|
+
valid_packages: ['base', 'full'],
|
|
59
|
+
provided: packageType
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Validate seat count (must be >= 25 and multiple of 25)
|
|
64
|
+
const seatValidation = validateSeatCount(seatCount);
|
|
65
|
+
if (!seatValidation.valid) {
|
|
66
|
+
return createErrorResponse(400, seatValidation.message, {
|
|
67
|
+
requested: seatCount,
|
|
68
|
+
corrected: seatValidation.correctedCount,
|
|
69
|
+
minimum: 25,
|
|
70
|
+
increment: 25
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Validate add-ons (only valid for 'base' package)
|
|
75
|
+
if (packageType === 'full' && addons.length > 0) {
|
|
76
|
+
return createErrorResponse(400, 'Full Platform package already includes all add-ons', {
|
|
77
|
+
tip: 'Use base package if you want to select specific add-ons'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Validate add-on IDs
|
|
82
|
+
for (const addonId of addons) {
|
|
83
|
+
const addon = getEnterpriseAddon(addonId);
|
|
84
|
+
if (!addon) {
|
|
85
|
+
return createErrorResponse(400, `Invalid add-on: ${addonId}`, {
|
|
86
|
+
valid_addons: ['engineering_intelligence', 'knowledge_continuity']
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check Stripe is configured
|
|
92
|
+
if (!stripe) {
|
|
93
|
+
return createErrorResponse(503, 'Payment system not configured', {
|
|
94
|
+
code: 'STRIPE_NOT_CONFIGURED'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Get user's client
|
|
99
|
+
const userQuery = `
|
|
100
|
+
SELECT u.client_id, u.create_date as account_created,
|
|
101
|
+
c.client_name, c.stripe_customer_id, c.subscription_status,
|
|
102
|
+
c.subscription_tier
|
|
103
|
+
FROM rapport.users u
|
|
104
|
+
JOIN rapport.clients c ON u.client_id = c.client_id
|
|
105
|
+
WHERE u.email_address = $1 AND u.active = true
|
|
106
|
+
`;
|
|
107
|
+
const userResult = await executeQuery(userQuery, [email]);
|
|
108
|
+
|
|
109
|
+
if (userResult.rowCount === 0) {
|
|
110
|
+
return createErrorResponse(404, 'User not found');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const user = userResult.rows[0];
|
|
114
|
+
|
|
115
|
+
// Check for existing active enterprise subscription
|
|
116
|
+
if (user.subscription_tier === 'enterprise' && user.subscription_status === 'active') {
|
|
117
|
+
return createErrorResponse(400, 'Already have active enterprise subscription. Use seats/addons endpoints to modify.', {
|
|
118
|
+
code: 'ENTERPRISE_SUBSCRIPTION_EXISTS'
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Calculate pricing
|
|
123
|
+
const costBreakdown = calculateEnterpriseCost(packageType, seatCount, addons, billingPeriod);
|
|
124
|
+
const volumeDiscount = getVolumeDiscount(seatCount);
|
|
125
|
+
|
|
126
|
+
// Build line items for Stripe checkout
|
|
127
|
+
const lineItems = [];
|
|
128
|
+
|
|
129
|
+
// Get seat pack quantity (1 pack = 25 seats)
|
|
130
|
+
const seatPacks = seatCount / 25;
|
|
131
|
+
|
|
132
|
+
// Main package line item
|
|
133
|
+
const mainPriceId = getEnterprisePriceId(packageType, billingPeriod);
|
|
134
|
+
if (!mainPriceId) {
|
|
135
|
+
return createErrorResponse(500, 'Enterprise price not configured');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
lineItems.push({
|
|
139
|
+
price: mainPriceId,
|
|
140
|
+
quantity: seatPacks
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Add-on line items (only for 'base' package)
|
|
144
|
+
if (packageType === 'base') {
|
|
145
|
+
for (const addonId of addons) {
|
|
146
|
+
const addonPriceId = getAddonPriceId(addonId, billingPeriod);
|
|
147
|
+
if (addonPriceId) {
|
|
148
|
+
lineItems.push({
|
|
149
|
+
price: addonPriceId,
|
|
150
|
+
quantity: seatPacks
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Create Stripe Checkout session
|
|
157
|
+
const sessionParams = {
|
|
158
|
+
mode: 'subscription',
|
|
159
|
+
payment_method_types: ['card'],
|
|
160
|
+
customer_email: email,
|
|
161
|
+
line_items: lineItems,
|
|
162
|
+
success_url: `${process.env.APP_URL || 'https://mindmeld.dev'}/enterprise/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
163
|
+
cancel_url: `${process.env.APP_URL || 'https://mindmeld.dev'}/enterprise/cancel`,
|
|
164
|
+
metadata: {
|
|
165
|
+
client_id: user.client_id,
|
|
166
|
+
user_email: email,
|
|
167
|
+
tier: 'enterprise',
|
|
168
|
+
enterprise_package: packageType,
|
|
169
|
+
seat_count: seatCount.toString(),
|
|
170
|
+
addons: JSON.stringify(addons),
|
|
171
|
+
billing_period: billingPeriod
|
|
172
|
+
},
|
|
173
|
+
subscription_data: {
|
|
174
|
+
metadata: {
|
|
175
|
+
client_id: user.client_id,
|
|
176
|
+
tier: 'enterprise',
|
|
177
|
+
enterprise_package: packageType,
|
|
178
|
+
seat_count: seatCount.toString(),
|
|
179
|
+
addons: JSON.stringify(addons)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Apply volume discount as coupon if applicable
|
|
185
|
+
if (volumeDiscount.discount > 0) {
|
|
186
|
+
// Look up or create volume discount coupon
|
|
187
|
+
const couponId = `VOLUME_${Math.round(volumeDiscount.discount * 100)}`;
|
|
188
|
+
try {
|
|
189
|
+
await stripe.coupons.retrieve(couponId);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
// Create coupon if it doesn't exist
|
|
192
|
+
if (e.code === 'resource_missing') {
|
|
193
|
+
await stripe.coupons.create({
|
|
194
|
+
id: couponId,
|
|
195
|
+
percent_off: volumeDiscount.discount * 100,
|
|
196
|
+
duration: 'forever',
|
|
197
|
+
name: volumeDiscount.label
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
sessionParams.discounts = [{ coupon: couponId }];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// If customer already exists in Stripe, use that
|
|
205
|
+
if (user.stripe_customer_id) {
|
|
206
|
+
delete sessionParams.customer_email;
|
|
207
|
+
sessionParams.customer = user.stripe_customer_id;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const session = await stripe.checkout.sessions.create(sessionParams);
|
|
211
|
+
|
|
212
|
+
// Store pending session with enterprise details
|
|
213
|
+
const sessionQuery = `
|
|
214
|
+
INSERT INTO rapport.stripe_checkout_sessions (
|
|
215
|
+
session_id,
|
|
216
|
+
client_id,
|
|
217
|
+
user_email,
|
|
218
|
+
tier,
|
|
219
|
+
billing_period,
|
|
220
|
+
seat_count,
|
|
221
|
+
enterprise_package,
|
|
222
|
+
addons,
|
|
223
|
+
status
|
|
224
|
+
)
|
|
225
|
+
VALUES ($1, $2, $3, 'enterprise', $4, $5, $6, $7, 'pending')
|
|
226
|
+
`;
|
|
227
|
+
await executeQuery(sessionQuery, [
|
|
228
|
+
session.id,
|
|
229
|
+
user.client_id,
|
|
230
|
+
email,
|
|
231
|
+
billingPeriod,
|
|
232
|
+
seatCount,
|
|
233
|
+
packageType,
|
|
234
|
+
JSON.stringify(addons)
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
return createSuccessResponse(
|
|
238
|
+
{
|
|
239
|
+
Records: [{
|
|
240
|
+
checkoutUrl: session.url,
|
|
241
|
+
sessionId: session.id,
|
|
242
|
+
package: pkgConfig.displayName,
|
|
243
|
+
seatCount,
|
|
244
|
+
addons: addons.map(id => getEnterpriseAddon(id)?.name),
|
|
245
|
+
pricing: {
|
|
246
|
+
subtotal: costBreakdown.subtotal,
|
|
247
|
+
volumeDiscount: volumeDiscount.discount > 0 ? {
|
|
248
|
+
percent: volumeDiscount.discount * 100,
|
|
249
|
+
label: volumeDiscount.label,
|
|
250
|
+
amount: costBreakdown.discountAmount
|
|
251
|
+
} : null,
|
|
252
|
+
total: costBreakdown.total,
|
|
253
|
+
billingPeriod,
|
|
254
|
+
breakdown: costBreakdown.breakdown
|
|
255
|
+
}
|
|
256
|
+
}]
|
|
257
|
+
},
|
|
258
|
+
`Enterprise checkout session created - ${pkgConfig.displayName} with ${seatCount} seats`,
|
|
259
|
+
{
|
|
260
|
+
Total_Records: 1,
|
|
261
|
+
Request_ID,
|
|
262
|
+
Timestamp: new Date().toISOString()
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error('Handler Error:', error);
|
|
268
|
+
return handleError(error);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
exports.handler = wrapHandler(createEnterpriseCheckout);
|