From 09d4b2e5a9918ad1790b22ef41198572b261ef8d Mon Sep 17 00:00:00 2001 From: Arslan Kamchybekov Date: Sat, 12 Oct 2024 16:10:51 -0500 Subject: [PATCH] stripe setup complete --- backend/src/app.module.ts | 8 ++- backend/src/insurance/insurance.schema.ts | 4 ++ backend/src/stripe/stripe.controller.ts | 12 +++- backend/src/stripe/stripe.service.ts | 29 ++++++++-- frontend/package-lock.json | 33 ++++++++++- frontend/package.json | 1 + frontend/src/pages/checkout.tsx | 67 +++++++++++++++++++++++ 7 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 frontend/src/pages/checkout.tsx diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 2bc4c71..4bccc57 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -7,10 +7,12 @@ import { ServiceController } from "./services/service.controller"; import { ServiceService } from "./services/service.service"; import { AuthModule } from "./auth/auth.module"; import { StripeModule } from './stripe/stripe.module'; +import { StripeController } from './stripe/stripe.controller'; +import { StripeService } from "./stripe/stripe.service"; @Module({ - imports: [AuthModule, StripeModule], - controllers: [InsuranceController, UserController, ServiceController], - providers: [InsuranceService, UserService, ServiceService], + imports: [AuthModule, StripeModule.forRootAsync()], + controllers: [InsuranceController, UserController, ServiceController, StripeController], + providers: [InsuranceService, UserService, ServiceService, StripeService] }) export class AppModule {} diff --git a/backend/src/insurance/insurance.schema.ts b/backend/src/insurance/insurance.schema.ts index c654561..90f4bdd 100644 --- a/backend/src/insurance/insurance.schema.ts +++ b/backend/src/insurance/insurance.schema.ts @@ -6,6 +6,8 @@ export interface IInsurancePlan extends Document { monthlyPremium: number; coverageDetails: string; eligibility: string; + productId?: string; + priceId?: string; } const InsurancePlanSchema = new Schema( @@ -15,6 +17,8 @@ const InsurancePlanSchema = new Schema( monthlyPremium: { type: Number, required: true }, coverageDetails: { type: String, required: true }, eligibility: { type: String, required: true }, + productId: { type: String, default: null }, + priceId: { type: String, default: null }, }, { timestamps: true } ); diff --git a/backend/src/stripe/stripe.controller.ts b/backend/src/stripe/stripe.controller.ts index 4c01f1f..14720a6 100644 --- a/backend/src/stripe/stripe.controller.ts +++ b/backend/src/stripe/stripe.controller.ts @@ -1,13 +1,21 @@ import { Controller, Get } from '@nestjs/common'; import { StripeService } from './stripe.service'; import { Roles } from '../auth/roles.decorator'; -import { Post, Param, Req } from '@nestjs/common'; +import { Post, Param, Req, Body } from '@nestjs/common'; +import { UseGuards } from '@nestjs/common'; +import { JwtGuard } from '../auth/jwt-auth.guard'; +@UseGuards(JwtGuard) @Controller('subscriptions') export class StripeController { constructor(private readonly stripeService: StripeService) {} - @Roles('user') + @Post('create-setup-intent') + async createSetupIntent(@Body() body: { customerId: string }) { + const setupIntent = await this.stripeService.createSetupIntent(body.customerId); + return { clientSecret: setupIntent.client_secret }; + } + @Post('subscribe/:planId') async subscribeToPlan(@Param('planId') planId: string, @Req() req) { const userId = req.user.sub; diff --git a/backend/src/stripe/stripe.service.ts b/backend/src/stripe/stripe.service.ts index be2ad52..36aa45a 100644 --- a/backend/src/stripe/stripe.service.ts +++ b/backend/src/stripe/stripe.service.ts @@ -7,8 +7,14 @@ import { InsurancePlan } from 'src/insurance/insurance.schema'; export class StripeService { private stripe: Stripe; - constructor(@Inject('STRIPE_API_KEY') private readonly apiKey: string) { - this.stripe = new Stripe(this.apiKey) + constructor() { + this.stripe = new Stripe(process.env.STRIPE_API_KEY) + } + + async createSetupIntent(customerId: string): Promise { + return await this.stripe.setupIntents.create({ + customer: customerId, + }); } async createSubscription(userId: string, insuranceId: string) { @@ -24,11 +30,26 @@ export class StripeService { if (!user.stripeCustomerId) { const customer = await this.stripe.customers.create({ email: user.email, }); user.stripeCustomerId = customer.id; + await user.save(); } - + + const stripeProduct = await this.stripe.products.create({ name: insurance.name }); + + const stripePrice = await this.stripe.prices.create({ + product: stripeProduct.id, + unit_amount: insurance.monthlyPremium * 100, + currency: 'usd', + recurring: { + interval: 'month', + }, + }); + + const insurancePlan = await InsurancePlan.findByIdAndUpdate(insuranceId, { productId: stripeProduct.id, priceId: stripePrice.id }, { new: true }); + const subscription = await this.stripe.subscriptions.create({ customer: user.stripeCustomerId, - items: [{ price_data: { unit_amount: insurance.monthlyPremium * 100, currency: 'usd', product: insurance.id, recurring: { interval: 'month' } } }], + items: [{ price_data: { unit_amount: insurance.monthlyPremium * 100, currency: 'usd', product: insurancePlan.productId, recurring: { interval: 'month' } } }], + expand: ['latest_invoice.payment_intent'], }); return subscription; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 34d6a62..cde482e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "dependencies": { "@reduxjs/toolkit": "^2.2.7", + "@stripe/react-stripe-js": "^2.8.1", "@stripe/stripe-js": "^4.8.0", "next": "^14.2.14", "nodemon": "^3.1.7", @@ -358,6 +359,20 @@ } } }, + "node_modules/@stripe/react-stripe-js": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.8.1.tgz", + "integrity": "sha512-C410jVKOATinXLalWotab6E6jlWAlbqUDWL9q1km0p5UHrvnihjjYzA8imYXc4xc4Euf9GeKDQc4n35HKZvgwg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": "^1.44.1 || ^2.0.0 || ^3.0.0 || ^4.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@stripe/stripe-js": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-4.8.0.tgz", @@ -1514,7 +1529,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1783,6 +1797,17 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -1850,6 +1875,12 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-redux": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9085a4a..fb187c1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@reduxjs/toolkit": "^2.2.7", + "@stripe/react-stripe-js": "^2.8.1", "@stripe/stripe-js": "^4.8.0", "next": "^14.2.14", "nodemon": "^3.1.7", diff --git a/frontend/src/pages/checkout.tsx b/frontend/src/pages/checkout.tsx new file mode 100644 index 0000000..95fc26d --- /dev/null +++ b/frontend/src/pages/checkout.tsx @@ -0,0 +1,67 @@ +import { useEffect, useState } from 'react'; +import { loadStripe } from '@stripe/stripe-js'; +import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; + +const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || ''); + +const CheckoutForm = ({customerId}) => { + const stripe = useStripe(); + const elements = useElements(); + const [isProcessing, setIsProcessing] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!stripe || !elements) return; + + setIsProcessing(true); + + const cardElement = elements.getElement(CardElement); + + // Call your backend to create a SetupIntent and get the clientSecret + const { client_secret } = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/subscriptions/create-setup-intent`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ customerId }), // Pass customerId + }).then(res => res.json()); + + console.log('Client Secret:', client_secret); + + const { error, setupIntent } = await stripe.confirmCardSetup(client_secret, { + // IntegrationError: Missing value for stripe.confirmCardSetup intent secret: value should be a client_secret string. + payment_method: { + card: cardElement, + }, + }); + + if (error) { + console.error(error); + // Handle error + } else { + console.log('Setup Intent:', setupIntent); + // Now your customer has a payment method attached + // You can call your backend to create a subscription for this customer + } + + setIsProcessing(false); + }; + + return ( +
+ + + + ); +}; + +const StripeCheckout = ({ customerId }) => { + return ( + + + + ); +}; + +export default StripeCheckout;