bizmatch-project/bizmatch-server/src/listings/commercial-property.service.ts

259 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 { commercials, PG_CONNECTION } from '../drizzle/schema';
import { FileService } from '../file/file.service';
import { GeoService } from '../geo/geo.service';
import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model';
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model';
import { getDistanceQuery } from '../utils';
@Injectable()
export class CommercialPropertyService {
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: CommercialPropertyListingCriteria, user: JwtUser): SQL[] {
const whereConditions: SQL[] = [];
if (criteria.city && criteria.searchType === 'exact') {
whereConditions.push(sql`${commercials.location}->>'name' ilike ${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(commercials, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
}
if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(schema.commercials.type, criteria.types));
}
if (criteria.state) {
whereConditions.push(sql`${schema.commercials.location}->>'state' = ${criteria.state}`);
}
if (criteria.minPrice) {
whereConditions.push(gte(schema.commercials.price, criteria.minPrice));
}
if (criteria.maxPrice) {
whereConditions.push(lte(schema.commercials.price, criteria.maxPrice));
}
if (criteria.title) {
whereConditions.push(or(ilike(schema.commercials.title, `%${criteria.title}%`), ilike(schema.commercials.description, `%${criteria.title}%`)));
}
if (user?.role !== 'admin') {
whereConditions.push(or(eq(commercials.email, user?.email), ne(commercials.draft, true)));
}
// whereConditions.push(and(eq(schema.users.customerType, 'professional')));
return whereConditions;
}
// #### Find by criteria ########################################
async searchCommercialProperties(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<any> {
const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12;
const query = this.conn.select({ commercial: commercials }).from(commercials).leftJoin(schema.users, eq(commercials.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(commercials.price));
break;
case 'priceDesc':
query.orderBy(desc(commercials.price));
break;
case 'creationDateFirst':
query.orderBy(asc(commercials.created));
break;
case 'creationDateLast':
query.orderBy(desc(commercials.created));
break;
default:
// Keine spezifische Sortierung, Standardverhalten kann hier eingefügt werden
break;
}
// Paginierung
query.limit(length).offset(start);
const data = await query;
const results = data.map(r => r.commercial);
const totalCount = await this.getCommercialPropertiesCount(criteria, user);
return {
results,
totalCount,
};
}
async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<number> {
const countQuery = this.conn.select({ value: count() }).from(schema.commercials).leftJoin(schema.users, eq(commercials.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;
}
// #### Find by ID ########################################
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
const conditions = [];
if (user?.role !== 'admin') {
conditions.push(or(eq(commercials.email, user?.email), ne(commercials.draft, true)));
}
conditions.push(sql`${commercials.id} = ${id}`);
const result = await this.conn
.select()
.from(commercials)
.where(and(...conditions));
if (result.length > 0) {
return result[0] as CommercialPropertyListing;
} else {
throw new BadRequestException(`No entry available for ${id}`);
}
}
// #### Find by User EMail ########################################
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
const conditions = [];
conditions.push(eq(commercials.email, email));
if (email !== user?.email && user?.role !== 'admin') {
conditions.push(ne(commercials.draft, true));
}
const listings = (await this.conn
.select()
.from(commercials)
.where(and(...conditions))) as CommercialPropertyListing[];
return listings as CommercialPropertyListing[];
}
// #### Find Favorites ########################################
async findFavoriteListings(user: JwtUser): Promise<CommercialPropertyListing[]> {
const userFavorites = await this.conn
.select()
.from(commercials)
.where(arrayContains(commercials.favoritesForUser, [user.email]));
return userFavorites;
}
// #### Find by imagePath ########################################
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
const result = await this.conn
.select()
.from(commercials)
.where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
return result[0] as CommercialPropertyListing;
}
// #### CREATE ########################################
async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
try {
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
data.updated = new Date();
CommercialPropertyListingSchema.parse(data);
const convertedCommercialPropertyListing = data;
delete convertedCommercialPropertyListing.id;
const [createdListing] = await this.conn.insert(commercials).values(convertedCommercialPropertyListing).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 CommercialProps ########################################
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing, user: JwtUser): Promise<CommercialPropertyListing> {
try {
const [existingListing] = await this.conn.select().from(commercials).where(eq(commercials.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 || !user) {
data.favoritesForUser = existingListing.favoritesForUser;
}
CommercialPropertyListingSchema.parse(data);
const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId));
const difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x)));
if (difference.length > 0) {
this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`);
data.imageOrder = imageOrder;
}
const convertedCommercialPropertyListing = data;
const [updateListing] = await this.conn.update(commercials).set(convertedCommercialPropertyListing).where(eq(commercials.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;
}
}
// ##############################################################
// Images for commercial Properties
// ##############################################################
async deleteImage(imagePath: string, serial: string, name: string) {
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
const index = listing.imageOrder.findIndex(im => im === name);
if (index > -1) {
listing.imageOrder.splice(index, 1);
await this.updateCommercialPropertyListing(listing.id, listing, null);
}
}
async addImage(imagePath: string, serial: string, imagename: string) {
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
listing.imageOrder.push(imagename);
await this.updateCommercialPropertyListing(listing.id, listing, null);
}
// #### DELETE ########################################
async deleteListing(id: string): Promise<void> {
await this.conn.delete(commercials).where(eq(commercials.id, id));
}
// #### DELETE Favorite ###################################
async deleteFavorite(id: string, user: JwtUser): Promise<void> {
await this.conn
.update(commercials)
.set({
favoritesForUser: sql`array_remove(${commercials.favoritesForUser}, ${user.email})`,
})
.where(sql`${commercials.id} = ${id}`);
}
// ##############################################################
// States
// ##############################################################
// async getStates(): Promise<any[]> {
// return await this.conn
// .select({ state: commercials.state, count: sql<number>`count(${commercials.id})`.mapWith(Number) })
// .from(commercials)
// .groupBy(sql`${commercials.state}`)
// .orderBy(sql`count desc`);
// }
}