Skip to content

Commit

Permalink
stripe setup complete
Browse files Browse the repository at this point in the history
  • Loading branch information
ArslanKamchybekov committed Oct 12, 2024
1 parent 4353a86 commit 09d4b2e
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 10 deletions.
8 changes: 5 additions & 3 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
4 changes: 4 additions & 0 deletions backend/src/insurance/insurance.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export interface IInsurancePlan extends Document {
monthlyPremium: number;
coverageDetails: string;
eligibility: string;
productId?: string;
priceId?: string;
}

const InsurancePlanSchema = new Schema<IInsurancePlan>(
Expand All @@ -15,6 +17,8 @@ const InsurancePlanSchema = new Schema<IInsurancePlan>(
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 }
);
Expand Down
12 changes: 10 additions & 2 deletions backend/src/stripe/stripe.controller.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
29 changes: 25 additions & 4 deletions backend/src/stripe/stripe.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Stripe.SetupIntent> {
return await this.stripe.setupIntents.create({
customer: customerId,
});
}

async createSubscription(userId: string, insuranceId: string) {
Expand All @@ -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;
Expand Down
33 changes: 32 additions & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
67 changes: 67 additions & 0 deletions frontend/src/pages/checkout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<form onSubmit={handleSubmit}>
<CardElement />
<button type="submit" disabled={isProcessing}>
{isProcessing ? 'Processing...' : 'Submit'}
</button>
</form>
);
};

const StripeCheckout = ({ customerId }) => {
return (
<Elements stripe={stripePromise}>
<CheckoutForm customerId={customerId} />
</Elements>
);
};

export default StripeCheckout;

0 comments on commit 09d4b2e

Please sign in to comment.