Stripe Pricing + Subscriptions
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -25,10 +25,6 @@ import { UserModule } from './user/user.module.js';
|
||||
// const __dirname = path.dirname(__filename);
|
||||
|
||||
function loadEnvFiles() {
|
||||
// Load the .env file
|
||||
dotenv.config();
|
||||
console.log('Loaded .env file');
|
||||
|
||||
// Determine which additional env file to load
|
||||
let envFilePath = '';
|
||||
const host = process.env.HOST_NAME || '';
|
||||
@@ -48,6 +44,13 @@ function loadEnvFiles() {
|
||||
} else {
|
||||
console.log(`No additional .env file found for HOST_NAME: ${host}`);
|
||||
}
|
||||
|
||||
// Load the .env file
|
||||
dotenv.config();
|
||||
console.log('Loaded .env file');
|
||||
// Output all loaded environment variables
|
||||
console.log('Loaded environment variables:');
|
||||
console.log(JSON.stringify(process.env, null, 2));
|
||||
}
|
||||
|
||||
loadEnvFiles();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import ky from 'ky';
|
||||
import { KeycloakUser } from 'src/models/main.model';
|
||||
import urlcat from 'urlcat';
|
||||
|
||||
@Injectable()
|
||||
@@ -37,7 +38,7 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
public async getUsers() {
|
||||
public async getUsers(): Promise<KeycloakUser[]> {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = `${process.env.host}${process.env.usersURL}`;
|
||||
const response = await ky
|
||||
@@ -48,7 +49,7 @@ export class AuthService {
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
return response as KeycloakUser[];
|
||||
}
|
||||
public async getUser(userid: string) {
|
||||
const token = await this.getAccessToken();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Stripe from 'stripe';
|
||||
import { BusinessListing, CommercialPropertyListing, Sender, User } from './db.model.js';
|
||||
import { State } from './server.model.js';
|
||||
|
||||
@@ -258,6 +259,7 @@ export interface ModalResult {
|
||||
export interface Checkout {
|
||||
priceId: string;
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
export function isEmpty(value: any): boolean {
|
||||
// Check for undefined or null
|
||||
@@ -298,7 +300,7 @@ export interface ValidationMessage {
|
||||
field: string;
|
||||
message: string;
|
||||
}
|
||||
export function createDefaultUser(email: string, firstname: string, lastname: string, subscriptionPlan: 'free' | 'professional' | 'broker'): User {
|
||||
export function createDefaultUser(email: string, firstname: string, lastname: string, subscriptionPlan: 'professional' | 'broker'): User {
|
||||
return {
|
||||
id: undefined,
|
||||
email,
|
||||
@@ -374,3 +376,4 @@ export function createDefaultBusinessListing(): BusinessListing {
|
||||
listingsCategory: 'business',
|
||||
};
|
||||
}
|
||||
export type StripeSubscription = Stripe.Subscription;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, HttpException, HttpStatus, Post, Req, Res } from '@nestjs/common';
|
||||
import { Body, Controller, Get, HttpException, HttpStatus, Param, Post, Req, Res } from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { Checkout } from 'src/models/main.model.js';
|
||||
import Stripe from 'stripe';
|
||||
@@ -13,8 +13,8 @@ export class PaymentController {
|
||||
// return this.paymentService.createSubscription(subscriptionData);
|
||||
// }
|
||||
@Post('create-checkout-session')
|
||||
async calculateTax(@Body() checkout: Checkout) {
|
||||
return this.paymentService.checkout(checkout);
|
||||
async createCheckoutSession(@Body() checkout: Checkout) {
|
||||
return this.paymentService.createCheckoutSession(checkout);
|
||||
}
|
||||
@Post('webhook')
|
||||
async handleWebhook(@Req() req: Request, @Res() res: Response): Promise<void> {
|
||||
@@ -33,4 +33,8 @@ export class PaymentController {
|
||||
throw new HttpException('Webhook Error', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
@Get('subscriptions/:email')
|
||||
async findSubscriptionsById(@Param('email') email: string): Promise<any> {
|
||||
return await this.paymentService.getSubscription(email);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module.js';
|
||||
import { AuthService } from '../auth/auth.service.js';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
@@ -10,8 +12,8 @@ import { PaymentController } from './payment.controller.js';
|
||||
import { PaymentService } from './payment.service.js';
|
||||
|
||||
@Module({
|
||||
imports: [DrizzleModule, UserModule, MailModule],
|
||||
providers: [PaymentService, UserService, MailService, FileService, GeoService],
|
||||
imports: [DrizzleModule, UserModule, MailModule, AuthModule],
|
||||
providers: [PaymentService, UserService, MailService, FileService, GeoService, AuthService],
|
||||
controllers: [PaymentController],
|
||||
})
|
||||
export class PaymentModule {}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import Stripe from 'stripe';
|
||||
import { Logger } from 'winston';
|
||||
import { AuthService } from '../auth/auth.service.js';
|
||||
import * as schema from '../drizzle/schema.js';
|
||||
import { PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { MailService } from '../mail/mail.service.js';
|
||||
@@ -21,33 +22,64 @@ export class PaymentService {
|
||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||
private readonly userService: UserService,
|
||||
private readonly mailService: MailService,
|
||||
private readonly authService: AuthService,
|
||||
) {
|
||||
this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2024-06-20',
|
||||
});
|
||||
}
|
||||
async checkout(checkout: Checkout) {
|
||||
async createCheckoutSession(checkout: Checkout) {
|
||||
try {
|
||||
const session = await this.stripe.checkout.sessions.create({
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price: checkout.priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: 'http://localhost:4200/success',
|
||||
cancel_url: 'http://localhost:4200/pricing',
|
||||
customer_email: checkout.email,
|
||||
shipping_address_collection: {
|
||||
allowed_countries: ['US'],
|
||||
},
|
||||
client_reference_id: '1234',
|
||||
locale: 'en',
|
||||
let customerId;
|
||||
const existingCustomers = await this.stripe.customers.list({
|
||||
email: checkout.email,
|
||||
limit: 1,
|
||||
});
|
||||
if (existingCustomers.data.length > 0) {
|
||||
// Kunde existiert
|
||||
customerId = existingCustomers.data[0].id;
|
||||
} else {
|
||||
// Kunde existiert nicht, neuen Kunden erstellen
|
||||
const newCustomer = await this.stripe.customers.create({
|
||||
email: checkout.email,
|
||||
name: checkout.name,
|
||||
});
|
||||
customerId = newCustomer.id;
|
||||
}
|
||||
const price = await this.stripe.prices.retrieve(checkout.priceId);
|
||||
if (price.product) {
|
||||
const product = await this.stripe.products.retrieve(price.product as string);
|
||||
const session = await this.stripe.checkout.sessions.create({
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price: checkout.priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: 'http://localhost:4200/success',
|
||||
cancel_url: 'http://localhost:4200/pricing',
|
||||
// customer_email: checkout.email,
|
||||
customer: customerId,
|
||||
// customer_details:{
|
||||
// name: checkout.name, // Hier wird der Name des Kunden übergeben
|
||||
// },
|
||||
shipping_address_collection: {
|
||||
allowed_countries: ['US'],
|
||||
},
|
||||
|
||||
return session;
|
||||
client_reference_id: '1234',
|
||||
locale: 'en',
|
||||
subscription_data: {
|
||||
trial_end: Math.floor(new Date().setMonth(new Date().getMonth() + 3) / 1000),
|
||||
},
|
||||
metadata: { plan: product.name },
|
||||
});
|
||||
return session;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
throw new BadRequestException(`error during checkout: ${e}`);
|
||||
}
|
||||
@@ -57,8 +89,15 @@ export class PaymentService {
|
||||
}
|
||||
async handleCheckoutSessionCompleted(session: Stripe.Checkout.Session): Promise<void> {
|
||||
this.logger.info(JSON.stringify(session));
|
||||
//const jwtUser:JwtUser = {firstname:,lastname:}
|
||||
const user = await this.userService.getUserByMail(session.customer_details.email);
|
||||
const keycloakUsers = await this.authService.getUsers();
|
||||
const keycloakUser = keycloakUsers.find(u => u.email === session.customer_details.email);
|
||||
const user = await this.userService.getUserByMail(session.customer_details.email, {
|
||||
userId: keycloakUser.id,
|
||||
firstname: keycloakUser.firstName,
|
||||
lastname: keycloakUser.lastName,
|
||||
username: keycloakUser.email,
|
||||
roles: [],
|
||||
});
|
||||
user.stripeCustomerId = session.customer as string;
|
||||
user.subscriptionId = session.subscription as string;
|
||||
user.planActive = true;
|
||||
@@ -78,4 +117,20 @@ export class PaymentService {
|
||||
this.userService.saveUser(user);
|
||||
this.mailService.sendSubscriptionConfirmation(user);
|
||||
}
|
||||
async getSubscription(email: string): Promise<Stripe.Subscription[]> {
|
||||
const existingCustomers = await this.stripe.customers.list({
|
||||
email: email,
|
||||
limit: 1,
|
||||
});
|
||||
if (existingCustomers.data.length > 0) {
|
||||
const subscriptions = await this.stripe.subscriptions.list({
|
||||
customer: existingCustomers.data[0].id,
|
||||
status: 'all', // Optional: Gibt Abos in allen Status zurück, wie 'active', 'canceled', etc.
|
||||
limit: 20, // Optional: Begrenze die Anzahl der zurückgegebenen Abonnements
|
||||
});
|
||||
return subscriptions.data.filter(s => s.status === 'active' || s.status === 'trialing');
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export class UserService {
|
||||
.from(schema.users)
|
||||
.where(sql`email = ${email}`)) as User[];
|
||||
if (users.length === 0) {
|
||||
const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname, 'free') };
|
||||
const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname, null) };
|
||||
const u = await this.saveUser(user);
|
||||
return convertDrizzleUserToUser(u);
|
||||
} else {
|
||||
@@ -147,6 +147,7 @@ export class UserService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getStates(): Promise<any[]> {
|
||||
const query = sql`SELECT jsonb_array_elements(${schema.users.areasServed}) ->> 'state' AS state, COUNT(DISTINCT ${schema.users.id}) AS count FROM ${schema.users} GROUP BY state ORDER BY count DESC`;
|
||||
const result = await this.conn.execute(query);
|
||||
|
||||
Reference in New Issue
Block a user