bizmatch-project/bizmatch-server/src/listings/business-listing.service.ts

250 lines
9.8 KiB
TypeScript

import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { and, count, 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.js';
import { businesses, PG_CONNECTION } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js';
import { GeoService } from '../geo/geo.service.js';
import { BusinessListing, BusinessListingSchema } from '../models/db.model.js';
import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
import { convertBusinessToDrizzleBusiness, convertDrizzleBusinessToBusiness, getDistanceQuery, splitName } from '../utils.js';
@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(ilike(businesses.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(eq(businesses.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 (criteria.brokerName) {
// whereConditions.push(or(ilike(schema.users.firstname, `%${criteria.brokerName}%`), ilike(schema.users.lastname, `%${criteria.brokerName}%`)));
// }
if (!user?.roles?.includes('ADMIN') ?? false) {
whereConditions.push(or(eq(businesses.email, user?.username), 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);
}
// Paginierung
query.limit(length).offset(start);
const data = await query;
const totalCount = await this.getBusinessListingsCount(criteria, user);
const results = data.map(r => r.business).map(r => convertDrizzleBusinessToBusiness(r));
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?.roles?.includes('ADMIN') ?? false) {
conditions.push(or(eq(businesses.email, user?.username), ne(businesses.draft, true)));
}
conditions.push(sql`${businesses.id} = ${id}`);
let result = await this.conn
.select()
.from(businesses)
.where(and(...conditions));
if (result.length > 0) {
return convertDrizzleBusinessToBusiness(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.imageName, emailToDirName(email)));
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(ne(businesses.draft, true));
}
const listings = (await this.conn
.select()
.from(businesses)
.where(and(...conditions))) as BusinessListing[];
return listings.map(l => convertDrizzleBusinessToBusiness(l));
}
// #### 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();
const validatedBusinessListing = BusinessListingSchema.parse(data);
const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
delete convertedBusinessListing.id;
const [createdListing] = await this.conn.insert(businesses).values(convertedBusinessListing).returning();
return convertDrizzleBusinessToBusiness(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): Promise<BusinessListing> {
try {
data.updated = new Date();
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
const validatedBusinessListing = BusinessListingSchema.parse(data);
const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
const [updateListing] = await this.conn.update(businesses).set(convertedBusinessListing).where(eq(businesses.id, id)).returning();
return convertDrizzleBusinessToBusiness(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));
}
// ##############################################################
// States
// ##############################################################
async getStates(): Promise<any[]> {
return await this.conn
.select({ state: businesses.state, count: sql<number>`count(${businesses.id})`.mapWith(Number) })
.from(businesses)
.groupBy(sql`${businesses.state}`)
.orderBy(sql`count desc`);
}
}