@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,1168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Tiers Configuration
|
|
3
|
+
* MindMeld/Rapport v3
|
|
4
|
+
*
|
|
5
|
+
* Pricing Model (per user/month):
|
|
6
|
+
* - Free: $0 (basic invariants only, no standards)
|
|
7
|
+
* - Team: $49 (community contribution) / $99 (private)
|
|
8
|
+
* - Professional: $149 (community contribution) / $199 (private)
|
|
9
|
+
* - Enterprise: Contact sales
|
|
10
|
+
*
|
|
11
|
+
* Community Contribution:
|
|
12
|
+
* - Lower price tiers contribute learned patterns back to community
|
|
13
|
+
* - Private tiers keep all patterns within the organization
|
|
14
|
+
*
|
|
15
|
+
* Early Adopter Discount:
|
|
16
|
+
* - 80% off first year if subscribed within 30 days of launch
|
|
17
|
+
* - Coupon code: FOUNDER80 (auto-applied at checkout)
|
|
18
|
+
*
|
|
19
|
+
* Standards Hierarchy:
|
|
20
|
+
* - Free: NO standards (basic invariants only)
|
|
21
|
+
* - Team: Community standards (curated set, no picker - all enabled)
|
|
22
|
+
* - Professional: Full library with standards picker (user preferences)
|
|
23
|
+
* - Enterprise: Custom packs + full library + preferences
|
|
24
|
+
*
|
|
25
|
+
* Standards Packages:
|
|
26
|
+
* - Community Standards: Curated patterns from MindMeld community
|
|
27
|
+
* - Compliance Standards: SOC2/HIPAA/PCI patterns
|
|
28
|
+
* - AWS Best Practices: Well-Architected patterns
|
|
29
|
+
* - Custom Packs: Enterprise-specific curated standards
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Early Adopter Discount Configuration
|
|
34
|
+
*
|
|
35
|
+
* Auto-applied at checkout for eligible users (no manual code entry needed)
|
|
36
|
+
* Code can still be shared for referral tracking
|
|
37
|
+
*/
|
|
38
|
+
const EARLY_ADOPTER_DISCOUNT = {
|
|
39
|
+
enabled: true,
|
|
40
|
+
discountPercent: 80, // 80% off
|
|
41
|
+
eligibilityWindowDays: 30, // Must subscribe within 30 days of account creation
|
|
42
|
+
durationMonths: 12, // Discount applies for first 12 months
|
|
43
|
+
stripeCouponId: process.env.STRIPE_COUPON_FOUNDER || 'FOUNDER80',
|
|
44
|
+
autoApply: true, // Automatically apply at checkout if eligible
|
|
45
|
+
shareableCode: 'FOUNDER80', // For referral/sharing (same discount)
|
|
46
|
+
description: '80% off your first year - Founding Member Special',
|
|
47
|
+
displayMessage: 'FOUNDER80 applied - Founding Member discount!'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Standards packages available by tier
|
|
52
|
+
*/
|
|
53
|
+
const STANDARDS_PACKAGES = {
|
|
54
|
+
community: {
|
|
55
|
+
name: 'Community Standards',
|
|
56
|
+
description: 'Curated patterns from the MindMeld community',
|
|
57
|
+
source: 'api:mindmeld.dev/standards/community',
|
|
58
|
+
categories: ['patterns', 'integrations', 'frameworks', 'development_principles', 'cost_optimization', 'api_design', 'testing'],
|
|
59
|
+
pickable: false // Team tier gets all, no picking
|
|
60
|
+
},
|
|
61
|
+
compliance: {
|
|
62
|
+
name: 'Compliance Standards',
|
|
63
|
+
description: 'SOC2, HIPAA, PCI-DSS compliance patterns',
|
|
64
|
+
source: 'api:mindmeld.dev/standards/compliance',
|
|
65
|
+
categories: ['soc2', 'hipaa', 'pci', 'gdpr', 'audit'],
|
|
66
|
+
pickable: true // Professional+ can pick which to enable
|
|
67
|
+
},
|
|
68
|
+
aws: {
|
|
69
|
+
name: 'AWS Best Practices',
|
|
70
|
+
description: 'Well-Architected Framework patterns',
|
|
71
|
+
source: 'api:mindmeld.dev/standards/aws',
|
|
72
|
+
categories: ['security', 'reliability', 'performance', 'cost', 'operations'],
|
|
73
|
+
pickable: true // Professional+ can pick which to enable
|
|
74
|
+
},
|
|
75
|
+
custom: {
|
|
76
|
+
name: 'Custom Packs',
|
|
77
|
+
description: 'Enterprise-specific curated standards packages',
|
|
78
|
+
source: 'api:mindmeld.dev/standards/custom',
|
|
79
|
+
categories: ['enterprise_custom'],
|
|
80
|
+
pickable: true // Enterprise can configure custom packs
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const SUBSCRIPTION_TIERS = {
|
|
85
|
+
free: {
|
|
86
|
+
name: 'Free',
|
|
87
|
+
displayName: 'Solo Developer',
|
|
88
|
+
priceMonthly: 0,
|
|
89
|
+
priceAnnual: 0,
|
|
90
|
+
maxCollaborators: 1, // Just the user
|
|
91
|
+
maxProjects: 3,
|
|
92
|
+
maxInvariants: 10,
|
|
93
|
+
standards: [], // NO standards - basic invariants only
|
|
94
|
+
canPickStandards: false,
|
|
95
|
+
features: ['local_context', 'basic_invariants'],
|
|
96
|
+
stripeProductId: null,
|
|
97
|
+
stripePriceIdMonthly: null,
|
|
98
|
+
stripePriceIdAnnual: null
|
|
99
|
+
},
|
|
100
|
+
team: {
|
|
101
|
+
name: 'Team',
|
|
102
|
+
displayName: 'Pro Solo',
|
|
103
|
+
priceMonthly: 49, // Per user - $49/user/mo (community contribution)
|
|
104
|
+
priceAnnual: 490, // ~2 months free
|
|
105
|
+
perUser: true,
|
|
106
|
+
maxCollaborators: 5,
|
|
107
|
+
maxProjects: 5,
|
|
108
|
+
maxInvariants: 50,
|
|
109
|
+
standards: ['community'], // ~25 community-curated standards
|
|
110
|
+
canPickStandards: false, // No picker - all community standards enabled
|
|
111
|
+
communityContribution: true, // Patterns feed back to community
|
|
112
|
+
features: [
|
|
113
|
+
'local_context',
|
|
114
|
+
'shared_invariants',
|
|
115
|
+
'cross_platform_sync',
|
|
116
|
+
'team_memory',
|
|
117
|
+
'community_standards',
|
|
118
|
+
'community_feedback' // Patterns feed back to community repo
|
|
119
|
+
],
|
|
120
|
+
stripeProductId: process.env.STRIPE_PRODUCT_TEAM || 'prod_Tn3KhkNBqHwfJ1',
|
|
121
|
+
stripePriceIdMonthly: process.env.STRIPE_PRICE_TEAM_MONTHLY || 'price_1SpTDmQoD4pT2xXuMG8yUqiz',
|
|
122
|
+
stripePriceIdAnnual: process.env.STRIPE_PRICE_TEAM_ANNUAL || 'price_1SpTDrQoD4pT2xXu5qU33axg'
|
|
123
|
+
},
|
|
124
|
+
team_private: {
|
|
125
|
+
name: 'Team Private',
|
|
126
|
+
displayName: 'Pro Solo (Private)',
|
|
127
|
+
priceMonthly: 99, // Per user - $99/user/mo (private standards)
|
|
128
|
+
priceAnnual: 990, // ~2 months free
|
|
129
|
+
perUser: true,
|
|
130
|
+
maxCollaborators: 5,
|
|
131
|
+
maxProjects: 5,
|
|
132
|
+
maxInvariants: 50,
|
|
133
|
+
standards: ['community'], // Access to community standards
|
|
134
|
+
canPickStandards: false,
|
|
135
|
+
communityContribution: false, // Patterns stay private
|
|
136
|
+
features: [
|
|
137
|
+
'local_context',
|
|
138
|
+
'shared_invariants',
|
|
139
|
+
'cross_platform_sync',
|
|
140
|
+
'team_memory',
|
|
141
|
+
'community_standards',
|
|
142
|
+
'private_patterns' // Patterns do NOT feed back to community
|
|
143
|
+
],
|
|
144
|
+
stripeProductId: process.env.STRIPE_PRODUCT_TEAM_PRIVATE || 'prod_Tn3KEmFEbA3y2T',
|
|
145
|
+
stripePriceIdMonthly: process.env.STRIPE_PRICE_TEAM_PRIVATE_MONTHLY || 'price_1SpTE5QoD4pT2xXu0ry2yL7j',
|
|
146
|
+
stripePriceIdAnnual: process.env.STRIPE_PRICE_TEAM_PRIVATE_ANNUAL || 'price_1SpTE6QoD4pT2xXuCiDZ37Ge'
|
|
147
|
+
},
|
|
148
|
+
professional: {
|
|
149
|
+
name: 'Professional',
|
|
150
|
+
displayName: 'Growing Team',
|
|
151
|
+
priceMonthly: 149, // Per user - $149/user/mo (community contribution)
|
|
152
|
+
priceAnnual: 1490, // ~2 months free
|
|
153
|
+
perUser: true,
|
|
154
|
+
maxCollaborators: 10,
|
|
155
|
+
maxProjects: null, // Unlimited
|
|
156
|
+
maxInvariants: 200,
|
|
157
|
+
standards: ['community', 'compliance', 'aws'], // Full 100+ standards library
|
|
158
|
+
canPickStandards: true, // Can pick which standards to enable
|
|
159
|
+
communityContribution: true, // Patterns feed back to community
|
|
160
|
+
features: [
|
|
161
|
+
'local_context',
|
|
162
|
+
'shared_invariants',
|
|
163
|
+
'cross_platform_sync',
|
|
164
|
+
'team_memory',
|
|
165
|
+
'priority_support',
|
|
166
|
+
'analytics',
|
|
167
|
+
'standards_picker',
|
|
168
|
+
'community_standards',
|
|
169
|
+
'compliance_standards',
|
|
170
|
+
'aws_standards',
|
|
171
|
+
'community_feedback'
|
|
172
|
+
],
|
|
173
|
+
stripeProductId: process.env.STRIPE_PRODUCT_PROFESSIONAL || 'prod_Tn3Kz8u744njE8',
|
|
174
|
+
stripePriceIdMonthly: process.env.STRIPE_PRICE_PROFESSIONAL_MONTHLY || 'price_1SpTELQoD4pT2xXukzY5iodd',
|
|
175
|
+
stripePriceIdAnnual: process.env.STRIPE_PRICE_PROFESSIONAL_ANNUAL || 'price_1SpTELQoD4pT2xXuwoa3zAtA'
|
|
176
|
+
},
|
|
177
|
+
professional_private: {
|
|
178
|
+
name: 'Professional Private',
|
|
179
|
+
displayName: 'Growing Team (Private)',
|
|
180
|
+
priceMonthly: 199, // Per user - $199/user/mo (private standards)
|
|
181
|
+
priceAnnual: 1990, // ~2 months free
|
|
182
|
+
perUser: true,
|
|
183
|
+
maxCollaborators: 10,
|
|
184
|
+
maxProjects: null, // Unlimited
|
|
185
|
+
maxInvariants: 200,
|
|
186
|
+
standards: ['community', 'compliance', 'aws'], // Full 100+ standards library
|
|
187
|
+
canPickStandards: true, // Can pick which standards to enable
|
|
188
|
+
communityContribution: false, // Patterns stay private
|
|
189
|
+
features: [
|
|
190
|
+
'local_context',
|
|
191
|
+
'shared_invariants',
|
|
192
|
+
'cross_platform_sync',
|
|
193
|
+
'team_memory',
|
|
194
|
+
'priority_support',
|
|
195
|
+
'analytics',
|
|
196
|
+
'standards_picker',
|
|
197
|
+
'community_standards',
|
|
198
|
+
'compliance_standards',
|
|
199
|
+
'aws_standards',
|
|
200
|
+
'private_patterns'
|
|
201
|
+
],
|
|
202
|
+
stripeProductId: process.env.STRIPE_PRODUCT_PROFESSIONAL_PRIVATE || 'prod_Tn3LxewfKvzfNc',
|
|
203
|
+
stripePriceIdMonthly: process.env.STRIPE_PRICE_PROFESSIONAL_PRIVATE_MONTHLY || 'price_1SpTEaQoD4pT2xXujb9l87Su',
|
|
204
|
+
stripePriceIdAnnual: process.env.STRIPE_PRICE_PROFESSIONAL_PRIVATE_ANNUAL || 'price_1SpTEaQoD4pT2xXuxQx8CTIv'
|
|
205
|
+
},
|
|
206
|
+
enterprise: {
|
|
207
|
+
name: 'Enterprise',
|
|
208
|
+
displayName: 'Organization',
|
|
209
|
+
priceMonthly: null, // Contact sales
|
|
210
|
+
priceAnnual: null, // Contact sales
|
|
211
|
+
contactSales: true,
|
|
212
|
+
perUser: false,
|
|
213
|
+
maxCollaborators: null, // Unlimited
|
|
214
|
+
maxProjects: null, // Unlimited
|
|
215
|
+
maxInvariants: null, // Unlimited
|
|
216
|
+
standards: ['community', 'compliance', 'aws', 'custom'], // All standards + custom packs
|
|
217
|
+
canPickStandards: true, // Can pick which standards to enable + custom packs
|
|
218
|
+
features: [
|
|
219
|
+
'local_context',
|
|
220
|
+
'shared_invariants',
|
|
221
|
+
'cross_platform_sync',
|
|
222
|
+
'team_memory',
|
|
223
|
+
'priority_support',
|
|
224
|
+
'analytics',
|
|
225
|
+
'standards_picker',
|
|
226
|
+
'custom_packs',
|
|
227
|
+
'community_standards',
|
|
228
|
+
'compliance_standards',
|
|
229
|
+
'aws_standards',
|
|
230
|
+
'sso',
|
|
231
|
+
'audit_trail',
|
|
232
|
+
'knowledge_curation',
|
|
233
|
+
'attribution_tracking',
|
|
234
|
+
'dedicated_support',
|
|
235
|
+
'custom_integrations'
|
|
236
|
+
],
|
|
237
|
+
stripeProductId: process.env.STRIPE_PRODUCT_ENTERPRISE || 'prod_Tn3Lmdaa1yhQtD',
|
|
238
|
+
stripePriceIdMonthly: null, // Contact sales
|
|
239
|
+
stripePriceIdAnnual: null // Contact sales
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get tier configuration
|
|
245
|
+
* @param {string} tierName - Tier identifier
|
|
246
|
+
* @returns {object|null} Tier configuration
|
|
247
|
+
*/
|
|
248
|
+
function getTierConfig(tierName) {
|
|
249
|
+
return SUBSCRIPTION_TIERS[tierName?.toLowerCase()] || null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if tier has a feature
|
|
254
|
+
* @param {string} tierName - Tier identifier
|
|
255
|
+
* @param {string} feature - Feature name
|
|
256
|
+
* @returns {boolean}
|
|
257
|
+
*/
|
|
258
|
+
function hasFeature(tierName, feature) {
|
|
259
|
+
const tier = getTierConfig(tierName);
|
|
260
|
+
return tier ? tier.features.includes(feature) : false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check subscription limits
|
|
265
|
+
* @param {object} client - Client record with subscription info
|
|
266
|
+
* @param {string} action - Action being performed (add_collaborator, create_project, etc.)
|
|
267
|
+
* @param {object} counts - Current counts { collaborators, projects, invariants }
|
|
268
|
+
* @returns {object} { allowed: boolean, message: string, limit: number }
|
|
269
|
+
*/
|
|
270
|
+
function checkSubscriptionLimits(client, action, counts) {
|
|
271
|
+
const tier = getTierConfig(client.subscription_tier || 'free');
|
|
272
|
+
if (!tier) {
|
|
273
|
+
return { allowed: false, message: 'Invalid subscription tier', limit: 0 };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
switch (action) {
|
|
277
|
+
case 'add_collaborator':
|
|
278
|
+
const collabLimit = tier.maxCollaborators;
|
|
279
|
+
if (counts.collaborators >= collabLimit) {
|
|
280
|
+
return {
|
|
281
|
+
allowed: false,
|
|
282
|
+
message: `${tier.displayName} tier allows up to ${collabLimit} collaborators. Upgrade to add more.`,
|
|
283
|
+
limit: collabLimit
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
|
|
288
|
+
case 'create_project':
|
|
289
|
+
const projectLimit = tier.maxProjects;
|
|
290
|
+
if (counts.projects >= projectLimit) {
|
|
291
|
+
return {
|
|
292
|
+
allowed: false,
|
|
293
|
+
message: `${tier.displayName} tier allows up to ${projectLimit} projects. Upgrade to create more.`,
|
|
294
|
+
limit: projectLimit
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
break;
|
|
298
|
+
|
|
299
|
+
case 'add_invariant':
|
|
300
|
+
const invariantLimit = tier.maxInvariants;
|
|
301
|
+
if (invariantLimit !== null && counts.invariants >= invariantLimit) {
|
|
302
|
+
return {
|
|
303
|
+
allowed: false,
|
|
304
|
+
message: `${tier.displayName} tier allows up to ${invariantLimit} invariants. Upgrade for more.`,
|
|
305
|
+
limit: invariantLimit
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { allowed: true, message: 'OK', limit: null };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get Stripe price ID for tier and billing interval
|
|
316
|
+
* @param {string} tierName - Tier identifier
|
|
317
|
+
* @param {string} billingInterval - 'monthly' or 'annual'
|
|
318
|
+
* @returns {string|null} Stripe price ID
|
|
319
|
+
*/
|
|
320
|
+
function getStripePriceId(tierName, billingInterval = 'monthly') {
|
|
321
|
+
const tier = getTierConfig(tierName);
|
|
322
|
+
if (!tier) return null;
|
|
323
|
+
|
|
324
|
+
return billingInterval === 'annual'
|
|
325
|
+
? tier.stripePriceIdAnnual
|
|
326
|
+
: tier.stripePriceIdMonthly;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get Stripe product ID for tier
|
|
331
|
+
* @param {string} tierName - Tier identifier
|
|
332
|
+
* @returns {string|null} Stripe product ID
|
|
333
|
+
*/
|
|
334
|
+
function getStripeProductId(tierName) {
|
|
335
|
+
const tier = getTierConfig(tierName);
|
|
336
|
+
return tier ? tier.stripeProductId : null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get recommended upgrade for a limit reason
|
|
341
|
+
* @param {string} currentTier - Current tier name
|
|
342
|
+
* @param {string} reason - Reason for upgrade (collaborators, projects, features)
|
|
343
|
+
* @returns {object} { tier: string, name: string, price: number }
|
|
344
|
+
*/
|
|
345
|
+
function getRecommendedUpgrade(currentTier, reason) {
|
|
346
|
+
const tierOrder = ['free', 'team', 'professional', 'enterprise'];
|
|
347
|
+
const currentIndex = tierOrder.indexOf(currentTier?.toLowerCase() || 'free');
|
|
348
|
+
|
|
349
|
+
// Recommend next tier up
|
|
350
|
+
const nextTier = tierOrder[Math.min(currentIndex + 1, tierOrder.length - 1)];
|
|
351
|
+
const tier = getTierConfig(nextTier);
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
tier: nextTier,
|
|
355
|
+
name: tier.displayName,
|
|
356
|
+
price: tier.priceMonthly,
|
|
357
|
+
perUser: tier.perUser
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Calculate monthly cost for tier based on user count
|
|
363
|
+
* @param {string} tierName - Tier identifier
|
|
364
|
+
* @param {number} userCount - Number of users
|
|
365
|
+
* @param {string} billingInterval - 'monthly' or 'annual'
|
|
366
|
+
* @returns {number} Total monthly cost
|
|
367
|
+
*/
|
|
368
|
+
function calculateTierCost(tierName, userCount, billingInterval = 'monthly') {
|
|
369
|
+
const tier = getTierConfig(tierName);
|
|
370
|
+
if (!tier) return 0;
|
|
371
|
+
|
|
372
|
+
const price = billingInterval === 'annual'
|
|
373
|
+
? tier.priceAnnual / 12
|
|
374
|
+
: tier.priceMonthly;
|
|
375
|
+
|
|
376
|
+
return tier.perUser ? price * userCount : price;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get all tiers for display
|
|
381
|
+
* @returns {array} Array of tier configs with tier key
|
|
382
|
+
*/
|
|
383
|
+
function getAllTiers() {
|
|
384
|
+
return Object.entries(SUBSCRIPTION_TIERS).map(([key, config]) => ({
|
|
385
|
+
id: key,
|
|
386
|
+
...config
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get standards packages available for a tier
|
|
392
|
+
* @param {string} tierName - Tier identifier
|
|
393
|
+
* @returns {array} Array of standards package configs
|
|
394
|
+
*/
|
|
395
|
+
function getTierStandards(tierName) {
|
|
396
|
+
const tier = getTierConfig(tierName);
|
|
397
|
+
if (!tier) return []; // Default to no standards (free tier behavior)
|
|
398
|
+
|
|
399
|
+
return tier.standards.map(std => STANDARDS_PACKAGES[std]).filter(Boolean);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Check if tier has access to a standards package
|
|
404
|
+
* @param {string} tierName - Tier identifier
|
|
405
|
+
* @param {string} standardsPackage - Package name (community, compliance, aws, custom)
|
|
406
|
+
* @returns {boolean}
|
|
407
|
+
*/
|
|
408
|
+
function hasStandardsAccess(tierName, standardsPackage) {
|
|
409
|
+
const tier = getTierConfig(tierName);
|
|
410
|
+
return tier ? tier.standards.includes(standardsPackage) : false;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Check if tier can pick/select specific standards
|
|
415
|
+
* Only professional and enterprise tiers can pick standards.
|
|
416
|
+
* Team tier gets curated set with no picking.
|
|
417
|
+
* Free tier gets no standards.
|
|
418
|
+
* @param {string} tierName - Tier identifier
|
|
419
|
+
* @returns {boolean}
|
|
420
|
+
*/
|
|
421
|
+
function canPickStandards(tierName) {
|
|
422
|
+
const tier = getTierConfig(tierName);
|
|
423
|
+
return tier ? tier.canPickStandards === true : false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Get all standards packages info
|
|
428
|
+
* @returns {object} STANDARDS_PACKAGES object
|
|
429
|
+
*/
|
|
430
|
+
function getAllStandardsPackages() {
|
|
431
|
+
return STANDARDS_PACKAGES;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get enabled standards for a user based on their tier and preferences
|
|
436
|
+
* @param {string} userId - User ID (email)
|
|
437
|
+
* @param {string} companyId - Company ID
|
|
438
|
+
* @param {string} tier - Subscription tier
|
|
439
|
+
* @param {object} options - Optional parameters
|
|
440
|
+
* @param {function} options.queryUserPreferences - Async function to query user standard preferences
|
|
441
|
+
* @param {function} options.queryCustomPack - Async function to query enterprise custom pack
|
|
442
|
+
* @returns {Promise<array>} Array of enabled standard IDs
|
|
443
|
+
*/
|
|
444
|
+
async function getEnabledStandardsForUser(userId, companyId, tier, options = {}) {
|
|
445
|
+
const tierConfig = getTierConfig(tier);
|
|
446
|
+
|
|
447
|
+
// Free tier: NO standards
|
|
448
|
+
if (!tierConfig || tier === 'free') {
|
|
449
|
+
return [];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Team tier: All community standards enabled (no picker)
|
|
453
|
+
if (tier === 'team') {
|
|
454
|
+
// Return all community standards - no preferences, all enabled
|
|
455
|
+
const communityPackage = STANDARDS_PACKAGES.community;
|
|
456
|
+
if (options.queryAllCommunityStandards) {
|
|
457
|
+
// Query all community standards from the database/API
|
|
458
|
+
return await options.queryAllCommunityStandards();
|
|
459
|
+
}
|
|
460
|
+
// Fallback: return package info indicating all community standards
|
|
461
|
+
return [{
|
|
462
|
+
package: 'community',
|
|
463
|
+
allEnabled: true,
|
|
464
|
+
categories: communityPackage.categories
|
|
465
|
+
}];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Professional tier: Full library with picker (user preferences)
|
|
469
|
+
if (tier === 'professional') {
|
|
470
|
+
if (options.queryUserPreferences) {
|
|
471
|
+
// Query user's enabled standards preferences
|
|
472
|
+
const preferences = await options.queryUserPreferences(userId, companyId);
|
|
473
|
+
return preferences.enabledStandards || [];
|
|
474
|
+
}
|
|
475
|
+
// Fallback: return available packages with picker info
|
|
476
|
+
return tierConfig.standards.map(pkg => ({
|
|
477
|
+
package: pkg,
|
|
478
|
+
pickable: true,
|
|
479
|
+
enabled: [] // User needs to configure preferences
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Enterprise tier: Custom packs + full library + preferences
|
|
484
|
+
if (tier === 'enterprise') {
|
|
485
|
+
const enabledStandards = [];
|
|
486
|
+
|
|
487
|
+
// Get custom pack if configured
|
|
488
|
+
if (options.queryCustomPack) {
|
|
489
|
+
const customPack = await options.queryCustomPack(companyId);
|
|
490
|
+
if (customPack && customPack.standards) {
|
|
491
|
+
enabledStandards.push({
|
|
492
|
+
package: 'custom',
|
|
493
|
+
standards: customPack.standards,
|
|
494
|
+
packName: customPack.name
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Get user preferences for standard library
|
|
500
|
+
if (options.queryUserPreferences) {
|
|
501
|
+
const preferences = await options.queryUserPreferences(userId, companyId);
|
|
502
|
+
if (preferences.enabledStandards) {
|
|
503
|
+
enabledStandards.push({
|
|
504
|
+
package: 'library',
|
|
505
|
+
standards: preferences.enabledStandards
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Fallback: return available packages with picker info
|
|
511
|
+
if (enabledStandards.length === 0) {
|
|
512
|
+
return tierConfig.standards.map(pkg => ({
|
|
513
|
+
package: pkg,
|
|
514
|
+
pickable: true,
|
|
515
|
+
enabled: []
|
|
516
|
+
}));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return enabledStandards;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Unknown tier: return empty
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Normalize tier name (handles legacy 'starter' -> 'team' mapping)
|
|
528
|
+
* @param {string} tierName - Tier identifier (may be legacy)
|
|
529
|
+
* @returns {string} Normalized tier name
|
|
530
|
+
*/
|
|
531
|
+
function normalizeTierName(tierName) {
|
|
532
|
+
const normalized = tierName?.toLowerCase();
|
|
533
|
+
// Handle legacy tier names
|
|
534
|
+
if (normalized === 'starter') {
|
|
535
|
+
return 'team';
|
|
536
|
+
}
|
|
537
|
+
return normalized || 'free';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Check if user is eligible for early adopter discount
|
|
542
|
+
* @param {Date} accountCreatedAt - When user account was created
|
|
543
|
+
* @returns {object} { eligible: boolean, daysRemaining: number, discount: object }
|
|
544
|
+
*/
|
|
545
|
+
function checkEarlyAdopterEligibility(accountCreatedAt) {
|
|
546
|
+
if (!EARLY_ADOPTER_DISCOUNT.enabled) {
|
|
547
|
+
return { eligible: false, daysRemaining: 0, discount: null };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const createdDate = new Date(accountCreatedAt);
|
|
551
|
+
const now = new Date();
|
|
552
|
+
const daysSinceCreation = Math.floor((now - createdDate) / (1000 * 60 * 60 * 24));
|
|
553
|
+
const daysRemaining = Math.max(0, EARLY_ADOPTER_DISCOUNT.eligibilityWindowDays - daysSinceCreation);
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
eligible: daysSinceCreation <= EARLY_ADOPTER_DISCOUNT.eligibilityWindowDays,
|
|
557
|
+
daysRemaining,
|
|
558
|
+
daysSinceCreation,
|
|
559
|
+
discount: EARLY_ADOPTER_DISCOUNT
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Get early adopter discount config
|
|
565
|
+
* @returns {object} EARLY_ADOPTER_DISCOUNT config
|
|
566
|
+
*/
|
|
567
|
+
function getEarlyAdopterDiscount() {
|
|
568
|
+
return EARLY_ADOPTER_DISCOUNT;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Calculate discounted price for early adopter
|
|
573
|
+
* @param {string} tierName - Tier identifier
|
|
574
|
+
* @param {number} userCount - Number of users
|
|
575
|
+
* @param {string} billingInterval - 'monthly' or 'annual'
|
|
576
|
+
* @returns {object} { originalPrice, discountedPrice, savings, discountPercent }
|
|
577
|
+
*/
|
|
578
|
+
function calculateEarlyAdopterPrice(tierName, userCount, billingInterval = 'monthly') {
|
|
579
|
+
const originalPrice = calculateTierCost(tierName, userCount, billingInterval);
|
|
580
|
+
const discountedPrice = originalPrice * (1 - EARLY_ADOPTER_DISCOUNT.discountPercent / 100);
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
originalPrice,
|
|
584
|
+
discountedPrice: Math.round(discountedPrice * 100) / 100,
|
|
585
|
+
savings: Math.round((originalPrice - discountedPrice) * 100) / 100,
|
|
586
|
+
discountPercent: EARLY_ADOPTER_DISCOUNT.discountPercent,
|
|
587
|
+
discountDuration: `${EARLY_ADOPTER_DISCOUNT.durationMonths} months`
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Check if tier requires community contribution (lower price tier)
|
|
593
|
+
* @param {string} tierName - Tier identifier
|
|
594
|
+
* @returns {boolean}
|
|
595
|
+
*/
|
|
596
|
+
function requiresCommunityContribution(tierName) {
|
|
597
|
+
const tier = getTierConfig(tierName);
|
|
598
|
+
return tier ? tier.communityContribution === true : false;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Get the private variant of a tier
|
|
603
|
+
* @param {string} tierName - Base tier name (e.g., 'team')
|
|
604
|
+
* @returns {string|null} Private tier name or null
|
|
605
|
+
*/
|
|
606
|
+
function getPrivateTierVariant(tierName) {
|
|
607
|
+
const privateTier = `${tierName}_private`;
|
|
608
|
+
return getTierConfig(privateTier) ? privateTier : null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Get base tier from private variant
|
|
613
|
+
* @param {string} tierName - Tier name (may be private variant)
|
|
614
|
+
* @returns {string} Base tier name
|
|
615
|
+
*/
|
|
616
|
+
function getBaseTier(tierName) {
|
|
617
|
+
return tierName?.replace('_private', '') || tierName;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ==============================================
|
|
621
|
+
// BILLING TYPE FUNCTIONS
|
|
622
|
+
// ==============================================
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Valid billing types
|
|
626
|
+
* - stripe: Standard credit card billing via Stripe
|
|
627
|
+
* - invoice: Enterprise invoice billing (billable_users tracked)
|
|
628
|
+
* - internal: Internal/free clients (no billing)
|
|
629
|
+
*/
|
|
630
|
+
const BILLING_TYPES = {
|
|
631
|
+
STRIPE: 'stripe',
|
|
632
|
+
INVOICE: 'invoice',
|
|
633
|
+
INTERNAL: 'internal'
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Check if client uses Stripe billing
|
|
638
|
+
* @param {object} client - Client record
|
|
639
|
+
* @returns {boolean}
|
|
640
|
+
*/
|
|
641
|
+
function usesStripeBilling(client) {
|
|
642
|
+
return (client.billing_type || 'stripe') === BILLING_TYPES.STRIPE;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Check if client is enterprise invoice billing
|
|
647
|
+
* @param {object} client - Client record
|
|
648
|
+
* @returns {boolean}
|
|
649
|
+
*/
|
|
650
|
+
function usesInvoiceBilling(client) {
|
|
651
|
+
return client.billing_type === BILLING_TYPES.INVOICE;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Check if client is internal (free)
|
|
656
|
+
* @param {object} client - Client record
|
|
657
|
+
* @returns {boolean}
|
|
658
|
+
*/
|
|
659
|
+
function isInternalClient(client) {
|
|
660
|
+
return client.billing_type === BILLING_TYPES.INTERNAL;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Check collaborator limits based on billing type
|
|
665
|
+
* @param {object} client - Client record with billing_type and subscription_tier
|
|
666
|
+
* @param {number} currentCollaboratorCount - Current number of collaborators
|
|
667
|
+
* @returns {object} { allowed: boolean, message: string, billingAction: string|null }
|
|
668
|
+
*/
|
|
669
|
+
function checkCollaboratorBillingLimits(client, currentCollaboratorCount) {
|
|
670
|
+
const billingType = client.billing_type || 'stripe';
|
|
671
|
+
|
|
672
|
+
switch (billingType) {
|
|
673
|
+
case BILLING_TYPES.INTERNAL:
|
|
674
|
+
// Internal clients have no limits
|
|
675
|
+
return {
|
|
676
|
+
allowed: true,
|
|
677
|
+
message: 'OK',
|
|
678
|
+
billingAction: null,
|
|
679
|
+
unlimited: true
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
case BILLING_TYPES.INVOICE:
|
|
683
|
+
// Enterprise invoice clients - no hard limits, just increment billable users
|
|
684
|
+
return {
|
|
685
|
+
allowed: true,
|
|
686
|
+
message: 'OK',
|
|
687
|
+
billingAction: 'increment_billable_users',
|
|
688
|
+
unlimited: true
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
case BILLING_TYPES.STRIPE:
|
|
692
|
+
default:
|
|
693
|
+
// Standard Stripe billing - use subscription tier limits
|
|
694
|
+
const limitCheck = checkSubscriptionLimits(
|
|
695
|
+
client,
|
|
696
|
+
'add_collaborator',
|
|
697
|
+
{ collaborators: currentCollaboratorCount }
|
|
698
|
+
);
|
|
699
|
+
return {
|
|
700
|
+
...limitCheck,
|
|
701
|
+
billingAction: limitCheck.allowed ? null : 'upgrade_required'
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Get billing type display name
|
|
708
|
+
* @param {string} billingType - Billing type code
|
|
709
|
+
* @returns {string} Human-readable name
|
|
710
|
+
*/
|
|
711
|
+
function getBillingTypeDisplayName(billingType) {
|
|
712
|
+
switch (billingType) {
|
|
713
|
+
case BILLING_TYPES.INVOICE:
|
|
714
|
+
return 'Enterprise Invoice';
|
|
715
|
+
case BILLING_TYPES.INTERNAL:
|
|
716
|
+
return 'Internal (Free)';
|
|
717
|
+
case BILLING_TYPES.STRIPE:
|
|
718
|
+
default:
|
|
719
|
+
return 'Credit Card (Stripe)';
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ==============================================
|
|
724
|
+
// ENTERPRISE PACKAGES & ADD-ONS
|
|
725
|
+
// ==============================================
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Enterprise package configurations
|
|
729
|
+
* Sold in seat packs of 25 minimum
|
|
730
|
+
*/
|
|
731
|
+
const ENTERPRISE_PACKAGES = {
|
|
732
|
+
base: {
|
|
733
|
+
name: 'Pro+ Base',
|
|
734
|
+
displayName: 'Enterprise Pro+ Base',
|
|
735
|
+
pricePerSeat: 179,
|
|
736
|
+
minSeats: 25,
|
|
737
|
+
seatIncrement: 25,
|
|
738
|
+
features: [
|
|
739
|
+
'sso',
|
|
740
|
+
'audit_trail',
|
|
741
|
+
'dedicated_support',
|
|
742
|
+
'99_9_sla',
|
|
743
|
+
'admin_dashboard',
|
|
744
|
+
'all_professional_features'
|
|
745
|
+
],
|
|
746
|
+
stripeProductId: process.env.STRIPE_PRODUCT_ENTERPRISE || 'prod_Tn3Lmdaa1yhQtD',
|
|
747
|
+
stripePriceIdMonthly25: process.env.STRIPE_PRICE_ENT_BASE_MONTHLY || 'price_1SrXDCQoD4pT2xXuS8RPpS9X',
|
|
748
|
+
stripePriceIdAnnual25: process.env.STRIPE_PRICE_ENT_BASE_ANNUAL || 'price_1SrXDCQoD4pT2xXukOTjeUpv'
|
|
749
|
+
},
|
|
750
|
+
full: {
|
|
751
|
+
name: 'Full Platform',
|
|
752
|
+
displayName: 'Enterprise Full Platform',
|
|
753
|
+
pricePerSeat: 219,
|
|
754
|
+
minSeats: 25,
|
|
755
|
+
seatIncrement: 25,
|
|
756
|
+
includes: ['base', 'engineering_intelligence', 'knowledge_continuity'],
|
|
757
|
+
features: [
|
|
758
|
+
'sso',
|
|
759
|
+
'audit_trail',
|
|
760
|
+
'dedicated_support',
|
|
761
|
+
'99_9_sla',
|
|
762
|
+
'admin_dashboard',
|
|
763
|
+
'all_professional_features',
|
|
764
|
+
// Engineering Intelligence features
|
|
765
|
+
'activity_dashboards',
|
|
766
|
+
'attention_alerts',
|
|
767
|
+
'session_commit_correlation',
|
|
768
|
+
'roi_reporting',
|
|
769
|
+
// Knowledge Continuity features
|
|
770
|
+
'knowledge_capture',
|
|
771
|
+
'turnover_protection',
|
|
772
|
+
'onboarding_acceleration',
|
|
773
|
+
'knowledge_transfer_docs'
|
|
774
|
+
],
|
|
775
|
+
stripeProductId: process.env.STRIPE_PRODUCT_ENTERPRISE || 'prod_Tn3Lmdaa1yhQtD',
|
|
776
|
+
stripePriceIdMonthly25: process.env.STRIPE_PRICE_ENT_FULL_MONTHLY || 'price_1SrXDCQoD4pT2xXuBKUuXRsT',
|
|
777
|
+
stripePriceIdAnnual25: process.env.STRIPE_PRICE_ENT_FULL_ANNUAL || 'price_1SrXDDQoD4pT2xXuIeWZ0VDa'
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Enterprise add-on modules
|
|
783
|
+
* Can be added to 'base' package (already included in 'full')
|
|
784
|
+
*/
|
|
785
|
+
const ENTERPRISE_ADDONS = {
|
|
786
|
+
engineering_intelligence: {
|
|
787
|
+
id: 'engineering_intelligence',
|
|
788
|
+
name: 'Engineering Intelligence',
|
|
789
|
+
displayName: 'Engineering Intelligence Add-on',
|
|
790
|
+
description: 'Activity dashboards, attention alerts, session-to-commit correlation, ROI reporting',
|
|
791
|
+
pricePerSeat: 29,
|
|
792
|
+
minSeats: 25,
|
|
793
|
+
requires: ['enterprise_base'],
|
|
794
|
+
features: [
|
|
795
|
+
'activity_dashboards',
|
|
796
|
+
'attention_alerts',
|
|
797
|
+
'session_commit_correlation',
|
|
798
|
+
'roi_reporting'
|
|
799
|
+
],
|
|
800
|
+
stripeProductId: process.env.STRIPE_PRODUCT_ADDON_ENG_INTEL || 'prod_TpBat7IZySP7MB',
|
|
801
|
+
stripePriceIdMonthly25: process.env.STRIPE_PRICE_ADDON_ENG_INTEL_MONTHLY || 'price_1SrXDXQoD4pT2xXuk4bDjKm8',
|
|
802
|
+
stripePriceIdAnnual25: process.env.STRIPE_PRICE_ADDON_ENG_INTEL_ANNUAL || 'price_1SrXDYQoD4pT2xXuD3EdKBs1'
|
|
803
|
+
},
|
|
804
|
+
knowledge_continuity: {
|
|
805
|
+
id: 'knowledge_continuity',
|
|
806
|
+
name: 'Knowledge Continuity',
|
|
807
|
+
displayName: 'Knowledge Continuity Add-on',
|
|
808
|
+
description: 'Key employee capture, turnover protection, onboarding acceleration, knowledge transfer docs',
|
|
809
|
+
pricePerSeat: 49,
|
|
810
|
+
minSeats: 25,
|
|
811
|
+
requires: ['enterprise_base'],
|
|
812
|
+
features: [
|
|
813
|
+
'knowledge_capture',
|
|
814
|
+
'turnover_protection',
|
|
815
|
+
'onboarding_acceleration',
|
|
816
|
+
'knowledge_transfer_docs'
|
|
817
|
+
],
|
|
818
|
+
stripeProductId: process.env.STRIPE_PRODUCT_ADDON_KNOWLEDGE || 'prod_TpBaOkMV4fsctv',
|
|
819
|
+
stripePriceIdMonthly25: process.env.STRIPE_PRICE_ADDON_KNOWLEDGE_MONTHLY || 'price_1SrXDYQoD4pT2xXuFYZy90r0',
|
|
820
|
+
stripePriceIdAnnual25: process.env.STRIPE_PRICE_ADDON_KNOWLEDGE_ANNUAL || 'price_1SrXDYQoD4pT2xXuxULSElxH'
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Volume discount tiers for enterprise
|
|
826
|
+
* Applied based on total seat count
|
|
827
|
+
*/
|
|
828
|
+
const VOLUME_DISCOUNTS = [
|
|
829
|
+
{ minSeats: 150, discount: 0.15, label: '15% off' },
|
|
830
|
+
{ minSeats: 100, discount: 0.12, label: '12% off' },
|
|
831
|
+
{ minSeats: 75, discount: 0.08, label: '8% off' },
|
|
832
|
+
{ minSeats: 50, discount: 0.05, label: '5% off' },
|
|
833
|
+
{ minSeats: 25, discount: 0, label: 'Standard pricing' }
|
|
834
|
+
];
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Get volume discount for seat count
|
|
838
|
+
* @param {number} seatCount - Number of seats
|
|
839
|
+
* @returns {object} { discount: number, label: string }
|
|
840
|
+
*/
|
|
841
|
+
function getVolumeDiscount(seatCount) {
|
|
842
|
+
for (const tier of VOLUME_DISCOUNTS) {
|
|
843
|
+
if (seatCount >= tier.minSeats) {
|
|
844
|
+
return { discount: tier.discount, label: tier.label };
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return { discount: 0, label: 'Standard pricing' };
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Validate enterprise seat count
|
|
852
|
+
* Must be >= 25 and multiple of 25
|
|
853
|
+
* @param {number} seatCount - Requested seat count
|
|
854
|
+
* @returns {object} { valid: boolean, message: string, correctedCount: number }
|
|
855
|
+
*/
|
|
856
|
+
function validateSeatCount(seatCount) {
|
|
857
|
+
const minSeats = 25;
|
|
858
|
+
const increment = 25;
|
|
859
|
+
|
|
860
|
+
if (!seatCount || seatCount < minSeats) {
|
|
861
|
+
return {
|
|
862
|
+
valid: false,
|
|
863
|
+
message: `Enterprise requires minimum ${minSeats} seats`,
|
|
864
|
+
correctedCount: minSeats
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (seatCount % increment !== 0) {
|
|
869
|
+
const corrected = Math.ceil(seatCount / increment) * increment;
|
|
870
|
+
return {
|
|
871
|
+
valid: false,
|
|
872
|
+
message: `Seats must be in increments of ${increment}. Rounded up to ${corrected}.`,
|
|
873
|
+
correctedCount: corrected
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return {
|
|
878
|
+
valid: true,
|
|
879
|
+
message: 'OK',
|
|
880
|
+
correctedCount: seatCount
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Calculate enterprise subscription cost
|
|
886
|
+
* @param {string} packageType - 'base' or 'full'
|
|
887
|
+
* @param {number} seatCount - Number of seats (must be multiple of 25)
|
|
888
|
+
* @param {string[]} addons - Array of addon IDs (only for 'base' package)
|
|
889
|
+
* @param {string} billingInterval - 'monthly' or 'annual'
|
|
890
|
+
* @returns {object} { subtotal, discount, discountAmount, total, breakdown }
|
|
891
|
+
*/
|
|
892
|
+
function calculateEnterpriseCost(packageType, seatCount, addons = [], billingInterval = 'monthly') {
|
|
893
|
+
const pkg = ENTERPRISE_PACKAGES[packageType];
|
|
894
|
+
if (!pkg) {
|
|
895
|
+
throw new Error(`Invalid enterprise package: ${packageType}`);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const volumeDiscount = getVolumeDiscount(seatCount);
|
|
899
|
+
const breakdown = [];
|
|
900
|
+
|
|
901
|
+
// Base package cost
|
|
902
|
+
let subtotal = pkg.pricePerSeat * seatCount;
|
|
903
|
+
breakdown.push({
|
|
904
|
+
item: pkg.name,
|
|
905
|
+
pricePerSeat: pkg.pricePerSeat,
|
|
906
|
+
seats: seatCount,
|
|
907
|
+
amount: pkg.pricePerSeat * seatCount
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// Add-on costs (only for 'base' package, 'full' already includes everything)
|
|
911
|
+
if (packageType === 'base' && addons.length > 0) {
|
|
912
|
+
for (const addonId of addons) {
|
|
913
|
+
const addon = ENTERPRISE_ADDONS[addonId];
|
|
914
|
+
if (addon) {
|
|
915
|
+
const addonCost = addon.pricePerSeat * seatCount;
|
|
916
|
+
subtotal += addonCost;
|
|
917
|
+
breakdown.push({
|
|
918
|
+
item: addon.name,
|
|
919
|
+
pricePerSeat: addon.pricePerSeat,
|
|
920
|
+
seats: seatCount,
|
|
921
|
+
amount: addonCost
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Apply volume discount
|
|
928
|
+
const discountAmount = subtotal * volumeDiscount.discount;
|
|
929
|
+
let total = subtotal - discountAmount;
|
|
930
|
+
|
|
931
|
+
// Annual pricing (2 months free = 10 months for price of 12)
|
|
932
|
+
if (billingInterval === 'annual') {
|
|
933
|
+
total = total * 10; // 10 months worth
|
|
934
|
+
return {
|
|
935
|
+
subtotal: subtotal * 12,
|
|
936
|
+
discount: volumeDiscount.discount,
|
|
937
|
+
discountLabel: volumeDiscount.label,
|
|
938
|
+
discountAmount: discountAmount * 12,
|
|
939
|
+
annualDiscount: subtotal * 2, // 2 months free
|
|
940
|
+
total,
|
|
941
|
+
monthlyEquivalent: total / 12,
|
|
942
|
+
breakdown,
|
|
943
|
+
billingInterval: 'annual'
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return {
|
|
948
|
+
subtotal,
|
|
949
|
+
discount: volumeDiscount.discount,
|
|
950
|
+
discountLabel: volumeDiscount.label,
|
|
951
|
+
discountAmount,
|
|
952
|
+
total,
|
|
953
|
+
breakdown,
|
|
954
|
+
billingInterval: 'monthly'
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Get enterprise package configuration
|
|
960
|
+
* @param {string} packageType - 'base' or 'full'
|
|
961
|
+
* @returns {object|null} Package configuration
|
|
962
|
+
*/
|
|
963
|
+
function getEnterprisePackage(packageType) {
|
|
964
|
+
return ENTERPRISE_PACKAGES[packageType] || null;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Get enterprise add-on configuration
|
|
969
|
+
* @param {string} addonId - Add-on identifier
|
|
970
|
+
* @returns {object|null} Add-on configuration
|
|
971
|
+
*/
|
|
972
|
+
function getEnterpriseAddon(addonId) {
|
|
973
|
+
return ENTERPRISE_ADDONS[addonId] || null;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Get all enterprise add-ons
|
|
978
|
+
* @returns {object} ENTERPRISE_ADDONS object
|
|
979
|
+
*/
|
|
980
|
+
function getAllEnterpriseAddons() {
|
|
981
|
+
return ENTERPRISE_ADDONS;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Check if client has enterprise feature access
|
|
986
|
+
* @param {object} client - Client record with enterprise_package and subscribed_addons
|
|
987
|
+
* @param {string} feature - Feature to check
|
|
988
|
+
* @returns {boolean}
|
|
989
|
+
*/
|
|
990
|
+
function hasEnterpriseFeature(client, feature) {
|
|
991
|
+
if (client.subscription_tier !== 'enterprise') {
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const pkg = ENTERPRISE_PACKAGES[client.enterprise_package];
|
|
996
|
+
if (!pkg) {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Check if feature is in package
|
|
1001
|
+
if (pkg.features.includes(feature)) {
|
|
1002
|
+
return true;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Check add-ons
|
|
1006
|
+
const addons = client.subscribed_addons || [];
|
|
1007
|
+
for (const addonId of addons) {
|
|
1008
|
+
const addon = ENTERPRISE_ADDONS[addonId];
|
|
1009
|
+
if (addon && addon.features.includes(feature)) {
|
|
1010
|
+
return true;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Get all features available to enterprise client
|
|
1019
|
+
* @param {object} client - Client record
|
|
1020
|
+
* @returns {string[]} Array of available feature IDs
|
|
1021
|
+
*/
|
|
1022
|
+
function getEnterpriseFeatures(client) {
|
|
1023
|
+
if (client.subscription_tier !== 'enterprise') {
|
|
1024
|
+
return [];
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const pkg = ENTERPRISE_PACKAGES[client.enterprise_package];
|
|
1028
|
+
if (!pkg) {
|
|
1029
|
+
return [];
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const features = new Set(pkg.features);
|
|
1033
|
+
|
|
1034
|
+
// Add features from subscribed add-ons
|
|
1035
|
+
const addons = client.subscribed_addons || [];
|
|
1036
|
+
for (const addonId of addons) {
|
|
1037
|
+
const addon = ENTERPRISE_ADDONS[addonId];
|
|
1038
|
+
if (addon) {
|
|
1039
|
+
addon.features.forEach(f => features.add(f));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
return Array.from(features);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Check enterprise seat limits
|
|
1048
|
+
* @param {object} client - Client record with seat_count
|
|
1049
|
+
* @param {number} currentUserCount - Current number of users
|
|
1050
|
+
* @returns {object} { allowed, message, seatsAvailable, billingAction }
|
|
1051
|
+
*/
|
|
1052
|
+
function checkEnterpriseSeatLimits(client, currentUserCount) {
|
|
1053
|
+
if (client.subscription_tier !== 'enterprise') {
|
|
1054
|
+
return { allowed: true, message: 'Not enterprise tier' };
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
const seatCount = client.seat_count || 25;
|
|
1058
|
+
const seatsAvailable = seatCount - currentUserCount;
|
|
1059
|
+
|
|
1060
|
+
if (currentUserCount >= seatCount) {
|
|
1061
|
+
return {
|
|
1062
|
+
allowed: false,
|
|
1063
|
+
message: `All ${seatCount} enterprise seats are in use. Purchase additional seats to add more users.`,
|
|
1064
|
+
seatsAvailable: 0,
|
|
1065
|
+
seatsUsed: currentUserCount,
|
|
1066
|
+
seatCount,
|
|
1067
|
+
billingAction: 'purchase_seats'
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
return {
|
|
1072
|
+
allowed: true,
|
|
1073
|
+
message: 'OK',
|
|
1074
|
+
seatsAvailable,
|
|
1075
|
+
seatsUsed: currentUserCount,
|
|
1076
|
+
seatCount,
|
|
1077
|
+
billingAction: null
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Get Stripe price ID for enterprise package
|
|
1083
|
+
* @param {string} packageType - 'base' or 'full'
|
|
1084
|
+
* @param {string} billingInterval - 'monthly' or 'annual'
|
|
1085
|
+
* @returns {string|null} Stripe price ID
|
|
1086
|
+
*/
|
|
1087
|
+
function getEnterprisePriceId(packageType, billingInterval = 'monthly') {
|
|
1088
|
+
const pkg = ENTERPRISE_PACKAGES[packageType];
|
|
1089
|
+
if (!pkg) return null;
|
|
1090
|
+
|
|
1091
|
+
return billingInterval === 'annual'
|
|
1092
|
+
? pkg.stripePriceIdAnnual25
|
|
1093
|
+
: pkg.stripePriceIdMonthly25;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Get Stripe price ID for enterprise add-on
|
|
1098
|
+
* @param {string} addonId - Add-on identifier
|
|
1099
|
+
* @param {string} billingInterval - 'monthly' or 'annual'
|
|
1100
|
+
* @returns {string|null} Stripe price ID
|
|
1101
|
+
*/
|
|
1102
|
+
function getAddonPriceId(addonId, billingInterval = 'monthly') {
|
|
1103
|
+
const addon = ENTERPRISE_ADDONS[addonId];
|
|
1104
|
+
if (!addon) return null;
|
|
1105
|
+
|
|
1106
|
+
return billingInterval === 'annual'
|
|
1107
|
+
? addon.stripePriceIdAnnual25
|
|
1108
|
+
: addon.stripePriceIdMonthly25;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
module.exports = {
|
|
1112
|
+
// Constants
|
|
1113
|
+
SUBSCRIPTION_TIERS,
|
|
1114
|
+
STANDARDS_PACKAGES,
|
|
1115
|
+
EARLY_ADOPTER_DISCOUNT,
|
|
1116
|
+
ENTERPRISE_PACKAGES,
|
|
1117
|
+
ENTERPRISE_ADDONS,
|
|
1118
|
+
VOLUME_DISCOUNTS,
|
|
1119
|
+
|
|
1120
|
+
// Tier functions
|
|
1121
|
+
getTierConfig,
|
|
1122
|
+
hasFeature,
|
|
1123
|
+
checkSubscriptionLimits,
|
|
1124
|
+
getStripePriceId,
|
|
1125
|
+
getStripeProductId,
|
|
1126
|
+
getRecommendedUpgrade,
|
|
1127
|
+
calculateTierCost,
|
|
1128
|
+
getAllTiers,
|
|
1129
|
+
normalizeTierName,
|
|
1130
|
+
|
|
1131
|
+
// Standards functions
|
|
1132
|
+
getTierStandards,
|
|
1133
|
+
hasStandardsAccess,
|
|
1134
|
+
getAllStandardsPackages,
|
|
1135
|
+
canPickStandards,
|
|
1136
|
+
getEnabledStandardsForUser,
|
|
1137
|
+
|
|
1138
|
+
// Community vs Private functions
|
|
1139
|
+
requiresCommunityContribution,
|
|
1140
|
+
getPrivateTierVariant,
|
|
1141
|
+
getBaseTier,
|
|
1142
|
+
|
|
1143
|
+
// Early adopter discount functions
|
|
1144
|
+
checkEarlyAdopterEligibility,
|
|
1145
|
+
getEarlyAdopterDiscount,
|
|
1146
|
+
calculateEarlyAdopterPrice,
|
|
1147
|
+
|
|
1148
|
+
// Billing type functions
|
|
1149
|
+
BILLING_TYPES,
|
|
1150
|
+
usesStripeBilling,
|
|
1151
|
+
usesInvoiceBilling,
|
|
1152
|
+
isInternalClient,
|
|
1153
|
+
checkCollaboratorBillingLimits,
|
|
1154
|
+
getBillingTypeDisplayName,
|
|
1155
|
+
|
|
1156
|
+
// Enterprise functions
|
|
1157
|
+
getVolumeDiscount,
|
|
1158
|
+
validateSeatCount,
|
|
1159
|
+
calculateEnterpriseCost,
|
|
1160
|
+
getEnterprisePackage,
|
|
1161
|
+
getEnterpriseAddon,
|
|
1162
|
+
getAllEnterpriseAddons,
|
|
1163
|
+
hasEnterpriseFeature,
|
|
1164
|
+
getEnterpriseFeatures,
|
|
1165
|
+
checkEnterpriseSeatLimits,
|
|
1166
|
+
getEnterprisePriceId,
|
|
1167
|
+
getAddonPriceId
|
|
1168
|
+
};
|