@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,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seats Update Handler
|
|
3
|
+
* Adjust seat count for enterprise subscriptions
|
|
4
|
+
*
|
|
5
|
+
* POST /api/stripe/subscription/seats
|
|
6
|
+
* Body: { seatCount: 50 }
|
|
7
|
+
* Auth: Cognito JWT required
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
wrapHandler,
|
|
12
|
+
executeQuery,
|
|
13
|
+
createSuccessResponse,
|
|
14
|
+
createErrorResponse,
|
|
15
|
+
handleError,
|
|
16
|
+
validateSeatCount,
|
|
17
|
+
calculateEnterpriseCost,
|
|
18
|
+
getVolumeDiscount
|
|
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
|
+
* Update enterprise seat count
|
|
33
|
+
*/
|
|
34
|
+
async function updateSeats({ 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 { seatCount } = requestBody;
|
|
44
|
+
|
|
45
|
+
// Validate seat count
|
|
46
|
+
const seatValidation = validateSeatCount(seatCount);
|
|
47
|
+
if (!seatValidation.valid) {
|
|
48
|
+
return createErrorResponse(400, seatValidation.message, {
|
|
49
|
+
requested: seatCount,
|
|
50
|
+
corrected: seatValidation.correctedCount,
|
|
51
|
+
minimum: 25,
|
|
52
|
+
increment: 25
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check Stripe is configured
|
|
57
|
+
if (!stripe) {
|
|
58
|
+
return createErrorResponse(503, 'Payment system not configured');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Get client with enterprise subscription
|
|
62
|
+
const clientQuery = `
|
|
63
|
+
SELECT c.client_id, c.subscription_tier, c.subscription_status,
|
|
64
|
+
c.stripe_subscription_id, c.seat_count, c.enterprise_package,
|
|
65
|
+
c.subscribed_addons, c.stripe_customer_id
|
|
66
|
+
FROM rapport.users u
|
|
67
|
+
JOIN rapport.clients c ON u.client_id = c.client_id
|
|
68
|
+
WHERE u.email_address = $1 AND u.active = true
|
|
69
|
+
`;
|
|
70
|
+
const clientResult = await executeQuery(clientQuery, [email]);
|
|
71
|
+
|
|
72
|
+
if (clientResult.rowCount === 0) {
|
|
73
|
+
return createErrorResponse(404, 'User not found');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const client = clientResult.rows[0];
|
|
77
|
+
|
|
78
|
+
// Verify enterprise subscription
|
|
79
|
+
if (client.subscription_tier !== 'enterprise') {
|
|
80
|
+
return createErrorResponse(400, 'Seat updates only available for enterprise subscriptions');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!client.stripe_subscription_id) {
|
|
84
|
+
return createErrorResponse(400, 'No active Stripe subscription found');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const currentSeats = client.seat_count || 25;
|
|
88
|
+
const newSeats = seatValidation.correctedCount;
|
|
89
|
+
|
|
90
|
+
if (newSeats === currentSeats) {
|
|
91
|
+
return createSuccessResponse(
|
|
92
|
+
{ message: 'No change in seat count', seatCount: currentSeats },
|
|
93
|
+
'Seat count unchanged'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Calculate new seat packs
|
|
98
|
+
const currentPacks = currentSeats / 25;
|
|
99
|
+
const newPacks = newSeats / 25;
|
|
100
|
+
|
|
101
|
+
// Get subscription from Stripe
|
|
102
|
+
const subscription = await stripe.subscriptions.retrieve(client.stripe_subscription_id);
|
|
103
|
+
|
|
104
|
+
// Update all subscription items with new quantity
|
|
105
|
+
const updates = subscription.items.data.map(item => ({
|
|
106
|
+
id: item.id,
|
|
107
|
+
quantity: newPacks
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
// Check if volume discount changes
|
|
111
|
+
const currentDiscount = getVolumeDiscount(currentSeats);
|
|
112
|
+
const newDiscount = getVolumeDiscount(newSeats);
|
|
113
|
+
const discountChanged = currentDiscount.discount !== newDiscount.discount;
|
|
114
|
+
|
|
115
|
+
// Update subscription
|
|
116
|
+
const updateParams = {
|
|
117
|
+
items: updates,
|
|
118
|
+
proration_behavior: 'create_prorations'
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Update volume discount coupon if needed
|
|
122
|
+
if (discountChanged && newDiscount.discount > 0) {
|
|
123
|
+
const couponId = `VOLUME_${Math.round(newDiscount.discount * 100)}`;
|
|
124
|
+
try {
|
|
125
|
+
await stripe.coupons.retrieve(couponId);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
if (e.code === 'resource_missing') {
|
|
128
|
+
await stripe.coupons.create({
|
|
129
|
+
id: couponId,
|
|
130
|
+
percent_off: newDiscount.discount * 100,
|
|
131
|
+
duration: 'forever',
|
|
132
|
+
name: newDiscount.label
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
updateParams.coupon = couponId;
|
|
137
|
+
} else if (discountChanged && newDiscount.discount === 0) {
|
|
138
|
+
// Remove discount
|
|
139
|
+
updateParams.coupon = '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await stripe.subscriptions.update(client.stripe_subscription_id, updateParams);
|
|
143
|
+
|
|
144
|
+
// Update client record
|
|
145
|
+
await executeQuery(`
|
|
146
|
+
UPDATE rapport.clients
|
|
147
|
+
SET seat_count = $2, last_updated = CURRENT_TIMESTAMP
|
|
148
|
+
WHERE client_id = $1
|
|
149
|
+
`, [client.client_id, newSeats]);
|
|
150
|
+
|
|
151
|
+
// Calculate new pricing
|
|
152
|
+
const addons = client.subscribed_addons || [];
|
|
153
|
+
const newCost = calculateEnterpriseCost(
|
|
154
|
+
client.enterprise_package,
|
|
155
|
+
newSeats,
|
|
156
|
+
addons,
|
|
157
|
+
'monthly' // Assuming monthly for display
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return createSuccessResponse(
|
|
161
|
+
{
|
|
162
|
+
Records: [{
|
|
163
|
+
previousSeatCount: currentSeats,
|
|
164
|
+
newSeatCount: newSeats,
|
|
165
|
+
change: newSeats - currentSeats,
|
|
166
|
+
volumeDiscount: newDiscount.discount > 0 ? newDiscount : null,
|
|
167
|
+
discountChanged,
|
|
168
|
+
newMonthlyTotal: newCost.total,
|
|
169
|
+
prorated: true
|
|
170
|
+
}]
|
|
171
|
+
},
|
|
172
|
+
`Seat count updated from ${currentSeats} to ${newSeats}`,
|
|
173
|
+
{
|
|
174
|
+
Request_ID,
|
|
175
|
+
Timestamp: new Date().toISOString()
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error('Handler Error:', error);
|
|
181
|
+
return handleError(error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
exports.handler = wrapHandler(updateSeats);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Cancel Handler
|
|
3
|
+
* Cancels subscription (immediately or at period end)
|
|
4
|
+
*
|
|
5
|
+
* DELETE /api/stripe/subscription/cancel
|
|
6
|
+
* Query: { immediately: boolean }
|
|
7
|
+
* Auth: Cognito JWT required
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError } = 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
|
+
* Cancel subscription
|
|
24
|
+
*/
|
|
25
|
+
async function cancelSubscription({ queryStringParameters = {}, 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 immediately = queryStringParameters.immediately === 'true';
|
|
37
|
+
|
|
38
|
+
if (!stripe) {
|
|
39
|
+
return createErrorResponse(503, 'Payment system not configured');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Get user's client and subscription
|
|
43
|
+
const userQuery = `
|
|
44
|
+
SELECT
|
|
45
|
+
u.client_id,
|
|
46
|
+
c.stripe_customer_id,
|
|
47
|
+
c.subscription_tier,
|
|
48
|
+
c.subscription_status
|
|
49
|
+
FROM rapport.users u
|
|
50
|
+
JOIN rapport.clients c ON u.client_id = c.client_id
|
|
51
|
+
WHERE u.email_address = $1 AND u.active = true
|
|
52
|
+
`;
|
|
53
|
+
const userResult = await executeQuery(userQuery, [email]);
|
|
54
|
+
|
|
55
|
+
if (userResult.rowCount === 0) {
|
|
56
|
+
return createErrorResponse(404, 'User not found');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const user = userResult.rows[0];
|
|
60
|
+
|
|
61
|
+
if (!user.stripe_customer_id || user.subscription_status !== 'active') {
|
|
62
|
+
return createErrorResponse(400, 'No active subscription to cancel');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Get subscription from Stripe
|
|
66
|
+
const subscriptions = await stripe.subscriptions.list({
|
|
67
|
+
customer: user.stripe_customer_id,
|
|
68
|
+
status: 'active',
|
|
69
|
+
limit: 1
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (subscriptions.data.length === 0) {
|
|
73
|
+
// Update local state to match Stripe
|
|
74
|
+
await executeQuery(`
|
|
75
|
+
UPDATE rapport.clients
|
|
76
|
+
SET subscription_status = 'canceled',
|
|
77
|
+
subscription_tier = 'free',
|
|
78
|
+
last_updated = CURRENT_TIMESTAMP
|
|
79
|
+
WHERE client_id = $1
|
|
80
|
+
`, [user.client_id]);
|
|
81
|
+
|
|
82
|
+
return createErrorResponse(400, 'No active subscription found in Stripe');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const subscription = subscriptions.data[0];
|
|
86
|
+
let canceledSubscription;
|
|
87
|
+
let accessEndsAt;
|
|
88
|
+
|
|
89
|
+
if (immediately) {
|
|
90
|
+
// Cancel immediately with proration
|
|
91
|
+
canceledSubscription = await stripe.subscriptions.cancel(subscription.id, {
|
|
92
|
+
prorate: true
|
|
93
|
+
});
|
|
94
|
+
accessEndsAt = new Date();
|
|
95
|
+
|
|
96
|
+
// Update client immediately
|
|
97
|
+
await executeQuery(`
|
|
98
|
+
UPDATE rapport.clients
|
|
99
|
+
SET subscription_status = 'canceled',
|
|
100
|
+
subscription_tier = 'free',
|
|
101
|
+
subscription_ends_at = CURRENT_TIMESTAMP,
|
|
102
|
+
last_updated = CURRENT_TIMESTAMP
|
|
103
|
+
WHERE client_id = $1
|
|
104
|
+
`, [user.client_id]);
|
|
105
|
+
|
|
106
|
+
} else {
|
|
107
|
+
// Cancel at end of billing period
|
|
108
|
+
canceledSubscription = await stripe.subscriptions.update(subscription.id, {
|
|
109
|
+
cancel_at_period_end: true
|
|
110
|
+
});
|
|
111
|
+
accessEndsAt = new Date(subscription.current_period_end * 1000);
|
|
112
|
+
|
|
113
|
+
// Update client to canceling status
|
|
114
|
+
await executeQuery(`
|
|
115
|
+
UPDATE rapport.clients
|
|
116
|
+
SET subscription_status = 'canceling',
|
|
117
|
+
subscription_ends_at = $2,
|
|
118
|
+
last_updated = CURRENT_TIMESTAMP
|
|
119
|
+
WHERE client_id = $1
|
|
120
|
+
`, [user.client_id, accessEndsAt]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check data limits for free tier
|
|
124
|
+
const usageQuery = `
|
|
125
|
+
SELECT
|
|
126
|
+
(SELECT COUNT(*) FROM rapport.user_entitlements WHERE client_id = $1) as collaborators,
|
|
127
|
+
(SELECT COUNT(*) FROM rapport.projects p
|
|
128
|
+
JOIN rapport.companies co ON p.company_id = co.company_id
|
|
129
|
+
WHERE co.client_id = $1 AND p.archived = false) as projects
|
|
130
|
+
`;
|
|
131
|
+
const usageResult = await executeQuery(usageQuery, [user.client_id]);
|
|
132
|
+
const usage = usageResult.rows[0];
|
|
133
|
+
|
|
134
|
+
const warnings = [];
|
|
135
|
+
if (parseInt(usage.collaborators) > 1) {
|
|
136
|
+
warnings.push(`You have ${usage.collaborators} collaborators. Free tier allows 1.`);
|
|
137
|
+
}
|
|
138
|
+
if (parseInt(usage.projects) > 3) {
|
|
139
|
+
warnings.push(`You have ${usage.projects} projects. Free tier allows 3.`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return createSuccessResponse(
|
|
143
|
+
{
|
|
144
|
+
Records: [{
|
|
145
|
+
status: immediately ? 'canceled' : 'canceling',
|
|
146
|
+
access_ends_at: accessEndsAt.toISOString(),
|
|
147
|
+
immediately,
|
|
148
|
+
previous_tier: user.subscription_tier,
|
|
149
|
+
new_tier: 'free',
|
|
150
|
+
warnings
|
|
151
|
+
}]
|
|
152
|
+
},
|
|
153
|
+
immediately
|
|
154
|
+
? 'Subscription canceled immediately'
|
|
155
|
+
: 'Subscription will cancel at end of billing period',
|
|
156
|
+
{
|
|
157
|
+
Total_Records: 1,
|
|
158
|
+
Request_ID,
|
|
159
|
+
Timestamp: new Date().toISOString()
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Handler Error:', error);
|
|
165
|
+
return handleError(error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
exports.handler = wrapHandler(cancelSubscription);
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Create Handler
|
|
3
|
+
* Creates Stripe Checkout session for new subscription
|
|
4
|
+
*
|
|
5
|
+
* POST /api/stripe/subscription/create
|
|
6
|
+
* Body: { tier, billingPeriod }
|
|
7
|
+
* Auth: Cognito JWT required
|
|
8
|
+
*
|
|
9
|
+
* Following HoneyDo subscriptionCreatePost.js pattern
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
wrapHandler,
|
|
14
|
+
executeQuery,
|
|
15
|
+
createSuccessResponse,
|
|
16
|
+
createErrorResponse,
|
|
17
|
+
handleError,
|
|
18
|
+
getTierConfig,
|
|
19
|
+
getStripePriceId,
|
|
20
|
+
checkEarlyAdopterEligibility,
|
|
21
|
+
getEarlyAdopterDiscount,
|
|
22
|
+
calculateEarlyAdopterPrice
|
|
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 subscription checkout session
|
|
37
|
+
*/
|
|
38
|
+
async function createSubscription({ body: requestBody = {}, requestContext }) {
|
|
39
|
+
try {
|
|
40
|
+
const Request_ID = requestContext.requestId;
|
|
41
|
+
// REST API: requestContext.authorizer.claims.email
|
|
42
|
+
// HTTP API: requestContext.authorizer.jwt.claims.email
|
|
43
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
44
|
+
|
|
45
|
+
if (!email) {
|
|
46
|
+
return createErrorResponse(401, 'Authentication required');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { tier, billingPeriod = 'monthly' } = requestBody;
|
|
50
|
+
|
|
51
|
+
// Validate tier
|
|
52
|
+
const tierConfig = getTierConfig(tier);
|
|
53
|
+
if (!tierConfig) {
|
|
54
|
+
return createErrorResponse(400, 'Invalid subscription tier', {
|
|
55
|
+
valid_tiers: ['team', 'professional', 'enterprise']
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (tier === 'free') {
|
|
60
|
+
return createErrorResponse(400, 'Cannot subscribe to free tier');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check Stripe is configured
|
|
64
|
+
if (!stripe) {
|
|
65
|
+
return createErrorResponse(503, 'Payment system not configured', {
|
|
66
|
+
code: 'STRIPE_NOT_CONFIGURED'
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get user's client and account creation date
|
|
71
|
+
const userQuery = `
|
|
72
|
+
SELECT u.client_id, u.create_date as account_created,
|
|
73
|
+
c.client_name, c.stripe_customer_id, c.subscription_status
|
|
74
|
+
FROM rapport.users u
|
|
75
|
+
JOIN rapport.clients c ON u.client_id = c.client_id
|
|
76
|
+
WHERE u.email_address = $1 AND u.active = true
|
|
77
|
+
`;
|
|
78
|
+
const userResult = await executeQuery(userQuery, [email]);
|
|
79
|
+
|
|
80
|
+
if (userResult.rowCount === 0) {
|
|
81
|
+
return createErrorResponse(404, 'User not found');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const user = userResult.rows[0];
|
|
85
|
+
|
|
86
|
+
// Check early adopter eligibility
|
|
87
|
+
const earlyAdopterStatus = checkEarlyAdopterEligibility(user.account_created);
|
|
88
|
+
const discountConfig = getEarlyAdopterDiscount();
|
|
89
|
+
|
|
90
|
+
// Check for existing active subscription
|
|
91
|
+
if (user.subscription_status === 'active' && user.stripe_customer_id) {
|
|
92
|
+
return createErrorResponse(400, 'Already have active subscription. Use update to change tier.', {
|
|
93
|
+
code: 'SUBSCRIPTION_EXISTS'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Get Stripe price ID
|
|
98
|
+
const priceId = getStripePriceId(tier, billingPeriod);
|
|
99
|
+
if (!priceId) {
|
|
100
|
+
return createErrorResponse(500, 'Stripe price not configured for this tier');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Count billable users for per-seat pricing
|
|
104
|
+
const usageQuery = `
|
|
105
|
+
SELECT COUNT(*) as user_count
|
|
106
|
+
FROM rapport.user_entitlements
|
|
107
|
+
WHERE client_id = $1
|
|
108
|
+
`;
|
|
109
|
+
const usageResult = await executeQuery(usageQuery, [user.client_id]);
|
|
110
|
+
const userCount = parseInt(usageResult.rows[0].user_count) || 1;
|
|
111
|
+
|
|
112
|
+
// Create Stripe Checkout session
|
|
113
|
+
const sessionParams = {
|
|
114
|
+
mode: 'subscription',
|
|
115
|
+
payment_method_types: ['card'],
|
|
116
|
+
customer_email: email,
|
|
117
|
+
line_items: [{
|
|
118
|
+
price: priceId,
|
|
119
|
+
quantity: tierConfig.perUser ? userCount : 1
|
|
120
|
+
}],
|
|
121
|
+
success_url: `${process.env.APP_URL || 'https://mindmeld.dev'}/subscription/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
122
|
+
cancel_url: `${process.env.APP_URL || 'https://mindmeld.dev'}/subscription/cancel`,
|
|
123
|
+
metadata: {
|
|
124
|
+
client_id: user.client_id,
|
|
125
|
+
user_email: email,
|
|
126
|
+
tier: tier,
|
|
127
|
+
billing_period: billingPeriod,
|
|
128
|
+
early_adopter: earlyAdopterStatus.eligible ? 'true' : 'false'
|
|
129
|
+
},
|
|
130
|
+
subscription_data: {
|
|
131
|
+
metadata: {
|
|
132
|
+
client_id: user.client_id,
|
|
133
|
+
tier: tier
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Auto-apply early adopter discount if eligible
|
|
139
|
+
if (earlyAdopterStatus.eligible && discountConfig.autoApply) {
|
|
140
|
+
sessionParams.discounts = [{
|
|
141
|
+
coupon: discountConfig.stripeCouponId
|
|
142
|
+
}];
|
|
143
|
+
console.log(`[Subscription] Auto-applying ${discountConfig.stripeCouponId} for ${email}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// If customer already exists in Stripe, use that
|
|
147
|
+
if (user.stripe_customer_id) {
|
|
148
|
+
delete sessionParams.customer_email;
|
|
149
|
+
sessionParams.customer = user.stripe_customer_id;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const session = await stripe.checkout.sessions.create(sessionParams);
|
|
153
|
+
|
|
154
|
+
// Store pending session
|
|
155
|
+
const sessionQuery = `
|
|
156
|
+
INSERT INTO rapport.stripe_checkout_sessions (
|
|
157
|
+
session_id,
|
|
158
|
+
client_id,
|
|
159
|
+
user_email,
|
|
160
|
+
tier,
|
|
161
|
+
billing_period,
|
|
162
|
+
status
|
|
163
|
+
)
|
|
164
|
+
VALUES ($1, $2, $3, $4, $5, 'pending')
|
|
165
|
+
`;
|
|
166
|
+
await executeQuery(sessionQuery, [
|
|
167
|
+
session.id,
|
|
168
|
+
user.client_id,
|
|
169
|
+
email,
|
|
170
|
+
tier,
|
|
171
|
+
billingPeriod
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
// Calculate pricing with potential discount
|
|
175
|
+
const basePrice = tierConfig.perUser
|
|
176
|
+
? tierConfig.priceMonthly * userCount
|
|
177
|
+
: tierConfig.priceMonthly;
|
|
178
|
+
|
|
179
|
+
const discountInfo = earlyAdopterStatus.eligible
|
|
180
|
+
? calculateEarlyAdopterPrice(tier, userCount, billingPeriod)
|
|
181
|
+
: null;
|
|
182
|
+
|
|
183
|
+
return createSuccessResponse(
|
|
184
|
+
{
|
|
185
|
+
Records: [{
|
|
186
|
+
checkoutUrl: session.url,
|
|
187
|
+
sessionId: session.id,
|
|
188
|
+
tier: tierConfig.displayName,
|
|
189
|
+
price: basePrice,
|
|
190
|
+
billingPeriod,
|
|
191
|
+
userCount: tierConfig.perUser ? userCount : null,
|
|
192
|
+
discount: earlyAdopterStatus.eligible ? {
|
|
193
|
+
applied: true,
|
|
194
|
+
code: discountConfig.shareableCode,
|
|
195
|
+
message: discountConfig.displayMessage,
|
|
196
|
+
originalPrice: discountInfo.originalPrice,
|
|
197
|
+
discountedPrice: discountInfo.discountedPrice,
|
|
198
|
+
savings: discountInfo.savings,
|
|
199
|
+
percent: discountInfo.discountPercent,
|
|
200
|
+
duration: discountInfo.discountDuration,
|
|
201
|
+
daysRemaining: earlyAdopterStatus.daysRemaining
|
|
202
|
+
} : null
|
|
203
|
+
}]
|
|
204
|
+
},
|
|
205
|
+
earlyAdopterStatus.eligible
|
|
206
|
+
? `Checkout session created - ${discountConfig.displayMessage}`
|
|
207
|
+
: 'Checkout session created',
|
|
208
|
+
{
|
|
209
|
+
Total_Records: 1,
|
|
210
|
+
Request_ID,
|
|
211
|
+
Timestamp: new Date().toISOString()
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error('Handler Error:', error);
|
|
217
|
+
return handleError(error);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
exports.handler = wrapHandler(createSubscription);
|