Fix business filtering logic and add docker sync guide

This commit is contained in:
2026-01-12 13:58:45 +01:00
parent 4f8fd77f7d
commit adeefb199c
21 changed files with 538 additions and 69 deletions

0
bizmatch-server/prod.dump Executable file → Normal file
View File

View File

@@ -5,7 +5,7 @@ import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
import { ZodError } from 'zod';
import * as schema from '../drizzle/schema';
import { businesses_json, PG_CONNECTION, users_json } from '../drizzle/schema';
import { businesses_json, PG_CONNECTION } from '../drizzle/schema';
import { GeoService } from '../geo/geo.service';
import { BusinessListing, BusinessListingSchema } from '../models/db.model';
import { BusinessListingCriteria, JwtUser } from '../models/main.model';
@@ -18,27 +18,30 @@ export class BusinessListingService {
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
private geoService?: GeoService,
) {}
) { }
private getWhereConditions(criteria: BusinessListingCriteria, user: JwtUser): SQL[] {
const whereConditions: SQL[] = [];
this.logger.info('getWhereConditions start', { criteria: JSON.stringify(criteria) });
if (criteria.city && criteria.searchType === 'exact') {
whereConditions.push(sql`(${businesses_json.data}->'location'->>'name') ILIKE ${criteria.city.name}`);
}
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
this.logger.debug('Adding radius search filter', { city: criteria.city.name, radius: criteria.radius });
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
whereConditions.push(sql`${getDistanceQuery(businesses_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
whereConditions.push(sql`(${getDistanceQuery(businesses_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius})`);
}
if (criteria.types && Array.isArray(criteria.types) && criteria.types.length > 0) {
const validTypes = criteria.types.filter(t => t !== null && t !== undefined && t !== '');
if (validTypes.length > 0) {
whereConditions.push(inArray(sql`${businesses_json.data}->>'type'`, validTypes));
}
if (criteria.types && criteria.types.length > 0) {
this.logger.warn('Adding business category filter', { types: criteria.types });
// Use explicit SQL with IN for robust JSONB comparison
const typeValues = criteria.types.map(t => sql`${t}`);
whereConditions.push(sql`((${businesses_json.data}->>'type') IN (${sql.join(typeValues, sql`, `)}))`);
}
if (criteria.state) {
this.logger.debug('Adding state filter', { state: criteria.state });
whereConditions.push(sql`(${businesses_json.data}->'location'->>'state') = ${criteria.state}`);
}
@@ -105,27 +108,30 @@ export class BusinessListingService {
if (criteria.title && criteria.title.trim() !== '') {
const searchTerm = `%${criteria.title.trim()}%`;
whereConditions.push(
or(
sql`(${businesses_json.data}->>'title') ILIKE ${searchTerm}`,
sql`(${businesses_json.data}->>'description') ILIKE ${searchTerm}`
)
sql`((${businesses_json.data}->>'title') ILIKE ${searchTerm} OR (${businesses_json.data}->>'description') ILIKE ${searchTerm})`
);
}
if (criteria.brokerName) {
const { firstname, lastname } = splitName(criteria.brokerName);
if (firstname === lastname) {
whereConditions.push(sql`(${users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} OR (${users_json.data}->>'lastname') ILIKE ${`%${lastname}%`}`);
whereConditions.push(
sql`((${schema.users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} OR (${schema.users_json.data}->>'lastname') ILIKE ${`%${lastname}%`})`
);
} else {
whereConditions.push(sql`(${users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} AND (${users_json.data}->>'lastname') ILIKE ${`%${lastname}%`}`);
whereConditions.push(
sql`((${schema.users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} AND (${schema.users_json.data}->>'lastname') ILIKE ${`%${lastname}%`})`
);
}
}
if (criteria.email) {
whereConditions.push(eq(users_json.email, criteria.email));
whereConditions.push(eq(schema.users_json.email, criteria.email));
}
if (user?.role !== 'admin') {
whereConditions.push(or(eq(businesses_json.email, user?.email), sql`(${businesses_json.data}->>'draft')::boolean IS NOT TRUE`));
whereConditions.push(
sql`((${businesses_json.email} = ${user?.email || null}) OR (${businesses_json.data}->>'draft')::boolean IS NOT TRUE)`
);
}
whereConditions.push(and(sql`(${users_json.data}->>'customerType') = 'professional'`, sql`(${users_json.data}->>'customerSubType') = 'broker'`));
this.logger.warn('whereConditions count', { count: whereConditions.length });
return whereConditions;
}
@@ -135,24 +141,21 @@ export class BusinessListingService {
const query = this.conn
.select({
business: businesses_json,
brokerFirstName: sql`${users_json.data}->>'firstname'`.as('brokerFirstName'),
brokerLastName: sql`${users_json.data}->>'lastname'`.as('brokerLastName'),
brokerFirstName: sql`${schema.users_json.data}->>'firstname'`.as('brokerFirstName'),
brokerLastName: sql`${schema.users_json.data}->>'lastname'`.as('brokerLastName'),
})
.from(businesses_json)
.leftJoin(users_json, eq(businesses_json.email, users_json.email));
.leftJoin(schema.users_json, eq(businesses_json.email, schema.users_json.email));
const whereConditions = this.getWhereConditions(criteria, user);
// Uncomment for debugging filter issues:
// this.logger.info('Filter Criteria:', { criteria });
// this.logger.info('Where Conditions Count:', { count: whereConditions.length });
this.logger.warn('Filter Criteria:', { criteria: JSON.stringify(criteria) });
if (whereConditions.length > 0) {
const whereClause = and(...whereConditions);
query.where(whereClause);
const whereClause = sql.join(whereConditions, sql` AND `);
query.where(sql`(${whereClause})`);
// Uncomment for debugging SQL queries:
// this.logger.info('Generated SQL:', { sql: query.toSQL() });
this.logger.warn('Generated SQL:', { sql: query.toSQL().sql, params: query.toSQL().params });
}
// Sortierung
@@ -228,13 +231,13 @@ export class BusinessListingService {
}
async getBusinessListingsCount(criteria: BusinessListingCriteria, user: JwtUser): Promise<number> {
const countQuery = this.conn.select({ value: count() }).from(businesses_json).leftJoin(users_json, eq(businesses_json.email, users_json.email));
const countQuery = this.conn.select({ value: count() }).from(businesses_json).leftJoin(schema.users_json, eq(businesses_json.email, schema.users_json.email));
const whereConditions = this.getWhereConditions(criteria, user);
if (whereConditions.length > 0) {
const whereClause = and(...whereConditions);
countQuery.where(whereClause);
const whereClause = sql.join(whereConditions, sql` AND `);
countQuery.where(sql`(${whereClause})`);
}
const [{ value: totalCount }] = await countQuery;

View File

@@ -10,7 +10,7 @@ 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';
import { getDistanceQuery, splitName } from '../utils';
import { generateSlug, extractShortIdFromSlug, isSlug } from '../utils/slug.utils';
@Injectable()
@@ -20,7 +20,7 @@ export class CommercialPropertyService {
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
private fileService?: FileService,
private geoService?: GeoService,
) {}
) { }
private getWhereConditions(criteria: CommercialPropertyListingCriteria, user: JwtUser): SQL[] {
const whereConditions: SQL[] = [];
@@ -32,7 +32,10 @@ export class CommercialPropertyService {
whereConditions.push(sql`${getDistanceQuery(commercials_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
}
if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(sql`${commercials_json.data}->>'type'`, criteria.types));
this.logger.warn('Adding commercial property type filter', { types: criteria.types });
// Use explicit SQL with IN for robust JSONB comparison
const typeValues = criteria.types.map(t => sql`${t}`);
whereConditions.push(sql`((${commercials_json.data}->>'type') IN (${sql.join(typeValues, sql`, `)}))`);
}
if (criteria.state) {
@@ -48,12 +51,32 @@ export class CommercialPropertyService {
}
if (criteria.title) {
whereConditions.push(sql`(${commercials_json.data}->>'title') ILIKE ${`%${criteria.title}%`} OR (${commercials_json.data}->>'description') ILIKE ${`%${criteria.title}%`}`);
whereConditions.push(
sql`((${commercials_json.data}->>'title') ILIKE ${`%${criteria.title}%`} OR (${commercials_json.data}->>'description') ILIKE ${`%${criteria.title}%`})`
);
}
if (criteria.brokerName) {
const { firstname, lastname } = splitName(criteria.brokerName);
if (firstname === lastname) {
// Single word: search either first OR last name
whereConditions.push(
sql`((${schema.users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} OR (${schema.users_json.data}->>'lastname') ILIKE ${`%${lastname}%`})`
);
} else {
// Multiple words: search both first AND last name
whereConditions.push(
sql`((${schema.users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} AND (${schema.users_json.data}->>'lastname') ILIKE ${`%${lastname}%`})`
);
}
}
if (user?.role !== 'admin') {
whereConditions.push(or(eq(commercials_json.email, user?.email), sql`(${commercials_json.data}->>'draft')::boolean IS NOT TRUE`));
whereConditions.push(
sql`((${commercials_json.email} = ${user?.email || null}) OR (${commercials_json.data}->>'draft')::boolean IS NOT TRUE)`
);
}
// whereConditions.push(and(eq(schema.users.customerType, 'professional')));
this.logger.warn('whereConditions count', { count: whereConditions.length });
return whereConditions;
}
// #### Find by criteria ########################################
@@ -63,9 +86,13 @@ export class CommercialPropertyService {
const query = this.conn.select({ commercial: commercials_json }).from(commercials_json).leftJoin(schema.users_json, eq(commercials_json.email, schema.users_json.email));
const whereConditions = this.getWhereConditions(criteria, user);
this.logger.warn('Filter Criteria:', { criteria: JSON.stringify(criteria) });
if (whereConditions.length > 0) {
const whereClause = and(...whereConditions);
query.where(whereClause);
const whereClause = sql.join(whereConditions, sql` AND `);
query.where(sql`(${whereClause})`);
this.logger.warn('Generated SQL:', { sql: query.toSQL().sql, params: query.toSQL().params });
}
// Sortierung
switch (criteria.sortBy) {
@@ -103,8 +130,8 @@ export class CommercialPropertyService {
const whereConditions = this.getWhereConditions(criteria, user);
if (whereConditions.length > 0) {
const whereClause = and(...whereConditions);
countQuery.where(whereClause);
const whereClause = sql.join(whereConditions, sql` AND `);
countQuery.where(sql`(${whereClause})`);
}
const [{ value: totalCount }] = await countQuery;

View File

@@ -34,6 +34,7 @@ export const CustomerTypeEnum = z.enum(['buyer', 'seller', 'professional']);
export const SubscriptionTypeEnum = z.enum(['free', 'professional', 'broker']);
export const CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
export const ListingsCategoryEnum = z.enum(['commercialProperty', 'business']);
export const ShareCategoryEnum = z.enum(['commercialProperty', 'business', 'user']);
export const ZodEventTypeEnum = z.enum(['view', 'print', 'email', 'facebook', 'x', 'linkedin', 'contact', 'favorite', 'emailus', 'pricing']);
export type EventTypeEnum = z.infer<typeof ZodEventTypeEnum>;
const PropertyTypeEnum = z.enum(['retail', 'land', 'industrial', 'office', 'mixedUse', 'multifamily', 'uncategorized']);
@@ -186,6 +187,7 @@ export const UserSchema = z
updated: z.date().optional().nullable(),
subscriptionId: z.string().optional().nullable(),
subscriptionPlan: SubscriptionTypeEnum.optional().nullable(),
favoritesForUser: z.array(z.string()),
showInDirectory: z.boolean(),
})
.superRefine((data, ctx) => {
@@ -369,7 +371,7 @@ export const ShareByEMailSchema = z.object({
listingTitle: z.string().optional().nullable(),
url: z.string().url({ message: 'Invalid URL format' }).optional().nullable(),
id: z.string().optional().nullable(),
type: ListingsCategoryEnum,
type: ShareCategoryEnum,
});
export type ShareByEMail = z.infer<typeof ShareByEMailSchema>;

View File

@@ -96,6 +96,7 @@ export interface CommercialPropertyListingCriteria extends ListCriteria {
minPrice: number;
maxPrice: number;
title: string;
brokerName: string;
criteriaType: 'commercialPropertyListings';
}
export interface UserListingCriteria extends ListCriteria {
@@ -358,6 +359,7 @@ export function createDefaultUser(email: string, firstname: string, lastname: st
updated: new Date(),
subscriptionId: null,
subscriptionPlan: subscriptionPlan,
favoritesForUser: [],
showInDirectory: false,
};
}