292 lines
11 KiB
TypeScript
292 lines
11 KiB
TypeScript
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
import { and, arrayContains, asc, count, desc, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
|
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
|
import { Logger } from 'winston';
|
|
import { ZodError } from 'zod';
|
|
import * as schema from '../drizzle/schema';
|
|
import { businesses, PG_CONNECTION } from '../drizzle/schema';
|
|
import { FileService } from '../file/file.service';
|
|
import { GeoService } from '../geo/geo.service';
|
|
import { BusinessListing, BusinessListingSchema } from '../models/db.model';
|
|
import { BusinessListingCriteria, JwtUser } from '../models/main.model';
|
|
import { getDistanceQuery, splitName } from '../utils';
|
|
|
|
@Injectable()
|
|
export class BusinessListingService {
|
|
constructor(
|
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
|
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
|
private fileService?: FileService,
|
|
private geoService?: GeoService,
|
|
) {}
|
|
|
|
private getWhereConditions(criteria: BusinessListingCriteria, user: JwtUser): SQL[] {
|
|
const whereConditions: SQL[] = [];
|
|
|
|
if (criteria.city && criteria.searchType === 'exact') {
|
|
whereConditions.push(sql`${businesses.location}->>'name' ilike ${criteria.city.name}`);
|
|
//whereConditions.push(ilike(businesses.location-->'city', `%${criteria.city.name}%`));
|
|
}
|
|
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
|
|
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
|
|
whereConditions.push(sql`${getDistanceQuery(businesses, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
|
}
|
|
if (criteria.types && criteria.types.length > 0) {
|
|
whereConditions.push(inArray(businesses.type, criteria.types));
|
|
}
|
|
|
|
if (criteria.state) {
|
|
whereConditions.push(sql`${businesses.location}->>'state' = ${criteria.state}`);
|
|
}
|
|
|
|
if (criteria.minPrice) {
|
|
whereConditions.push(gte(businesses.price, criteria.minPrice));
|
|
}
|
|
|
|
if (criteria.maxPrice) {
|
|
whereConditions.push(lte(businesses.price, criteria.maxPrice));
|
|
}
|
|
|
|
if (criteria.minRevenue) {
|
|
whereConditions.push(gte(businesses.salesRevenue, criteria.minRevenue));
|
|
}
|
|
|
|
if (criteria.maxRevenue) {
|
|
whereConditions.push(lte(businesses.salesRevenue, criteria.maxRevenue));
|
|
}
|
|
|
|
if (criteria.minCashFlow) {
|
|
whereConditions.push(gte(businesses.cashFlow, criteria.minCashFlow));
|
|
}
|
|
|
|
if (criteria.maxCashFlow) {
|
|
whereConditions.push(lte(businesses.cashFlow, criteria.maxCashFlow));
|
|
}
|
|
|
|
if (criteria.minNumberEmployees) {
|
|
whereConditions.push(gte(businesses.employees, criteria.minNumberEmployees));
|
|
}
|
|
|
|
if (criteria.maxNumberEmployees) {
|
|
whereConditions.push(lte(businesses.employees, criteria.maxNumberEmployees));
|
|
}
|
|
|
|
if (criteria.establishedSince) {
|
|
whereConditions.push(gte(businesses.established, criteria.establishedSince));
|
|
}
|
|
|
|
if (criteria.establishedUntil) {
|
|
whereConditions.push(lte(businesses.established, criteria.establishedUntil));
|
|
}
|
|
|
|
if (criteria.realEstateChecked) {
|
|
whereConditions.push(eq(businesses.realEstateIncluded, criteria.realEstateChecked));
|
|
}
|
|
|
|
if (criteria.leasedLocation) {
|
|
whereConditions.push(eq(businesses.leasedLocation, criteria.leasedLocation));
|
|
}
|
|
|
|
if (criteria.franchiseResale) {
|
|
whereConditions.push(eq(businesses.franchiseResale, criteria.franchiseResale));
|
|
}
|
|
|
|
if (criteria.title) {
|
|
whereConditions.push(or(ilike(businesses.title, `%${criteria.title}%`), ilike(businesses.description, `%${criteria.title}%`)));
|
|
}
|
|
if (criteria.brokerName) {
|
|
const { firstname, lastname } = splitName(criteria.brokerName);
|
|
if (firstname === lastname) {
|
|
whereConditions.push(or(ilike(schema.users.firstname, `%${firstname}%`), ilike(schema.users.lastname, `%${lastname}%`)));
|
|
} else {
|
|
whereConditions.push(and(ilike(schema.users.firstname, `%${firstname}%`), ilike(schema.users.lastname, `%${lastname}%`)));
|
|
}
|
|
}
|
|
if (user?.role !== 'admin') {
|
|
whereConditions.push(or(eq(businesses.email, user?.email), ne(businesses.draft, true)));
|
|
}
|
|
whereConditions.push(and(eq(schema.users.customerType, 'professional'), eq(schema.users.customerSubType, 'broker')));
|
|
return whereConditions;
|
|
}
|
|
async searchBusinessListings(criteria: BusinessListingCriteria, user: JwtUser) {
|
|
const start = criteria.start ? criteria.start : 0;
|
|
const length = criteria.length ? criteria.length : 12;
|
|
const query = this.conn
|
|
.select({
|
|
business: businesses,
|
|
brokerFirstName: schema.users.firstname,
|
|
brokerLastName: schema.users.lastname,
|
|
})
|
|
.from(businesses)
|
|
.leftJoin(schema.users, eq(businesses.email, schema.users.email));
|
|
|
|
const whereConditions = this.getWhereConditions(criteria, user);
|
|
|
|
if (whereConditions.length > 0) {
|
|
const whereClause = and(...whereConditions);
|
|
query.where(whereClause);
|
|
}
|
|
|
|
// Sortierung
|
|
switch (criteria.sortBy) {
|
|
case 'priceAsc':
|
|
query.orderBy(asc(businesses.price));
|
|
break;
|
|
case 'priceDesc':
|
|
query.orderBy(desc(businesses.price));
|
|
break;
|
|
case 'srAsc':
|
|
query.orderBy(asc(businesses.salesRevenue));
|
|
break;
|
|
case 'srDesc':
|
|
query.orderBy(desc(businesses.salesRevenue));
|
|
break;
|
|
case 'cfAsc':
|
|
query.orderBy(asc(businesses.cashFlow));
|
|
break;
|
|
case 'cfDesc':
|
|
query.orderBy(desc(businesses.cashFlow));
|
|
break;
|
|
case 'creationDateFirst':
|
|
query.orderBy(asc(businesses.created));
|
|
break;
|
|
case 'creationDateLast':
|
|
query.orderBy(desc(businesses.created));
|
|
break;
|
|
default:
|
|
// Keine spezifische Sortierung, Standardverhalten kann hier eingefügt werden
|
|
break;
|
|
}
|
|
// Paginierung
|
|
query.limit(length).offset(start);
|
|
|
|
const data = await query;
|
|
const totalCount = await this.getBusinessListingsCount(criteria, user);
|
|
const results = data.map(r => r.business);
|
|
return {
|
|
results,
|
|
totalCount,
|
|
};
|
|
}
|
|
|
|
async getBusinessListingsCount(criteria: BusinessListingCriteria, user: JwtUser): Promise<number> {
|
|
const countQuery = this.conn.select({ value: count() }).from(businesses).leftJoin(schema.users, eq(businesses.email, schema.users.email));
|
|
|
|
const whereConditions = this.getWhereConditions(criteria, user);
|
|
|
|
if (whereConditions.length > 0) {
|
|
const whereClause = and(...whereConditions);
|
|
countQuery.where(whereClause);
|
|
}
|
|
|
|
const [{ value: totalCount }] = await countQuery;
|
|
return totalCount;
|
|
}
|
|
|
|
async findBusinessesById(id: string, user: JwtUser): Promise<BusinessListing> {
|
|
const conditions = [];
|
|
if (user?.role !== 'admin') {
|
|
conditions.push(or(eq(businesses.email, user?.email), ne(businesses.draft, true)));
|
|
}
|
|
conditions.push(sql`${businesses.id} = ${id}`);
|
|
const result = await this.conn
|
|
.select()
|
|
.from(businesses)
|
|
.where(and(...conditions));
|
|
if (result.length > 0) {
|
|
return result[0] as BusinessListing;
|
|
} else {
|
|
throw new BadRequestException(`No entry available for ${id}`);
|
|
}
|
|
}
|
|
|
|
async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
|
|
const conditions = [];
|
|
conditions.push(eq(businesses.email, email));
|
|
if (email !== user?.email && user?.role !== 'admin') {
|
|
conditions.push(ne(businesses.draft, true));
|
|
}
|
|
const listings = (await this.conn
|
|
.select()
|
|
.from(businesses)
|
|
.where(and(...conditions))) as BusinessListing[];
|
|
|
|
return listings;
|
|
}
|
|
// #### Find Favorites ########################################
|
|
async findFavoriteListings(user: JwtUser): Promise<BusinessListing[]> {
|
|
const userFavorites = await this.conn
|
|
.select()
|
|
.from(businesses)
|
|
.where(arrayContains(businesses.favoritesForUser, [user.email]));
|
|
return userFavorites;
|
|
}
|
|
// #### CREATE ########################################
|
|
async createListing(data: BusinessListing): Promise<BusinessListing> {
|
|
try {
|
|
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
|
data.updated = new Date();
|
|
BusinessListingSchema.parse(data);
|
|
const convertedBusinessListing = data;
|
|
delete convertedBusinessListing.id;
|
|
const [createdListing] = await this.conn.insert(businesses).values(convertedBusinessListing).returning();
|
|
return createdListing;
|
|
} catch (error) {
|
|
if (error instanceof ZodError) {
|
|
const filteredErrors = error.errors
|
|
.map(item => ({
|
|
...item,
|
|
field: item.path[0],
|
|
}))
|
|
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
|
|
throw new BadRequestException(filteredErrors);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
// #### UPDATE Business ########################################
|
|
async updateBusinessListing(id: string, data: BusinessListing, user: JwtUser): Promise<BusinessListing> {
|
|
try {
|
|
const [existingListing] = await this.conn.select().from(businesses).where(eq(businesses.id, id));
|
|
|
|
if (!existingListing) {
|
|
throw new NotFoundException(`Business listing with id ${id} not found`);
|
|
}
|
|
data.updated = new Date();
|
|
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
|
if (existingListing.email === user?.email) {
|
|
data.favoritesForUser = existingListing.favoritesForUser;
|
|
}
|
|
BusinessListingSchema.parse(data);
|
|
const convertedBusinessListing = data;
|
|
const [updateListing] = await this.conn.update(businesses).set(convertedBusinessListing).where(eq(businesses.id, id)).returning();
|
|
return updateListing;
|
|
} catch (error) {
|
|
if (error instanceof ZodError) {
|
|
const filteredErrors = error.errors
|
|
.map(item => ({
|
|
...item,
|
|
field: item.path[0],
|
|
}))
|
|
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
|
|
throw new BadRequestException(filteredErrors);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
// #### DELETE ########################################
|
|
async deleteListing(id: string): Promise<void> {
|
|
await this.conn.delete(businesses).where(eq(businesses.id, id));
|
|
}
|
|
// #### DELETE Favorite ###################################
|
|
async deleteFavorite(id: string, user: JwtUser): Promise<void> {
|
|
await this.conn
|
|
.update(businesses)
|
|
.set({
|
|
favoritesForUser: sql`array_remove(${businesses.favoritesForUser}, ${user.email})`,
|
|
})
|
|
.where(sql`${businesses.id} = ${id}`);
|
|
}
|
|
}
|