Stripe Pricing + Subscriptions
This commit is contained in:
parent
48bff89526
commit
b4609d07ba
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);
|
// const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
function loadEnvFiles() {
|
function loadEnvFiles() {
|
||||||
// Load the .env file
|
|
||||||
dotenv.config();
|
|
||||||
console.log('Loaded .env file');
|
|
||||||
|
|
||||||
// Determine which additional env file to load
|
// Determine which additional env file to load
|
||||||
let envFilePath = '';
|
let envFilePath = '';
|
||||||
const host = process.env.HOST_NAME || '';
|
const host = process.env.HOST_NAME || '';
|
||||||
|
|
@ -48,6 +44,13 @@ function loadEnvFiles() {
|
||||||
} else {
|
} else {
|
||||||
console.log(`No additional .env file found for HOST_NAME: ${host}`);
|
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();
|
loadEnvFiles();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import ky from 'ky';
|
import ky from 'ky';
|
||||||
|
import { KeycloakUser } from 'src/models/main.model';
|
||||||
import urlcat from 'urlcat';
|
import urlcat from 'urlcat';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -37,7 +38,7 @@ export class AuthService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUsers() {
|
public async getUsers(): Promise<KeycloakUser[]> {
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
const URL = `${process.env.host}${process.env.usersURL}`;
|
const URL = `${process.env.host}${process.env.usersURL}`;
|
||||||
const response = await ky
|
const response = await ky
|
||||||
|
|
@ -48,7 +49,7 @@ export class AuthService {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.json();
|
.json();
|
||||||
return response;
|
return response as KeycloakUser[];
|
||||||
}
|
}
|
||||||
public async getUser(userid: string) {
|
public async getUser(userid: string) {
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Stripe from 'stripe';
|
||||||
import { BusinessListing, CommercialPropertyListing, Sender, User } from './db.model.js';
|
import { BusinessListing, CommercialPropertyListing, Sender, User } from './db.model.js';
|
||||||
import { State } from './server.model.js';
|
import { State } from './server.model.js';
|
||||||
|
|
||||||
|
|
@ -258,6 +259,7 @@ export interface ModalResult {
|
||||||
export interface Checkout {
|
export interface Checkout {
|
||||||
priceId: string;
|
priceId: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
export function isEmpty(value: any): boolean {
|
export function isEmpty(value: any): boolean {
|
||||||
// Check for undefined or null
|
// Check for undefined or null
|
||||||
|
|
@ -298,7 +300,7 @@ export interface ValidationMessage {
|
||||||
field: string;
|
field: string;
|
||||||
message: 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 {
|
return {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
email,
|
email,
|
||||||
|
|
@ -374,3 +376,4 @@ export function createDefaultBusinessListing(): BusinessListing {
|
||||||
listingsCategory: 'business',
|
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 { Request, Response } from 'express';
|
||||||
import { Checkout } from 'src/models/main.model.js';
|
import { Checkout } from 'src/models/main.model.js';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
@ -13,8 +13,8 @@ export class PaymentController {
|
||||||
// return this.paymentService.createSubscription(subscriptionData);
|
// return this.paymentService.createSubscription(subscriptionData);
|
||||||
// }
|
// }
|
||||||
@Post('create-checkout-session')
|
@Post('create-checkout-session')
|
||||||
async calculateTax(@Body() checkout: Checkout) {
|
async createCheckoutSession(@Body() checkout: Checkout) {
|
||||||
return this.paymentService.checkout(checkout);
|
return this.paymentService.createCheckoutSession(checkout);
|
||||||
}
|
}
|
||||||
@Post('webhook')
|
@Post('webhook')
|
||||||
async handleWebhook(@Req() req: Request, @Res() res: Response): Promise<void> {
|
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);
|
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 { 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 { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
import { GeoService } from '../geo/geo.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';
|
import { PaymentService } from './payment.service.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DrizzleModule, UserModule, MailModule],
|
imports: [DrizzleModule, UserModule, MailModule, AuthModule],
|
||||||
providers: [PaymentService, UserService, MailService, FileService, GeoService],
|
providers: [PaymentService, UserService, MailService, FileService, GeoService, AuthService],
|
||||||
controllers: [PaymentController],
|
controllers: [PaymentController],
|
||||||
})
|
})
|
||||||
export class PaymentModule {}
|
export class PaymentModule {}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
|
import { AuthService } from '../auth/auth.service.js';
|
||||||
import * as schema from '../drizzle/schema.js';
|
import * as schema from '../drizzle/schema.js';
|
||||||
import { PG_CONNECTION } from '../drizzle/schema.js';
|
import { PG_CONNECTION } from '../drizzle/schema.js';
|
||||||
import { MailService } from '../mail/mail.service.js';
|
import { MailService } from '../mail/mail.service.js';
|
||||||
|
|
@ -21,13 +22,33 @@ export class PaymentService {
|
||||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly mailService: MailService,
|
private readonly mailService: MailService,
|
||||||
|
private readonly authService: AuthService,
|
||||||
) {
|
) {
|
||||||
this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||||||
apiVersion: '2024-06-20',
|
apiVersion: '2024-06-20',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async checkout(checkout: Checkout) {
|
async createCheckoutSession(checkout: Checkout) {
|
||||||
try {
|
try {
|
||||||
|
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({
|
const session = await this.stripe.checkout.sessions.create({
|
||||||
mode: 'subscription',
|
mode: 'subscription',
|
||||||
payment_method_types: ['card'],
|
payment_method_types: ['card'],
|
||||||
|
|
@ -39,15 +60,26 @@ export class PaymentService {
|
||||||
],
|
],
|
||||||
success_url: 'http://localhost:4200/success',
|
success_url: 'http://localhost:4200/success',
|
||||||
cancel_url: 'http://localhost:4200/pricing',
|
cancel_url: 'http://localhost:4200/pricing',
|
||||||
customer_email: checkout.email,
|
// customer_email: checkout.email,
|
||||||
|
customer: customerId,
|
||||||
|
// customer_details:{
|
||||||
|
// name: checkout.name, // Hier wird der Name des Kunden übergeben
|
||||||
|
// },
|
||||||
shipping_address_collection: {
|
shipping_address_collection: {
|
||||||
allowed_countries: ['US'],
|
allowed_countries: ['US'],
|
||||||
},
|
},
|
||||||
|
|
||||||
client_reference_id: '1234',
|
client_reference_id: '1234',
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
|
subscription_data: {
|
||||||
|
trial_end: Math.floor(new Date().setMonth(new Date().getMonth() + 3) / 1000),
|
||||||
|
},
|
||||||
|
metadata: { plan: product.name },
|
||||||
});
|
});
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new BadRequestException(`error during checkout: ${e}`);
|
throw new BadRequestException(`error during checkout: ${e}`);
|
||||||
}
|
}
|
||||||
|
|
@ -57,8 +89,15 @@ export class PaymentService {
|
||||||
}
|
}
|
||||||
async handleCheckoutSessionCompleted(session: Stripe.Checkout.Session): Promise<void> {
|
async handleCheckoutSessionCompleted(session: Stripe.Checkout.Session): Promise<void> {
|
||||||
this.logger.info(JSON.stringify(session));
|
this.logger.info(JSON.stringify(session));
|
||||||
//const jwtUser:JwtUser = {firstname:,lastname:}
|
const keycloakUsers = await this.authService.getUsers();
|
||||||
const user = await this.userService.getUserByMail(session.customer_details.email);
|
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.stripeCustomerId = session.customer as string;
|
||||||
user.subscriptionId = session.subscription as string;
|
user.subscriptionId = session.subscription as string;
|
||||||
user.planActive = true;
|
user.planActive = true;
|
||||||
|
|
@ -78,4 +117,20 @@ export class PaymentService {
|
||||||
this.userService.saveUser(user);
|
this.userService.saveUser(user);
|
||||||
this.mailService.sendSubscriptionConfirmation(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)
|
.from(schema.users)
|
||||||
.where(sql`email = ${email}`)) as User[];
|
.where(sql`email = ${email}`)) as User[];
|
||||||
if (users.length === 0) {
|
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);
|
const u = await this.saveUser(user);
|
||||||
return convertDrizzleUserToUser(u);
|
return convertDrizzleUserToUser(u);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -147,6 +147,7 @@ export class UserService {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStates(): Promise<any[]> {
|
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 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);
|
const result = await this.conn.execute(query);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { HomeComponent } from './pages/home/home.component';
|
||||||
import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.component';
|
import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.component';
|
||||||
import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component';
|
import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component';
|
||||||
import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component';
|
import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component';
|
||||||
|
import { LoginComponent } from './pages/login/login.component';
|
||||||
import { PricingComponent } from './pages/pricing/pricing.component';
|
import { PricingComponent } from './pages/pricing/pricing.component';
|
||||||
import { AccountComponent } from './pages/subscription/account/account.component';
|
import { AccountComponent } from './pages/subscription/account/account.component';
|
||||||
import { EditBusinessListingComponent } from './pages/subscription/edit-business-listing/edit-business-listing.component';
|
import { EditBusinessListingComponent } from './pages/subscription/edit-business-listing/edit-business-listing.component';
|
||||||
|
|
@ -56,6 +57,10 @@ export const routes: Routes = [
|
||||||
canActivate: [ListingCategoryGuard],
|
canActivate: [ListingCategoryGuard],
|
||||||
component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'login/:page',
|
||||||
|
component: LoginComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'notfound',
|
path: 'notfound',
|
||||||
component: NotFoundComponent,
|
component: NotFoundComponent,
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ export class HeaderComponent {
|
||||||
}
|
}
|
||||||
login() {
|
login() {
|
||||||
this.keycloakService.login({
|
this.keycloakService.login({
|
||||||
redirectUri: window.location.href,
|
redirectUri: `${window.location.origin}/login${this.router.routerState.snapshot.url}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
register() {
|
register() {
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ export class HomeComponent {
|
||||||
}
|
}
|
||||||
login() {
|
login() {
|
||||||
this.keycloakService.login({
|
this.keycloakService.login({
|
||||||
redirectUri: window.location.href,
|
redirectUri: `${window.location.origin}/login${this.router.routerState.snapshot.url}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
register() {
|
register() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
import { SubscriptionsService } from '../../services/subscriptions.service';
|
||||||
|
import { UserService } from '../../services/user.service';
|
||||||
|
import { map2User } from '../../utils/utils';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, RouterModule],
|
||||||
|
template: ``,
|
||||||
|
})
|
||||||
|
export class LoginComponent {
|
||||||
|
page: string | undefined = this.activatedRoute.snapshot.params['page'] as string | undefined;
|
||||||
|
constructor(public userService: UserService, private activatedRoute: ActivatedRoute, private keycloakService: KeycloakService, private router: Router, private subscriptionService: SubscriptionsService) {}
|
||||||
|
async ngOnInit() {
|
||||||
|
const token = await this.keycloakService.getToken();
|
||||||
|
const keycloakUser = map2User(token);
|
||||||
|
const email = keycloakUser.email;
|
||||||
|
const user = await this.userService.getByMail(email);
|
||||||
|
if (!user.subscriptionPlan) {
|
||||||
|
//this.router.navigate(['/pricing']);
|
||||||
|
const subscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(user.email));
|
||||||
|
const activeSubscription = subscriptions.filter(s => s.status === 'active');
|
||||||
|
if (activeSubscription.length > 0) {
|
||||||
|
user.subscriptionPlan = activeSubscription[0].metadata['plan'] === 'Broker Plan' ? 'broker' : 'professional';
|
||||||
|
this.userService.save(user);
|
||||||
|
} else {
|
||||||
|
this.router.navigate([`/pricing`]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.router.navigate([`/${this.page}`]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { StripeService } from 'ngx-stripe';
|
import { StripeService } from 'ngx-stripe';
|
||||||
import { switchMap } from 'rxjs';
|
import { switchMap } from 'rxjs';
|
||||||
|
import { Checkout, KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { UserService } from '../../services/user.service';
|
||||||
import { SharedModule } from '../../shared/shared/shared.module';
|
import { SharedModule } from '../../shared/shared/shared.module';
|
||||||
|
import { map2User } from '../../utils/utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-pricing',
|
selector: 'app-pricing',
|
||||||
|
|
@ -17,26 +20,40 @@ import { SharedModule } from '../../shared/shared/shared.module';
|
||||||
export class PricingComponent {
|
export class PricingComponent {
|
||||||
private apiBaseUrl = environment.apiBaseUrl;
|
private apiBaseUrl = environment.apiBaseUrl;
|
||||||
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||||
constructor(public keycloakService: KeycloakService, private http: HttpClient, private stripeService: StripeService, private activatedRoute: ActivatedRoute) {}
|
keycloakUser: KeycloakUser;
|
||||||
|
constructor(public keycloakService: KeycloakService, private http: HttpClient, private stripeService: StripeService, private activatedRoute: ActivatedRoute, private userService: UserService, private router: Router) {}
|
||||||
|
|
||||||
ngOnInit() {
|
async ngOnInit() {
|
||||||
|
const token = await this.keycloakService.getToken();
|
||||||
|
this.keycloakUser = map2User(token);
|
||||||
if (this.id) {
|
if (this.id) {
|
||||||
this.checkout(atob(this.id));
|
this.checkout({ priceId: atob(this.id), email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
register(priceId?: string) {
|
async register(priceId?: string) {
|
||||||
|
if (this.keycloakUser) {
|
||||||
|
if (!priceId) {
|
||||||
|
const user = await this.userService.getByMail(this.keycloakUser.email);
|
||||||
|
user.subscriptionPlan = 'free';
|
||||||
|
await this.userService.save(user);
|
||||||
|
this.router.navigate([`/account`]);
|
||||||
|
} else {
|
||||||
|
this.checkout({ priceId: atob(this.id), email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (priceId) {
|
if (priceId) {
|
||||||
this.keycloakService.register({ redirectUri: `${window.location.origin}/pricing/${btoa(priceId)}` });
|
this.keycloakService.register({ redirectUri: `${window.location.origin}/pricing/${btoa(priceId)}` });
|
||||||
} else {
|
} else {
|
||||||
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
|
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
checkout(priceId) {
|
checkout(checkout: Checkout) {
|
||||||
// Check the server.js tab to see an example implementation
|
// Check the server.js tab to see an example implementation
|
||||||
this.http
|
this.http
|
||||||
.post(`${this.apiBaseUrl}/bizmatch/payment/create-checkout-session`, { priceId })
|
.post(`${this.apiBaseUrl}/bizmatch/payment/create-checkout-session`, checkout)
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((session: any) => {
|
switchMap((session: any) => {
|
||||||
return this.stripeService.redirectToCheckout({ sessionId: session.id });
|
return this.stripeService.redirectToCheckout({ sessionId: session.id });
|
||||||
|
|
|
||||||
|
|
@ -220,46 +220,42 @@
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Level</th>
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Level</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Start Date</th>
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Start Date</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Modified</th>
|
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">End Date</th>
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">End Date</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Next Settlement</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
@for (subscription of subscriptions; track subscriptions; let i=$index){
|
||||||
<tr>
|
<tr>
|
||||||
@for (subscription of userSubscriptions; track userSubscriptions){
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ getLevel(i) }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ subscription.id }}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ getStartDate(i) }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ subscription.level }}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ getEndDate(i) }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ subscription.start | date }}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ getNextSettlement(i) }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ subscription.modified | date }}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ getStatus(i) }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ subscription.end | date }}</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ subscription.status }}</td>
|
|
||||||
}
|
|
||||||
</tr>
|
</tr>
|
||||||
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex justify-start">
|
||||||
<div class="mt-8 sm:hidden">
|
<button routerLink="/pricing" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">Upgrade Subscription Plan</button>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="mt-8 sm:hidden">
|
||||||
<h3 class="text-lg font-medium text-gray-700 mb-1">Membership Level</h3>
|
<h3 class="text-lg font-medium text-gray-700 mb-1">Membership Level</h3>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@for (subscription of userSubscriptions; track userSubscriptions){
|
@for (subscription of userSubscriptions; track userSubscriptions){
|
||||||
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
<div class="px-4 py-5 sm:px-6">
|
<div class="px-4 py-5 sm:px-6">
|
||||||
<dl class="grid grid-cols-1 gap-x-4 gap-y-2 sm:grid-cols-2">
|
<dl class="grid grid-cols-1 gap-x-4 gap-y-2 sm:grid-cols-2">
|
||||||
<div class="sm:col-span-1 flex">
|
|
||||||
<dt class="text-sm font-bold text-gray-500 mr-2">ID</dt>
|
|
||||||
<dd class="text-sm text-gray-900">{{ subscription.id }}</dd>
|
|
||||||
</div>
|
|
||||||
<div class="sm:col-span-1 flex">
|
<div class="sm:col-span-1 flex">
|
||||||
<dt class="text-sm font-bold text-gray-500 mr-2">Level</dt>
|
<dt class="text-sm font-bold text-gray-500 mr-2">Level</dt>
|
||||||
<dd class="text-sm text-gray-900">{{ subscription.level }}</dd>
|
<dd class="text-sm text-gray-900">{{ level }}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:col-span-1 flex">
|
<div class="sm:col-span-1 flex">
|
||||||
<dt class="text-sm font-bold text-gray-500 mr-2">Start Date</dt>
|
<dt class="text-sm font-bold text-gray-500 mr-2">Start Date</dt>
|
||||||
|
|
@ -282,7 +278,7 @@
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { TitleCasePipe } from '@angular/common';
|
import { DatePipe, TitleCasePipe } from '@angular/common';
|
||||||
import { ChangeDetectorRef, Component } from '@angular/core';
|
import { ChangeDetectorRef, Component } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
@ -10,7 +10,7 @@ import { ImageCropperComponent } from 'ngx-image-cropper';
|
||||||
import { QuillModule } from 'ngx-quill';
|
import { QuillModule } from 'ngx-quill';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { AutoCompleteCompleteEvent, Invoice, Subscription, UploadParams, ValidationMessage, createDefaultUser, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { AutoCompleteCompleteEvent, Invoice, StripeSubscription, UploadParams, ValidationMessage, createDefaultUser, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
|
import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
|
||||||
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
|
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
|
||||||
|
|
@ -53,15 +53,13 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
ValidatedCountyComponent,
|
ValidatedCountyComponent,
|
||||||
],
|
],
|
||||||
providers: [TitleCasePipe],
|
providers: [TitleCasePipe, DatePipe],
|
||||||
templateUrl: './account.component.html',
|
templateUrl: './account.component.html',
|
||||||
styleUrl: './account.component.scss',
|
styleUrl: './account.component.scss',
|
||||||
})
|
})
|
||||||
export class AccountComponent {
|
export class AccountComponent {
|
||||||
id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||||
user: User;
|
user: User;
|
||||||
subscriptions: Array<Subscription>;
|
|
||||||
userSubscriptions: Array<Subscription> = [];
|
|
||||||
companyLogoUrl: string;
|
companyLogoUrl: string;
|
||||||
profileUrl: string;
|
profileUrl: string;
|
||||||
type: 'company' | 'profile';
|
type: 'company' | 'profile';
|
||||||
|
|
@ -79,9 +77,9 @@ export class AccountComponent {
|
||||||
customerTypeOptions: Array<{ value: string; label: string }> = [];
|
customerTypeOptions: Array<{ value: string; label: string }> = [];
|
||||||
customerSubTypeOptions: Array<{ value: string; label: string }> = [];
|
customerSubTypeOptions: Array<{ value: string; label: string }> = [];
|
||||||
tooltipTarget = 'tooltip-areasServed';
|
tooltipTarget = 'tooltip-areasServed';
|
||||||
|
subscriptions: StripeSubscription[] | any[];
|
||||||
constructor(
|
constructor(
|
||||||
public userService: UserService,
|
public userService: UserService,
|
||||||
private subscriptionService: SubscriptionsService,
|
|
||||||
private geoService: GeoService,
|
private geoService: GeoService,
|
||||||
public selectOptions: SelectOptionsService,
|
public selectOptions: SelectOptionsService,
|
||||||
private cdref: ChangeDetectorRef,
|
private cdref: ChangeDetectorRef,
|
||||||
|
|
@ -95,6 +93,8 @@ export class AccountComponent {
|
||||||
private sharedService: SharedService,
|
private sharedService: SharedService,
|
||||||
private titleCasePipe: TitleCasePipe,
|
private titleCasePipe: TitleCasePipe,
|
||||||
private validationMessagesService: ValidationMessagesService,
|
private validationMessagesService: ValidationMessagesService,
|
||||||
|
private subscriptionService: SubscriptionsService,
|
||||||
|
private datePipe: DatePipe,
|
||||||
) {}
|
) {}
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -109,7 +109,10 @@ export class AccountComponent {
|
||||||
this.user = await this.userService.getByMail(email);
|
this.user = await this.userService.getByMail(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userSubscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.id));
|
this.subscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.email));
|
||||||
|
if (this.subscriptions.length === 0) {
|
||||||
|
this.subscriptions = [{ ended_at: null, start_date: Math.floor(new Date(this.user.created).getTime() / 1000), status: null, metadata: { plan: 'Free Plan' } }];
|
||||||
|
}
|
||||||
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
||||||
this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
||||||
|
|
||||||
|
|
@ -133,7 +136,7 @@ export class AccountComponent {
|
||||||
const confirmed = await this.confirmationService.showConfirmation({ message: 'Are you sure you want to switch to Buyer ? All your listings as well as all your professionals informations will be deleted' });
|
const confirmed = await this.confirmationService.showConfirmation({ message: 'Are you sure you want to switch to Buyer ? All your listings as well as all your professionals informations will be deleted' });
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
const id = this.user.id;
|
const id = this.user.id;
|
||||||
this.user = createDefaultUser(this.user.email, this.user.firstname, this.user.lastname, 'free');
|
this.user = createDefaultUser(this.user.email, this.user.firstname, this.user.lastname, null);
|
||||||
this.user.customerType = 'buyer';
|
this.user.customerType = 'buyer';
|
||||||
this.user.id = id;
|
this.user.id = id;
|
||||||
this.imageService.deleteLogoImagesByMail(this.user.email);
|
this.imageService.deleteLogoImagesByMail(this.user.email);
|
||||||
|
|
@ -244,4 +247,19 @@ export class AccountComponent {
|
||||||
this.user.areasServed[index].county = null;
|
this.user.areasServed[index].county = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
getLevel(i: number) {
|
||||||
|
return this.subscriptions[i].metadata.plan;
|
||||||
|
}
|
||||||
|
getStartDate(i: number) {
|
||||||
|
return this.datePipe.transform(new Date(this.subscriptions[i].start_date * 1000));
|
||||||
|
}
|
||||||
|
getEndDate(i: number) {
|
||||||
|
return this.subscriptions[i].status === 'trialing' ? this.datePipe.transform(new Date(this.subscriptions[i].current_period_end * 1000)) : '---';
|
||||||
|
}
|
||||||
|
getNextSettlement(i: number) {
|
||||||
|
return this.subscriptions[i].status === 'active' ? this.datePipe.transform(new Date(this.subscriptions[i].current_period_end * 1000)) : '---';
|
||||||
|
}
|
||||||
|
getStatus(i: number) {
|
||||||
|
return this.subscriptions[i].status ? this.subscriptions[i].status : '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Subscription } from '../../../../bizmatch-server/src/models/main.model';
|
import { StripeSubscription } from '../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
|
@ -11,7 +11,7 @@ export class SubscriptionsService {
|
||||||
private apiBaseUrl = environment.apiBaseUrl;
|
private apiBaseUrl = environment.apiBaseUrl;
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
getAllSubscriptions(id: string): Observable<Subscription[]> {
|
getAllSubscriptions(email: string): Observable<StripeSubscription[]> {
|
||||||
return this.http.get<Subscription[]>(`${this.apiBaseUrl}/bizmatch/user/subscriptions/${id}`);
|
return this.http.get<StripeSubscription[]>(`${this.apiBaseUrl}/bizmatch/payment/subscriptions/${email}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue