0.13.3 : add stripe and subscription plan
Some checks failed
Build release Docker image / Build Docker Images (push) Failing after 4m18s

This commit is contained in:
2026-03-05 18:19:34 +01:00
parent ce37fafddf
commit bc63c56da6
10 changed files with 9608 additions and 5 deletions

View File

@@ -5,7 +5,12 @@ export default [
"strapi::cors", "strapi::cors",
"strapi::poweredBy", "strapi::poweredBy",
"strapi::query", "strapi::query",
"strapi::body", {
name: "strapi::body",
config: {
includeUnparsed: true, // INDISPENSABLE pour les webhooks
},
},
{ {
name: "strapi::session", name: "strapi::session",
config: { config: {

22
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "harmony-back", "name": "harmony-back",
"version": "0.13.1", "version": "0.13.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "harmony-back", "name": "harmony-back",
"version": "0.13.1", "version": "0.13.2",
"dependencies": { "dependencies": {
"@strapi/data-transfer": "^5.8.1", "@strapi/data-transfer": "^5.8.1",
"@strapi/plugin-documentation": "^5.8.1", "@strapi/plugin-documentation": "^5.8.1",
@@ -21,6 +21,7 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^6.30.3", "react-router-dom": "^6.30.3",
"strapi-v5-plugin-populate-deep": "^4.0.5", "strapi-v5-plugin-populate-deep": "^4.0.5",
"stripe": "^20.4.0",
"styled-components": "^6.0.0" "styled-components": "^6.0.0"
}, },
"devDependencies": { "devDependencies": {
@@ -19736,6 +19737,23 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/stripe": {
"version": "20.4.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-20.4.0.tgz",
"integrity": "sha512-F/aN1IQ9vHmlyLNi3DkiIbyzQb6gyBG0uYFd/VrEVQSc9BLtlgknPUx0EvzZdBMRLFuRaPFIFd7Mxwtg7Pbwzw==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"peerDependencies": {
"@types/node": ">=16"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/strnum": { "node_modules/strnum": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "harmony-back", "name": "harmony-back",
"version": "0.13.2", "version": "0.13.3",
"private": true, "private": true,
"description": "A Strapi application", "description": "A Strapi application",
"scripts": { "scripts": {
@@ -26,6 +26,7 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^6.30.3", "react-router-dom": "^6.30.3",
"strapi-v5-plugin-populate-deep": "^4.0.5", "strapi-v5-plugin-populate-deep": "^4.0.5",
"stripe": "^20.4.0",
"styled-components": "^6.0.0" "styled-components": "^6.0.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -0,0 +1,32 @@
{
"kind": "collectionType",
"collectionName": "orders",
"info": {
"singularName": "order",
"pluralName": "orders",
"displayName": "Order",
"description": ""
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"stripeId": {
"type": "string",
"unique": true
},
"amount": {
"type": "decimal"
},
"planType": {
"type": "string"
},
"user": {
"type": "relation",
"relation": "manyToOne",
"target": "plugin::users-permissions.user",
"inversedBy": "orders"
}
}
}

View File

@@ -0,0 +1,75 @@
import { factories } from "@strapi/strapi";
import Stripe from "stripe";
export default factories.createCoreController(
"api::order.order" as any,
({ strapi }) => ({
async handleWebhook(ctx) {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const sig = ctx.request.headers["stripe-signature"];
const unparsedBody = ctx.request.body[Symbol.for("unparsedBody")];
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
unparsedBody,
sig as string,
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch (err: any) {
strapi.log.error(`❌ Erreur de signature Webhook: ${err.message}`);
return ctx.badRequest(`Webhook Error: ${err.message}`);
}
// IMPORTANT : On logue le type d'événement reçu pour débugger
strapi.log.info(`📩 Événement reçu : ${event.type}`);
// On ne traite QUE le checkout.session.completed
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
const userId = session.metadata?.userId;
const planType = session.metadata?.planType as
| "free"
| "pro"
| "premium";
if (userId) {
try {
// Création de la commande
await strapi.documents("api::order.order").create({
data: {
stripeId: session.id,
amount: session.amount_total ? session.amount_total / 100 : 0,
planType: planType,
user: userId,
},
});
// Mise à jour de l'utilisateur
await strapi.documents("plugin::users-permissions.user").update({
documentId: userId,
data: {
isPremium: true,
subscriptionPlan: planType,
stripeCustomerId: session.customer as string,
subscriptionId: session.subscription as string,
trialStartedAt: null,
},
});
strapi.log.info(
`✅ Droits mis à jour pour l'utilisateur ${userId}`,
);
} catch (dbError) {
strapi.log.error(`❌ Erreur base de données : ${dbError.message}`);
// On répond quand même 200 à Stripe pour éviter les retries infinis
}
}
}
// INDISPENSABLE : Répondre 200 OK à Stripe pour TOUS les événements
// Cela évite que Stripe ne renvoie la requête 10 fois
ctx.send({ received: true });
},
}),
);

View File

@@ -0,0 +1,24 @@
export default {
routes: [
// La route pour le Webhook Stripe
{
method: "POST",
path: "/stripe/webhook",
handler: "api::order.order.handleWebhook",
config: {
auth: false,
},
},
// Les routes automatiques (find, findOne, create, etc.)
{
method: "GET",
path: "/orders",
handler: "api::order.order.find",
},
{
method: "GET",
path: "/orders/:id",
handler: "api::order.order.findOne",
},
],
};

View File

@@ -0,0 +1,7 @@
/**
* order service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::order.order');

View File

@@ -242,6 +242,28 @@
}, },
"education": { "education": {
"type": "string" "type": "string"
},
"subscriptionPlan": {
"type": "enumeration",
"enum": [
"free",
"pro",
"premium"
],
"default": "free"
},
"maxChoirs": {
"type": "integer",
"default": 0
},
"trialStartedAt": {
"type": "date"
},
"orders": {
"type": "relation",
"relation": "oneToMany",
"target": "api::order.order",
"mappedBy": "user"
} }
} }
} }

View File

@@ -1400,6 +1400,38 @@ export interface ApiNotificationNotification
}; };
} }
export interface ApiOrderOrder extends Struct.CollectionTypeSchema {
collectionName: 'orders';
info: {
description: '';
displayName: 'Order';
pluralName: 'orders';
singularName: 'order';
};
options: {
draftAndPublish: false;
};
attributes: {
amount: Schema.Attribute.Decimal;
createdAt: Schema.Attribute.DateTime;
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
locale: Schema.Attribute.String & Schema.Attribute.Private;
localizations: Schema.Attribute.Relation<'oneToMany', 'api::order.order'> &
Schema.Attribute.Private;
planType: Schema.Attribute.String;
publishedAt: Schema.Attribute.DateTime;
stripeId: Schema.Attribute.String & Schema.Attribute.Unique;
updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private;
user: Schema.Attribute.Relation<
'manyToOne',
'plugin::users-permissions.user'
>;
};
}
export interface ApiPagePage extends Struct.CollectionTypeSchema { export interface ApiPagePage extends Struct.CollectionTypeSchema {
collectionName: 'pages'; collectionName: 'pages';
info: { info: {
@@ -2113,7 +2145,9 @@ export interface PluginUsersPermissionsUser
'plugin::users-permissions.user' 'plugin::users-permissions.user'
> & > &
Schema.Attribute.Private; Schema.Attribute.Private;
maxChoirs: Schema.Attribute.Integer & Schema.Attribute.DefaultTo<0>;
name: Schema.Attribute.String; name: Schema.Attribute.String;
orders: Schema.Attribute.Relation<'oneToMany', 'api::order.order'>;
parameter: Schema.Attribute.Component<'configuration.parameter', false>; parameter: Schema.Attribute.Component<'configuration.parameter', false>;
password: Schema.Attribute.Password & password: Schema.Attribute.Password &
Schema.Attribute.Private & Schema.Attribute.Private &
@@ -2138,8 +2172,11 @@ export interface PluginUsersPermissionsUser
'plugin::users-permissions.role' 'plugin::users-permissions.role'
>; >;
saved_posts: Schema.Attribute.Relation<'oneToMany', 'api::post.post'>; saved_posts: Schema.Attribute.Relation<'oneToMany', 'api::post.post'>;
subscriptionPlan: Schema.Attribute.Enumeration<['free', 'pro', 'premium']> &
Schema.Attribute.DefaultTo<'free'>;
surname: Schema.Attribute.String; surname: Schema.Attribute.String;
tags: Schema.Attribute.JSON; tags: Schema.Attribute.JSON;
trialStartedAt: Schema.Attribute.Date;
updatedAt: Schema.Attribute.DateTime; updatedAt: Schema.Attribute.DateTime;
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
Schema.Attribute.Private; Schema.Attribute.Private;
@@ -2201,6 +2238,7 @@ declare module '@strapi/strapi' {
'api::mails.mails': ApiMailsMails; 'api::mails.mails': ApiMailsMails;
'api::message.message': ApiMessageMessage; 'api::message.message': ApiMessageMessage;
'api::notification.notification': ApiNotificationNotification; 'api::notification.notification': ApiNotificationNotification;
'api::order.order': ApiOrderOrder;
'api::page.page': ApiPagePage; 'api::page.page': ApiPagePage;
'api::permissions-template.permissions-template': ApiPermissionsTemplatePermissionsTemplate; 'api::permissions-template.permissions-template': ApiPermissionsTemplatePermissionsTemplate;
'api::post-ownership.post-ownership': ApiPostOwnershipPostOwnership; 'api::post-ownership.post-ownership': ApiPostOwnershipPostOwnership;