@intranefr/superbackend 1.4.3
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/.commiat +4 -0
- package/.env.example +47 -0
- package/README.md +110 -0
- package/index.js +94 -0
- package/package.json +67 -0
- package/public/css/styles.css +139 -0
- package/public/js/animations.js +41 -0
- package/sdk/error-tracking/browser/package.json +16 -0
- package/sdk/error-tracking/browser/src/core.js +270 -0
- package/sdk/error-tracking/browser/src/embed.js +18 -0
- package/sdk/error-tracking/browser/src/index.js +1 -0
- package/server.js +5 -0
- package/src/admin/endpointRegistry.js +300 -0
- package/src/controllers/admin.controller.js +321 -0
- package/src/controllers/adminAssets.controller.js +530 -0
- package/src/controllers/adminAssetsStorage.controller.js +260 -0
- package/src/controllers/adminEjsVirtual.controller.js +354 -0
- package/src/controllers/adminFeatureFlags.controller.js +155 -0
- package/src/controllers/adminHeadless.controller.js +1071 -0
- package/src/controllers/adminI18n.controller.js +604 -0
- package/src/controllers/adminJsonConfigs.controller.js +97 -0
- package/src/controllers/adminLlm.controller.js +273 -0
- package/src/controllers/adminMigration.controller.js +257 -0
- package/src/controllers/adminSeoConfig.controller.js +515 -0
- package/src/controllers/adminStats.controller.js +121 -0
- package/src/controllers/adminUploadNamespaces.controller.js +208 -0
- package/src/controllers/assets.controller.js +248 -0
- package/src/controllers/auth.controller.js +93 -0
- package/src/controllers/billing.controller.js +223 -0
- package/src/controllers/featureFlags.controller.js +35 -0
- package/src/controllers/forms.controller.js +217 -0
- package/src/controllers/globalSettings.controller.js +252 -0
- package/src/controllers/headlessCrud.controller.js +126 -0
- package/src/controllers/i18n.controller.js +12 -0
- package/src/controllers/invite.controller.js +249 -0
- package/src/controllers/jsonConfigs.controller.js +19 -0
- package/src/controllers/metrics.controller.js +149 -0
- package/src/controllers/notificationAdmin.controller.js +264 -0
- package/src/controllers/notifications.controller.js +131 -0
- package/src/controllers/org.controller.js +357 -0
- package/src/controllers/orgAdmin.controller.js +491 -0
- package/src/controllers/stripeAdmin.controller.js +410 -0
- package/src/controllers/user.controller.js +361 -0
- package/src/controllers/userAdmin.controller.js +277 -0
- package/src/controllers/waitingList.controller.js +167 -0
- package/src/controllers/webhook.controller.js +200 -0
- package/src/middleware/auth.js +66 -0
- package/src/middleware/errorCapture.js +170 -0
- package/src/middleware/headlessApiTokenAuth.js +57 -0
- package/src/middleware/org.js +108 -0
- package/src/middleware.js +901 -0
- package/src/models/ActionEvent.js +31 -0
- package/src/models/ActivityLog.js +41 -0
- package/src/models/Asset.js +84 -0
- package/src/models/AuditEvent.js +93 -0
- package/src/models/EmailLog.js +28 -0
- package/src/models/ErrorAggregate.js +72 -0
- package/src/models/FormSubmission.js +41 -0
- package/src/models/GlobalSetting.js +38 -0
- package/src/models/HeadlessApiToken.js +24 -0
- package/src/models/HeadlessModelDefinition.js +41 -0
- package/src/models/I18nEntry.js +77 -0
- package/src/models/I18nLocale.js +33 -0
- package/src/models/Invite.js +70 -0
- package/src/models/JsonConfig.js +46 -0
- package/src/models/Notification.js +60 -0
- package/src/models/Organization.js +57 -0
- package/src/models/OrganizationMember.js +43 -0
- package/src/models/StripeCatalogItem.js +77 -0
- package/src/models/StripeWebhookEvent.js +57 -0
- package/src/models/User.js +89 -0
- package/src/models/VirtualEjsFile.js +60 -0
- package/src/models/VirtualEjsFileVersion.js +43 -0
- package/src/models/VirtualEjsGroupChange.js +32 -0
- package/src/models/WaitingList.js +41 -0
- package/src/models/Webhook.js +63 -0
- package/src/models/Workflow.js +29 -0
- package/src/models/WorkflowExecution.js +12 -0
- package/src/routes/admin.routes.js +26 -0
- package/src/routes/adminAssets.routes.js +28 -0
- package/src/routes/adminAssetsStorage.routes.js +13 -0
- package/src/routes/adminAudit.routes.js +196 -0
- package/src/routes/adminEjsVirtual.routes.js +17 -0
- package/src/routes/adminErrors.routes.js +164 -0
- package/src/routes/adminFeatureFlags.routes.js +12 -0
- package/src/routes/adminHeadless.routes.js +38 -0
- package/src/routes/adminI18n.routes.js +22 -0
- package/src/routes/adminJsonConfigs.routes.js +15 -0
- package/src/routes/adminLlm.routes.js +12 -0
- package/src/routes/adminMigration.routes.js +81 -0
- package/src/routes/adminSeoConfig.routes.js +20 -0
- package/src/routes/adminUploadNamespaces.routes.js +13 -0
- package/src/routes/assets.routes.js +21 -0
- package/src/routes/auth.routes.js +12 -0
- package/src/routes/billing.routes.js +11 -0
- package/src/routes/errorTracking.routes.js +31 -0
- package/src/routes/featureFlags.routes.js +9 -0
- package/src/routes/forms.routes.js +9 -0
- package/src/routes/formsAdmin.routes.js +13 -0
- package/src/routes/globalSettings.routes.js +18 -0
- package/src/routes/headless.routes.js +15 -0
- package/src/routes/i18n.routes.js +8 -0
- package/src/routes/invite.routes.js +9 -0
- package/src/routes/jsonConfigs.routes.js +8 -0
- package/src/routes/log.routes.js +111 -0
- package/src/routes/metrics.routes.js +9 -0
- package/src/routes/notificationAdmin.routes.js +15 -0
- package/src/routes/notifications.routes.js +12 -0
- package/src/routes/org.routes.js +31 -0
- package/src/routes/orgAdmin.routes.js +20 -0
- package/src/routes/publicAssets.routes.js +7 -0
- package/src/routes/stripeAdmin.routes.js +20 -0
- package/src/routes/user.routes.js +22 -0
- package/src/routes/userAdmin.routes.js +15 -0
- package/src/routes/waitingList.routes.js +13 -0
- package/src/routes/waitingListAdmin.routes.js +9 -0
- package/src/routes/webhook.routes.js +32 -0
- package/src/routes/workflowWebhook.routes.js +54 -0
- package/src/routes/workflows.routes.js +110 -0
- package/src/services/assets.service.js +110 -0
- package/src/services/audit.service.js +62 -0
- package/src/services/auditLogger.js +165 -0
- package/src/services/ejsVirtual.service.js +614 -0
- package/src/services/email.service.js +351 -0
- package/src/services/errorLogger.js +221 -0
- package/src/services/featureFlags.service.js +202 -0
- package/src/services/forms.service.js +214 -0
- package/src/services/globalSettings.service.js +49 -0
- package/src/services/headlessApiTokens.service.js +158 -0
- package/src/services/headlessCrypto.service.js +31 -0
- package/src/services/headlessModels.service.js +356 -0
- package/src/services/i18n.service.js +314 -0
- package/src/services/i18nInferredKeys.service.js +337 -0
- package/src/services/jsonConfigs.service.js +392 -0
- package/src/services/llm.service.js +749 -0
- package/src/services/migration.service.js +581 -0
- package/src/services/migrationAssets/fsLocal.js +58 -0
- package/src/services/migrationAssets/index.js +134 -0
- package/src/services/migrationAssets/s3.js +75 -0
- package/src/services/migrationAssets/sftp.js +92 -0
- package/src/services/notification.service.js +212 -0
- package/src/services/objectStorage.service.js +514 -0
- package/src/services/seoConfig.service.js +402 -0
- package/src/services/storage.js +150 -0
- package/src/services/stripe.service.js +185 -0
- package/src/services/stripeHelper.service.js +264 -0
- package/src/services/uploadNamespaces.service.js +326 -0
- package/src/services/webhook.service.js +157 -0
- package/src/services/workflow.service.js +271 -0
- package/src/utils/asyncHandler.js +5 -0
- package/src/utils/encryption.js +80 -0
- package/src/utils/jwt.js +40 -0
- package/src/utils/orgRoles.js +156 -0
- package/src/utils/validation.js +26 -0
- package/src/utils/webhookRetry.js +93 -0
- package/views/admin-assets.ejs +444 -0
- package/views/admin-audit.ejs +283 -0
- package/views/admin-coolify-deploy.ejs +207 -0
- package/views/admin-dashboard-home.ejs +291 -0
- package/views/admin-dashboard.ejs +397 -0
- package/views/admin-ejs-virtual.ejs +280 -0
- package/views/admin-errors.ejs +368 -0
- package/views/admin-feature-flags.ejs +390 -0
- package/views/admin-forms.ejs +526 -0
- package/views/admin-global-settings.ejs +436 -0
- package/views/admin-headless.ejs +2020 -0
- package/views/admin-i18n-locales.ejs +221 -0
- package/views/admin-i18n.ejs +728 -0
- package/views/admin-json-configs.ejs +410 -0
- package/views/admin-llm.ejs +884 -0
- package/views/admin-metrics.ejs +274 -0
- package/views/admin-migration.ejs +814 -0
- package/views/admin-notifications.ejs +430 -0
- package/views/admin-organizations.ejs +984 -0
- package/views/admin-seo-config.ejs +673 -0
- package/views/admin-stripe-pricing.ejs +558 -0
- package/views/admin-test.ejs +342 -0
- package/views/admin-users.ejs +452 -0
- package/views/admin-waiting-list.ejs +547 -0
- package/views/admin-webhooks.ejs +329 -0
- package/views/admin-workflows.ejs +310 -0
- package/views/partials/admin-assets-script.ejs +2022 -0
- package/views/partials/admin-test-sidebar.ejs +14 -0
- package/views/partials/dashboard/nav-items.ejs +66 -0
- package/views/partials/dashboard/palette.ejs +63 -0
- package/views/partials/dashboard/sidebar.ejs +21 -0
- package/views/partials/dashboard/tab-bar.ejs +26 -0
- package/views/partials/footer.ejs +3 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const User = require("../models/User");
|
|
2
|
+
const stripeHelper = require("./stripeHelper.service");
|
|
3
|
+
|
|
4
|
+
async function getStripe() {
|
|
5
|
+
const client = await stripeHelper.getStripeClient();
|
|
6
|
+
if (!client) {
|
|
7
|
+
throw new Error("Stripe is not configured");
|
|
8
|
+
}
|
|
9
|
+
return client;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class StripeService {
|
|
13
|
+
async handleCheckoutSessionCompleted(session) {
|
|
14
|
+
const customerId = session.customer;
|
|
15
|
+
const subscriptionId = session.subscription;
|
|
16
|
+
const userId = session.metadata?.userId;
|
|
17
|
+
const billingMode = session.metadata?.billingMode || session.mode;
|
|
18
|
+
|
|
19
|
+
if (!customerId) {
|
|
20
|
+
throw new Error("No customer ID in session");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let user = await User.findOne({ stripeCustomerId: customerId });
|
|
24
|
+
if (!user && userId) {
|
|
25
|
+
user = await User.findById(userId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!user) {
|
|
29
|
+
console.warn(`User not found for customer ${customerId}, userId in metadata: ${userId}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (subscriptionId) {
|
|
34
|
+
user.stripeSubscriptionId = subscriptionId;
|
|
35
|
+
|
|
36
|
+
// Update plan based on subscription
|
|
37
|
+
await this.updateUserPlanFromSubscription(user, subscriptionId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// For one-off (payment mode) checkouts, mark the user as active immediately
|
|
41
|
+
if (billingMode === "payment" || session.mode === "payment") {
|
|
42
|
+
user.subscriptionStatus = "active";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await user.save();
|
|
46
|
+
console.log(
|
|
47
|
+
`Checkout completed for user ${user._id}, subscription: ${subscriptionId || "none"}, mode: ${session.mode}, plan: ${user.currentPlan}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async handleSubscriptionCreated(subscription) {
|
|
52
|
+
const customerId = subscription.customer;
|
|
53
|
+
const user = await User.findOne({ stripeCustomerId: customerId });
|
|
54
|
+
|
|
55
|
+
if (!user) {
|
|
56
|
+
console.warn(`User not found for customer ${customerId}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
user.stripeSubscriptionId = subscription.id;
|
|
61
|
+
|
|
62
|
+
const statusMapping = this.getStatusMapping();
|
|
63
|
+
user.subscriptionStatus = statusMapping[subscription.status] || subscription.status;
|
|
64
|
+
|
|
65
|
+
// Update plan based on subscription
|
|
66
|
+
await this.updateUserPlanFromSubscription(user, subscription.id);
|
|
67
|
+
|
|
68
|
+
await user.save();
|
|
69
|
+
console.log(`Subscription created for user ${user._id}: ${subscription.id}, status: ${user.subscriptionStatus}, plan: ${user.currentPlan}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async handleSubscriptionUpdated(subscription, previousAttributes) {
|
|
73
|
+
const customerId = subscription.customer;
|
|
74
|
+
const user = await User.findOne({ stripeCustomerId: customerId });
|
|
75
|
+
|
|
76
|
+
if (!user) {
|
|
77
|
+
console.warn(`User not found for customer ${customerId}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
user.stripeSubscriptionId = subscription.id;
|
|
82
|
+
|
|
83
|
+
const statusMapping = this.getStatusMapping();
|
|
84
|
+
const newStatus = statusMapping[subscription.status] || subscription.status;
|
|
85
|
+
const oldStatus = user.subscriptionStatus;
|
|
86
|
+
|
|
87
|
+
user.subscriptionStatus = newStatus;
|
|
88
|
+
|
|
89
|
+
// Update plan based on subscription
|
|
90
|
+
await this.updateUserPlanFromSubscription(user, subscription.id);
|
|
91
|
+
|
|
92
|
+
await user.save();
|
|
93
|
+
|
|
94
|
+
if (oldStatus !== newStatus) {
|
|
95
|
+
console.log(`Subscription updated for user ${user._id}: ${oldStatus} -> ${newStatus}, plan: ${user.currentPlan}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async handleSubscriptionDeleted(subscription) {
|
|
100
|
+
const customerId = subscription.customer;
|
|
101
|
+
const user = await User.findOne({ stripeCustomerId: customerId });
|
|
102
|
+
|
|
103
|
+
if (!user) {
|
|
104
|
+
console.warn(`User not found for customer ${customerId}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
user.subscriptionStatus = "cancelled";
|
|
109
|
+
user.currentPlan = "free"; // Reset to free when subscription is deleted
|
|
110
|
+
|
|
111
|
+
await user.save();
|
|
112
|
+
console.log(`Subscription deleted for user ${user._id}, plan reset to free`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async handleInvoicePaymentSucceeded(invoice) {
|
|
116
|
+
const customerId = invoice.customer;
|
|
117
|
+
const user = await User.findOne({ stripeCustomerId: customerId });
|
|
118
|
+
|
|
119
|
+
if (!user) {
|
|
120
|
+
console.warn(`User not found for customer ${customerId}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (user.subscriptionStatus !== "active") {
|
|
125
|
+
user.subscriptionStatus = "active";
|
|
126
|
+
|
|
127
|
+
// Update plan when invoice is paid
|
|
128
|
+
if (user.stripeSubscriptionId) {
|
|
129
|
+
await this.updateUserPlanFromSubscription(user, user.stripeSubscriptionId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await user.save();
|
|
133
|
+
console.log(`Invoice paid for user ${user._id}, status updated to active, plan: ${user.currentPlan}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async handleInvoicePaymentFailed(invoice) {
|
|
138
|
+
const customerId = invoice.customer;
|
|
139
|
+
const user = await User.findOne({ stripeCustomerId: customerId });
|
|
140
|
+
|
|
141
|
+
if (!user) {
|
|
142
|
+
console.warn(`User not found for customer ${customerId}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
user.subscriptionStatus = "past_due";
|
|
147
|
+
await user.save();
|
|
148
|
+
console.log(`Invoice payment failed for user ${user._id}, status updated to past_due`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Update user's currentPlan based on their Stripe subscription
|
|
153
|
+
* Uses catalog mapping first, then falls back to legacy env mapping for backward compatibility
|
|
154
|
+
*/
|
|
155
|
+
async updateUserPlanFromSubscription(user, subscriptionId) {
|
|
156
|
+
try {
|
|
157
|
+
const stripe = await getStripe();
|
|
158
|
+
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
|
159
|
+
const priceId = subscription.items.data[0]?.price.id;
|
|
160
|
+
|
|
161
|
+
const plan = await stripeHelper.resolvePlanKeyFromPriceId(priceId);
|
|
162
|
+
|
|
163
|
+
user.currentPlan = plan;
|
|
164
|
+
console.log(`Updated user ${user.email} plan to ${plan} based on price ID ${priceId}`);
|
|
165
|
+
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('Error updating user plan from subscription:', error);
|
|
168
|
+
// Don't change plan if we can't verify
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getStatusMapping() {
|
|
173
|
+
return {
|
|
174
|
+
'active': 'active',
|
|
175
|
+
'past_due': 'past_due',
|
|
176
|
+
'unpaid': 'unpaid',
|
|
177
|
+
'canceled': 'cancelled',
|
|
178
|
+
'incomplete': 'incomplete',
|
|
179
|
+
'incomplete_expired': 'incomplete_expired',
|
|
180
|
+
'trialing': 'trialing'
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = new StripeService();
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
const StripeCatalogItem = require('../models/StripeCatalogItem');
|
|
2
|
+
const globalSettingsService = require('./globalSettings.service');
|
|
3
|
+
|
|
4
|
+
let stripeClient = null;
|
|
5
|
+
let stripeClientKey = null;
|
|
6
|
+
|
|
7
|
+
async function getStripeSecretKey() {
|
|
8
|
+
const envKey = process.env.STRIPE_SECRET_KEY || null;
|
|
9
|
+
if (envKey) return envKey;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const settingKey = await globalSettingsService.getSettingValue(
|
|
13
|
+
'STRIPE_SECRET_KEY',
|
|
14
|
+
null,
|
|
15
|
+
);
|
|
16
|
+
return settingKey || null;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('Error reading STRIPE_SECRET_KEY from settings:', error);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function isStripeConfigured() {
|
|
24
|
+
const key = await getStripeSecretKey();
|
|
25
|
+
return !!(key && key.startsWith('sk_'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function getStripeClient() {
|
|
29
|
+
const key = await getStripeSecretKey();
|
|
30
|
+
if (!key || !key.startsWith('sk_')) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!stripeClient || stripeClientKey !== key) {
|
|
35
|
+
stripeClient = require('stripe')(key);
|
|
36
|
+
stripeClientKey = key;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return stripeClient;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resetStripeClient() {
|
|
43
|
+
stripeClient = null;
|
|
44
|
+
stripeClientKey = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function resolvePlanKeyFromPriceId(priceId) {
|
|
48
|
+
if (!priceId) {
|
|
49
|
+
return 'free';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const catalogItem = await StripeCatalogItem.findOne({
|
|
54
|
+
stripePriceId: priceId,
|
|
55
|
+
active: true
|
|
56
|
+
}).lean();
|
|
57
|
+
|
|
58
|
+
if (catalogItem) {
|
|
59
|
+
return catalogItem.planKey;
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Error looking up catalog item:', error);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (priceId === process.env.STRIPE_PRICE_ID_CREATOR) {
|
|
66
|
+
return 'creator';
|
|
67
|
+
}
|
|
68
|
+
if (priceId === process.env.STRIPE_PRICE_ID_PRO) {
|
|
69
|
+
return 'pro';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.warn(`Unknown price ID: ${priceId}, defaulting to 'creator' for active subscription`);
|
|
73
|
+
return 'creator';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function findExistingPrice(stripe, productId, currency, unitAmount, recurring) {
|
|
77
|
+
try {
|
|
78
|
+
const prices = await stripe.prices.list({
|
|
79
|
+
product: productId,
|
|
80
|
+
currency: currency,
|
|
81
|
+
active: true,
|
|
82
|
+
limit: 100
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
for (const price of prices.data) {
|
|
86
|
+
if (price.unit_amount !== unitAmount) continue;
|
|
87
|
+
|
|
88
|
+
if (recurring) {
|
|
89
|
+
if (price.type === 'recurring' &&
|
|
90
|
+
price.recurring?.interval === recurring.interval &&
|
|
91
|
+
(price.recurring?.interval_count || 1) === (recurring.interval_count || 1)) {
|
|
92
|
+
return price;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
if (price.type === 'one_time') {
|
|
96
|
+
return price;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('Error searching for existing price:', error);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function upsertStripeProductAndPrice({
|
|
108
|
+
productName,
|
|
109
|
+
productDescription,
|
|
110
|
+
planKey,
|
|
111
|
+
displayName,
|
|
112
|
+
billingType,
|
|
113
|
+
currency,
|
|
114
|
+
unitAmount,
|
|
115
|
+
interval,
|
|
116
|
+
intervalCount,
|
|
117
|
+
metadata,
|
|
118
|
+
existingProductId,
|
|
119
|
+
adminId
|
|
120
|
+
}) {
|
|
121
|
+
const stripe = await getStripeClient();
|
|
122
|
+
if (!stripe) {
|
|
123
|
+
throw new Error('Stripe is not configured');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let product;
|
|
127
|
+
if (existingProductId) {
|
|
128
|
+
product = await stripe.products.update(existingProductId, {
|
|
129
|
+
name: productName,
|
|
130
|
+
description: productDescription || undefined,
|
|
131
|
+
metadata: { planKey, ...metadata }
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
product = await stripe.products.create({
|
|
135
|
+
name: productName,
|
|
136
|
+
description: productDescription || undefined,
|
|
137
|
+
metadata: { planKey, ...metadata }
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const recurring = billingType === 'subscription' ? {
|
|
142
|
+
interval: interval || 'month',
|
|
143
|
+
interval_count: intervalCount || 1
|
|
144
|
+
} : null;
|
|
145
|
+
|
|
146
|
+
let price = await findExistingPrice(stripe, product.id, currency, unitAmount, recurring);
|
|
147
|
+
|
|
148
|
+
if (!price) {
|
|
149
|
+
const priceData = {
|
|
150
|
+
product: product.id,
|
|
151
|
+
currency: currency || 'usd',
|
|
152
|
+
unit_amount: unitAmount,
|
|
153
|
+
metadata: { planKey }
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (recurring) {
|
|
157
|
+
priceData.recurring = recurring;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
price = await stripe.prices.create(priceData);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const catalogItem = await StripeCatalogItem.findOneAndUpdate(
|
|
164
|
+
{ stripePriceId: price.id },
|
|
165
|
+
{
|
|
166
|
+
stripeProductId: product.id,
|
|
167
|
+
stripePriceId: price.id,
|
|
168
|
+
planKey,
|
|
169
|
+
displayName,
|
|
170
|
+
description: productDescription || '',
|
|
171
|
+
billingType,
|
|
172
|
+
currency: currency || 'usd',
|
|
173
|
+
unitAmount,
|
|
174
|
+
interval: recurring?.interval || null,
|
|
175
|
+
intervalCount: recurring?.interval_count || 1,
|
|
176
|
+
active: true,
|
|
177
|
+
metadata,
|
|
178
|
+
updatedByAdminId: adminId
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
upsert: true,
|
|
182
|
+
new: true,
|
|
183
|
+
setDefaultsOnInsert: true
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if (!catalogItem.createdByAdminId) {
|
|
188
|
+
catalogItem.createdByAdminId = adminId;
|
|
189
|
+
await catalogItem.save();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
product,
|
|
194
|
+
price,
|
|
195
|
+
catalogItem
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function importStripePrice(stripePriceId, planKey, displayName, adminId) {
|
|
200
|
+
const stripe = await getStripeClient();
|
|
201
|
+
if (!stripe) {
|
|
202
|
+
throw new Error('Stripe is not configured');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const price = await stripe.prices.retrieve(stripePriceId, {
|
|
206
|
+
expand: ['product']
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (!price) {
|
|
210
|
+
throw new Error('Price not found in Stripe');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const product = price.product;
|
|
214
|
+
const productId = typeof product === 'string' ? product : product.id;
|
|
215
|
+
const productName = typeof product === 'object' ? product.name : '';
|
|
216
|
+
|
|
217
|
+
const billingType = price.type === 'recurring' ? 'subscription' : 'one_time';
|
|
218
|
+
|
|
219
|
+
const catalogItem = await StripeCatalogItem.findOneAndUpdate(
|
|
220
|
+
{ stripePriceId: price.id },
|
|
221
|
+
{
|
|
222
|
+
stripeProductId: productId,
|
|
223
|
+
stripePriceId: price.id,
|
|
224
|
+
planKey,
|
|
225
|
+
displayName: displayName || productName || planKey,
|
|
226
|
+
description: typeof product === 'object' ? (product.description || '') : '',
|
|
227
|
+
billingType,
|
|
228
|
+
currency: price.currency,
|
|
229
|
+
unitAmount: price.unit_amount,
|
|
230
|
+
interval: price.recurring?.interval || null,
|
|
231
|
+
intervalCount: price.recurring?.interval_count || 1,
|
|
232
|
+
active: price.active,
|
|
233
|
+
metadata: price.metadata || {},
|
|
234
|
+
updatedByAdminId: adminId
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
upsert: true,
|
|
238
|
+
new: true,
|
|
239
|
+
setDefaultsOnInsert: true
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (!catalogItem.createdByAdminId) {
|
|
244
|
+
catalogItem.createdByAdminId = adminId;
|
|
245
|
+
await catalogItem.save();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
price,
|
|
250
|
+
product,
|
|
251
|
+
catalogItem
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = {
|
|
256
|
+
getStripeSecretKey,
|
|
257
|
+
isStripeConfigured,
|
|
258
|
+
getStripeClient,
|
|
259
|
+
resetStripeClient,
|
|
260
|
+
resolvePlanKeyFromPriceId,
|
|
261
|
+
findExistingPrice,
|
|
262
|
+
upsertStripeProductAndPrice,
|
|
263
|
+
importStripePrice
|
|
264
|
+
};
|