@intlayer/backend 3.2.2 → 3.3.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/dist/cjs/controllers/stripe.controller.cjs +110 -5
- package/dist/cjs/controllers/stripe.controller.cjs.map +1 -1
- package/dist/cjs/index.cjs +9 -2
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/middlewares/request.middleware.cjs +0 -4
- package/dist/cjs/middlewares/request.middleware.cjs.map +1 -1
- package/dist/cjs/routes/stripe.routes.cjs +8 -2
- package/dist/cjs/routes/stripe.routes.cjs.map +1 -1
- package/dist/cjs/schemas/plans.schema.cjs +27 -5
- package/dist/cjs/schemas/plans.schema.cjs.map +1 -1
- package/dist/cjs/services/organization.service.cjs +5 -27
- package/dist/cjs/services/organization.service.cjs.map +1 -1
- package/dist/cjs/services/subscription.service.cjs +110 -116
- package/dist/cjs/services/subscription.service.cjs.map +1 -1
- package/dist/cjs/types/plan.types.cjs.map +1 -1
- package/dist/cjs/utils/errors/errorCodes.cjs +107 -3
- package/dist/cjs/utils/errors/errorCodes.cjs.map +1 -1
- package/dist/cjs/utils/plan.cjs +3 -3
- package/dist/cjs/utils/plan.cjs.map +1 -1
- package/dist/cjs/webhooks/stripe.webhook.cjs +102 -89
- package/dist/cjs/webhooks/stripe.webhook.cjs.map +1 -1
- package/dist/esm/controllers/stripe.controller.mjs +99 -5
- package/dist/esm/controllers/stripe.controller.mjs.map +1 -1
- package/dist/esm/index.mjs +10 -3
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/middlewares/request.middleware.mjs +0 -4
- package/dist/esm/middlewares/request.middleware.mjs.map +1 -1
- package/dist/esm/routes/stripe.routes.mjs +12 -3
- package/dist/esm/routes/stripe.routes.mjs.map +1 -1
- package/dist/esm/schemas/plans.schema.mjs +27 -5
- package/dist/esm/schemas/plans.schema.mjs.map +1 -1
- package/dist/esm/services/organization.service.mjs +5 -25
- package/dist/esm/services/organization.service.mjs.map +1 -1
- package/dist/esm/services/subscription.service.mjs +101 -120
- package/dist/esm/services/subscription.service.mjs.map +1 -1
- package/dist/esm/utils/errors/errorCodes.mjs +107 -3
- package/dist/esm/utils/errors/errorCodes.mjs.map +1 -1
- package/dist/esm/utils/plan.mjs +3 -3
- package/dist/esm/utils/plan.mjs.map +1 -1
- package/dist/esm/webhooks/stripe.webhook.mjs +103 -90
- package/dist/esm/webhooks/stripe.webhook.mjs.map +1 -1
- package/dist/types/controllers/stripe.controller.d.ts +14 -0
- package/dist/types/controllers/stripe.controller.d.ts.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/routes/stripe.routes.d.ts +6 -1
- package/dist/types/routes/stripe.routes.d.ts.map +1 -1
- package/dist/types/schemas/plans.schema.d.ts.map +1 -1
- package/dist/types/services/organization.service.d.ts +1 -8
- package/dist/types/services/organization.service.d.ts.map +1 -1
- package/dist/types/services/subscription.service.d.ts +5 -21
- package/dist/types/services/subscription.service.d.ts.map +1 -1
- package/dist/types/types/plan.types.d.ts +2 -1
- package/dist/types/types/plan.types.d.ts.map +1 -1
- package/dist/types/utils/errors/errorCodes.d.ts +104 -0
- package/dist/types/utils/errors/errorCodes.d.ts.map +1 -1
- package/dist/types/webhooks/stripe.webhook.d.ts +7 -2
- package/dist/types/webhooks/stripe.webhook.d.ts.map +1 -1
- package/package.json +10 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/webhooks/stripe.webhook.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../../../src/webhooks/stripe.webhook.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { getOrganizationById } from '@services/organization.service';\nimport {\n addOrUpdateSubscription,\n cancelSubscription,\n changeSubscriptionStatus,\n} from '@services/subscription.service';\nimport { GenericError } from '@utils/errors';\nimport type { Request, Response } from 'express';\nimport type { Locales } from 'intlayer';\nimport { Stripe } from 'stripe';\nimport type { Plan } from '@/types/plan.types';\n\n// Initialize the Stripe client with the secret key\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\ntype SubscriptionMetadata = {\n locale: Locales; // Localization setting (e.g., 'en', 'fr', 'es')\n userId: string; // ID of the user associated with the subscription\n organizationId: string; // ID of the organization associated with the subscription\n};\n\n/**\n * Stripe webhook handler for processing subscription and invoice events.\n * @param req - Express request object.\n * @param res - Express response object.\n */\nexport const stripeWebhook = async (req: Request, res: Response) => {\n const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!; // Webhook secret for verifying event signatures\n const sig = req.headers['stripe-signature']!; // Retrieve the signature from the webhook request headers\n\n let event: Stripe.Event;\n\n // Verify the webhook signature to ensure the request is authentic\n try {\n event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);\n } catch (err) {\n // Respond with a 400 status code if the signature verification fails\n res.status(400).send(`Webhook Error: ${(err as Error).message}`);\n return;\n }\n\n // Utility function to extract metadata from a Stripe customer\n const extractMetadata = async (customerId: string) => {\n const customer = await stripe.customers.retrieve(customerId); // Retrieve customer details from Stripe\n return (customer as Stripe.Customer).metadata as SubscriptionMetadata; // Return the metadata object\n };\n\n // Handles subscription-related events (creation, update, deletion)\n const handleSubscriptionEvent = async (\n subscription: Stripe.Subscription,\n statusOverride?: Plan['status'] // Optionally override the subscription status\n ) => {\n const { id: subscriptionId, customer } = subscription;\n const priceId = subscription.items.data[0]?.price?.id; // Extract the price ID from subscription items\n\n if (!customer) {\n throw new GenericError('STRIPE_SUBSCRIPTION_NO_CUSTOMER');\n }\n\n const customerId = customer as string;\n const { locale, userId, organizationId } =\n await extractMetadata(customerId); // Extract metadata from the customer\n\n // Set localization in response locals if available\n if (locale) {\n res.locals.locales = locale;\n }\n\n const organization = await getOrganizationById(organizationId); // Fetch organization details by ID\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND');\n }\n\n const status = statusOverride ?? subscription.status; // Use the provided status override or the subscription's status\n\n // Update or create a subscription record in the database\n await addOrUpdateSubscription(\n subscriptionId,\n priceId!,\n customerId,\n userId,\n organization,\n status\n );\n };\n\n // Handles invoice-related events (payment success or failure)\n const handleInvoiceEvent = async (\n invoice: Stripe.Invoice,\n status: 'active' | 'incomplete'\n ) => {\n const subscriptionId = invoice.subscription as string; // Extract the subscription ID from the invoice\n if (!subscriptionId) {\n logger.warn('Subscription ID is undefined in invoice.');\n return;\n }\n\n // Retrieve the subscription details from Stripe\n const subscription = await stripe.subscriptions.retrieve(subscriptionId);\n const organization = await getOrganizationById(\n subscription.metadata.organizationId\n );\n\n // Prevent duplicate subscriptions by canceling conflicting subscriptions\n if (\n organization.plan.subscriptionId &&\n organization.plan.subscriptionId !== subscriptionId\n ) {\n await stripe.subscriptions.cancel(subscriptionId);\n }\n\n const customerId = invoice.customer as string;\n const { locale, userId, organizationId } =\n await extractMetadata(customerId);\n\n // Set localization in response locals if available\n if (locale) {\n res.locals.locales = locale;\n }\n\n // Update the subscription status in the database\n await changeSubscriptionStatus(\n subscriptionId,\n status,\n userId,\n organizationId\n );\n };\n\n try {\n // Log the event type for debugging and monitoring\n logger.info(`Triggered event type ${event.type}`);\n\n // Handle specific event types\n switch (event.type) {\n case 'customer.subscription.created': {\n logger.info(`Handled event type ${event.type}`);\n // Process a new subscription creation event\n await handleSubscriptionEvent(event.data.object as Stripe.Subscription);\n break;\n }\n case 'customer.subscription.updated': {\n logger.info(`Handled event type ${event.type}`);\n // Process a subscription update event\n await handleSubscriptionEvent(event.data.object as Stripe.Subscription);\n break;\n }\n case 'customer.subscription.deleted': {\n logger.info(`Handled event type ${event.type}`);\n const subscription = event.data\n .object as unknown as Stripe.Subscription;\n const customerId = subscription.customer as string;\n const { locale, organizationId } = await extractMetadata(customerId);\n\n // Set localization in response locals if available\n if (locale) {\n res.locals.locales = locale;\n }\n\n // Handle subscription deletion by canceling it in the database\n await cancelSubscription(subscription.id, organizationId);\n break;\n }\n case 'invoice.payment_succeeded': {\n logger.info(`Handled event type ${event.type}`);\n // Handle successful invoice payment\n await handleInvoiceEvent(event.data.object as Stripe.Invoice, 'active');\n break;\n }\n case 'invoice.payment_failed': {\n logger.info(`Handled event type ${event.type}`);\n // Handle failed invoice payment\n await handleInvoiceEvent(\n event.data.object as Stripe.Invoice,\n 'incomplete'\n );\n break;\n }\n default:\n // Log unhandled event types for visibility\n logger.info(`Unhandled event type ${event.type}`);\n }\n\n // Respond to Stripe to confirm the event was processed successfully\n res.send();\n } catch (error) {\n // Log errors for debugging and respond with a 500 status code\n logger.error(\n `Error handling event ${event.type}: ${(error as Error).message}`\n );\n res.status(500).send('Server Error');\n }\n};\n"],"mappings":"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","names":[]}
|
|
@@ -1,32 +1,68 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as subscriptionService from './../services/subscription.service.mjs';
|
|
2
2
|
import { ErrorHandler } from './../utils/errors/index.mjs';
|
|
3
|
+
import { retrievePlanInformation } from './../utils/plan.mjs';
|
|
3
4
|
import { formatResponse } from './../utils/responseData.mjs';
|
|
5
|
+
import { t } from "express-intlayer";
|
|
4
6
|
import { Stripe } from "stripe";
|
|
7
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
|
5
8
|
const getSubscription = async (req, res) => {
|
|
6
9
|
try {
|
|
7
|
-
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
|
8
10
|
const { organization, user } = res.locals;
|
|
9
11
|
const { priceId } = req.body;
|
|
10
12
|
if (!organization) {
|
|
11
13
|
ErrorHandler.handleGenericErrorResponse(res, "ORGANIZATION_NOT_FOUND");
|
|
12
14
|
return;
|
|
13
15
|
}
|
|
14
|
-
|
|
16
|
+
if (!user) {
|
|
17
|
+
ErrorHandler.handleGenericErrorResponse(res, "USER_NOT_FOUND");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!organization.membersIds.map(String).includes(String(user._id))) {
|
|
21
|
+
ErrorHandler.handleGenericErrorResponse(
|
|
22
|
+
res,
|
|
23
|
+
"USER_NOT_ORGANIZATION_MEMBER"
|
|
24
|
+
);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!organization.adminsIds.map(String).includes(String(user._id))) {
|
|
28
|
+
ErrorHandler.handleGenericErrorResponse(
|
|
29
|
+
res,
|
|
30
|
+
"USER_NOT_ORGANIZATION_ADMIN"
|
|
31
|
+
);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const { period, type } = retrievePlanInformation(priceId);
|
|
35
|
+
if (organization.plan?.subscriptionId || organization.plan?.type === type && organization.plan?.period === period) {
|
|
36
|
+
ErrorHandler.handleGenericErrorResponse(res, "ALREADY_SUBSCRIBED", {
|
|
37
|
+
organizationId: organization._id
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let customerId = organization.plan?.customerId;
|
|
15
42
|
if (!customerId) {
|
|
16
43
|
const customer = await stripe.customers.create({
|
|
17
|
-
metadata: {
|
|
44
|
+
metadata: {
|
|
45
|
+
organizationId: String(organization._id),
|
|
46
|
+
userId: String(user._id),
|
|
47
|
+
// Include the locale for potential localization
|
|
48
|
+
locale: res.locals.locale
|
|
49
|
+
}
|
|
18
50
|
});
|
|
19
51
|
customerId = customer.id;
|
|
20
|
-
await saveStripeCustomerId(organization, customerId);
|
|
21
52
|
}
|
|
22
53
|
const subscription = await stripe.subscriptions.create({
|
|
23
54
|
customer: customerId,
|
|
55
|
+
// Associate the subscription with the customer
|
|
24
56
|
items: [{ price: priceId }],
|
|
57
|
+
// Set the price ID for the subscription
|
|
25
58
|
expand: ["latest_invoice.payment_intent"],
|
|
59
|
+
// Expand to get payment intent details
|
|
26
60
|
payment_settings: {
|
|
27
61
|
payment_method_types: ["card"]
|
|
62
|
+
// Specify payment method types
|
|
28
63
|
},
|
|
29
64
|
payment_behavior: "default_incomplete"
|
|
65
|
+
// Create the subscription in an incomplete state until payment is confirmed
|
|
30
66
|
});
|
|
31
67
|
if (!subscription) {
|
|
32
68
|
ErrorHandler.handleGenericErrorResponse(
|
|
@@ -43,8 +79,10 @@ const getSubscription = async (req, res) => {
|
|
|
43
79
|
const responseData = formatResponse({
|
|
44
80
|
data: {
|
|
45
81
|
subscriptionId: subscription.id,
|
|
82
|
+
// Retrieve the client secret from the payment intent to complete payment on the client side
|
|
46
83
|
clientSecret: subscription.latest_invoice.payment_intent?.client_secret ?? "",
|
|
47
84
|
status: subscription.status
|
|
85
|
+
// Subscription status (e.g., 'incomplete', 'active')
|
|
48
86
|
}
|
|
49
87
|
});
|
|
50
88
|
res.json(responseData);
|
|
@@ -54,7 +92,63 @@ const getSubscription = async (req, res) => {
|
|
|
54
92
|
return;
|
|
55
93
|
}
|
|
56
94
|
};
|
|
95
|
+
const cancelSubscription = async (_req, res) => {
|
|
96
|
+
try {
|
|
97
|
+
const { organization, user } = res.locals;
|
|
98
|
+
if (!organization) {
|
|
99
|
+
ErrorHandler.handleGenericErrorResponse(res, "ORGANIZATION_NOT_FOUND");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!user) {
|
|
103
|
+
ErrorHandler.handleGenericErrorResponse(res, "USER_NOT_FOUND");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!organization.adminsIds.map(String).includes(String(user._id))) {
|
|
107
|
+
ErrorHandler.handleGenericErrorResponse(
|
|
108
|
+
res,
|
|
109
|
+
"USER_NOT_ORGANIZATION_ADMIN"
|
|
110
|
+
);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!organization.plan?.subscriptionId) {
|
|
114
|
+
ErrorHandler.handleGenericErrorResponse(
|
|
115
|
+
res,
|
|
116
|
+
"ORGANIZATION_PLAN_NOT_FOUND"
|
|
117
|
+
);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await stripe.subscriptions.cancel(organization.plan.subscriptionId);
|
|
121
|
+
const plan = await subscriptionService.cancelSubscription(
|
|
122
|
+
organization.plan.subscriptionId,
|
|
123
|
+
String(organization._id)
|
|
124
|
+
);
|
|
125
|
+
if (!plan) {
|
|
126
|
+
ErrorHandler.handleGenericErrorResponse(
|
|
127
|
+
res,
|
|
128
|
+
"ORGANIZATION_PLAN_NOT_FOUND"
|
|
129
|
+
);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const formattedPlan = formatResponse({
|
|
133
|
+
message: t({
|
|
134
|
+
en: "Subscription cancelled successfully",
|
|
135
|
+
fr: "Souscription annul\xE9e avec succ\xE8s",
|
|
136
|
+
es: "Suscripci\xF3n cancelada con \xE9xito"
|
|
137
|
+
}),
|
|
138
|
+
description: t({
|
|
139
|
+
en: "Your subscription has been cancelled successfully",
|
|
140
|
+
fr: "Votre souscription a \xE9t\xE9 annul\xE9e avec succ\xE8s",
|
|
141
|
+
es: "Su suscripci\xF3n ha sido cancelada con \xE9xito"
|
|
142
|
+
}),
|
|
143
|
+
data: plan
|
|
144
|
+
});
|
|
145
|
+
res.json(formattedPlan);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
ErrorHandler.handleAppErrorResponse(res, error);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
57
150
|
export {
|
|
151
|
+
cancelSubscription,
|
|
58
152
|
getSubscription
|
|
59
153
|
};
|
|
60
154
|
//# sourceMappingURL=stripe.controller.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/controllers/stripe.controller.ts"],"sourcesContent":["import { ResponseWithInformation } from '@middlewares/sessionAuth.middleware';\nimport { saveStripeCustomerId } from '@services/organization.service';\nimport { ErrorHandler, AppError } from '@utils/errors';\nimport { formatResponse, ResponseData } from '@utils/responseData';\nimport { Request } from 'express';\nimport { Stripe } from 'stripe';\n\nexport type GetCheckoutSessionBody = {\n organizationId: string;\n priceId: string;\n};\n\ntype CheckoutSessionData = {\n subscriptionId: string;\n clientSecret: string;\n status: Stripe.Subscription.Status;\n};\n\nexport type GetCheckoutSessionResult = ResponseData<CheckoutSessionData>;\n\nexport const getSubscription = async (\n req: Request<undefined, undefined, GetCheckoutSessionBody>,\n res: ResponseWithInformation<GetCheckoutSessionResult>\n): Promise<void> => {\n try {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const { organization, user } = res.locals;\n const { priceId } = req.body;\n\n if (!organization) {\n ErrorHandler.handleGenericErrorResponse(res, 'ORGANIZATION_NOT_FOUND');\n return;\n }\n\n // Fetch or create a Stripe customer for the organization\n let { customerId } = organization.plan;\n\n if (!customerId) {\n const customer = await stripe.customers.create({\n metadata: { organizationId: String(organization._id) },\n });\n customerId = customer.id;\n await saveStripeCustomerId(organization, customerId);\n }\n\n const subscription = await stripe.subscriptions.create({\n customer: customerId,\n items: [{ price: priceId }],\n expand: ['latest_invoice.payment_intent'],\n payment_settings: {\n payment_method_types: ['card'],\n },\n payment_behavior: 'default_incomplete',\n });\n\n if (!subscription) {\n ErrorHandler.handleGenericErrorResponse(\n res,\n 'SUBSCRIPTION_CREATION_FAILED',\n {\n user,\n organization,\n priceId,\n }\n );\n return;\n }\n\n const responseData = formatResponse<CheckoutSessionData>({\n data: {\n subscriptionId: subscription.id,\n clientSecret:\n (\n (subscription.latest_invoice as Stripe.Invoice)\n .payment_intent as Stripe.PaymentIntent\n )?.client_secret ?? '',\n status: subscription.status,\n },\n });\n\n res.json(responseData);\n\n return;\n } catch (error) {\n ErrorHandler.handleAppErrorResponse(res, error as AppError);\n return;\n }\n};\n"],"mappings":"AACA,SAAS,4BAA4B;AACrC,SAAS,oBAA8B;AACvC,SAAS,sBAAoC;AAE7C,SAAS,cAAc;AAehB,MAAM,kBAAkB,OAC7B,KACA,QACkB;AAClB,MAAI;AACF,UAAM,SAAS,IAAI,OAAO,QAAQ,IAAI,iBAAkB;AACxD,UAAM,EAAE,cAAc,KAAK,IAAI,IAAI;AACnC,UAAM,EAAE,QAAQ,IAAI,IAAI;AAExB,QAAI,CAAC,cAAc;AACjB,mBAAa,2BAA2B,KAAK,wBAAwB;AACrE;AAAA,IACF;AAGA,QAAI,EAAE,WAAW,IAAI,aAAa;AAElC,QAAI,CAAC,YAAY;AACf,YAAM,WAAW,MAAM,OAAO,UAAU,OAAO;AAAA,QAC7C,UAAU,EAAE,gBAAgB,OAAO,aAAa,GAAG,EAAE;AAAA,MACvD,CAAC;AACD,mBAAa,SAAS;AACtB,YAAM,qBAAqB,cAAc,UAAU;AAAA,IACrD;AAEA,UAAM,eAAe,MAAM,OAAO,cAAc,OAAO;AAAA,MACrD,UAAU;AAAA,MACV,OAAO,CAAC,EAAE,OAAO,QAAQ,CAAC;AAAA,MAC1B,QAAQ,CAAC,+BAA+B;AAAA,MACxC,kBAAkB;AAAA,QAChB,sBAAsB,CAAC,MAAM;AAAA,MAC/B;AAAA,MACA,kBAAkB;AAAA,IACpB,CAAC;AAED,QAAI,CAAC,cAAc;AACjB,mBAAa;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,UACE;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,MACF;AACA;AAAA,IACF;AAEA,UAAM,eAAe,eAAoC;AAAA,MACvD,MAAM;AAAA,QACJ,gBAAgB,aAAa;AAAA,QAC7B,cAEK,aAAa,eACX,gBACF,iBAAiB;AAAA,QACtB,QAAQ,aAAa;AAAA,MACvB;AAAA,IACF,CAAC;AAED,QAAI,KAAK,YAAY;AAErB;AAAA,EACF,SAAS,OAAO;AACd,iBAAa,uBAAuB,KAAK,KAAiB;AAC1D;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/controllers/stripe.controller.ts"],"sourcesContent":["import { ResponseWithInformation } from '@middlewares/sessionAuth.middleware';\nimport * as subscriptionService from '@services/subscription.service';\nimport { ErrorHandler, AppError } from '@utils/errors';\nimport { retrievePlanInformation } from '@utils/plan';\nimport { formatResponse, ResponseData } from '@utils/responseData';\nimport { Request } from 'express';\nimport { t } from 'express-intlayer';\nimport { Locales } from 'intlayer';\nimport { Stripe } from 'stripe';\nimport type { Organization } from '@/types/organization.types';\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\nexport type GetCheckoutSessionBody = {\n organizationId: string;\n priceId: string;\n};\n\ntype CheckoutSessionData = {\n subscriptionId: string;\n clientSecret: string;\n status: Stripe.Subscription.Status;\n};\n\nexport type GetCheckoutSessionResult = ResponseData<CheckoutSessionData>;\n\n/**\n * Handles subscription creation or update with Stripe and returns a ClientSecret.\n * @param req - Express request object.\n * @param res - Express response object.\n */\nexport const getSubscription = async (\n req: Request<undefined, undefined, GetCheckoutSessionBody>,\n res: ResponseWithInformation<GetCheckoutSessionResult>\n): Promise<void> => {\n try {\n // Extract organization and user from response locals (set by authentication middleware)\n const { organization, user } = res.locals;\n // Get the price ID (Stripe Price ID) from the request body\n const { priceId } = req.body;\n\n // Validate that the organization exists\n if (!organization) {\n ErrorHandler.handleGenericErrorResponse(res, 'ORGANIZATION_NOT_FOUND');\n return;\n }\n\n // Validate that the user exists\n if (!user) {\n ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_FOUND');\n return;\n }\n\n // Ensure the user is a member of the organization\n if (!organization.membersIds.map(String).includes(String(user._id))) {\n ErrorHandler.handleGenericErrorResponse(\n res,\n 'USER_NOT_ORGANIZATION_MEMBER'\n );\n return;\n }\n\n // Ensure the user is an admin of the organization\n if (!organization.adminsIds.map(String).includes(String(user._id))) {\n ErrorHandler.handleGenericErrorResponse(\n res,\n 'USER_NOT_ORGANIZATION_ADMIN'\n );\n return;\n }\n\n const { period, type } = retrievePlanInformation(priceId);\n\n if (\n organization.plan?.subscriptionId ||\n (organization.plan?.type === type && organization.plan?.period === period)\n ) {\n ErrorHandler.handleGenericErrorResponse(res, 'ALREADY_SUBSCRIBED', {\n organizationId: organization._id,\n });\n return;\n }\n\n // Attempt to retrieve the Stripe customer ID from the organization's plan\n let customerId = organization.plan?.customerId;\n\n if (!customerId) {\n // If no customer ID exists, create a new Stripe customer for the organization\n const customer = await stripe.customers.create({\n metadata: {\n organizationId: String(organization._id),\n userId: String(user._id),\n // Include the locale for potential localization\n locale: (res.locals as unknown as { locale: Locales }).locale,\n },\n });\n customerId = customer.id;\n }\n\n // If no subscription exists, create a new one\n const subscription = await stripe.subscriptions.create({\n customer: customerId, // Associate the subscription with the customer\n items: [{ price: priceId }], // Set the price ID for the subscription\n expand: ['latest_invoice.payment_intent'], // Expand to get payment intent details\n payment_settings: {\n payment_method_types: ['card'], // Specify payment method types\n },\n payment_behavior: 'default_incomplete', // Create the subscription in an incomplete state until payment is confirmed\n });\n\n // Handle subscription creation failure\n if (!subscription) {\n ErrorHandler.handleGenericErrorResponse(\n res,\n 'SUBSCRIPTION_CREATION_FAILED',\n {\n user,\n organization,\n priceId,\n }\n );\n return;\n }\n\n // Prepare the response data with subscription details\n const responseData = formatResponse<CheckoutSessionData>({\n data: {\n subscriptionId: subscription.id,\n // Retrieve the client secret from the payment intent to complete payment on the client side\n clientSecret:\n (\n (subscription.latest_invoice as Stripe.Invoice)\n .payment_intent as Stripe.PaymentIntent\n )?.client_secret ?? '',\n status: subscription.status, // Subscription status (e.g., 'incomplete', 'active')\n },\n });\n\n // Send the response back to the client\n res.json(responseData);\n\n return;\n } catch (error) {\n // Handle any errors that occur during the process\n\n ErrorHandler.handleAppErrorResponse(res, error as AppError);\n return;\n }\n};\n\ntype CancelSubscriptionData = Organization['plan'];\n\ntype CancelSubscriptionResult = ResponseData<CancelSubscriptionData>;\n\n/**\n * Cancels a subscription for an organization.\n * @param _req - Express request object.\n * @param res - Express response object.\n */\nexport const cancelSubscription = async (\n _req: Request,\n res: ResponseWithInformation<CancelSubscriptionResult>\n): Promise<void> => {\n try {\n // Extract the organization and user from the response locals\n // These are typically set by authentication middleware earlier in the request pipeline\n const { organization, user } = res.locals;\n\n // Validate that the organization exists\n if (!organization) {\n ErrorHandler.handleGenericErrorResponse(res, 'ORGANIZATION_NOT_FOUND');\n return;\n }\n\n // Validate that the user exists\n if (!user) {\n ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_FOUND');\n return;\n }\n\n // Check if the user is an admin of the organization\n if (!organization.adminsIds.map(String).includes(String(user._id))) {\n ErrorHandler.handleGenericErrorResponse(\n res,\n 'USER_NOT_ORGANIZATION_ADMIN'\n );\n return;\n }\n\n // Check if the organization has an active subscription to cancel\n if (!organization.plan?.subscriptionId) {\n ErrorHandler.handleGenericErrorResponse(\n res,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n return;\n }\n\n // Cancel the subscription on Stripe immediately using the subscription ID\n await stripe.subscriptions.cancel(organization.plan.subscriptionId);\n\n // Update the organization's plan in the database to reflect the cancellation\n const plan = await subscriptionService.cancelSubscription(\n organization.plan.subscriptionId,\n String(organization._id)\n );\n\n // If the plan could not be updated in the database, handle the error\n if (!plan) {\n ErrorHandler.handleGenericErrorResponse(\n res,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n return;\n }\n\n // Prepare a formatted response with a success message and the updated plan data\n const formattedPlan = formatResponse<CancelSubscriptionData>({\n message: t({\n en: 'Subscription cancelled successfully',\n fr: 'Souscription annulée avec succès',\n es: 'Suscripción cancelada con éxito',\n }),\n description: t({\n en: 'Your subscription has been cancelled successfully',\n fr: 'Votre souscription a été annulée avec succès',\n es: 'Su suscripción ha sido cancelada con éxito',\n }),\n data: plan!,\n });\n\n // Send the response back to the client\n res.json(formattedPlan);\n } catch (error) {\n // Handle any errors that occur during the cancellation process\n ErrorHandler.handleAppErrorResponse(res, error as AppError);\n }\n};\n"],"mappings":"AACA,YAAY,yBAAyB;AACrC,SAAS,oBAA8B;AACvC,SAAS,+BAA+B;AACxC,SAAS,sBAAoC;AAE7C,SAAS,SAAS;AAElB,SAAS,cAAc;AAGvB,MAAM,SAAS,IAAI,OAAO,QAAQ,IAAI,iBAAkB;AAoBjD,MAAM,kBAAkB,OAC7B,KACA,QACkB;AAClB,MAAI;AAEF,UAAM,EAAE,cAAc,KAAK,IAAI,IAAI;AAEnC,UAAM,EAAE,QAAQ,IAAI,IAAI;AAGxB,QAAI,CAAC,cAAc;AACjB,mBAAa,2BAA2B,KAAK,wBAAwB;AACrE;AAAA,IACF;AAGA,QAAI,CAAC,MAAM;AACT,mBAAa,2BAA2B,KAAK,gBAAgB;AAC7D;AAAA,IACF;AAGA,QAAI,CAAC,aAAa,WAAW,IAAI,MAAM,EAAE,SAAS,OAAO,KAAK,GAAG,CAAC,GAAG;AACnE,mBAAa;AAAA,QACX;AAAA,QACA;AAAA,MACF;AACA;AAAA,IACF;AAGA,QAAI,CAAC,aAAa,UAAU,IAAI,MAAM,EAAE,SAAS,OAAO,KAAK,GAAG,CAAC,GAAG;AAClE,mBAAa;AAAA,QACX;AAAA,QACA;AAAA,MACF;AACA;AAAA,IACF;AAEA,UAAM,EAAE,QAAQ,KAAK,IAAI,wBAAwB,OAAO;AAExD,QACE,aAAa,MAAM,kBAClB,aAAa,MAAM,SAAS,QAAQ,aAAa,MAAM,WAAW,QACnE;AACA,mBAAa,2BAA2B,KAAK,sBAAsB;AAAA,QACjE,gBAAgB,aAAa;AAAA,MAC/B,CAAC;AACD;AAAA,IACF;AAGA,QAAI,aAAa,aAAa,MAAM;AAEpC,QAAI,CAAC,YAAY;AAEf,YAAM,WAAW,MAAM,OAAO,UAAU,OAAO;AAAA,QAC7C,UAAU;AAAA,UACR,gBAAgB,OAAO,aAAa,GAAG;AAAA,UACvC,QAAQ,OAAO,KAAK,GAAG;AAAA;AAAA,UAEvB,QAAS,IAAI,OAA0C;AAAA,QACzD;AAAA,MACF,CAAC;AACD,mBAAa,SAAS;AAAA,IACxB;AAGA,UAAM,eAAe,MAAM,OAAO,cAAc,OAAO;AAAA,MACrD,UAAU;AAAA;AAAA,MACV,OAAO,CAAC,EAAE,OAAO,QAAQ,CAAC;AAAA;AAAA,MAC1B,QAAQ,CAAC,+BAA+B;AAAA;AAAA,MACxC,kBAAkB;AAAA,QAChB,sBAAsB,CAAC,MAAM;AAAA;AAAA,MAC/B;AAAA,MACA,kBAAkB;AAAA;AAAA,IACpB,CAAC;AAGD,QAAI,CAAC,cAAc;AACjB,mBAAa;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,UACE;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,MACF;AACA;AAAA,IACF;AAGA,UAAM,eAAe,eAAoC;AAAA,MACvD,MAAM;AAAA,QACJ,gBAAgB,aAAa;AAAA;AAAA,QAE7B,cAEK,aAAa,eACX,gBACF,iBAAiB;AAAA,QACtB,QAAQ,aAAa;AAAA;AAAA,MACvB;AAAA,IACF,CAAC;AAGD,QAAI,KAAK,YAAY;AAErB;AAAA,EACF,SAAS,OAAO;AAGd,iBAAa,uBAAuB,KAAK,KAAiB;AAC1D;AAAA,EACF;AACF;AAWO,MAAM,qBAAqB,OAChC,MACA,QACkB;AAClB,MAAI;AAGF,UAAM,EAAE,cAAc,KAAK,IAAI,IAAI;AAGnC,QAAI,CAAC,cAAc;AACjB,mBAAa,2BAA2B,KAAK,wBAAwB;AACrE;AAAA,IACF;AAGA,QAAI,CAAC,MAAM;AACT,mBAAa,2BAA2B,KAAK,gBAAgB;AAC7D;AAAA,IACF;AAGA,QAAI,CAAC,aAAa,UAAU,IAAI,MAAM,EAAE,SAAS,OAAO,KAAK,GAAG,CAAC,GAAG;AAClE,mBAAa;AAAA,QACX;AAAA,QACA;AAAA,MACF;AACA;AAAA,IACF;AAGA,QAAI,CAAC,aAAa,MAAM,gBAAgB;AACtC,mBAAa;AAAA,QACX;AAAA,QACA;AAAA,MACF;AACA;AAAA,IACF;AAGA,UAAM,OAAO,cAAc,OAAO,aAAa,KAAK,cAAc;AAGlE,UAAM,OAAO,MAAM,oBAAoB;AAAA,MACrC,aAAa,KAAK;AAAA,MAClB,OAAO,aAAa,GAAG;AAAA,IACzB;AAGA,QAAI,CAAC,MAAM;AACT,mBAAa;AAAA,QACX;AAAA,QACA;AAAA,MACF;AACA;AAAA,IACF;AAGA,UAAM,gBAAgB,eAAuC;AAAA,MAC3D,SAAS,EAAE;AAAA,QACT,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN,CAAC;AAAA,MACD,aAAa,EAAE;AAAA,QACb,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MACN,CAAC;AAAA,MACD,MAAM;AAAA,IACR,CAAC;AAGD,QAAI,KAAK,aAAa;AAAA,EACxB,SAAS,OAAO;AAEd,iBAAa,uBAAuB,KAAK,KAAiB;AAAA,EAC5D;AACF;","names":[]}
|
package/dist/esm/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import compression from "compression";
|
|
|
2
2
|
import cookieParser from "cookie-parser";
|
|
3
3
|
import cors from "cors";
|
|
4
4
|
import dotenv from "dotenv";
|
|
5
|
-
import express
|
|
5
|
+
import express from "express";
|
|
6
6
|
import { intlayer, t } from "express-intlayer";
|
|
7
7
|
import helmet from "helmet";
|
|
8
8
|
import {
|
|
@@ -41,7 +41,11 @@ app.use(cookieParser());
|
|
|
41
41
|
app.use(intlayer());
|
|
42
42
|
const isDev = env === "development";
|
|
43
43
|
connectDB();
|
|
44
|
-
app.post(
|
|
44
|
+
app.post(
|
|
45
|
+
"/webhook/stripe",
|
|
46
|
+
express.raw({ type: "application/json" }),
|
|
47
|
+
stripeWebhook
|
|
48
|
+
);
|
|
45
49
|
app.use(compression());
|
|
46
50
|
app.use(express.json({ limit: "50mb" }));
|
|
47
51
|
app.use(express.urlencoded({ extended: true }));
|
|
@@ -54,7 +58,10 @@ const corsOptions = {
|
|
|
54
58
|
"Content-Type",
|
|
55
59
|
"credentials",
|
|
56
60
|
"cache-control",
|
|
57
|
-
"Access-Control-Allow-Origin"
|
|
61
|
+
"Access-Control-Allow-Origin",
|
|
62
|
+
"private-state-token-redemption",
|
|
63
|
+
"private-state-token-issuance",
|
|
64
|
+
"browsing-topics"
|
|
58
65
|
],
|
|
59
66
|
exposedHeaders: [""],
|
|
60
67
|
preflightContinue: false,
|
package/dist/esm/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/index.ts"],"sourcesContent":["/* eslint-disable import/order */\n\n// Libraries\nimport compression from 'compression';\nimport cookieParser from 'cookie-parser';\nimport cors, { type CorsOptions } from 'cors';\nimport dotenv from 'dotenv';\nimport express, {
|
|
1
|
+
{"version":3,"sources":["../../src/index.ts"],"sourcesContent":["/* eslint-disable import/order */\n\n// Libraries\nimport compression from 'compression';\nimport cookieParser from 'cookie-parser';\nimport cors, { type CorsOptions } from 'cors';\nimport dotenv from 'dotenv';\nimport express, { type Express } from 'express';\nimport { intlayer, t } from 'express-intlayer';\nimport helmet from 'helmet';\n\n// Middlewares\nimport {\n attachOAuthInstance,\n authenticateOAuth2,\n RequestWithOAuth2Information,\n} from '@middlewares/oAuth2.middleware';\nimport { logAPIRequestURL } from '@middlewares/request.middleware';\nimport {\n checkUser,\n checkOrganization,\n checkProject,\n checkAdmin,\n ResponseWithInformation,\n} from '@middlewares/sessionAuth.middleware';\n\n// Routes\nimport { dictionaryRouter } from '@routes/dictionary.routes';\nimport { organizationRouter } from '@routes/organization.routes';\nimport { projectRouter } from '@routes/project.routes';\nimport { sessionAuthRouter } from '@routes/sessionAuth.routes';\nimport { userRouter } from '@routes/user.routes';\nimport { stripeRouter } from '@routes/stripe.routes';\n\n// Webhooks\nimport { stripeWebhook } from '@webhooks/stripe.webhook';\n\n// Controllers\nimport { getOAuth2Token } from '@controllers/oAuth2.controller';\nimport {\n getSessionInformation,\n setCSRFToken,\n} from '@controllers/sessionAuth.controller';\n\n// Utils\nimport { doubleCsrfProtection } from '@utils/CSRF';\nimport { connectDB } from '@utils/mongoDB/connectDB';\n\n// Logger\nimport { logger } from './logger';\n\nconst app: Express = express();\n\napp.disable('x-powered-by'); // Disabled to prevent attackers from knowing that the app is running Express\napp.use(helmet());\n\n// Environment variables\nconst env = app.get('env');\n\nlogger.info(`run as ${env}`);\n\ndotenv.config({ path: ['.env', `.env.${env}`] });\n\n// Parse incoming requests with cookies\napp.use(cookieParser());\n\n// Load internationalization request handler\napp.use(intlayer());\n\nconst isDev = env === 'development';\n\n// Connect to MongoDB\nconnectDB();\n\n// Stripe\napp.post(\n '/webhook/stripe',\n express.raw({ type: 'application/json' }),\n stripeWebhook\n);\n\n// Compress all HTTP responses\napp.use(compression());\n\n// Parse incoming requests with JSON payloads\napp.use(express.json({ limit: '50mb' }));\n\n// Parse incoming requests with urlencoded payloads\napp.use(express.urlencoded({ extended: true }));\n\n// CORS\nconst whitelist: string[] = [process.env.CLIENT_URL!];\nconst corsOptions: CorsOptions = {\n origin: whitelist,\n credentials: true,\n allowedHeaders: [\n 'authorization',\n 'Content-Type',\n 'credentials',\n 'cache-control',\n 'Access-Control-Allow-Origin',\n 'private-state-token-redemption',\n 'private-state-token-issuance',\n 'browsing-topics',\n ],\n\n exposedHeaders: [''],\n preflightContinue: false,\n methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',\n};\napp.use(cors(corsOptions));\nlogger.info('url whitelist : ', whitelist.join(', '));\n\n// Liveness check\napp.get('/', (_req, res) => {\n res.send(\n t({\n en: 'Ok - locale: en',\n fr: 'Ok - locale: fr',\n es: 'Ok - locale: es',\n })\n );\n});\n\n// middleware - jwt & session auth\napp.use(/(.*)/, checkUser);\napp.use(/(.*)/, checkOrganization);\napp.use(/(.*)/, checkProject);\napp.use(/(.*)/, checkAdmin);\n\n// debug\nif (isDev) {\n app.use(logAPIRequestURL);\n}\n\n// Sessions\napp.get('/session', getSessionInformation);\napp.use('/api/auth', sessionAuthRouter);\n\n// CSRF\napp.get('/csrf-token', setCSRFToken);\n\n// oAuth2\napp.use(/(.*)/, attachOAuthInstance);\napp.post('/oauth2/token', getOAuth2Token); // Route to get the token\napp.use(/(.*)/, (req, res, next) => {\n // If the request is not already authenticated check the oAuth2 token\n if (!res.locals.authType) {\n return authenticateOAuth2(\n req as RequestWithOAuth2Information,\n res as ResponseWithInformation,\n next\n );\n }\n next();\n});\n\n// CSRF protection\napp.use(/(.*)/, (req, res, next) => {\n // If the request is authenticated using the session auth check the CSRF token\n if (res.locals.authType === 'session') {\n return doubleCsrfProtection(req, res, next);\n }\n next();\n});\n\n// Routes\napp.use('/api/user', userRouter);\napp.use('/api/organization', organizationRouter);\napp.use('/api/project', projectRouter);\napp.use('/api/dictionary', dictionaryRouter);\napp.use('/api/stripe', stripeRouter);\n\n// Server\napp.listen(process.env.PORT, () => {\n logger.info(`Listening on port ${process.env.PORT}`);\n});\n\n// Export tu use as serverless function\nexport default app;\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { logger } from './../logger/index.mjs';
|
|
2
1
|
const logAPIRequestURL = (req, res, next) => {
|
|
3
2
|
const queryDetails = {
|
|
4
3
|
params: req.params,
|
|
@@ -6,9 +5,6 @@ const logAPIRequestURL = (req, res, next) => {
|
|
|
6
5
|
body: req.body,
|
|
7
6
|
locals: res.locals
|
|
8
7
|
};
|
|
9
|
-
logger.info(
|
|
10
|
-
`API Request - ${req.method} - ${req.originalUrl} - ${JSON.stringify(queryDetails, null, 2)}`
|
|
11
|
-
);
|
|
12
8
|
next();
|
|
13
9
|
};
|
|
14
10
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/middlewares/request.middleware.ts"],"sourcesContent":["import { logger } from '@logger';\nimport type { Request, NextFunction } from 'express';\nimport type { ResponseWithInformation } from './sessionAuth.middleware';\n\nexport const logAPIRequestURL = (\n req: Request,\n res: ResponseWithInformation,\n next: NextFunction\n): void => {\n const queryDetails = {\n params: req.params,\n query: req.query,\n body: req.body,\n locals: res.locals,\n };\n\n logger.info(\n
|
|
1
|
+
{"version":3,"sources":["../../../src/middlewares/request.middleware.ts"],"sourcesContent":["import { logger } from '@logger';\nimport type { Request, NextFunction } from 'express';\nimport type { ResponseWithInformation } from './sessionAuth.middleware';\n\nexport const logAPIRequestURL = (\n req: Request,\n res: ResponseWithInformation,\n next: NextFunction\n): void => {\n const queryDetails = {\n params: req.params,\n query: req.query,\n body: req.body,\n locals: res.locals,\n };\n\n // logger.info(\n // `API Request - ${req.method} - ${req.originalUrl} - ${JSON.stringify(queryDetails, null, 2)}`\n // );\n\n next();\n};\n"],"mappings":"AAIO,MAAM,mBAAmB,CAC9B,KACA,KACA,SACS;AACT,QAAM,eAAe;AAAA,IACnB,QAAQ,IAAI;AAAA,IACZ,OAAO,IAAI;AAAA,IACX,MAAM,IAAI;AAAA,IACV,QAAQ,IAAI;AAAA,EACd;AAMA,OAAK;AACP;","names":[]}
|
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
cancelSubscription,
|
|
3
|
+
getSubscription
|
|
4
|
+
} from './../controllers/stripe.controller.mjs';
|
|
2
5
|
import { Router } from "express";
|
|
3
6
|
const stripeRouter = Router();
|
|
4
7
|
const baseURL = `${process.env.BACKEND_URL}/api/stipe`;
|
|
5
8
|
const stripeRoutes = {
|
|
6
|
-
|
|
9
|
+
createSubscription: {
|
|
7
10
|
urlModel: "/create-subscription",
|
|
8
11
|
url: `${baseURL}/create-subscription`,
|
|
9
12
|
method: "POST"
|
|
13
|
+
},
|
|
14
|
+
cancelSubscription: {
|
|
15
|
+
urlModel: "/cancel-subscription",
|
|
16
|
+
url: `${baseURL}/cancel-subscription`,
|
|
17
|
+
method: "POST"
|
|
10
18
|
}
|
|
11
19
|
};
|
|
12
|
-
stripeRouter.post(stripeRoutes.
|
|
20
|
+
stripeRouter.post(stripeRoutes.createSubscription.urlModel, getSubscription);
|
|
21
|
+
stripeRouter.post(stripeRoutes.cancelSubscription.urlModel, cancelSubscription);
|
|
13
22
|
export {
|
|
14
23
|
stripeRouter,
|
|
15
24
|
stripeRoutes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/routes/stripe.routes.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"sources":["../../../src/routes/stripe.routes.ts"],"sourcesContent":["import {\n cancelSubscription,\n getSubscription,\n} from '@controllers/stripe.controller';\nimport { Router } from 'express';\nimport { Routes } from '@/types/Routes';\n\nexport const stripeRouter: Router = Router();\n\nconst baseURL = `${process.env.BACKEND_URL}/api/stipe`;\n\nexport const stripeRoutes = {\n createSubscription: {\n urlModel: '/create-subscription',\n url: `${baseURL}/create-subscription`,\n method: 'POST',\n },\n cancelSubscription: {\n urlModel: '/cancel-subscription',\n url: `${baseURL}/cancel-subscription`,\n method: 'POST',\n },\n} satisfies Routes;\n\nstripeRouter.post(stripeRoutes.createSubscription.urlModel, getSubscription);\n\nstripeRouter.post(stripeRoutes.cancelSubscription.urlModel, cancelSubscription);\n"],"mappings":"AAAA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AAGhB,MAAM,eAAuB,OAAO;AAE3C,MAAM,UAAU,GAAG,QAAQ,IAAI,WAAW;AAEnC,MAAM,eAAe;AAAA,EAC1B,oBAAoB;AAAA,IAClB,UAAU;AAAA,IACV,KAAK,GAAG,OAAO;AAAA,IACf,QAAQ;AAAA,EACV;AAAA,EACA,oBAAoB;AAAA,IAClB,UAAU;AAAA,IACV,KAAK,GAAG,OAAO;AAAA,IACf,QAAQ;AAAA,EACV;AACF;AAEA,aAAa,KAAK,aAAa,mBAAmB,UAAU,eAAe;AAE3E,aAAa,KAAK,aAAa,mBAAmB,UAAU,kBAAkB;","names":[]}
|
|
@@ -4,22 +4,44 @@ const planSchema = new Schema(
|
|
|
4
4
|
type: {
|
|
5
5
|
type: String,
|
|
6
6
|
required: true,
|
|
7
|
-
enum: ["
|
|
8
|
-
|
|
7
|
+
enum: ["PREMIUM", "ENTERPRISE"]
|
|
8
|
+
},
|
|
9
|
+
period: {
|
|
10
|
+
type: String,
|
|
11
|
+
required: true,
|
|
12
|
+
enum: ["MONTHLY", "YEARLY"],
|
|
13
|
+
default: "MONTHLY"
|
|
9
14
|
},
|
|
10
15
|
creatorId: {
|
|
11
16
|
type: Schema.Types.ObjectId,
|
|
12
17
|
ref: "User",
|
|
13
18
|
required: true
|
|
14
19
|
},
|
|
20
|
+
subscriptionId: {
|
|
21
|
+
type: String,
|
|
22
|
+
required: true
|
|
23
|
+
},
|
|
24
|
+
customerId: {
|
|
25
|
+
type: String,
|
|
26
|
+
required: true
|
|
27
|
+
},
|
|
15
28
|
priceId: {
|
|
16
|
-
type: String
|
|
29
|
+
type: String,
|
|
30
|
+
required: true
|
|
17
31
|
},
|
|
18
32
|
status: {
|
|
19
33
|
type: String,
|
|
20
34
|
required: true,
|
|
21
|
-
enum: [
|
|
22
|
-
|
|
35
|
+
enum: [
|
|
36
|
+
"active",
|
|
37
|
+
"canceled",
|
|
38
|
+
"past_due",
|
|
39
|
+
"unpaid",
|
|
40
|
+
"incomplete",
|
|
41
|
+
"incomplete_expired",
|
|
42
|
+
"paused",
|
|
43
|
+
"trialing"
|
|
44
|
+
]
|
|
23
45
|
}
|
|
24
46
|
},
|
|
25
47
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/schemas/plans.schema.ts"],"sourcesContent":["import { Schema } from 'mongoose';\nimport { Plan } from '@/types/plan.types';\n\nexport const planSchema = new Schema<Plan>(\n {\n type: {\n type: String,\n required: true,\n enum: ['
|
|
1
|
+
{"version":3,"sources":["../../../src/schemas/plans.schema.ts"],"sourcesContent":["import { Schema } from 'mongoose';\nimport { Plan } from '@/types/plan.types';\n\nexport const planSchema = new Schema<Plan>(\n {\n type: {\n type: String,\n required: true,\n enum: ['PREMIUM', 'ENTERPRISE'],\n },\n period: {\n type: String,\n required: true,\n enum: ['MONTHLY', 'YEARLY'],\n default: 'MONTHLY',\n },\n creatorId: {\n type: Schema.Types.ObjectId,\n ref: 'User',\n required: true,\n },\n subscriptionId: {\n type: String,\n required: true,\n },\n customerId: {\n type: String,\n required: true,\n },\n priceId: {\n type: String,\n required: true,\n },\n status: {\n type: String,\n required: true,\n enum: [\n 'active',\n 'canceled',\n 'past_due',\n 'unpaid',\n 'incomplete',\n 'incomplete_expired',\n 'paused',\n 'trialing',\n ],\n },\n },\n {\n timestamps: true,\n }\n);\n"],"mappings":"AAAA,SAAS,cAAc;AAGhB,MAAM,aAAa,IAAI;AAAA,EAC5B;AAAA,IACE,MAAM;AAAA,MACJ,MAAM;AAAA,MACN,UAAU;AAAA,MACV,MAAM,CAAC,WAAW,YAAY;AAAA,IAChC;AAAA,IACA,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,UAAU;AAAA,MACV,MAAM,CAAC,WAAW,QAAQ;AAAA,MAC1B,SAAS;AAAA,IACX;AAAA,IACA,WAAW;AAAA,MACT,MAAM,OAAO,MAAM;AAAA,MACnB,KAAK;AAAA,MACL,UAAU;AAAA,IACZ;AAAA,IACA,gBAAgB;AAAA,MACd,MAAM;AAAA,MACN,UAAU;AAAA,IACZ;AAAA,IACA,YAAY;AAAA,MACV,MAAM;AAAA,MACN,UAAU;AAAA,IACZ;AAAA,IACA,SAAS;AAAA,MACP,MAAM;AAAA,MACN,UAAU;AAAA,IACZ;AAAA,IACA,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,UAAU;AAAA,MACV,MAAM;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,YAAY;AAAA,EACd;AACF;","names":[]}
|
|
@@ -36,11 +36,6 @@ const createOrganization = async (organization, userId) => {
|
|
|
36
36
|
creatorId: userId,
|
|
37
37
|
membersIds: [userId],
|
|
38
38
|
adminsIds: [userId],
|
|
39
|
-
plan: {
|
|
40
|
-
name: "FREE",
|
|
41
|
-
statue: "ACTIVE",
|
|
42
|
-
creatorId: userId
|
|
43
|
-
},
|
|
44
39
|
...organization
|
|
45
40
|
});
|
|
46
41
|
return result;
|
|
@@ -73,19 +68,14 @@ const deleteOrganizationById = async (organizationId) => {
|
|
|
73
68
|
}
|
|
74
69
|
return organization;
|
|
75
70
|
};
|
|
76
|
-
const saveStripeCustomerId = async (organization, customerId) => {
|
|
77
|
-
if (!organization) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
await OrganizationModel.updateOne(
|
|
81
|
-
{ _id: organization._id },
|
|
82
|
-
{ $set: { plan: { customerId } } }
|
|
83
|
-
);
|
|
84
|
-
};
|
|
85
71
|
const updatePlan = async (organization, plan) => {
|
|
72
|
+
let prevPlan = organization.plan ?? {};
|
|
73
|
+
if (typeof prevPlan?.toObject === "function") {
|
|
74
|
+
prevPlan = prevPlan.toObject();
|
|
75
|
+
}
|
|
86
76
|
const updateOrganizationResult = await OrganizationModel.updateOne(
|
|
87
77
|
{ _id: organization._id },
|
|
88
|
-
{ $set: { plan: { ...
|
|
78
|
+
{ $set: { plan: { ...prevPlan, ...plan } } },
|
|
89
79
|
{ new: true }
|
|
90
80
|
);
|
|
91
81
|
if (updateOrganizationResult.matchedCount === 0) {
|
|
@@ -96,23 +86,13 @@ const updatePlan = async (organization, plan) => {
|
|
|
96
86
|
const updatedOrganization = await getOrganizationById(organization._id);
|
|
97
87
|
return updatedOrganization;
|
|
98
88
|
};
|
|
99
|
-
const getOrganizationByCustomerId = async (customerId) => {
|
|
100
|
-
const organization = await OrganizationModel.findOne({
|
|
101
|
-
plan: {
|
|
102
|
-
customerId
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
return organization;
|
|
106
|
-
};
|
|
107
89
|
export {
|
|
108
90
|
countOrganizations,
|
|
109
91
|
createOrganization,
|
|
110
92
|
deleteOrganizationById,
|
|
111
93
|
findOrganizations,
|
|
112
|
-
getOrganizationByCustomerId,
|
|
113
94
|
getOrganizationById,
|
|
114
95
|
getOrganizationsByOwner,
|
|
115
|
-
saveStripeCustomerId,
|
|
116
96
|
updateOrganizationById,
|
|
117
97
|
updatePlan
|
|
118
98
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/services/organization.service.ts"],"sourcesContent":["import { OrganizationModel } from '@models/organization.model';\nimport { GenericError } from '@utils/errors';\nimport type { OrganizationFilters } from '@utils/filtersAndPagination/getOrganizationFiltersAndPagination';\nimport {\n type OrganizationFields,\n validateOrganization,\n} from '@utils/validation/validateOrganization';\nimport type { ObjectId } from 'mongoose';\nimport type {\n Organization,\n OrganizationCreationData,\n OrganizationDocument,\n} from '@/types/organization.types';\nimport type { Plan } from '@/types/plan.types';\n\n/**\n * Finds organizations based on filters and pagination options.\n * @param filters - MongoDB filter query.\n * @param skip - Number of documents to skip.\n * @param limit - Number of documents to limit.\n * @returns List of organizations matching the filters.\n */\nexport const findOrganizations = async (\n filters: OrganizationFilters,\n skip: number,\n limit: number\n): Promise<OrganizationDocument[]> => {\n return await OrganizationModel.find(filters).skip(skip).limit(limit);\n};\n\n/**\n * Finds an organization by its ID.\n * @param organizationId - The ID of the organization to find.\n * @returns The organization matching the ID.\n */\nexport const getOrganizationById = async (\n organizationId: ObjectId | string\n): Promise<OrganizationDocument> => {\n const organization = await OrganizationModel.findById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', { organizationId });\n }\n\n return organization;\n};\n\n/**\n * Retrieves an organization by its owner.\n * @param userId - The ID of the user to find the organization.\n * @returns The organizations matching the user ID.\n */\nexport const getOrganizationsByOwner = async (\n userId: string | ObjectId\n): Promise<OrganizationDocument[] | null> => {\n const organization = await OrganizationModel.find({\n creatorId: userId,\n });\n\n return organization;\n};\n\n/**\n * Counts the total number of organizations that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of organizations.\n */\nexport const countOrganizations = async (\n filters: OrganizationFilters\n): Promise<number> => {\n const result = await OrganizationModel.countDocuments(filters);\n\n if (typeof result === 'undefined') {\n throw new GenericError('ORGANIZATION_COUNT_FAILED', { filters });\n }\n\n return result;\n};\n\n/**\n * Creates a new organization in the database.\n * @param organization - The organization data to create.\n * @returns The created organization.\n */\nexport const createOrganization = async (\n organization: OrganizationCreationData,\n userId: string | ObjectId\n): Promise<OrganizationDocument> => {\n const errors = validateOrganization(organization, ['name']);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('ORGANIZATION_INVALID_FIELDS', { errors });\n }\n\n try {\n const result = await OrganizationModel.create({\n creatorId: userId,\n membersIds: [userId],\n adminsIds: [userId],\n
|
|
1
|
+
{"version":3,"sources":["../../../src/services/organization.service.ts"],"sourcesContent":["import { OrganizationModel } from '@models/organization.model';\nimport { GenericError } from '@utils/errors';\nimport type { OrganizationFilters } from '@utils/filtersAndPagination/getOrganizationFiltersAndPagination';\nimport {\n type OrganizationFields,\n validateOrganization,\n} from '@utils/validation/validateOrganization';\nimport type { ObjectId } from 'mongoose';\nimport type {\n Organization,\n OrganizationCreationData,\n OrganizationDocument,\n} from '@/types/organization.types';\nimport type { Plan, PlanDocument } from '@/types/plan.types';\n\n/**\n * Finds organizations based on filters and pagination options.\n * @param filters - MongoDB filter query.\n * @param skip - Number of documents to skip.\n * @param limit - Number of documents to limit.\n * @returns List of organizations matching the filters.\n */\nexport const findOrganizations = async (\n filters: OrganizationFilters,\n skip: number,\n limit: number\n): Promise<OrganizationDocument[]> => {\n return await OrganizationModel.find(filters).skip(skip).limit(limit);\n};\n\n/**\n * Finds an organization by its ID.\n * @param organizationId - The ID of the organization to find.\n * @returns The organization matching the ID.\n */\nexport const getOrganizationById = async (\n organizationId: ObjectId | string\n): Promise<OrganizationDocument> => {\n const organization = await OrganizationModel.findById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', { organizationId });\n }\n\n return organization;\n};\n\n/**\n * Retrieves an organization by its owner.\n * @param userId - The ID of the user to find the organization.\n * @returns The organizations matching the user ID.\n */\nexport const getOrganizationsByOwner = async (\n userId: string | ObjectId\n): Promise<OrganizationDocument[] | null> => {\n const organization = await OrganizationModel.find({\n creatorId: userId,\n });\n\n return organization;\n};\n\n/**\n * Counts the total number of organizations that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of organizations.\n */\nexport const countOrganizations = async (\n filters: OrganizationFilters\n): Promise<number> => {\n const result = await OrganizationModel.countDocuments(filters);\n\n if (typeof result === 'undefined') {\n throw new GenericError('ORGANIZATION_COUNT_FAILED', { filters });\n }\n\n return result;\n};\n\n/**\n * Creates a new organization in the database.\n * @param organization - The organization data to create.\n * @returns The created organization.\n */\nexport const createOrganization = async (\n organization: OrganizationCreationData,\n userId: string | ObjectId\n): Promise<OrganizationDocument> => {\n const errors = validateOrganization(organization, ['name']);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('ORGANIZATION_INVALID_FIELDS', { errors });\n }\n\n try {\n const result = await OrganizationModel.create({\n creatorId: userId,\n membersIds: [userId],\n adminsIds: [userId],\n ...organization,\n });\n\n return result;\n } catch (error) {\n throw new GenericError('ORGANIZATION_CREATION_FAILED', { error });\n }\n};\n\n/**\n * Updates an existing organization in the database by its ID.\n * @param organizationId - The ID of the organization to update.\n * @param organization - The updated organization data.\n * @returns The updated organization.\n */\nexport const updateOrganizationById = async (\n organizationId: ObjectId | string,\n organization: Partial<Organization>\n): Promise<OrganizationDocument> => {\n const updatedKeys = Object.keys(organization) as OrganizationFields;\n const errors = validateOrganization(organization, updatedKeys);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('ORGANIZATION_INVALID_FIELDS', {\n organizationId,\n errors,\n });\n }\n\n const result = await OrganizationModel.updateOne(\n { _id: organizationId },\n organization\n );\n\n if (result.matchedCount === 0) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', { organizationId });\n }\n\n return await getOrganizationById(organizationId);\n};\n\n/**\n * Deletes an organization from the database by its ID.\n * @param organizationId - The ID of the organization to delete.\n * @returns The result of the deletion operation.\n */\nexport const deleteOrganizationById = async (\n organizationId: ObjectId | string\n): Promise<OrganizationDocument> => {\n const organization =\n await OrganizationModel.findByIdAndDelete(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', { organizationId });\n }\n\n return organization;\n};\n\n/**\n * Updates an existing plan in the database by its ID.\n * @param planId - The ID of the plan to update.\n * @param plan - The updated plan data.\n * @returns The updated plan.\n */\nexport const updatePlan = async (\n organization: Organization | OrganizationDocument,\n plan: Partial<Plan>\n): Promise<OrganizationDocument | null> => {\n let prevPlan = organization.plan ?? {};\n\n if (typeof (prevPlan as PlanDocument)?.toObject === 'function') {\n prevPlan = (prevPlan as PlanDocument).toObject();\n }\n\n const updateOrganizationResult = await OrganizationModel.updateOne(\n { _id: organization._id },\n { $set: { plan: { ...prevPlan, ...plan } } },\n { new: true }\n );\n\n if (updateOrganizationResult.matchedCount === 0) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization._id,\n });\n }\n\n const updatedOrganization = await getOrganizationById(organization._id);\n\n return updatedOrganization;\n};\n"],"mappings":"AAAA,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAE7B;AAAA,EAEE;AAAA,OACK;AAgBA,MAAM,oBAAoB,OAC/B,SACA,MACA,UACoC;AACpC,SAAO,MAAM,kBAAkB,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,KAAK;AACrE;AAOO,MAAM,sBAAsB,OACjC,mBACkC;AAClC,QAAM,eAAe,MAAM,kBAAkB,SAAS,cAAc;AAEpE,MAAI,CAAC,cAAc;AACjB,UAAM,IAAI,aAAa,0BAA0B,EAAE,eAAe,CAAC;AAAA,EACrE;AAEA,SAAO;AACT;AAOO,MAAM,0BAA0B,OACrC,WAC2C;AAC3C,QAAM,eAAe,MAAM,kBAAkB,KAAK;AAAA,IAChD,WAAW;AAAA,EACb,CAAC;AAED,SAAO;AACT;AAOO,MAAM,qBAAqB,OAChC,YACoB;AACpB,QAAM,SAAS,MAAM,kBAAkB,eAAe,OAAO;AAE7D,MAAI,OAAO,WAAW,aAAa;AACjC,UAAM,IAAI,aAAa,6BAA6B,EAAE,QAAQ,CAAC;AAAA,EACjE;AAEA,SAAO;AACT;AAOO,MAAM,qBAAqB,OAChC,cACA,WACkC;AAClC,QAAM,SAAS,qBAAqB,cAAc,CAAC,MAAM,CAAC;AAE1D,MAAI,OAAO,KAAK,MAAM,EAAE,SAAS,GAAG;AAClC,UAAM,IAAI,aAAa,+BAA+B,EAAE,OAAO,CAAC;AAAA,EAClE;AAEA,MAAI;AACF,UAAM,SAAS,MAAM,kBAAkB,OAAO;AAAA,MAC5C,WAAW;AAAA,MACX,YAAY,CAAC,MAAM;AAAA,MACnB,WAAW,CAAC,MAAM;AAAA,MAClB,GAAG;AAAA,IACL,CAAC;AAED,WAAO;AAAA,EACT,SAAS,OAAO;AACd,UAAM,IAAI,aAAa,gCAAgC,EAAE,MAAM,CAAC;AAAA,EAClE;AACF;AAQO,MAAM,yBAAyB,OACpC,gBACA,iBACkC;AAClC,QAAM,cAAc,OAAO,KAAK,YAAY;AAC5C,QAAM,SAAS,qBAAqB,cAAc,WAAW;AAE7D,MAAI,OAAO,KAAK,MAAM,EAAE,SAAS,GAAG;AAClC,UAAM,IAAI,aAAa,+BAA+B;AAAA,MACpD;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,SAAS,MAAM,kBAAkB;AAAA,IACrC,EAAE,KAAK,eAAe;AAAA,IACtB;AAAA,EACF;AAEA,MAAI,OAAO,iBAAiB,GAAG;AAC7B,UAAM,IAAI,aAAa,8BAA8B,EAAE,eAAe,CAAC;AAAA,EACzE;AAEA,SAAO,MAAM,oBAAoB,cAAc;AACjD;AAOO,MAAM,yBAAyB,OACpC,mBACkC;AAClC,QAAM,eACJ,MAAM,kBAAkB,kBAAkB,cAAc;AAE1D,MAAI,CAAC,cAAc;AACjB,UAAM,IAAI,aAAa,0BAA0B,EAAE,eAAe,CAAC;AAAA,EACrE;AAEA,SAAO;AACT;AAQO,MAAM,aAAa,OACxB,cACA,SACyC;AACzC,MAAI,WAAW,aAAa,QAAQ,CAAC;AAErC,MAAI,OAAQ,UAA2B,aAAa,YAAY;AAC9D,eAAY,SAA0B,SAAS;AAAA,EACjD;AAEA,QAAM,2BAA2B,MAAM,kBAAkB;AAAA,IACvD,EAAE,KAAK,aAAa,IAAI;AAAA,IACxB,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,GAAG,KAAK,EAAE,EAAE;AAAA,IAC3C,EAAE,KAAK,KAAK;AAAA,EACd;AAEA,MAAI,yBAAyB,iBAAiB,GAAG;AAC/C,UAAM,IAAI,aAAa,8BAA8B;AAAA,MACnD,gBAAgB,aAAa;AAAA,IAC/B,CAAC;AAAA,EACH;AAEA,QAAM,sBAAsB,MAAM,oBAAoB,aAAa,GAAG;AAEtE,SAAO;AACT;","names":[]}
|