0.13.3 : add stripe and subscription plan
Some checks failed
Build release Docker image / Build Docker Images (push) Failing after 4m18s
Some checks failed
Build release Docker image / Build Docker Images (push) Failing after 4m18s
This commit is contained in:
@@ -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
22
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
32
src/api/order/content-types/order/schema.json
Normal file
32
src/api/order/content-types/order/schema.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/api/order/controllers/order.ts
Normal file
75
src/api/order/controllers/order.ts
Normal 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 });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
24
src/api/order/routes/order.ts
Normal file
24
src/api/order/routes/order.ts
Normal 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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
7
src/api/order/services/order.ts
Normal file
7
src/api/order/services/order.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* order service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreService('api::order.order');
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
types/generated/contentTypes.d.ts
vendored
38
types/generated/contentTypes.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user