From f190e151c86a8ff3add9085a881fb80c6d0856a2 Mon Sep 17 00:00:00 2001 From: brandonw504 Date: Tue, 24 Jun 2025 23:19:51 -0700 Subject: [PATCH 1/5] Stripe payment intents --- app/(api)/_datalib/_resolvers/Order.ts | 5 ++ app/(api)/_datalib/_services/Orders.ts | 70 +++++++++++++++++++ app/(api)/_datalib/_typeDefs/Order.ts | 12 ++++ app/(api)/_types/Order.ts | 1 + package-lock.json | 26 ++++++- package.json | 1 + .../migration.sql | 2 + .../20250625061418_order_total/migration.sql | 8 +++ prisma/schema/Order.prisma | 2 + 9 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20250625054346_order_payment_intent/migration.sql create mode 100644 prisma/migrations/20250625061418_order_total/migration.sql diff --git a/app/(api)/_datalib/_resolvers/Order.ts b/app/(api)/_datalib/_resolvers/Order.ts index ef05b45..858b56c 100644 --- a/app/(api)/_datalib/_resolvers/Order.ts +++ b/app/(api)/_datalib/_resolvers/Order.ts @@ -50,6 +50,11 @@ const resolvers = { }, ctx: ApolloContext ) => Orders.editProductQuantity(args.id, args.productToUpdate, ctx), + processOrder: async ( + _: never, + args: { input: OrderInput; products: [OrderProductInput] }, + ctx: ApolloContext + ) => Orders.processOrder(args.input, args.products, ctx), }, }; diff --git a/app/(api)/_datalib/_services/Orders.ts b/app/(api)/_datalib/_services/Orders.ts index 7d9d613..5caf99c 100644 --- a/app/(api)/_datalib/_services/Orders.ts +++ b/app/(api)/_datalib/_services/Orders.ts @@ -3,6 +3,11 @@ import prisma from '../_prisma/client'; import { OrderInput, OrderProductInput } from '@datatypes/Order'; import { ApolloContext } from '../apolloServer'; import { Prisma } from '@prisma/client'; +import Stripe from 'stripe'; + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: '2025-05-28.basil', // explicitly set the API version +}); export default class Orders { //CREATE @@ -12,6 +17,7 @@ export default class Orders { const order = prisma.order.create({ data: { ...input, // Spread the input fields + total: 0, status: 'pending', // Default status created_at: new Date(), // Current timestamp }, @@ -312,4 +318,68 @@ export default class Orders { return false; } } + + // PROCESS W/STRIPE + static async processOrder( + input: OrderInput, + products: OrderProductInput[], + ctx: ApolloContext + ) { + if (!ctx.isOwner && !ctx.hasValidApiKey) return null; + // Lookup product prices from DB + const productIds = products.map((p) => p.product_id); + const dbProducts = await prisma.product.findMany({ + where: { id: { in: productIds } }, + }); + + const productMap = Object.fromEntries(dbProducts.map((p) => [p.id, p])); + + const total = products.reduce((sum, item) => { + const product = productMap[item.product_id]; + return sum + (product?.price ?? 0) * item.quantity; + }, 0); + + // Stripe counts payment amounts in cents + const amountInCents = Math.round(total * 100); + + // Create the order + const createdOrder = await prisma.order.create({ + data: { + ...input, + total: total, + status: 'pending payment', + created_at: new Date(), + products: { + create: products.map((p) => ({ + quantity: p.quantity, + product: { connect: { id: p.product_id } }, + })), + }, + }, + include: { products: { include: { product: true } } }, + }); + + // Create Stripe PaymentIntent + const paymentIntent = await stripe.paymentIntents.create({ + amount: amountInCents, + currency: 'usd', + metadata: { + orderId: createdOrder.id, + }, + }); + + // Save paymentIntentId to order + const updatedOrder = await prisma.order.update({ + where: { id: createdOrder.id }, + data: { paymentIntentId: paymentIntent.id }, + include: { products: { include: { product: true } } }, + }); + + revalidateCache(['orders', 'products']); + + return { + order: updatedOrder, + clientSecret: paymentIntent.client_secret, + }; + } } diff --git a/app/(api)/_datalib/_typeDefs/Order.ts b/app/(api)/_datalib/_typeDefs/Order.ts index 999d214..f2a1755 100644 --- a/app/(api)/_datalib/_typeDefs/Order.ts +++ b/app/(api)/_datalib/_typeDefs/Order.ts @@ -3,6 +3,8 @@ import gql from 'graphql-tag'; const typeDefs = gql` type Order { id: ID! + paymentIntentId: String + total: Float! products: [OrderProduct] customer_name: String! customer_email: String! @@ -39,6 +41,7 @@ const typeDefs = gql` } input OrderUpdateInput { + total: Float customer_name: String customer_email: String customer_phone_num: String @@ -65,6 +68,11 @@ const typeDefs = gql` quantity: Int! } + type ProcessOrderResult { + order: Order! + clientSecret: String! + } + type Query { order(id: ID!): Order orders( @@ -82,6 +90,10 @@ const typeDefs = gql` addProductToOrder(id: ID!, productToAdd: OrderProductInput!): Order removeProductFromOrder(id: ID!, product_id: ID!): Order editProductQuantity(id: ID!, productToUpdate: OrderProductInput!): Order + processOrder( + input: OrderInput! + products: [OrderProductInput!]! + ): ProcessOrderResult } `; diff --git a/app/(api)/_types/Order.ts b/app/(api)/_types/Order.ts index 5cb17cc..e71ca62 100644 --- a/app/(api)/_types/Order.ts +++ b/app/(api)/_types/Order.ts @@ -2,6 +2,7 @@ import { Product } from './Product'; export type Order = { id: number; + total: number; customer_name: string; customer_email: string; customer_phone_num: string; diff --git a/package-lock.json b/package-lock.json index b84db7d..48bcb29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "include-estore-manager", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@apollo/server": "^4.11.2", "@as-integrations/next": "^3.2.0", @@ -26,6 +27,7 @@ "react-quill": "^2.0.0", "readline": "^1.3.0", "sass": "^1.69.5", + "stripe": "^18.2.1", "validator": "^13.12.0", "zod": "^3.24.2" }, @@ -2151,7 +2153,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", + "version": "1.0.30001724", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz", + "integrity": "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==", "funding": [ { "type": "opencollective", @@ -7317,6 +7321,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-18.2.1.tgz", + "integrity": "sha512-GwB1B7WSwEBzW4dilgyJruUYhbGMscrwuyHsPUmSRKrGHZ5poSh2oU9XKdii5BFVJzXHn35geRvGJ6R8bYcp8w==", + "license": "MIT", + "dependencies": { + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + }, + "peerDependencies": { + "@types/node": ">=12.x.x" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "license": "MIT", diff --git a/package.json b/package.json index 3422cfb..1fc41d2 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "react-quill": "^2.0.0", "readline": "^1.3.0", "sass": "^1.69.5", + "stripe": "^18.2.1", "validator": "^13.12.0", "zod": "^3.24.2" }, diff --git a/prisma/migrations/20250625054346_order_payment_intent/migration.sql b/prisma/migrations/20250625054346_order_payment_intent/migration.sql new file mode 100644 index 0000000..2a255e0 --- /dev/null +++ b/prisma/migrations/20250625054346_order_payment_intent/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Order" ADD COLUMN "paymentIntentId" TEXT; diff --git a/prisma/migrations/20250625061418_order_total/migration.sql b/prisma/migrations/20250625061418_order_total/migration.sql new file mode 100644 index 0000000..68da700 --- /dev/null +++ b/prisma/migrations/20250625061418_order_total/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `total` to the `Order` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Order" ADD COLUMN "total" DOUBLE PRECISION NOT NULL; diff --git a/prisma/schema/Order.prisma b/prisma/schema/Order.prisma index d9df6b7..d599fd2 100644 --- a/prisma/schema/Order.prisma +++ b/prisma/schema/Order.prisma @@ -1,5 +1,7 @@ model Order { id Int @id @default(autoincrement()) + paymentIntentId String? + total Float customer_name String customer_email String customer_phone_num String From c12615cd8732c1758ac9a8021c57419acd92574e Mon Sep 17 00:00:00 2001 From: brandonw504 Date: Tue, 24 Jun 2025 23:21:01 -0700 Subject: [PATCH 2/5] Changed status to just pending --- app/(api)/_datalib/_services/Orders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(api)/_datalib/_services/Orders.ts b/app/(api)/_datalib/_services/Orders.ts index 5caf99c..4f6c585 100644 --- a/app/(api)/_datalib/_services/Orders.ts +++ b/app/(api)/_datalib/_services/Orders.ts @@ -347,7 +347,7 @@ export default class Orders { data: { ...input, total: total, - status: 'pending payment', + status: 'pending', created_at: new Date(), products: { create: products.map((p) => ({ From df98291c4e88c0f676ff9251c065e46abdebf15b Mon Sep 17 00:00:00 2001 From: brandonw504 Date: Tue, 24 Jun 2025 23:39:27 -0700 Subject: [PATCH 3/5] Wrapped in try catch --- app/(api)/_datalib/_services/Orders.ts | 97 ++++++++++++++------------ 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/app/(api)/_datalib/_services/Orders.ts b/app/(api)/_datalib/_services/Orders.ts index 4f6c585..2829edd 100644 --- a/app/(api)/_datalib/_services/Orders.ts +++ b/app/(api)/_datalib/_services/Orders.ts @@ -326,60 +326,65 @@ export default class Orders { ctx: ApolloContext ) { if (!ctx.isOwner && !ctx.hasValidApiKey) return null; - // Lookup product prices from DB - const productIds = products.map((p) => p.product_id); - const dbProducts = await prisma.product.findMany({ - where: { id: { in: productIds } }, - }); - const productMap = Object.fromEntries(dbProducts.map((p) => [p.id, p])); + try { + // Lookup product prices from DB + const productIds = products.map((p) => p.product_id); + const dbProducts = await prisma.product.findMany({ + where: { id: { in: productIds } }, + }); - const total = products.reduce((sum, item) => { - const product = productMap[item.product_id]; - return sum + (product?.price ?? 0) * item.quantity; - }, 0); + const productMap = Object.fromEntries(dbProducts.map((p) => [p.id, p])); - // Stripe counts payment amounts in cents - const amountInCents = Math.round(total * 100); + const total = products.reduce((sum, item) => { + const product = productMap[item.product_id]; + return sum + (product?.price ?? 0) * item.quantity; + }, 0); - // Create the order - const createdOrder = await prisma.order.create({ - data: { - ...input, - total: total, - status: 'pending', - created_at: new Date(), - products: { - create: products.map((p) => ({ - quantity: p.quantity, - product: { connect: { id: p.product_id } }, - })), + // Stripe counts payment amounts in cents + const amountInCents = Math.round(total * 100); + + // Create the order + const createdOrder = await prisma.order.create({ + data: { + ...input, + total: total, + status: 'pending', + created_at: new Date(), + products: { + create: products.map((p) => ({ + quantity: p.quantity, + product: { connect: { id: p.product_id } }, + })), + }, }, - }, - include: { products: { include: { product: true } } }, - }); + include: { products: { include: { product: true } } }, + }); - // Create Stripe PaymentIntent - const paymentIntent = await stripe.paymentIntents.create({ - amount: amountInCents, - currency: 'usd', - metadata: { - orderId: createdOrder.id, - }, - }); + // Create Stripe PaymentIntent + const paymentIntent = await stripe.paymentIntents.create({ + amount: amountInCents, + currency: 'usd', + metadata: { + orderId: createdOrder.id, + }, + }); - // Save paymentIntentId to order - const updatedOrder = await prisma.order.update({ - where: { id: createdOrder.id }, - data: { paymentIntentId: paymentIntent.id }, - include: { products: { include: { product: true } } }, - }); + // Save paymentIntentId to order + const updatedOrder = await prisma.order.update({ + where: { id: createdOrder.id }, + data: { paymentIntentId: paymentIntent.id }, + include: { products: { include: { product: true } } }, + }); - revalidateCache(['orders', 'products']); + revalidateCache(['orders', 'products']); - return { - order: updatedOrder, - clientSecret: paymentIntent.client_secret, - }; + return { + order: updatedOrder, + clientSecret: paymentIntent.client_secret, + }; + } catch (e) { + return e; + } } } From 8fcc46a80ed17fc663e824e2a0eeedc9ac01496b Mon Sep 17 00:00:00 2001 From: brandonw504 Date: Wed, 25 Jun 2025 21:58:31 -0700 Subject: [PATCH 4/5] test --- app/(api)/_datalib/_services/Orders.ts | 128 +++++++++++++------------ 1 file changed, 65 insertions(+), 63 deletions(-) diff --git a/app/(api)/_datalib/_services/Orders.ts b/app/(api)/_datalib/_services/Orders.ts index 2829edd..8c2b693 100644 --- a/app/(api)/_datalib/_services/Orders.ts +++ b/app/(api)/_datalib/_services/Orders.ts @@ -3,11 +3,11 @@ import prisma from '../_prisma/client'; import { OrderInput, OrderProductInput } from '@datatypes/Order'; import { ApolloContext } from '../apolloServer'; import { Prisma } from '@prisma/client'; -import Stripe from 'stripe'; +// import Stripe from 'stripe'; -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { - apiVersion: '2025-05-28.basil', // explicitly set the API version -}); +// const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { +// apiVersion: '2025-05-28.basil', // explicitly set the API version +// }); export default class Orders { //CREATE @@ -327,64 +327,66 @@ export default class Orders { ) { if (!ctx.isOwner && !ctx.hasValidApiKey) return null; - try { - // Lookup product prices from DB - const productIds = products.map((p) => p.product_id); - const dbProducts = await prisma.product.findMany({ - where: { id: { in: productIds } }, - }); - - const productMap = Object.fromEntries(dbProducts.map((p) => [p.id, p])); - - const total = products.reduce((sum, item) => { - const product = productMap[item.product_id]; - return sum + (product?.price ?? 0) * item.quantity; - }, 0); - - // Stripe counts payment amounts in cents - const amountInCents = Math.round(total * 100); - - // Create the order - const createdOrder = await prisma.order.create({ - data: { - ...input, - total: total, - status: 'pending', - created_at: new Date(), - products: { - create: products.map((p) => ({ - quantity: p.quantity, - product: { connect: { id: p.product_id } }, - })), - }, - }, - include: { products: { include: { product: true } } }, - }); - - // Create Stripe PaymentIntent - const paymentIntent = await stripe.paymentIntents.create({ - amount: amountInCents, - currency: 'usd', - metadata: { - orderId: createdOrder.id, - }, - }); - - // Save paymentIntentId to order - const updatedOrder = await prisma.order.update({ - where: { id: createdOrder.id }, - data: { paymentIntentId: paymentIntent.id }, - include: { products: { include: { product: true } } }, - }); - - revalidateCache(['orders', 'products']); - - return { - order: updatedOrder, - clientSecret: paymentIntent.client_secret, - }; - } catch (e) { - return e; - } + return null; + + // try { + // // Lookup product prices from DB + // const productIds = products.map((p) => p.product_id); + // const dbProducts = await prisma.product.findMany({ + // where: { id: { in: productIds } }, + // }); + + // const productMap = Object.fromEntries(dbProducts.map((p) => [p.id, p])); + + // const total = products.reduce((sum, item) => { + // const product = productMap[item.product_id]; + // return sum + (product?.price ?? 0) * item.quantity; + // }, 0); + + // // Stripe counts payment amounts in cents + // const amountInCents = Math.round(total * 100); + + // // Create the order + // const createdOrder = await prisma.order.create({ + // data: { + // ...input, + // total: total, + // status: 'pending', + // created_at: new Date(), + // products: { + // create: products.map((p) => ({ + // quantity: p.quantity, + // product: { connect: { id: p.product_id } }, + // })), + // }, + // }, + // include: { products: { include: { product: true } } }, + // }); + + // // Create Stripe PaymentIntent + // const paymentIntent = await stripe.paymentIntents.create({ + // amount: amountInCents, + // currency: 'usd', + // metadata: { + // orderId: createdOrder.id, + // }, + // }); + + // // Save paymentIntentId to order + // const updatedOrder = await prisma.order.update({ + // where: { id: createdOrder.id }, + // data: { paymentIntentId: paymentIntent.id }, + // include: { products: { include: { product: true } } }, + // }); + + // revalidateCache(['orders', 'products']); + + // return { + // order: updatedOrder, + // clientSecret: paymentIntent.client_secret, + // }; + // } catch (e) { + // return e; + // } } } From 4be0bf471f37797b67f659b335da5eb06af45e6c Mon Sep 17 00:00:00 2001 From: brandonw504 Date: Wed, 25 Jun 2025 22:46:26 -0700 Subject: [PATCH 5/5] Tried moving stripe init --- app/(api)/_datalib/_services/Orders.ts | 130 ++++++++++++------------- 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/app/(api)/_datalib/_services/Orders.ts b/app/(api)/_datalib/_services/Orders.ts index 8c2b693..c38a7ee 100644 --- a/app/(api)/_datalib/_services/Orders.ts +++ b/app/(api)/_datalib/_services/Orders.ts @@ -3,11 +3,7 @@ import prisma from '../_prisma/client'; import { OrderInput, OrderProductInput } from '@datatypes/Order'; import { ApolloContext } from '../apolloServer'; import { Prisma } from '@prisma/client'; -// import Stripe from 'stripe'; - -// const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { -// apiVersion: '2025-05-28.basil', // explicitly set the API version -// }); +import Stripe from 'stripe'; export default class Orders { //CREATE @@ -327,66 +323,68 @@ export default class Orders { ) { if (!ctx.isOwner && !ctx.hasValidApiKey) return null; - return null; - - // try { - // // Lookup product prices from DB - // const productIds = products.map((p) => p.product_id); - // const dbProducts = await prisma.product.findMany({ - // where: { id: { in: productIds } }, - // }); - - // const productMap = Object.fromEntries(dbProducts.map((p) => [p.id, p])); - - // const total = products.reduce((sum, item) => { - // const product = productMap[item.product_id]; - // return sum + (product?.price ?? 0) * item.quantity; - // }, 0); - - // // Stripe counts payment amounts in cents - // const amountInCents = Math.round(total * 100); - - // // Create the order - // const createdOrder = await prisma.order.create({ - // data: { - // ...input, - // total: total, - // status: 'pending', - // created_at: new Date(), - // products: { - // create: products.map((p) => ({ - // quantity: p.quantity, - // product: { connect: { id: p.product_id } }, - // })), - // }, - // }, - // include: { products: { include: { product: true } } }, - // }); - - // // Create Stripe PaymentIntent - // const paymentIntent = await stripe.paymentIntents.create({ - // amount: amountInCents, - // currency: 'usd', - // metadata: { - // orderId: createdOrder.id, - // }, - // }); - - // // Save paymentIntentId to order - // const updatedOrder = await prisma.order.update({ - // where: { id: createdOrder.id }, - // data: { paymentIntentId: paymentIntent.id }, - // include: { products: { include: { product: true } } }, - // }); - - // revalidateCache(['orders', 'products']); - - // return { - // order: updatedOrder, - // clientSecret: paymentIntent.client_secret, - // }; - // } catch (e) { - // return e; - // } + try { + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: '2025-05-28.basil', // explicitly set the API version + }); + + // Lookup product prices from DB + const productIds = products.map((p) => p.product_id); + const dbProducts = await prisma.product.findMany({ + where: { id: { in: productIds } }, + }); + + const productMap = Object.fromEntries(dbProducts.map((p) => [p.id, p])); + + const total = products.reduce((sum, item) => { + const product = productMap[item.product_id]; + return sum + (product?.price ?? 0) * item.quantity; + }, 0); + + // Stripe counts payment amounts in cents + const amountInCents = Math.round(total * 100); + + // Create the order + const createdOrder = await prisma.order.create({ + data: { + ...input, + total: total, + status: 'pending', + created_at: new Date(), + products: { + create: products.map((p) => ({ + quantity: p.quantity, + product: { connect: { id: p.product_id } }, + })), + }, + }, + include: { products: { include: { product: true } } }, + }); + + // Create Stripe PaymentIntent + const paymentIntent = await stripe.paymentIntents.create({ + amount: amountInCents, + currency: 'usd', + metadata: { + orderId: createdOrder.id, + }, + }); + + // Save paymentIntentId to order + const updatedOrder = await prisma.order.update({ + where: { id: createdOrder.id }, + data: { paymentIntentId: paymentIntent.id }, + include: { products: { include: { product: true } } }, + }); + + revalidateCache(['orders', 'products']); + + return { + order: updatedOrder, + clientSecret: paymentIntent.client_secret, + }; + } catch (e) { + return e; + } } }