diff --git a/.github/workflows/precheck.yaml b/.github/workflows/precheck.yaml index 6501c35..043c420 100644 --- a/.github/workflows/precheck.yaml +++ b/.github/workflows/precheck.yaml @@ -24,8 +24,6 @@ jobs: run: npm install - name: Build Next.js app - env: - NEXT_PUBLIC_GRAPHQL_ENDPOINT: ${{ vars.GRAPHQL_ENDPOINT }} run: npm run build - name: Success 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..c38a7ee 100644 --- a/app/(api)/_datalib/_services/Orders.ts +++ b/app/(api)/_datalib/_services/Orders.ts @@ -3,6 +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'; export default class Orders { //CREATE @@ -12,6 +13,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 +314,77 @@ 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; + + 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; + } + } } 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/20250530031411_add_payment_intent_id/migration.sql b/prisma/migrations/20250530031411_add_payment_intent_id/migration.sql new file mode 100644 index 0000000..2a255e0 --- /dev/null +++ b/prisma/migrations/20250530031411_add_payment_intent_id/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Order" ADD COLUMN "paymentIntentId" TEXT; 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