data-primals-engine 1.2.6-rc3 → 1.3.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 +38 -2
- package/client/package-lock.json +247 -4354
- package/client/package.json +16 -15
- package/client/src/App.jsx +13 -20
- package/client/src/App.scss +1 -0
- package/client/src/AssistantChat.jsx +5 -4
- package/client/src/DataEditor.jsx +2 -2
- package/client/src/DataTable.jsx +47 -3
- package/client/src/ExportDialog.jsx +2 -2
- package/client/src/Field.jsx +6 -18
- package/client/src/KanbanCard.jsx +4 -2
- package/client/src/KanbanConfigModal.jsx +5 -7
- package/client/src/ModelCreatorField.jsx +9 -9
- package/client/src/PackGallery.jsx +89 -9
- package/client/src/PackGallery.scss +58 -4
- package/client/src/RTETrans.jsx +11 -0
- package/client/src/RelationValue.jsx +3 -4
- package/client/src/constants.js +1 -1
- package/client/src/core/data.js +2 -1
- package/client/src/translations.js +80 -0
- package/package.json +8 -1
- package/server.js +4 -4
- package/src/constants.js +6 -0
- package/src/defaultModels.js +23 -10
- package/src/filter.js +35 -5
- package/src/i18n.js +1 -1
- package/src/modules/{assistant.js → assistant/assistant.js} +42 -27
- package/src/modules/assistant/constants.js +16 -0
- package/src/modules/data/data.core.js +1 -3
- package/src/modules/data/data.js +4601 -4525
- package/src/modules/data/data.routes.js +29 -3
- package/src/modules/mongodb.js +3 -1
- package/src/modules/user.js +12 -1
- package/src/modules/workflow.js +198 -117
- package/src/packs.js +1015 -9
- package/src/services/index.js +11 -0
- package/src/services/stripe.js +141 -0
- package/test/data.integration.test.js +66 -3
- package/test/workflow.actions.integration.test.js +474 -0
- package/test/workflow.integration.test.js +1 -1
package/src/packs.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {getModels} from "./modules/data/index.js";
|
|
2
|
+
import {providers} from "./modules/assistant/constants.js";
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
/*
|
|
@@ -106,6 +107,7 @@ export const getAllPacks = async () => {
|
|
|
106
107
|
"description": "A scalable workflow that sends campaign emails in chunks by dynamically querying contacts from an audience.",
|
|
107
108
|
"startStep": { "$link": { "name": "Start Campaign Processing", "_model": "workflowStep" } }
|
|
108
109
|
}],
|
|
110
|
+
|
|
109
111
|
"workflowAction": [
|
|
110
112
|
{
|
|
111
113
|
"name": "Set Campaign to 'in_progress'",
|
|
@@ -1354,16 +1356,10 @@ return { processedChunk: context.result.chunk };
|
|
|
1354
1356
|
"dataFilter": { "$or": [{"$eq": [{ "$type": "$seoDescription"}, "missing"]}, {"$eq": ["$seoDescription", ""]}]},
|
|
1355
1357
|
"isActive": true
|
|
1356
1358
|
}],
|
|
1357
|
-
"env":
|
|
1358
|
-
name:
|
|
1359
|
-
value: "demo"
|
|
1360
|
-
},{
|
|
1361
|
-
name: "GOOGLE_API_KEY",
|
|
1362
|
-
value: "demo"
|
|
1363
|
-
},{
|
|
1364
|
-
name: "DEEPSEEK_API_KEY",
|
|
1359
|
+
"env": Object.values(providers).map(m => ({
|
|
1360
|
+
name: m.key,
|
|
1365
1361
|
value: "demo"
|
|
1366
|
-
}
|
|
1362
|
+
}))
|
|
1367
1363
|
}
|
|
1368
1364
|
}
|
|
1369
1365
|
},
|
|
@@ -4051,6 +4047,1016 @@ return { processedChunk: context.result.chunk };
|
|
|
4051
4047
|
}
|
|
4052
4048
|
|
|
4053
4049
|
}
|
|
4050
|
+
},
|
|
4051
|
+
{
|
|
4052
|
+
"name": "Stripe Integration Pro",
|
|
4053
|
+
"description": `
|
|
4054
|
+
This package is a comprehensive solution for integrating Stripe into your application. It not only manages payments, but also automates the entire billing and customer management ecosystem. In summary, this package allows you to:
|
|
4055
|
+
|
|
4056
|
+
### 1. Automatically synchronize your Stripe data:
|
|
4057
|
+
Thanks to webhooks, all important information (customers, subscriptions, products, prices, invoices) is created and updated in real time in your local database. This gives you a reliable source of truth without manual effort.
|
|
4058
|
+
|
|
4059
|
+
### 2. Manage the complete subscription lifecycle:
|
|
4060
|
+
• Creation: When a customer subscribes, the package creates the subscription locally and can send a welcome email.
|
|
4061
|
+
• Updates: Status changes (e.g., from trial to active) or cancellations are automatically reflected. • Payment failures: If a subscription payment fails, an email is automatically sent to the customer asking them to update their payment information.
|
|
4062
|
+
|
|
4063
|
+
### 3. Automate payment and invoice processing:
|
|
4064
|
+
• Records every successful payment in your database. • Sends receipts by email after a payment. • Manages invoices (creation, payment, failure) and can send them to customers.
|
|
4065
|
+
|
|
4066
|
+
### 4. Facilitate the payment process for your users:
|
|
4067
|
+
• Includes a workflow to create Stripe Checkout sessions, whether for a one-time payment or to start a new subscription.
|
|
4068
|
+
|
|
4069
|
+
### 5. Provide an overview of your finances:
|
|
4070
|
+
• Offers a pre-configured dashboard with essential key performance indicators (KPIs):
|
|
4071
|
+
• Total revenue • Number of successful payments • Average payment value
|
|
4072
|
+
• Refund rate
|
|
4073
|
+
• Displays a graph of payment trends over time.
|
|
4074
|
+
|
|
4075
|
+
In short, this pack transforms your application into a robust and automated billing platform, while improving your customers' experience through clear and timely communications.
|
|
4076
|
+
|
|
4077
|
+
### Webhook Configuration
|
|
4078
|
+
|
|
4079
|
+
Ajoutez ces événements supplémentaires pour une synchronisation complète :
|
|
4080
|
+
|
|
4081
|
+
* Customer Events:
|
|
4082
|
+
- customer.created
|
|
4083
|
+
- customer.updated
|
|
4084
|
+
- customer.deleted
|
|
4085
|
+
|
|
4086
|
+
* Product Events:
|
|
4087
|
+
- product.created
|
|
4088
|
+
- product.updated
|
|
4089
|
+
- product.deleted
|
|
4090
|
+
- price.created
|
|
4091
|
+
- price.updated
|
|
4092
|
+
|
|
4093
|
+
### Webhook Configuration
|
|
4094
|
+
|
|
4095
|
+
To make the integration fully work, you need to configure a webhook in your Stripe Dashboard. This allows Stripe to send real-time notifications (like \`payment.succeeded\` or \`customer.subscription.created\`) to your application.
|
|
4096
|
+
|
|
4097
|
+
1. **Get Your Webhook URL:**
|
|
4098
|
+
Your application's webhook URL is:
|
|
4099
|
+
https://<your-domain.com>/api/actions/stripe-webhook
|
|
4100
|
+
Replace <your-domain.com> with your actual public domain.
|
|
4101
|
+
|
|
4102
|
+
2. **Add Endpoint in Stripe:**
|
|
4103
|
+
* Go to your Stripe Dashboard.
|
|
4104
|
+
* Navigate to **Developers > Webhooks**.
|
|
4105
|
+
* Click **+ Add an endpoint**.
|
|
4106
|
+
* Paste your URL in the **Endpoint URL** field.
|
|
4107
|
+
|
|
4108
|
+
3. **Select Events:**
|
|
4109
|
+
Click on **+ Select events** and choose the following events to listen to:
|
|
4110
|
+
* invoice.paid
|
|
4111
|
+
* invoice.payment_failed
|
|
4112
|
+
* customer.subscription.created
|
|
4113
|
+
* customer.subscription.updated
|
|
4114
|
+
* customer.subscription.deleted
|
|
4115
|
+
* payment_intent.succeeded
|
|
4116
|
+
|
|
4117
|
+
4. **Secure Your Webhook:**
|
|
4118
|
+
* After creating the endpoint, Stripe will show a **Signing secret**. Click to reveal it.
|
|
4119
|
+
* Copy this secret (it starts with \`whsec_...\`).
|
|
4120
|
+
* In your application's **\`env\` model**, find the variable named \`STRIPE_WEBHOOK_SECRET\` and paste the key there.
|
|
4121
|
+
|
|
4122
|
+
This ensures that your application only processes legitimate requests from Stripe.`,
|
|
4123
|
+
"tags": ["payment", "stripe", "e-commerce", "subscription", "billing"],
|
|
4124
|
+
"models": [
|
|
4125
|
+
"endpoint",
|
|
4126
|
+
"dashboard",
|
|
4127
|
+
"kpi",
|
|
4128
|
+
"workflow",
|
|
4129
|
+
"workflowStep",
|
|
4130
|
+
"workflowAction",
|
|
4131
|
+
"workflowTrigger",
|
|
4132
|
+
"env",
|
|
4133
|
+
"currency",
|
|
4134
|
+
{
|
|
4135
|
+
"name": "StripeCustomer",
|
|
4136
|
+
"description": "Maps local users to Stripe customers with billing details.",
|
|
4137
|
+
"fields": [
|
|
4138
|
+
{ "name": "user", "type": "relation", "relation": "user", "required": true, "asMain": true },
|
|
4139
|
+
{ "name": "stripeCustomerId", "type": "string", "required": true, "unique": true, "hiddenable": true },
|
|
4140
|
+
{ "name": "email", "type": "string", "required": true },
|
|
4141
|
+
{ "name": "name", "type": "string" },
|
|
4142
|
+
{ "name": "phone", "type": "string" },
|
|
4143
|
+
{ "name": "address", "type": "code","language": "json"}/* "fields": [
|
|
4144
|
+
{ "name": "line1", "type": "string" },
|
|
4145
|
+
{ "name": "line2", "type": "string" },
|
|
4146
|
+
{ "name": "city", "type": "string" },
|
|
4147
|
+
{ "name": "state", "type": "string" },
|
|
4148
|
+
{ "name": "postal_code", "type": "string" },
|
|
4149
|
+
{ "name": "country", "type": "string" }
|
|
4150
|
+
]}*/,
|
|
4151
|
+
{ "name": "taxExempt", "type": "enum", "items": ["none", "exempt", "reverse"], "default": "none" },
|
|
4152
|
+
{ "name": "defaultPaymentMethod", "type": "string" },
|
|
4153
|
+
{ "name": "invoiceSettings", "type": "code", "language": "json"}/* "fields": [
|
|
4154
|
+
{ "name": "customFields", "type": "array", "itemsType": "object" },
|
|
4155
|
+
{ "name": "footer", "type": "string" }
|
|
4156
|
+
]}*/,
|
|
4157
|
+
{ "name": "metadata", "type": "code", "language": "json" }
|
|
4158
|
+
]
|
|
4159
|
+
},
|
|
4160
|
+
{
|
|
4161
|
+
"name": "StripeSubscription",
|
|
4162
|
+
"description": "Tracks all subscription details with Stripe.",
|
|
4163
|
+
"fields": [
|
|
4164
|
+
{ "name": "stripeSubscriptionId", "type": "string", "required": true, "unique": true, "asMain": true },
|
|
4165
|
+
{ "name": "user", "type": "relation", "relation": "user", "required": true },
|
|
4166
|
+
{ "name": "customer", "type": "relation", "relation": "StripeCustomer", "required": true },
|
|
4167
|
+
{ "name": "plan", "type": "relation", "relation": "StripePlan", "required": true },
|
|
4168
|
+
{ "name": "status", "type": "enum", "items": ["trialing", "active", "past_due", "canceled", "unpaid", "incomplete", "incomplete_expired"], "required": true },
|
|
4169
|
+
{ "name": "currentPeriodStart", "type": "datetime" },
|
|
4170
|
+
{ "name": "currentPeriodEnd", "type": "datetime" },
|
|
4171
|
+
{ "name": "cancelAtPeriodEnd", "type": "boolean", "default": false },
|
|
4172
|
+
{ "name": "canceledAt", "type": "datetime" },
|
|
4173
|
+
{ "name": "daysUntilDue", "type": "number" },
|
|
4174
|
+
{ "name": "defaultPaymentMethod", "type": "string" },
|
|
4175
|
+
{ "name": "latestInvoice", "type": "relation", "relation": "StripeInvoice" },
|
|
4176
|
+
{ "name": "startDate", "type": "datetime" },
|
|
4177
|
+
{ "name": "trialEnd", "type": "datetime" },
|
|
4178
|
+
{ "name": "metadata", "type": "code", "language": "json" }
|
|
4179
|
+
]
|
|
4180
|
+
},
|
|
4181
|
+
{
|
|
4182
|
+
"name": "StripePlan",
|
|
4183
|
+
"description": "Subscription plans with detailed pricing information.",
|
|
4184
|
+
"fields": [
|
|
4185
|
+
{ "name": "name", "type": "string", "required": true, "asMain": true },
|
|
4186
|
+
{ "name": "description", "type": "richtext" },
|
|
4187
|
+
{ "name": "stripeProductId", "type": "string", "required": true, "unique": true },
|
|
4188
|
+
{ "name": "stripePriceId", "type": "string", "required": true, "unique": true },
|
|
4189
|
+
{ "name": "price", "type": "number", "required": true },
|
|
4190
|
+
{ "name": "currency", "type": "relation", "relation": "currency", "required": true },
|
|
4191
|
+
{ "name": "interval", "type": "enum", "items": ["day", "week", "month", "year"], "required": true },
|
|
4192
|
+
{ "name": "intervalCount", "type": "number", "default": 1 },
|
|
4193
|
+
{ "name": "trialPeriodDays", "type": "number" },
|
|
4194
|
+
{ "name": "active", "type": "boolean", "default": true },
|
|
4195
|
+
{ "name": "metadata", "type": "code", "language": "json" },
|
|
4196
|
+
{ "name": "features", "type": "array", "itemsType": "string" }
|
|
4197
|
+
]
|
|
4198
|
+
},
|
|
4199
|
+
{
|
|
4200
|
+
"name": "StripePayment",
|
|
4201
|
+
"description": "Detailed payment records with reconciliation data.",
|
|
4202
|
+
"fields": [
|
|
4203
|
+
{ "name": "stripePaymentIntentId", "type": "string", "required": true, "unique": true, "asMain": true, "hiddenable": true },
|
|
4204
|
+
{ "name": "user", "type": "relation", "relation": "user", "required": true },
|
|
4205
|
+
{ "name": "customer", "type": "relation", "relation": "StripeCustomer" },
|
|
4206
|
+
{ "name": "subscription", "type": "relation", "relation": "StripeSubscription" },
|
|
4207
|
+
{ "name": "invoice", "type": "relation", "relation": "StripeInvoice" },
|
|
4208
|
+
{ "name": "amount", "type": "number", "required": true },
|
|
4209
|
+
{ "name": "amountReceived", "type": "number" },
|
|
4210
|
+
{ "name": "currency", "type": "relation", "relation": "currency", "required": true },
|
|
4211
|
+
{ "name": "status", "type": "enum", "items": ["requires_payment_method", "requires_confirmation", "requires_action", "processing", "requires_capture", "canceled", "succeeded"], "required": true },
|
|
4212
|
+
{ "name": "paymentMethod", "type": "string" },
|
|
4213
|
+
{ "name": "paymentMethodDetails", "type": "code", "language": "json", "anonymized": true },
|
|
4214
|
+
{ "name": "receiptEmail", "type": "string" },
|
|
4215
|
+
{ "name": "receiptUrl", "type": "string" },
|
|
4216
|
+
{ "name": "created", "type": "datetime", "default": "now" },
|
|
4217
|
+
{ "name": "metadata", "type": "code", "language": "json" }
|
|
4218
|
+
]
|
|
4219
|
+
},
|
|
4220
|
+
{
|
|
4221
|
+
"name": "StripeInvoice",
|
|
4222
|
+
"description": "Complete invoice records from Stripe.",
|
|
4223
|
+
"fields": [
|
|
4224
|
+
{ "name": "stripeInvoiceId", "type": "string", "required": true, "unique": true, "asMain": true },
|
|
4225
|
+
{ "name": "customer", "type": "relation", "relation": "StripeCustomer", "required": true },
|
|
4226
|
+
{ "name": "subscription", "type": "relation", "relation": "StripeSubscription" },
|
|
4227
|
+
{ "name": "number", "type": "string" },
|
|
4228
|
+
{ "name": "amountDue", "type": "number", "required": true },
|
|
4229
|
+
{ "name": "amountPaid", "type": "number" },
|
|
4230
|
+
{ "name": "amountRemaining", "type": "number" },
|
|
4231
|
+
{ "name": "currency", "type": "relation", "relation": "currency", "required": true },
|
|
4232
|
+
{ "name": "status", "type": "enum", "items": ["draft", "open", "paid", "uncollectible", "void"], "required": true },
|
|
4233
|
+
{ "name": "periodStart", "type": "datetime" },
|
|
4234
|
+
{ "name": "periodEnd", "type": "datetime" },
|
|
4235
|
+
{ "name": "dueDate", "type": "datetime" },
|
|
4236
|
+
{ "name": "pdfUrl", "type": "string" },
|
|
4237
|
+
{ "name": "hostedInvoiceUrl", "type": "string" },
|
|
4238
|
+
{ "name": "lines", "type": "code", "language": "json" },
|
|
4239
|
+
{ "name": "created", "type": "datetime", "default": "now" },
|
|
4240
|
+
{ "name": "metadata", "type": "code", "language": "json" }
|
|
4241
|
+
]
|
|
4242
|
+
},
|
|
4243
|
+
{
|
|
4244
|
+
"name": "StripeRefund",
|
|
4245
|
+
"description": "Records of refunds processed through Stripe.",
|
|
4246
|
+
"fields": [
|
|
4247
|
+
{ "name": "stripeRefundId", "type": "string", "required": true, "unique": true, "asMain": true },
|
|
4248
|
+
{ "name": "payment", "type": "relation", "relation": "StripePayment", "required": true },
|
|
4249
|
+
{ "name": "amount", "type": "number", "required": true },
|
|
4250
|
+
{ "name": "currency", "type": "relation", "relation": "currency", "required": true },
|
|
4251
|
+
{ "name": "reason", "type": "enum", "items": ["duplicate", "fraudulent", "requested_by_customer", "expired_uncaptured_charge"] },
|
|
4252
|
+
{ "name": "status", "type": "enum", "items": ["pending", "succeeded", "failed", "canceled"], "required": true },
|
|
4253
|
+
{ "name": "receiptNumber", "type": "string" },
|
|
4254
|
+
{ "name": "created", "type": "datetime", "default": "now" }
|
|
4255
|
+
]
|
|
4256
|
+
}
|
|
4257
|
+
],
|
|
4258
|
+
"data": {
|
|
4259
|
+
"all": {
|
|
4260
|
+
"env": [
|
|
4261
|
+
{ "name": "STRIPE_SECRET_KEY", "value": "sk_test_YOUR_SECRET_KEY" },
|
|
4262
|
+
{ "name": "STRIPE_WEBHOOK_SECRET", "value": "whsec_YOUR_WEBHOOK_SECRET" }
|
|
4263
|
+
],
|
|
4264
|
+
"endpoint": [
|
|
4265
|
+
{
|
|
4266
|
+
"name": "Stripe Webhook Handler",
|
|
4267
|
+
"path": "stripe-webhook",
|
|
4268
|
+
"method": "POST",
|
|
4269
|
+
"isActive": true,
|
|
4270
|
+
"isPublic": true,
|
|
4271
|
+
"code": `
|
|
4272
|
+
// The entire Stripe event object is passed in the request body.
|
|
4273
|
+
const stripeEvent = context.request.body;
|
|
4274
|
+
|
|
4275
|
+
// We pass this event to the workflow for processing.
|
|
4276
|
+
await workflow.run('Process Stripe Webhook Events', { event: stripeEvent });
|
|
4277
|
+
|
|
4278
|
+
return { received: true };`
|
|
4279
|
+
},
|
|
4280
|
+
{
|
|
4281
|
+
"name": "Create Customer Portal",
|
|
4282
|
+
"path": "stripe/create-portal-session",
|
|
4283
|
+
"method": "POST",
|
|
4284
|
+
"isActive": true,
|
|
4285
|
+
"code": `
|
|
4286
|
+
// Expects the local user ID in the request body to find the Stripe Customer
|
|
4287
|
+
const { userId } = context.body;
|
|
4288
|
+
if (!userId) {
|
|
4289
|
+
return { status: 400, body: { error: 'User ID is required.' } };
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
// Find the corresponding Stripe Customer
|
|
4293
|
+
const stripeCustomer = await db.findOne('StripeCustomer', { user: userId });
|
|
4294
|
+
if (!stripeCustomer) {
|
|
4295
|
+
return { status: 404, body: { error: 'Stripe customer not found for this user.' } };
|
|
4296
|
+
}
|
|
4297
|
+
|
|
4298
|
+
// Run the workflow to get the portal URL
|
|
4299
|
+
const result = await workflow.run('Create Stripe Customer Portal Session', {
|
|
4300
|
+
stripeCustomerId: stripeCustomer.stripeCustomerId,
|
|
4301
|
+
returnUrl: 'https://votre-site.com/account/billing' // URL where user returns
|
|
4302
|
+
});
|
|
4303
|
+
|
|
4304
|
+
// The result of the workflow run will contain the portal session URL
|
|
4305
|
+
// (This part requires a way to retrieve the result of the workflow action)
|
|
4306
|
+
// For now, we can assume the workflow returns the URL in its context
|
|
4307
|
+
// A more advanced implementation would be needed here.
|
|
4308
|
+
|
|
4309
|
+
return { status: 200, body: { message: "Workflow started", runId: result.runId } };
|
|
4310
|
+
`
|
|
4311
|
+
}
|
|
4312
|
+
],
|
|
4313
|
+
"workflow": [
|
|
4314
|
+
{
|
|
4315
|
+
"name": "Process Stripe Webhook Events",
|
|
4316
|
+
"description": "Processes incoming Stripe webhook events and triggers appropriate actions.",
|
|
4317
|
+
"startStep": { "$link": { "name": "Check for Invoice Paid", "_model": "workflowStep" } }
|
|
4318
|
+
},
|
|
4319
|
+
{
|
|
4320
|
+
"name": "Subscription Lifecycle Management",
|
|
4321
|
+
"description": "Handles subscription creation, updates, and cancellations.",
|
|
4322
|
+
"startStep": { "$link": { "name": "Handle Subscription Created", "_model": "workflowStep" } }
|
|
4323
|
+
},
|
|
4324
|
+
{
|
|
4325
|
+
"name": "Payment Reconciliation",
|
|
4326
|
+
"description": "Ensures payments are properly recorded and reconciled with orders.",
|
|
4327
|
+
"startStep": { "$link": { "name": "Handle Payment Succeeded", "_model": "workflowStep" } }
|
|
4328
|
+
},
|
|
4329
|
+
{
|
|
4330
|
+
"name": "Invoice Processing",
|
|
4331
|
+
"description": "Handles invoice generation, payment, and reminders.",
|
|
4332
|
+
"startStep": { "$link": { "name": "Handle Invoice Created", "_model": "workflowStep" } }
|
|
4333
|
+
},
|
|
4334
|
+
{
|
|
4335
|
+
"name": "Create Stripe Checkout Session",
|
|
4336
|
+
"description": "Creates a Stripe Checkout session for a one-time payment or a subscription.",
|
|
4337
|
+
"startStep": { "$link": { "name": "Select Session Type", "_model": "workflowStep" } }
|
|
4338
|
+
},
|
|
4339
|
+
{
|
|
4340
|
+
"name": "Create Stripe Customer Portal Session",
|
|
4341
|
+
"description": "Generates a secure URL for a customer to access their Stripe portal.",
|
|
4342
|
+
"startStep": { "$link": { "name": "Generate Portal URL", "_model": "workflowStep" } }
|
|
4343
|
+
}
|
|
4344
|
+
],
|
|
4345
|
+
"workflowAction": [
|
|
4346
|
+
// Ajout dans la section workflowAction
|
|
4347
|
+
{
|
|
4348
|
+
"name": "Send Payment Failure Email",
|
|
4349
|
+
"description": "Envoie un email au client quand un paiement échoue avec instructions pour mettre à jour sa méthode de paiement.",
|
|
4350
|
+
"type": "SendEmail",
|
|
4351
|
+
"emailRecipients": ["{triggerData.customer.email}"],
|
|
4352
|
+
"emailSubject": "Payment Failed for Your Subscription",
|
|
4353
|
+
"emailContent": `
|
|
4354
|
+
<h1>Payment Failed</h1>
|
|
4355
|
+
<p>We couldn't process your payment for invoice #{triggerData.number}.</p>
|
|
4356
|
+
<p>Amount due: {triggerData.amountDue} {triggerData.currency.symbol}</p>
|
|
4357
|
+
<p><strong>Please update your payment method:</strong></p>
|
|
4358
|
+
<a href="{triggerData.hostedInvoiceUrl}" style="background-color: #E53E3E; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">
|
|
4359
|
+
Update Payment Method
|
|
4360
|
+
</a>
|
|
4361
|
+
<p>If you believe this is an error, please contact our support team.</p>
|
|
4362
|
+
`
|
|
4363
|
+
},
|
|
4364
|
+
{
|
|
4365
|
+
"name": "Send Subscription Welcome",
|
|
4366
|
+
"description": "Envoie un email de bienvenue pour un nouvel abonnement.",
|
|
4367
|
+
"type": "SendEmail",
|
|
4368
|
+
"emailRecipients": ["{triggerData.customer_email}"],
|
|
4369
|
+
"emailSubject": "Welcome to your new subscription!",
|
|
4370
|
+
"emailContent": `
|
|
4371
|
+
<h1>Thank you for subscribing!</h1>
|
|
4372
|
+
<p>We're excited to have you on board. Here are the details of your subscription:</p>
|
|
4373
|
+
<ul>
|
|
4374
|
+
<li>Plan: {context.plan.name}</li>
|
|
4375
|
+
<li>Amount: {context.plan.price} {context.plan.currency.code}/month</li>
|
|
4376
|
+
<li>Next billing date: {context.subscription.currentPeriodEnd}</li>
|
|
4377
|
+
</ul>
|
|
4378
|
+
<p>If you have any questions, please don't hesitate to contact our support team.</p>
|
|
4379
|
+
`
|
|
4380
|
+
},
|
|
4381
|
+
{
|
|
4382
|
+
"name": "Process Invoice Payment",
|
|
4383
|
+
"description": "Traite le paiement d'une facture et met à jour le statut.",
|
|
4384
|
+
"type": "ExecuteScript",
|
|
4385
|
+
"script": `
|
|
4386
|
+
const invoice = context.triggerData.data.object;
|
|
4387
|
+
|
|
4388
|
+
// Trouver l'abonnement associé
|
|
4389
|
+
const subscription = await db.findOne('StripeSubscription', {
|
|
4390
|
+
stripeSubscriptionId: invoice.subscription
|
|
4391
|
+
});
|
|
4392
|
+
|
|
4393
|
+
if (!subscription) {
|
|
4394
|
+
logger.warn('Subscription not found for invoice:', invoice.id);
|
|
4395
|
+
return { success: false, message: 'Subscription not found' };
|
|
4396
|
+
}
|
|
4397
|
+
|
|
4398
|
+
// Créer/mettre à jour l'enregistrement de facture
|
|
4399
|
+
const invoiceData = {
|
|
4400
|
+
stripeInvoiceId: invoice.id,
|
|
4401
|
+
customer: subscription.customer,
|
|
4402
|
+
subscription: subscription._id,
|
|
4403
|
+
number: invoice.number,
|
|
4404
|
+
amountDue: invoice.amount_due / 100,
|
|
4405
|
+
amountPaid: invoice.amount_paid / 100,
|
|
4406
|
+
amountRemaining: invoice.amount_remaining / 100,
|
|
4407
|
+
currency: subscription.plan.currency,
|
|
4408
|
+
status: invoice.status,
|
|
4409
|
+
periodStart: new Date(invoice.period_start * 1000),
|
|
4410
|
+
periodEnd: new Date(invoice.period_end * 1000),
|
|
4411
|
+
dueDate: invoice.due_date ? new Date(invoice.due_date * 1000) : null,
|
|
4412
|
+
pdfUrl: invoice.invoice_pdf,
|
|
4413
|
+
hostedInvoiceUrl: invoice.hosted_invoice_url,
|
|
4414
|
+
lines: JSON.stringify(invoice.lines.data),
|
|
4415
|
+
created: new Date(invoice.created * 1000),
|
|
4416
|
+
metadata: invoice.metadata ? JSON.stringify(invoice.metadata) : null
|
|
4417
|
+
};
|
|
4418
|
+
|
|
4419
|
+
await db.upsert('StripeInvoice',
|
|
4420
|
+
{ stripeInvoiceId: invoice.id },
|
|
4421
|
+
invoiceData
|
|
4422
|
+
);
|
|
4423
|
+
|
|
4424
|
+
// Si la facture est payée, mettre à jour le statut de l'abonnement
|
|
4425
|
+
if (invoice.status === 'paid') {
|
|
4426
|
+
await db.update('StripeSubscription',
|
|
4427
|
+
{ _id: subscription._id },
|
|
4428
|
+
{
|
|
4429
|
+
status: 'active',
|
|
4430
|
+
latestInvoice: invoice.id
|
|
4431
|
+
}
|
|
4432
|
+
);
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
return { success: true };
|
|
4436
|
+
`
|
|
4437
|
+
},
|
|
4438
|
+
{
|
|
4439
|
+
"name": "Send Invoice Email",
|
|
4440
|
+
"description": "Envoie la facture par email au client.",
|
|
4441
|
+
"type": "SendEmail",
|
|
4442
|
+
"emailRecipients": ["{triggerData.customer_email}"],
|
|
4443
|
+
"emailSubject": "Your invoice is ready",
|
|
4444
|
+
"emailContent": `
|
|
4445
|
+
<h1>Your Invoice #{triggerData.number}</h1>
|
|
4446
|
+
<p>Amount due: {triggerData.amount_due / 100} {triggerData.currency.toUpperCase()}</p>
|
|
4447
|
+
<p>Due date: {triggerData.due_date ? new Date(triggerData.due_date * 1000).toLocaleDateString() : 'Immediately'}</p>
|
|
4448
|
+
<p>You can view and pay your invoice <a href="{triggerData.hosted_invoice_url}">here</a>.</p>
|
|
4449
|
+
<p>Thank you for your business!</p>
|
|
4450
|
+
`
|
|
4451
|
+
},
|
|
4452
|
+
{
|
|
4453
|
+
"name": "Stripe: Create Customer",
|
|
4454
|
+
"description": "Creates a new customer in Stripe. Expects 'email' and 'name' in the triggerData.",
|
|
4455
|
+
"type": "HttpRequest",
|
|
4456
|
+
"method": "POST",
|
|
4457
|
+
"url": "https://api.stripe.com/v1/customers",
|
|
4458
|
+
"headers": { "Authorization": "Bearer {env.STRIPE_SECRET_KEY}", "Content-Type": "application/x-www-form-urlencoded" },
|
|
4459
|
+
"body": "email={triggerData.email}&name={triggerData.name}&description=Customer for {triggerData.email}&metadata[userId]={triggerData.user._id}"
|
|
4460
|
+
},
|
|
4461
|
+
{
|
|
4462
|
+
"name": "Stripe: Retrieve Payment Intent",
|
|
4463
|
+
"description": "Retrieves a Payment Intent object from Stripe.",
|
|
4464
|
+
"type": "HttpRequest",
|
|
4465
|
+
"method": "GET",
|
|
4466
|
+
"url": "https://api.stripe.com/v1/payment_intents/{triggerData.event.id}",
|
|
4467
|
+
"headers": { "Authorization": "Bearer {env.STRIPE_SECRET_KEY}" }
|
|
4468
|
+
},
|
|
4469
|
+
{
|
|
4470
|
+
"name": "Stripe Service: Verify Webhook",
|
|
4471
|
+
"description": "Uses the native Stripe service to securely verify a webhook signature.",
|
|
4472
|
+
"type": "ExecuteServiceFunction",
|
|
4473
|
+
"serviceName": "stripe",
|
|
4474
|
+
"functionName": "verifyWebhookSignature",
|
|
4475
|
+
"args": ["{triggerData.request.headers}", "{triggerData.request.rawBody}"]
|
|
4476
|
+
},
|
|
4477
|
+
{
|
|
4478
|
+
"name": "Stripe: Create Checkout Session (Payment)",
|
|
4479
|
+
"description": "Creates a Stripe Checkout session for a one-time payment.",
|
|
4480
|
+
"type": "HttpRequest",
|
|
4481
|
+
"method": "POST",
|
|
4482
|
+
"url": "https://api.stripe.com/v1/checkout/sessions",
|
|
4483
|
+
"headers": { "Authorization": "Bearer {env.STRIPE_SECRET_KEY}", "Content-Type": "application/json" },
|
|
4484
|
+
"body": {
|
|
4485
|
+
"payment_method_types": ["card"],
|
|
4486
|
+
"line_items": [{
|
|
4487
|
+
"price_data": {
|
|
4488
|
+
"currency": "{triggerData.currencyCode}",
|
|
4489
|
+
"product_data": { "name": "{triggerData.productName}" },
|
|
4490
|
+
"unit_amount": "{triggerData.amountInCents}"
|
|
4491
|
+
},
|
|
4492
|
+
"quantity": "{triggerData.quantity}"
|
|
4493
|
+
}],
|
|
4494
|
+
"mode": "payment",
|
|
4495
|
+
"success_url": "{triggerData.successUrl}",
|
|
4496
|
+
"cancel_url": "{triggerData.cancelUrl}",
|
|
4497
|
+
"customer_email": "{triggerData.customerEmail}",
|
|
4498
|
+
"metadata": "{triggerData.metadata}"
|
|
4499
|
+
}
|
|
4500
|
+
},
|
|
4501
|
+
{
|
|
4502
|
+
"name": "Stripe: Create Checkout Session (Subscription)",
|
|
4503
|
+
"description": "Creates a Stripe Checkout session for a new subscription.",
|
|
4504
|
+
"type": "HttpRequest",
|
|
4505
|
+
"method": "POST",
|
|
4506
|
+
"url": "https://api.stripe.com/v1/checkout/sessions",
|
|
4507
|
+
"headers": { "Authorization": "Bearer {env.STRIPE_SECRET_KEY}", "Content-Type": "application/json" },
|
|
4508
|
+
"body": {
|
|
4509
|
+
"payment_method_types": ["card"],
|
|
4510
|
+
"line_items": [{ "price": "{triggerData.priceId}", "quantity": 1 }],
|
|
4511
|
+
"mode": "subscription",
|
|
4512
|
+
"success_url": "{triggerData.successUrl}",
|
|
4513
|
+
"cancel_url": "{triggerData.cancelUrl}",
|
|
4514
|
+
"customer": "{triggerData.stripeCustomerId}"
|
|
4515
|
+
}
|
|
4516
|
+
},
|
|
4517
|
+
{
|
|
4518
|
+
"name": "Send Payment Receipt",
|
|
4519
|
+
"description": "Sends a receipt email for successful payments.",
|
|
4520
|
+
"type": "SendEmail",
|
|
4521
|
+
"emailRecipients": ["{context.httpResponse.receipt_email}"],
|
|
4522
|
+
"emailSubject": "Your Payment Receipt",
|
|
4523
|
+
"emailContent": `
|
|
4524
|
+
<h1>Thank you for your payment!</h1>
|
|
4525
|
+
<p>We've received your payment of {context.httpResponse.amount / 100} {context.httpResponse.currency.toUpperCase()}.</p>
|
|
4526
|
+
<p>Payment ID: {triggerData.payload.id}</p>
|
|
4527
|
+
<p>You can view your receipt <a href="{triggerData.payload.receipt_url}">here</a>.</p>
|
|
4528
|
+
<p>If you have any questions, please contact our support team.</p>
|
|
4529
|
+
`
|
|
4530
|
+
},
|
|
4531
|
+
{
|
|
4532
|
+
"name": "Process Refund",
|
|
4533
|
+
"description": "Handles refund creation and updates order status.",
|
|
4534
|
+
"type": "ExecuteScript",
|
|
4535
|
+
"script": `
|
|
4536
|
+
const { refund } = context.triggerData.payload;
|
|
4537
|
+
|
|
4538
|
+
// Find the original payment
|
|
4539
|
+
const payment = await db.findOne('StripePayment', {
|
|
4540
|
+
stripePaymentIntentId: refund.payment_intent
|
|
4541
|
+
});
|
|
4542
|
+
|
|
4543
|
+
if (!payment) {
|
|
4544
|
+
logger.warn('Original payment not found for refund:', refund.id);
|
|
4545
|
+
return { success: false, message: 'Original payment not found' };
|
|
4546
|
+
}
|
|
4547
|
+
|
|
4548
|
+
// Create refund record
|
|
4549
|
+
await db.create('StripeRefund', {
|
|
4550
|
+
stripeRefundId: refund.id,
|
|
4551
|
+
payment: payment._id,
|
|
4552
|
+
amount: refund.amount / 100,
|
|
4553
|
+
currency: payment.currency,
|
|
4554
|
+
reason: refund.reason,
|
|
4555
|
+
status: refund.status,
|
|
4556
|
+
receiptNumber: refund.receipt_number,
|
|
4557
|
+
created: new Date(refund.created * 1000)
|
|
4558
|
+
});
|
|
4559
|
+
|
|
4560
|
+
// Update order status if linked
|
|
4561
|
+
if (payment.order) {
|
|
4562
|
+
await db.update('order',
|
|
4563
|
+
{ _id: payment.order },
|
|
4564
|
+
{ status: 'refunded', refundId: refund.id }
|
|
4565
|
+
);
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
return { success: true };
|
|
4569
|
+
`
|
|
4570
|
+
},
|
|
4571
|
+
{
|
|
4572
|
+
"name": "Log Unhandled Stripe Event",
|
|
4573
|
+
"description": "Logs the type and ID of a Stripe event that was not handled by any other workflow step.",
|
|
4574
|
+
"type": "ExecuteScript",
|
|
4575
|
+
"script": `
|
|
4576
|
+
logger.warn('Unhandled Stripe Event Received: Type=' + context.triggerData.event.type + ', ID=' + context.triggerData.event.id);
|
|
4577
|
+
return { success: true, message: 'Event logged as unhandled.' };
|
|
4578
|
+
`
|
|
4579
|
+
},
|
|
4580
|
+
{
|
|
4581
|
+
"name": "Stripe: Create Customer Portal Session",
|
|
4582
|
+
"description": "Creates a session for the Stripe Customer Portal.",
|
|
4583
|
+
"type": "HttpRequest",
|
|
4584
|
+
"method": "POST",
|
|
4585
|
+
"url": "https://api.stripe.com/v1/billing_portal/sessions",
|
|
4586
|
+
"headers": {
|
|
4587
|
+
"Authorization": "Bearer {env.STRIPE_SECRET_KEY}",
|
|
4588
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
4589
|
+
},
|
|
4590
|
+
"body": "customer={triggerData.stripeCustomerId}&return_url={triggerData.returnUrl}"
|
|
4591
|
+
},
|
|
4592
|
+
{
|
|
4593
|
+
"name": "Sync Stripe Entity to Local DB",
|
|
4594
|
+
"description": "Synchronise une entité Stripe avec la base locale sans utiliser upsert",
|
|
4595
|
+
"type": "ExecuteScript",
|
|
4596
|
+
"script": `
|
|
4597
|
+
const event = context.triggerData;
|
|
4598
|
+
const stripeObject = event.data.object;
|
|
4599
|
+
const objectType = stripeObject.object;
|
|
4600
|
+
|
|
4601
|
+
// Helper function to safely get nested properties
|
|
4602
|
+
const getNested = (obj, path) => {
|
|
4603
|
+
return path.split('.').reduce((o, p) => (o && o[p] !== undefined ? o[p] : null), obj);
|
|
4604
|
+
};
|
|
4605
|
+
|
|
4606
|
+
// Helper function to safely stringify objects
|
|
4607
|
+
const safeStringify = (obj) => {
|
|
4608
|
+
try {
|
|
4609
|
+
return obj ? JSON.stringify(obj) : null;
|
|
4610
|
+
} catch (e) {
|
|
4611
|
+
logger.warn('Failed to stringify object:', e);
|
|
4612
|
+
return null;
|
|
4613
|
+
}
|
|
4614
|
+
};
|
|
4615
|
+
|
|
4616
|
+
const findCurrencyByCode = async (code) => {
|
|
4617
|
+
if (!code) return null;
|
|
4618
|
+
const currency = await db.findOne('currency', { code: code.toUpperCase() });
|
|
4619
|
+
return currency ? currency._id : null;
|
|
4620
|
+
};
|
|
4621
|
+
|
|
4622
|
+
const findCustomerByStripeId = async (stripeId) => {
|
|
4623
|
+
if (!stripeId) return null;
|
|
4624
|
+
const customer = await db.findOne('StripeCustomer', { stripeCustomerId: stripeId });
|
|
4625
|
+
return customer || null;
|
|
4626
|
+
};
|
|
4627
|
+
|
|
4628
|
+
const findSubscriptionByStripeId = async (stripeId) => {
|
|
4629
|
+
if (!stripeId) return null;
|
|
4630
|
+
const subscription = await db.findOne('StripeSubscription', { stripeSubscriptionId: stripeId });
|
|
4631
|
+
return subscription || null;
|
|
4632
|
+
};
|
|
4633
|
+
|
|
4634
|
+
const findInvoiceByStripeId = async (stripeId) => {
|
|
4635
|
+
if (!stripeId) return null;
|
|
4636
|
+
const invoice = await db.findOne('StripeInvoice', { stripeInvoiceId: stripeId });
|
|
4637
|
+
return invoice || null;
|
|
4638
|
+
};
|
|
4639
|
+
|
|
4640
|
+
const findUserFromCustomer = (customer) => {
|
|
4641
|
+
return customer ? customer.user : null;
|
|
4642
|
+
};
|
|
4643
|
+
|
|
4644
|
+
const findOrCreateCustomer = async (stripeCustomerId, email, name) => {
|
|
4645
|
+
let customer = await findCustomerByStripeId(stripeCustomerId);
|
|
4646
|
+
if (!customer) {
|
|
4647
|
+
logger.info(\`Customer not found locally, creating a new one for Stripe ID: \${stripeCustomerId}\`);
|
|
4648
|
+
const newCustomerData = {
|
|
4649
|
+
stripeCustomerId: stripeCustomerId,
|
|
4650
|
+
email: email,
|
|
4651
|
+
name: name
|
|
4652
|
+
};
|
|
4653
|
+
// Try to link to an existing user by email
|
|
4654
|
+
const user = await db.findOne('user', { email: email });
|
|
4655
|
+
if (user) {
|
|
4656
|
+
newCustomerData.user = user._id;
|
|
4657
|
+
}
|
|
4658
|
+
const result = await db.create('StripeCustomer', newCustomerData);
|
|
4659
|
+
customer = { _id: result.insertedIds[0], ...newCustomerData };
|
|
4660
|
+
}
|
|
4661
|
+
return customer;
|
|
4662
|
+
};
|
|
4663
|
+
|
|
4664
|
+
|
|
4665
|
+
let modelName;
|
|
4666
|
+
let idField;
|
|
4667
|
+
let dataToUpsert = {};
|
|
4668
|
+
|
|
4669
|
+
switch (objectType) {
|
|
4670
|
+
case 'customer':
|
|
4671
|
+
modelName = 'StripeCustomer';
|
|
4672
|
+
idField = 'stripeCustomerId';
|
|
4673
|
+
dataToUpsert = {
|
|
4674
|
+
stripeCustomerId: stripeObject.id,
|
|
4675
|
+
email: stripeObject.email || null,
|
|
4676
|
+
name: stripeObject.name || null,
|
|
4677
|
+
phone: stripeObject.phone || null,
|
|
4678
|
+
address: stripeObject.address ? safeStringify(stripeObject.address) : null,
|
|
4679
|
+
taxExempt: stripeObject.tax_exempt || 'none',
|
|
4680
|
+
defaultPaymentMethod: getNested(stripeObject, 'invoice_settings.default_payment_method'),
|
|
4681
|
+
invoiceSettings: stripeObject.invoice_settings ? safeStringify(stripeObject.invoice_settings) : null,
|
|
4682
|
+
metadata: stripeObject.metadata ? safeStringify(stripeObject.metadata) : null
|
|
4683
|
+
};
|
|
4684
|
+
break;
|
|
4685
|
+
|
|
4686
|
+
case 'subscription':
|
|
4687
|
+
modelName = 'StripeSubscription';
|
|
4688
|
+
idField = 'stripeSubscriptionId';
|
|
4689
|
+
dataToUpsert = {
|
|
4690
|
+
stripeSubscriptionId: stripeObject.id,
|
|
4691
|
+
status: stripeObject.status,
|
|
4692
|
+
currentPeriodStart: stripeObject.current_period_start ? new Date(stripeObject.current_period_start * 1000) : null,
|
|
4693
|
+
currentPeriodEnd: stripeObject.current_period_end ? new Date(stripeObject.current_period_end * 1000) : null,
|
|
4694
|
+
cancelAtPeriodEnd: stripeObject.cancel_at_period_end || false,
|
|
4695
|
+
canceledAt: stripeObject.canceled_at ? new Date(stripeObject.canceled_at * 1000) : null,
|
|
4696
|
+
daysUntilDue: stripeObject.days_until_due || null,
|
|
4697
|
+
defaultPaymentMethod: stripeObject.default_payment_method || null,
|
|
4698
|
+
startDate: stripeObject.start_date ? new Date(stripeObject.start_date * 1000) : null,
|
|
4699
|
+
trialEnd: stripeObject.trial_end ? new Date(stripeObject.trial_end * 1000) : null,
|
|
4700
|
+
metadata: stripeObject.metadata ? safeStringify(stripeObject.metadata) : null
|
|
4701
|
+
};
|
|
4702
|
+
|
|
4703
|
+
// Handle relations
|
|
4704
|
+
if (stripeObject.customer) {
|
|
4705
|
+
const customer = await db.findOne('StripeCustomer', { stripeCustomerId: stripeObject.customer });
|
|
4706
|
+
if (customer) dataToUpsert.customer = customer._id;
|
|
4707
|
+
}
|
|
4708
|
+
|
|
4709
|
+
if (stripeObject.items?.data?.[0]?.price?.id) {
|
|
4710
|
+
const priceId = stripeObject.items.data[0].price.id;
|
|
4711
|
+
const plan = await db.findOne('StripePlan', { stripePriceId: priceId });
|
|
4712
|
+
if (plan) dataToUpsert.plan = plan._id;
|
|
4713
|
+
}
|
|
4714
|
+
break;
|
|
4715
|
+
|
|
4716
|
+
case 'product':
|
|
4717
|
+
modelName = 'StripePlan';
|
|
4718
|
+
idField = 'stripeProductId';
|
|
4719
|
+
dataToUpsert = {
|
|
4720
|
+
stripeProductId: stripeObject.id,
|
|
4721
|
+
name: stripeObject.name || null,
|
|
4722
|
+
description: stripeObject.description || null,
|
|
4723
|
+
active: stripeObject.active !== false,
|
|
4724
|
+
metadata: stripeObject.metadata ? safeStringify(stripeObject.metadata) : null
|
|
4725
|
+
};
|
|
4726
|
+
break;
|
|
4727
|
+
|
|
4728
|
+
case 'price':
|
|
4729
|
+
modelName = 'StripePlan';
|
|
4730
|
+
idField = 'stripePriceId';
|
|
4731
|
+
dataToUpsert = {
|
|
4732
|
+
stripePriceId: stripeObject.id,
|
|
4733
|
+
price: stripeObject.unit_amount ? stripeObject.unit_amount / 100 : null,
|
|
4734
|
+
interval: stripeObject.recurring?.interval || null,
|
|
4735
|
+
intervalCount: stripeObject.recurring?.interval_count || 1
|
|
4736
|
+
};
|
|
4737
|
+
|
|
4738
|
+
if (stripeObject.metadata?.trial_period_days) {
|
|
4739
|
+
dataToUpsert.trialPeriodDays = parseInt(stripeObject.metadata.trial_period_days) || null;
|
|
4740
|
+
}
|
|
4741
|
+
|
|
4742
|
+
if (stripeObject.currency) {
|
|
4743
|
+
const currency = await db.findOne('currency', { code: stripeObject.currency.toUpperCase() });
|
|
4744
|
+
if (currency) dataToUpsert.currency = currency._id;
|
|
4745
|
+
}
|
|
4746
|
+
break;
|
|
4747
|
+
|
|
4748
|
+
case 'invoice':
|
|
4749
|
+
modelName = 'StripeInvoice';
|
|
4750
|
+
idField = 'stripeInvoiceId';
|
|
4751
|
+
|
|
4752
|
+
const customerForInvoice = await findCustomerByStripeId(stripeObject.customer);
|
|
4753
|
+
const subscriptionForInvoice = await findSubscriptionByStripeId(stripeObject.subscription);
|
|
4754
|
+
|
|
4755
|
+
dataToUpsert = {
|
|
4756
|
+
stripeInvoiceId: stripeObject.id,
|
|
4757
|
+
customer: customerForInvoice ? customerForInvoice._id : null,
|
|
4758
|
+
subscription: subscriptionForInvoice ? subscriptionForInvoice._id : null,
|
|
4759
|
+
number: stripeObject.number,
|
|
4760
|
+
amountDue: stripeObject.amount_due / 100,
|
|
4761
|
+
amountPaid: stripeObject.amount_paid / 100,
|
|
4762
|
+
amountRemaining: stripeObject.amount_remaining / 100,
|
|
4763
|
+
currency: await findCurrencyByCode(stripeObject.currency),
|
|
4764
|
+
status: stripeObject.status,
|
|
4765
|
+
periodStart: new Date(stripeObject.period_start * 1000),
|
|
4766
|
+
periodEnd: new Date(stripeObject.period_end * 1000),
|
|
4767
|
+
dueDate: stripeObject.due_date ? new Date(stripeObject.due_date * 1000) : null,
|
|
4768
|
+
pdfUrl: stripeObject.invoice_pdf,
|
|
4769
|
+
hostedInvoiceUrl: stripeObject.hosted_invoice_url,
|
|
4770
|
+
lines: safeStringify(stripeObject.lines?.data),
|
|
4771
|
+
created: new Date(stripeObject.created * 1000),
|
|
4772
|
+
metadata: safeStringify(stripeObject.metadata)
|
|
4773
|
+
};
|
|
4774
|
+
break;
|
|
4775
|
+
|
|
4776
|
+
case 'payment_intent':
|
|
4777
|
+
modelName = 'StripePayment';
|
|
4778
|
+
idField = 'stripePaymentIntentId';
|
|
4779
|
+
|
|
4780
|
+
const customerForPayment = await findCustomerByStripeId(stripeObject.customer);
|
|
4781
|
+
const userForPayment = findUserFromCustomer(customerForPayment);
|
|
4782
|
+
const invoiceForPayment = await findInvoiceByStripeId(stripeObject.invoice);
|
|
4783
|
+
const subscriptionForPayment = invoiceForPayment ? await findSubscriptionByStripeId(invoiceForPayment.subscription) : null;
|
|
4784
|
+
|
|
4785
|
+
dataToUpsert = {
|
|
4786
|
+
stripePaymentIntentId: stripeObject.id,
|
|
4787
|
+
user: userForPayment,
|
|
4788
|
+
customer: customerForPayment ? customerForPayment._id : null,
|
|
4789
|
+
subscription: subscriptionForPayment ? subscriptionForPayment._id : null,
|
|
4790
|
+
invoice: invoiceForPayment ? invoiceForPayment._id : null,
|
|
4791
|
+
amount: stripeObject.amount / 100,
|
|
4792
|
+
amountReceived: stripeObject.amount_received / 100,
|
|
4793
|
+
currency: await findCurrencyByCode(stripeObject.currency),
|
|
4794
|
+
status: stripeObject.status,
|
|
4795
|
+
paymentMethod: stripeObject.payment_method,
|
|
4796
|
+
paymentMethodDetails: safeStringify(stripeObject.payment_method_details),
|
|
4797
|
+
receiptEmail: stripeObject.receipt_email,
|
|
4798
|
+
receiptUrl: getNested(stripeObject, 'charges.data.0.receipt_url'),
|
|
4799
|
+
created: new Date(stripeObject.created * 1000),
|
|
4800
|
+
metadata: safeStringify(stripeObject.metadata)
|
|
4801
|
+
};
|
|
4802
|
+
break;
|
|
4803
|
+
|
|
4804
|
+
default:
|
|
4805
|
+
logger.warn('Unsupported Stripe object type:', objectType);
|
|
4806
|
+
return { success: false, message: 'Unsupported Stripe object type: ' + objectType };
|
|
4807
|
+
}
|
|
4808
|
+
|
|
4809
|
+
// Implémentation manuelle de upsert
|
|
4810
|
+
try {
|
|
4811
|
+
const filter = {};
|
|
4812
|
+
filter[idField] = stripeObject.id;
|
|
4813
|
+
|
|
4814
|
+
// 1. Vérifier si l'entité existe déjà
|
|
4815
|
+
const existing = await db.findOne(modelName, filter);
|
|
4816
|
+
|
|
4817
|
+
if (existing) {
|
|
4818
|
+
// 2. Mise à jour si l'entité existe
|
|
4819
|
+
await db.update(modelName, filter, dataToUpsert);
|
|
4820
|
+
logger.info(\`Updated \${modelName} with \${idField}: \${stripeObject.id}\`);
|
|
4821
|
+
} else {
|
|
4822
|
+
// 3. Création si l'entité n'existe pas
|
|
4823
|
+
await db.create(modelName, { ...filter, ...dataToUpsert });
|
|
4824
|
+
logger.info(\`Created new \${modelName} with \${idField}: \${stripeObject.id}\`);
|
|
4825
|
+
}
|
|
4826
|
+
|
|
4827
|
+
return { success: true };
|
|
4828
|
+
} catch (e) {
|
|
4829
|
+
logger.error('Failed to sync Stripe entity:', e);
|
|
4830
|
+
return { success: false, message: 'Database operation failed: ' + e.message };
|
|
4831
|
+
}
|
|
4832
|
+
`
|
|
4833
|
+
}
|
|
4834
|
+
],
|
|
4835
|
+
"workflowStep": [
|
|
4836
|
+
{
|
|
4837
|
+
"name": "Log Unhandled Event",
|
|
4838
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4839
|
+
"actions": { "$link": { "name": "Log Unhandled Stripe Event", "_model": "workflowAction" } },
|
|
4840
|
+
"isTerminal": true
|
|
4841
|
+
},
|
|
4842
|
+
// --- Main Webhook Router Steps ---
|
|
4843
|
+
{
|
|
4844
|
+
"name": "Check for Invoice Paid",
|
|
4845
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4846
|
+
"conditions": { "$eq": ["{triggerData.event.type}", "invoice.paid"] },
|
|
4847
|
+
"onSuccessStep": { "$link": { "name": "Handle Paid Invoice", "_model": "workflowStep" } },
|
|
4848
|
+
"onFailureStep": { "$link": { "name": "Check for Invoice Payment Failed", "_model": "workflowStep" } }
|
|
4849
|
+
},
|
|
4850
|
+
{
|
|
4851
|
+
"name": "Check for Invoice Payment Failed",
|
|
4852
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4853
|
+
"conditions": { "$eq": ["{triggerData.event.type}", "invoice.payment_failed"] },
|
|
4854
|
+
"onSuccessStep": { "$link": { "name": "Handle Failed Invoice", "_model": "workflowStep" } },
|
|
4855
|
+
"onFailureStep": { "$link": { "name": "Check for Subscription Created", "_model": "workflowStep" } }
|
|
4856
|
+
},
|
|
4857
|
+
{
|
|
4858
|
+
"name": "Check for Subscription Created",
|
|
4859
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4860
|
+
"conditions": { "$eq": ["{triggerData.event.type}", "customer.subscription.created"] },
|
|
4861
|
+
"onSuccessStep": { "$link": { "name": "Handle Subscription Created", "_model": "workflowStep" } },
|
|
4862
|
+
"onFailureStep": { "$link": { "name": "Check for Subscription Updated", "_model": "workflowStep" } }
|
|
4863
|
+
},
|
|
4864
|
+
{
|
|
4865
|
+
"name": "Check for Subscription Updated",
|
|
4866
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4867
|
+
"conditions": { "$in": ["{triggerData.event.type}", ["customer.subscription.updated", "customer.subscription.deleted"]] },
|
|
4868
|
+
"onSuccessStep": { "$link": { "name": "Handle Subscription Update/Delete", "_model": "workflowStep" } },
|
|
4869
|
+
"onFailureStep": { "$link": { "name": "Check for Payment Succeeded", "_model": "workflowStep" } }
|
|
4870
|
+
},
|
|
4871
|
+
{
|
|
4872
|
+
"name": "Check for Payment Succeeded",
|
|
4873
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4874
|
+
"conditions": { "$eq": ["{triggerData.event.type}", "payment_intent.succeeded"] },
|
|
4875
|
+
"onSuccessStep": { "$link": { "name": "Handle Payment Succeeded", "_model": "workflowStep" } },
|
|
4876
|
+
"onFailureStep": { "$link": { "name": "Check for Customer Updated", "_model": "workflowStep" } }
|
|
4877
|
+
},
|
|
4878
|
+
|
|
4879
|
+
// --- Action-performing Steps ---
|
|
4880
|
+
{
|
|
4881
|
+
"name": "Handle Paid Invoice",
|
|
4882
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4883
|
+
"actions": { "$link": { "name": "Sync Stripe Entity to Local DB", "_model": "workflowAction" } },
|
|
4884
|
+
"isTerminal": true
|
|
4885
|
+
},
|
|
4886
|
+
{
|
|
4887
|
+
"name": "Handle Failed Invoice",
|
|
4888
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4889
|
+
"actions": { "$link": { "name": "Send Payment Failure Email", "_model": "workflowAction" } },
|
|
4890
|
+
"isTerminal": true
|
|
4891
|
+
},
|
|
4892
|
+
{
|
|
4893
|
+
"name": "Handle Invoice Created",
|
|
4894
|
+
"workflow": { "$link": { "name": "Invoice Processing", "_model": "workflow" } },
|
|
4895
|
+
"actions": { "$link": { "name": "Send Payment Failure Email", "_model": "workflowAction" } },
|
|
4896
|
+
"isTerminal": true
|
|
4897
|
+
},
|
|
4898
|
+
{
|
|
4899
|
+
"name": "Handle Subscription Created",
|
|
4900
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4901
|
+
"actions": [
|
|
4902
|
+
{ "$link": { "name": "Sync Stripe Entity to Local DB", "_model": "workflowAction" } },
|
|
4903
|
+
{ "$link": { "name": "Send Subscription Welcome", "_model": "workflowAction" } }
|
|
4904
|
+
],
|
|
4905
|
+
"onSuccessStep": { "$link": { "name": "Handle Send Welcome Email", "_model": "workflowStep" } }
|
|
4906
|
+
},
|
|
4907
|
+
{
|
|
4908
|
+
"name": "Handle Send Welcome Email",
|
|
4909
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4910
|
+
"actions": { "$link": { "name": "Send Subscription Welcome", "_model": "workflowAction" } },
|
|
4911
|
+
"isTerminal": true
|
|
4912
|
+
},
|
|
4913
|
+
{
|
|
4914
|
+
"name": "Handle Subscription Update/Delete",
|
|
4915
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4916
|
+
"actions": { "$link": { "name": "Sync Stripe Entity to Local DB", "_model": "workflowAction" } },
|
|
4917
|
+
"isTerminal": true
|
|
4918
|
+
},
|
|
4919
|
+
{
|
|
4920
|
+
"name": "Handle Payment Succeeded",
|
|
4921
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4922
|
+
"actions": { "$link": { "name": "Sync Stripe Entity to Local DB", "_model": "workflowAction" } },
|
|
4923
|
+
"onSuccessStep": { "$link": { "name": "Handle Send Payment Receipt", "_model": "workflowStep" } }
|
|
4924
|
+
},
|
|
4925
|
+
{
|
|
4926
|
+
"name": "Handle Send Payment Receipt",
|
|
4927
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4928
|
+
"actions": { "$link": { "name": "Send Payment Receipt", "_model": "workflowAction" } },
|
|
4929
|
+
"isTerminal": true
|
|
4930
|
+
},
|
|
4931
|
+
|
|
4932
|
+
// --- Other Workflows Steps (Checkout, etc.) ---
|
|
4933
|
+
{
|
|
4934
|
+
"name": "Select Session Type",
|
|
4935
|
+
"workflow": { "$link": { "name": "Create Stripe Checkout Session", "_model": "workflow" } },
|
|
4936
|
+
"conditions": { "$eq": ["{triggerData.mode}", "subscription"] },
|
|
4937
|
+
"onSuccessStep": { "$link": { "name": "Create Subscription Checkout Session", "_model": "workflowStep" } },
|
|
4938
|
+
"onFailureStep": { "$link": { "name": "Create Payment Checkout Session", "_model": "workflowStep" } }
|
|
4939
|
+
},
|
|
4940
|
+
{
|
|
4941
|
+
"name": "Create Payment Checkout Session",
|
|
4942
|
+
"workflow": { "$link": { "name": "Create Stripe Checkout Session", "_model": "workflow" } },
|
|
4943
|
+
"actions": { "$link": { "name": "Stripe: Create Checkout Session (Payment)", "_model": "workflowAction" } },
|
|
4944
|
+
"isTerminal": true
|
|
4945
|
+
},
|
|
4946
|
+
{
|
|
4947
|
+
"name": "Create Subscription Checkout Session",
|
|
4948
|
+
"workflow": { "$link": { "name": "Create Stripe Checkout Session", "_model": "workflow" } },
|
|
4949
|
+
"actions": { "$link": { "name": "Stripe: Create Checkout Session (Subscription)", "_model": "workflowAction" } },
|
|
4950
|
+
"isTerminal": true
|
|
4951
|
+
},
|
|
4952
|
+
{
|
|
4953
|
+
"name": "Check for Customer Updated",
|
|
4954
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4955
|
+
"conditions": { "$in": ["{triggerData.event.type}", ["customer.created", "customer.updated", "customer.deleted"]] },
|
|
4956
|
+
"onSuccessStep": { "$link": { "name": "Handle Customer Update", "_model": "workflowStep" } },
|
|
4957
|
+
"onFailureStep": { "$link": { "name": "Check for Product Updated", "_model": "workflowStep" } }
|
|
4958
|
+
},
|
|
4959
|
+
{
|
|
4960
|
+
"name": "Check for Product Updated",
|
|
4961
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4962
|
+
"conditions": { "$in": ["{triggerData.event.type}", ["product.created", "product.updated", "product.deleted", "price.created", "price.updated"]] },
|
|
4963
|
+
"onSuccessStep": { "$link": { "name": "Handle Product Update", "_model": "workflowStep" } },
|
|
4964
|
+
"onFailureStep": { "$link": { "name": "Log Unhandled Event", "_model": "workflowStep" } }
|
|
4965
|
+
},
|
|
4966
|
+
{
|
|
4967
|
+
"name": "Handle Customer Update",
|
|
4968
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4969
|
+
"actions": { "$link": { "name": "Sync Stripe Entity to Local DB", "_model": "workflowAction" } },
|
|
4970
|
+
"isTerminal": true
|
|
4971
|
+
},
|
|
4972
|
+
{
|
|
4973
|
+
"name": "Handle Product Update",
|
|
4974
|
+
"workflow": { "$link": { "name": "Process Stripe Webhook Events", "_model": "workflow" } },
|
|
4975
|
+
"actions": { "$link": { "name": "Sync Stripe Entity to Local DB", "_model": "workflowAction" } },
|
|
4976
|
+
"isTerminal": true
|
|
4977
|
+
},
|
|
4978
|
+
{
|
|
4979
|
+
"name": "Generate Portal URL",
|
|
4980
|
+
"workflow": { "$link": { "name": "Create Stripe Customer Portal Session", "_model": "workflow" } },
|
|
4981
|
+
"actions": { "$link": { "name": "Stripe: Create Customer Portal Session", "_model": "workflowAction" } },
|
|
4982
|
+
"isTerminal": true
|
|
4983
|
+
}
|
|
4984
|
+
],
|
|
4985
|
+
"dashboard": [
|
|
4986
|
+
{
|
|
4987
|
+
"name": "Stripe Payments Overview",
|
|
4988
|
+
"description": "Key metrics and recent activity for Stripe payments.",
|
|
4989
|
+
"layout": [
|
|
4990
|
+
{
|
|
4991
|
+
"name": "Payment Metrics",
|
|
4992
|
+
"kpis": [
|
|
4993
|
+
"Total Revenue",
|
|
4994
|
+
"Successful Payments",
|
|
4995
|
+
"Avg. Payment Value",
|
|
4996
|
+
"Refund Rate"
|
|
4997
|
+
],
|
|
4998
|
+
"chartConfigs": [
|
|
4999
|
+
{
|
|
5000
|
+
"title": "Payments by Day",
|
|
5001
|
+
"type": "line",
|
|
5002
|
+
"model": "StripePayment",
|
|
5003
|
+
"xAxis": { "field": "created", "interval": "day" },
|
|
5004
|
+
"yAxis": { "field": "amount", "aggregation": "sum" },
|
|
5005
|
+
"filter": { "status": "succeeded" }
|
|
5006
|
+
}
|
|
5007
|
+
]
|
|
5008
|
+
}/*
|
|
5009
|
+
{
|
|
5010
|
+
"name": "Recent Activity",
|
|
5011
|
+
"component": "DataTable",
|
|
5012
|
+
"config": {
|
|
5013
|
+
"model": "StripePayment",
|
|
5014
|
+
"columns": ["created", "amount", "currency", "status", "user"],
|
|
5015
|
+
"limit": 10,
|
|
5016
|
+
"sort": { "created": -1 }
|
|
5017
|
+
}
|
|
5018
|
+
}*/
|
|
5019
|
+
]
|
|
5020
|
+
}
|
|
5021
|
+
],
|
|
5022
|
+
"kpi": [
|
|
5023
|
+
{
|
|
5024
|
+
"name": "Total Revenue",
|
|
5025
|
+
"targetModel": "StripePayment",
|
|
5026
|
+
"aggregationType": "sum",
|
|
5027
|
+
"aggregationField": "amount",
|
|
5028
|
+
"filter": { "status": "succeeded" },
|
|
5029
|
+
"unit": "$",
|
|
5030
|
+
"icon": "FaMoneyBillWave"
|
|
5031
|
+
},
|
|
5032
|
+
{
|
|
5033
|
+
"name": "Successful Payments",
|
|
5034
|
+
"targetModel": "StripePayment",
|
|
5035
|
+
"aggregationType": "count",
|
|
5036
|
+
"filter": { "status": "succeeded" },
|
|
5037
|
+
"icon": "FaCreditCard"
|
|
5038
|
+
},
|
|
5039
|
+
{
|
|
5040
|
+
"name": "Avg. Payment Value",
|
|
5041
|
+
"targetModel": "StripePayment",
|
|
5042
|
+
"aggregationType": "avg",
|
|
5043
|
+
"aggregationField": "amount",
|
|
5044
|
+
"filter": { "status": "succeeded" },
|
|
5045
|
+
"unit": "$",
|
|
5046
|
+
"icon": "FaCalculator"
|
|
5047
|
+
},
|
|
5048
|
+
{
|
|
5049
|
+
"name": "Refund Rate",
|
|
5050
|
+
"targetModel": "StripeRefund",
|
|
5051
|
+
"aggregationType": "ratio",
|
|
5052
|
+
"numerator": { "model": "StripeRefund", "aggregation": "count" },
|
|
5053
|
+
"denominator": { "model": "StripePayment", "aggregation": "count" },
|
|
5054
|
+
"unit": "%",
|
|
5055
|
+
"icon": "FaExchangeAlt"
|
|
5056
|
+
}
|
|
5057
|
+
]
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
4054
5060
|
}
|
|
4055
5061
|
];
|
|
4056
5062
|
}
|