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::poweredBy",
|
||||
"strapi::query",
|
||||
"strapi::body",
|
||||
{
|
||||
name: "strapi::body",
|
||||
config: {
|
||||
includeUnparsed: true, // INDISPENSABLE pour les webhooks
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "strapi::session",
|
||||
config: {
|
||||
|
||||
22
package-lock.json
generated
22
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "harmony-back",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "harmony-back",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.2",
|
||||
"dependencies": {
|
||||
"@strapi/data-transfer": "^5.8.1",
|
||||
"@strapi/plugin-documentation": "^5.8.1",
|
||||
@@ -21,6 +21,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"strapi-v5-plugin-populate-deep": "^4.0.5",
|
||||
"stripe": "^20.4.0",
|
||||
"styled-components": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -19736,6 +19737,23 @@
|
||||
"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": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "harmony-back",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.3",
|
||||
"private": true,
|
||||
"description": "A Strapi application",
|
||||
"scripts": {
|
||||
@@ -26,6 +26,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"strapi-v5-plugin-populate-deep": "^4.0.5",
|
||||
"stripe": "^20.4.0",
|
||||
"styled-components": "^6.0.0"
|
||||
},
|
||||
"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": {
|
||||
"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 {
|
||||
collectionName: 'pages';
|
||||
info: {
|
||||
@@ -2113,7 +2145,9 @@ export interface PluginUsersPermissionsUser
|
||||
'plugin::users-permissions.user'
|
||||
> &
|
||||
Schema.Attribute.Private;
|
||||
maxChoirs: Schema.Attribute.Integer & Schema.Attribute.DefaultTo<0>;
|
||||
name: Schema.Attribute.String;
|
||||
orders: Schema.Attribute.Relation<'oneToMany', 'api::order.order'>;
|
||||
parameter: Schema.Attribute.Component<'configuration.parameter', false>;
|
||||
password: Schema.Attribute.Password &
|
||||
Schema.Attribute.Private &
|
||||
@@ -2138,8 +2172,11 @@ export interface PluginUsersPermissionsUser
|
||||
'plugin::users-permissions.role'
|
||||
>;
|
||||
saved_posts: Schema.Attribute.Relation<'oneToMany', 'api::post.post'>;
|
||||
subscriptionPlan: Schema.Attribute.Enumeration<['free', 'pro', 'premium']> &
|
||||
Schema.Attribute.DefaultTo<'free'>;
|
||||
surname: Schema.Attribute.String;
|
||||
tags: Schema.Attribute.JSON;
|
||||
trialStartedAt: Schema.Attribute.Date;
|
||||
updatedAt: Schema.Attribute.DateTime;
|
||||
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||
Schema.Attribute.Private;
|
||||
@@ -2201,6 +2238,7 @@ declare module '@strapi/strapi' {
|
||||
'api::mails.mails': ApiMailsMails;
|
||||
'api::message.message': ApiMessageMessage;
|
||||
'api::notification.notification': ApiNotificationNotification;
|
||||
'api::order.order': ApiOrderOrder;
|
||||
'api::page.page': ApiPagePage;
|
||||
'api::permissions-template.permissions-template': ApiPermissionsTemplatePermissionsTemplate;
|
||||
'api::post-ownership.post-ownership': ApiPostOwnershipPostOwnership;
|
||||
|
||||
Reference in New Issue
Block a user