From 8992e638fe2a7056bdd5af99537d8cd867d3750d Mon Sep 17 00:00:00 2001 From: Fayssal Mechmeche Date: Sat, 13 Jul 2024 16:38:50 +0200 Subject: [PATCH] fix bug --- backend/app/models/sql/Order.ts | 5 ++ .../sql/OrderProductRepository.ts | 4 ++ backend/app/routers/CheckoutRouter.ts | 68 ++++++++++++++++--- backend/app/services/CheckoutService.ts | 25 +++++-- backend/migrations/16-order.ts | 22 ++++++ backend/package-lock.json | 8 ++- backend/package.json | 3 +- compose.yaml | 2 +- frontend/src/router/index.ts | 12 ++++ frontend/src/views/CheckoutCancelView.vue | 51 ++++++++++++++ frontend/src/views/CheckoutSuccessView.vue | 39 +++++++++++ frontend/src/views/CheckoutView.vue | 25 ++++--- 12 files changed, 238 insertions(+), 26 deletions(-) create mode 100644 backend/migrations/16-order.ts create mode 100644 frontend/src/views/CheckoutCancelView.vue create mode 100644 frontend/src/views/CheckoutSuccessView.vue diff --git a/backend/app/models/sql/Order.ts b/backend/app/models/sql/Order.ts index 446ae6aa..c9d6e2bc 100644 --- a/backend/app/models/sql/Order.ts +++ b/backend/app/models/sql/Order.ts @@ -13,6 +13,7 @@ export class Order extends Model { declare status: string; declare payment_status: string; declare reference: string; + declare session_id: string; declare userId: ForeignKey; } @@ -35,6 +36,10 @@ export default (sequelize: Sequelize) => { type: DataTypes.STRING, allowNull: false, }, + session_id: { + type: DataTypes.TEXT, + allowNull: false, + }, }, { sequelize, underscored: true }, ); diff --git a/backend/app/repositories/sql/OrderProductRepository.ts b/backend/app/repositories/sql/OrderProductRepository.ts index 13845f7e..a2ec8b1b 100644 --- a/backend/app/repositories/sql/OrderProductRepository.ts +++ b/backend/app/repositories/sql/OrderProductRepository.ts @@ -16,6 +16,10 @@ export class OrderProductRepository { return OrderProduct.findByPk(id); } + static async findByOrderId(orderId: number): Promise { + return OrderProduct.findAll({ where: { orderId } }); + } + static async update(orderProduct: OrderProduct): Promise { return orderProduct.save(); } diff --git a/backend/app/routers/CheckoutRouter.ts b/backend/app/routers/CheckoutRouter.ts index 997458d6..95e60a7a 100644 --- a/backend/app/routers/CheckoutRouter.ts +++ b/backend/app/routers/CheckoutRouter.ts @@ -1,11 +1,14 @@ import { Router } from 'express'; import { NextFunction, Request, Response } from 'express'; +import uniqid from 'uniqid'; import dotenv from 'dotenv'; import { CheckoutService } from '../services/CheckoutService'; import { CartService } from '../services/CartService'; import { CartRepository } from '../repositories/mongodb/CartRepository'; import { VariantRepository } from '../repositories/sql/VariantRepository'; import { auth } from '../middlewares/auth'; +import { OrderRepository } from '../repositories/sql/OrderRepository'; +import { OrderProductRepository } from '../repositories/sql/OrderProductRepository'; dotenv.config(); @@ -52,15 +55,19 @@ CheckoutRouter.post( } } try { + const reference = 'sneakpeak' + '-' + uniqid(); + const session = await CheckoutService.getCheckoutSession( cartProducts, res.locals.user.id, + reference, ); - + console.log(session); const order = await CheckoutService.createOrder( session.amount_total as number, - session.id as string, - parseInt(res.locals.user.id), + reference, + session.id, + res.locals.user.id, ); for (const item of cartProducts) { @@ -98,12 +105,51 @@ CheckoutRouter.post( }, ); -CheckoutRouter.get('/success', async (req: Request, res: Response) => { - const success_url = req.body.success_url; - res.json(success_url); -}); +CheckoutRouter.get( + '/success/:reference', + auth, + async (req: Request, res: Response) => { + const order = await OrderRepository.findByReference(req.params.reference); + if (!order) { + return res.status(404).json({ error: 'Order not found' }); + } + if (order.status === 'pending') { + res.redirect('/checkout/cancel/' + req.params.reference); + } + res.json(order); + }, +); -CheckoutRouter.get('/cancel', async (req: Request, res: Response) => { - const cancel_url = req.body.cancel_url; - res.json(cancel_url); -}); +CheckoutRouter.get( + '/cancel/:reference', + auth, + async (req: Request, res: Response) => { + console.log(req.params.reference); + const order = await OrderRepository.findByReference(req.params.reference); + if (!order) { + return res.status(404).json({ error: 'Order not found' }); + } + const orderProducts = await OrderProductRepository.findByOrderId(order.id); + const sessionid = order.session_id; + let linkPaiement = await CheckoutService.getCheckoutSessionById(sessionid); + console.log(linkPaiement); + if (!linkPaiement) { + return res.status(404).json({ error: 'Session not found' }); + } + + if (linkPaiement.payment_status === 'paid') { + return res.status(400).json({ error: 'Order already paid' }); + } + + if (linkPaiement.status === 'expired') { + linkPaiement = await CheckoutService.getCheckoutSession( + orderProducts, + res.locals.user.id, + order.reference, + ); + order.session_id = linkPaiement.id; + OrderRepository.update(order); + } + res.json(linkPaiement); + }, +); diff --git a/backend/app/services/CheckoutService.ts b/backend/app/services/CheckoutService.ts index 4d8ede4b..cb050714 100644 --- a/backend/app/services/CheckoutService.ts +++ b/backend/app/services/CheckoutService.ts @@ -17,8 +17,9 @@ export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, { export class CheckoutService { public static async getCheckoutSession( - cartProducts: CartProduct[], + cartProducts: CartProduct[] | OrderProduct[], userId: string, + reference: string, ): Promise { const products = cartProducts.map((product) => { return { @@ -37,14 +38,29 @@ export class CheckoutService { metadata: { userId: userId }, line_items: products, mode: 'payment', - cancel_url: 'http://localhost:5173/checkout/cancel', - success_url: 'http://localhost:5173/checkout/success', + cancel_url: 'http://localhost:5173/checkout/cancel/' + reference, + success_url: 'http://localhost:5173/checkout/success/' + reference, }); return session; } + + public static async getCheckoutSessionByOrderId( + orderId: number, + ): Promise { + const order = await OrderRepository.findById(orderId); + if (!order) throw new Error('Order not found'); + return await stripe.checkout.sessions.retrieve(order.session_id); + } + + public static async getCheckoutSessionById( + sessionId: string, + ): Promise { + return await stripe.checkout.sessions.retrieve(sessionId); + } public static async createOrder( total: number, reference: string, + session_id: string, userId: number, ): Promise { console.log(total, reference, userId); @@ -52,8 +68,9 @@ export class CheckoutService { total: total / 100, status: 'pending', payment_status: 'pending', - reference: 'sneakpeak' + '-' + reference, + reference: reference, userId: userId, + session_id: session_id, }); return await OrderRepository.create(new_order); diff --git a/backend/migrations/16-order.ts b/backend/migrations/16-order.ts new file mode 100644 index 00000000..10bcc96c --- /dev/null +++ b/backend/migrations/16-order.ts @@ -0,0 +1,22 @@ +import { DataTypes, Sequelize } from 'sequelize'; +import { Migration } from '../bin/migrate'; + +// ATTENTION à bien underscorer manuellement les noms de colonnes +export const up: Migration = async ({ + context: sequelize, +}: { + context: Sequelize; +}) => { + await sequelize.getQueryInterface().addColumn('orders', 'session_id', { + type: DataTypes.TEXT('long'), + allowNull: false, + }); +}; + +export const down: Migration = async ({ + context: sequelize, +}: { + context: Sequelize; +}) => { + await sequelize.getQueryInterface().removeColumn('orders', 'session_id'); +}; diff --git a/backend/package-lock.json b/backend/package-lock.json index a2a03884..b17a92f0 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -23,7 +23,8 @@ "sequelize": "^6.37.3", "slugify": "^1.6.6", "stripe": "^16.0.0", - "umzug": "^3.8.1" + "umzug": "^3.8.1", + "uniqid": "^5.4.0" }, "devDependencies": { "@eslint/js": "^9.2.0", @@ -5589,6 +5590,11 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/uniqid": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-5.4.0.tgz", + "integrity": "sha512-38JRbJ4Fj94VmnC7G/J/5n5SC7Ab46OM5iNtSstB/ko3l1b5g7ALt4qzHFgGciFkyiRNtDXtLNb+VsxtMSE77A==" + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", diff --git a/backend/package.json b/backend/package.json index e6ab1eb9..7ff9d46d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -49,8 +49,9 @@ "postmark": "^4.0.2", "sequelize": "^6.37.3", "slugify": "^1.6.6", + "stripe": "^16.0.0", "umzug": "^3.8.1", - "stripe": "^16.0.0" + "uniqid": "^5.4.0" }, "devDependencies": { "@eslint/js": "^9.2.0", diff --git a/compose.yaml b/compose.yaml index 7eff1acd..24681fbd 100644 --- a/compose.yaml +++ b/compose.yaml @@ -66,7 +66,7 @@ services: image: stripe/stripe-cli depends_on: - backend - command: listen --forward-to http://backend/webhook --api-key $STRIPE_SECRET_KEY + command: listen --forward-to http://backend:3000/webhook --api-key $STRIPE_SECRET_KEY volumes: postgres: diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index ef438438..16d81279 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -4,6 +4,8 @@ import EmailVerificationView from '../views/EmailVerificationView.vue' import SearchView from '@/views/SearchView.vue' import CartView from '@/views/CartView.vue' import CheckoutView from '@/views/CheckoutView.vue' +import CheckoutSuccessView from '@/views/CheckoutSuccessView.vue' +import CheckoutCancelView from '@/views/CheckoutCancelView.vue' import ResetPasswordView from '@/views/ResetPasswordView.vue' import CGUView from '@/views/legal/CGUView.vue' import { checkAuth } from '@/helpers/auth' @@ -37,6 +39,16 @@ const router = createRouter({ name: 'checkout', component: CheckoutView }, + { + path: '/checkout/success/:reference', + name: 'success', + component: CheckoutSuccessView + }, + { + path: '/checkout/cancel/:reference', + name: 'cancel', + component: CheckoutCancelView + }, { path: '/reset-password', name: 'reset_password', diff --git a/frontend/src/views/CheckoutCancelView.vue b/frontend/src/views/CheckoutCancelView.vue new file mode 100644 index 00000000..d2d54f6f --- /dev/null +++ b/frontend/src/views/CheckoutCancelView.vue @@ -0,0 +1,51 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/CheckoutSuccessView.vue b/frontend/src/views/CheckoutSuccessView.vue new file mode 100644 index 00000000..f3e04885 --- /dev/null +++ b/frontend/src/views/CheckoutSuccessView.vue @@ -0,0 +1,39 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/CheckoutView.vue b/frontend/src/views/CheckoutView.vue index 95570297..93391592 100644 --- a/frontend/src/views/CheckoutView.vue +++ b/frontend/src/views/CheckoutView.vue @@ -9,7 +9,6 @@ import { onMounted, onBeforeUnmount, reactive, ref, type Ref, watch } from 'vue' import { CheckoutApi } from '@/services/checkoutApi'; import { CartStore } from '@/store/cart'; import { CartApi } from '@/services/cartApi'; -import Toast from 'primevue/toast'; import { useToast } from 'primevue/usetoast'; @@ -171,13 +170,23 @@ onBeforeUnmount(() => {