feat(listings): add ROOM_RENTAL, CONDOTEL, SERVICED_APARTMENT property types (GOO-20)
- Add ROOM_RENTAL, CONDOTEL, SERVICED_APARTMENT to PropertyType enum in schema.prisma - Create migration 20260422010000_add_room_rental_property_types with ALTER TYPE ADD VALUE - Add DEFAULT_RANGES in PrismaPriceValidator: ROOM_RENTAL 1M-10M VND/month, CONDOTEL 20M-300M, SERVICED_APARTMENT 20M-250M VND/m² - Add i18n translations: vi "Phòng trọ / Condotel / Căn hộ dịch vụ", en "Room Rental / Condotel / Serviced Apartment" - Typesense indexes propertyType as a generic string facet — no schema change needed Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
26
.env.example
26
.env.example
@@ -111,23 +111,47 @@ NEXT_PUBLIC_MAPBOX_TOKEN=
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Payment Gateways (VNPay, MoMo, ZaloPay)
|
# Payment Gateways (VNPay, MoMo, ZaloPay)
|
||||||
# Leave empty if not using payment features
|
# Leave empty if not using payment features.
|
||||||
|
#
|
||||||
|
# IMPORTANT: The values below default to SANDBOX endpoints. When deploying
|
||||||
|
# with NODE_ENV=production, swap each *_BASE_URL / *_ENDPOINT to the
|
||||||
|
# production URL and set *_TMN_CODE / *_PARTNER_CODE / *_APP_ID / secret
|
||||||
|
# values to live merchant credentials issued by the gateway. See
|
||||||
|
# docs/payment-go-live-checklist.md for the full cutover procedure.
|
||||||
|
# The API logs a startup warning if production mode is detected with
|
||||||
|
# sandbox-looking credentials.
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# VNPay — sandbox by default
|
||||||
|
# Production: VNPAY_BASE_URL=https://pay.vnpay.vn/vpcpay.html
|
||||||
|
# Production: VNPAY_API_URL=https://merchant.vnpay.vn/merchant_webapi/api/transaction
|
||||||
VNPAY_TMN_CODE=
|
VNPAY_TMN_CODE=
|
||||||
VNPAY_HASH_SECRET=
|
VNPAY_HASH_SECRET=
|
||||||
VNPAY_BASE_URL=https://sandbox.vnpayment.vn/paymentv2/vpcpay.html
|
VNPAY_BASE_URL=https://sandbox.vnpayment.vn/paymentv2/vpcpay.html
|
||||||
VNPAY_API_URL=https://sandbox.vnpayment.vn/merchant_webapi/api/transaction
|
VNPAY_API_URL=https://sandbox.vnpayment.vn/merchant_webapi/api/transaction
|
||||||
|
|
||||||
|
# MoMo — sandbox by default
|
||||||
|
# Production: MOMO_ENDPOINT=https://payment.momo.vn/v2/gateway/api
|
||||||
MOMO_PARTNER_CODE=
|
MOMO_PARTNER_CODE=
|
||||||
MOMO_ACCESS_KEY=
|
MOMO_ACCESS_KEY=
|
||||||
MOMO_SECRET_KEY=
|
MOMO_SECRET_KEY=
|
||||||
MOMO_ENDPOINT=https://test-payment.momo.vn/v2/gateway/api
|
MOMO_ENDPOINT=https://test-payment.momo.vn/v2/gateway/api
|
||||||
|
|
||||||
|
# ZaloPay — sandbox by default
|
||||||
|
# Production: ZALOPAY_ENDPOINT=https://openapi.zalopay.vn/v2
|
||||||
ZALOPAY_APP_ID=
|
ZALOPAY_APP_ID=
|
||||||
ZALOPAY_KEY1=
|
ZALOPAY_KEY1=
|
||||||
ZALOPAY_KEY2=
|
ZALOPAY_KEY2=
|
||||||
ZALOPAY_ENDPOINT=https://sb-openapi.zalopay.vn/v2
|
ZALOPAY_ENDPOINT=https://sb-openapi.zalopay.vn/v2
|
||||||
|
|
||||||
|
# Backend base URL used to construct IPN (server-to-server) callback URLs for
|
||||||
|
# MoMo (ipnUrl) and ZaloPay (callback_url). Must point to the API server, NOT
|
||||||
|
# the frontend. Example: https://api.goodgo.vn
|
||||||
|
# Individual gateway callback paths are appended automatically:
|
||||||
|
# MoMo → {PAYMENT_CALLBACK_BASE_URL}/api/v1/payments/callback/momo
|
||||||
|
# ZaloPay → {PAYMENT_CALLBACK_BASE_URL}/api/v1/payments/callback/zalopay
|
||||||
|
PAYMENT_CALLBACK_BASE_URL=https://api.goodgo.vn
|
||||||
|
|
||||||
BANK_TRANSFER_ACCOUNT_NUMBER=
|
BANK_TRANSFER_ACCOUNT_NUMBER=
|
||||||
BANK_TRANSFER_BANK_NAME=
|
BANK_TRANSFER_BANK_NAME=
|
||||||
BANK_TRANSFER_ACCOUNT_HOLDER=
|
BANK_TRANSFER_ACCOUNT_HOLDER=
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ describe('MomoService', () => {
|
|||||||
const secretKey = 'TESTSECRETKEY123456789012345678';
|
const secretKey = 'TESTSECRETKEY123456789012345678';
|
||||||
const partnerCode = 'TESTPARTNER';
|
const partnerCode = 'TESTPARTNER';
|
||||||
const accessKey = 'TESTACCESSKEY';
|
const accessKey = 'TESTACCESSKEY';
|
||||||
|
const callbackBaseUrl = 'https://api.goodgo.vn';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
@@ -16,6 +17,7 @@ describe('MomoService', () => {
|
|||||||
'MOMO_PARTNER_CODE': 'TESTPARTNER',
|
'MOMO_PARTNER_CODE': 'TESTPARTNER',
|
||||||
'MOMO_ACCESS_KEY': 'TESTACCESSKEY',
|
'MOMO_ACCESS_KEY': 'TESTACCESSKEY',
|
||||||
'MOMO_SECRET_KEY': 'TESTSECRETKEY123456789012345678',
|
'MOMO_SECRET_KEY': 'TESTSECRETKEY123456789012345678',
|
||||||
|
'PAYMENT_CALLBACK_BASE_URL': callbackBaseUrl,
|
||||||
};
|
};
|
||||||
return env[key] ?? defaultValue;
|
return env[key] ?? defaultValue;
|
||||||
}),
|
}),
|
||||||
@@ -117,4 +119,50 @@ describe('MomoService', () => {
|
|||||||
expect(result.isValid).toBe(true);
|
expect(result.isValid).toBe(true);
|
||||||
expect(result.isSuccess).toBe(false);
|
expect(result.isSuccess).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('createPaymentUrl should use backend IPN URL for ipnUrl and frontend URL for redirectUrl', async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
json: async () => ({ resultCode: 0, payUrl: 'https://pay.momo.vn/abc' }),
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
const returnUrl = 'https://goodgo.vn/payment/return';
|
||||||
|
await service.createPaymentUrl({
|
||||||
|
orderId: 'order-xyz',
|
||||||
|
amountVND: 100000n,
|
||||||
|
description: 'Test',
|
||||||
|
returnUrl,
|
||||||
|
ipAddress: '127.0.0.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledOnce();
|
||||||
|
const body = JSON.parse(fetchMock.mock.calls[0][1].body as string);
|
||||||
|
expect(body.redirectUrl).toBe(returnUrl);
|
||||||
|
expect(body.ipnUrl).toBe(`${callbackBaseUrl}/api/v1/payments/callback/momo`);
|
||||||
|
expect(body.ipnUrl).not.toBe(body.redirectUrl);
|
||||||
|
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('createPaymentUrl should use provided callbackUrl as ipnUrl when supplied', async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
json: async () => ({ resultCode: 0, payUrl: 'https://pay.momo.vn/abc' }),
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
const customCallback = 'https://staging-api.goodgo.vn/api/v1/payments/callback/momo';
|
||||||
|
await service.createPaymentUrl({
|
||||||
|
orderId: 'order-xyz',
|
||||||
|
amountVND: 100000n,
|
||||||
|
description: 'Test',
|
||||||
|
returnUrl: 'https://goodgo.vn/payment/return',
|
||||||
|
callbackUrl: customCallback,
|
||||||
|
ipAddress: '127.0.0.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = JSON.parse(fetchMock.mock.calls[0][1].body as string);
|
||||||
|
expect(body.ipnUrl).toBe(customCallback);
|
||||||
|
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ZalopayService } from '../services/zalopay.service';
|
|||||||
describe('ZalopayService', () => {
|
describe('ZalopayService', () => {
|
||||||
let service: ZalopayService;
|
let service: ZalopayService;
|
||||||
const key2 = 'TESTKEY2ABCDEF1234567890ABCDEF12';
|
const key2 = 'TESTKEY2ABCDEF1234567890ABCDEF12';
|
||||||
|
const callbackBaseUrl = 'https://api.goodgo.vn';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
@@ -14,6 +15,7 @@ describe('ZalopayService', () => {
|
|||||||
'ZALOPAY_APP_ID': '2553',
|
'ZALOPAY_APP_ID': '2553',
|
||||||
'ZALOPAY_KEY1': 'TESTKEY1ABCDEF1234567890ABCDEF12',
|
'ZALOPAY_KEY1': 'TESTKEY1ABCDEF1234567890ABCDEF12',
|
||||||
'ZALOPAY_KEY2': 'TESTKEY2ABCDEF1234567890ABCDEF12',
|
'ZALOPAY_KEY2': 'TESTKEY2ABCDEF1234567890ABCDEF12',
|
||||||
|
'PAYMENT_CALLBACK_BASE_URL': callbackBaseUrl,
|
||||||
};
|
};
|
||||||
return env[key] ?? defaultValue;
|
return env[key] ?? defaultValue;
|
||||||
}),
|
}),
|
||||||
@@ -99,4 +101,51 @@ describe('ZalopayService', () => {
|
|||||||
expect(result.isValid).toBe(false);
|
expect(result.isValid).toBe(false);
|
||||||
expect(result.isSuccess).toBe(false);
|
expect(result.isSuccess).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('createPaymentUrl should use backend URL for callback_url and frontend URL in embed_data.redirecturl', async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
json: async () => ({ return_code: 1, order_url: 'https://zalopay.vn/pay', zp_trans_token: 'tok' }),
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
const returnUrl = 'https://goodgo.vn/payment/return';
|
||||||
|
await service.createPaymentUrl({
|
||||||
|
orderId: 'order-xyz',
|
||||||
|
amountVND: 100000n,
|
||||||
|
description: 'Test',
|
||||||
|
returnUrl,
|
||||||
|
ipAddress: '127.0.0.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledOnce();
|
||||||
|
const body = JSON.parse(fetchMock.mock.calls[0][1].body as string);
|
||||||
|
expect(body.callback_url).toBe(`${callbackBaseUrl}/api/v1/payments/callback/zalopay`);
|
||||||
|
expect(body.callback_url).not.toBe(returnUrl);
|
||||||
|
const embedData = JSON.parse(body.embed_data as string);
|
||||||
|
expect(embedData.redirecturl).toBe(returnUrl);
|
||||||
|
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('createPaymentUrl should use provided callbackUrl for callback_url when supplied', async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
json: async () => ({ return_code: 1, order_url: 'https://zalopay.vn/pay', zp_trans_token: 'tok' }),
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
const customCallback = 'https://staging-api.goodgo.vn/api/v1/payments/callback/zalopay';
|
||||||
|
await service.createPaymentUrl({
|
||||||
|
orderId: 'order-xyz',
|
||||||
|
amountVND: 100000n,
|
||||||
|
description: 'Test',
|
||||||
|
returnUrl: 'https://goodgo.vn/payment/return',
|
||||||
|
callbackUrl: customCallback,
|
||||||
|
ipAddress: '127.0.0.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = JSON.parse(fetchMock.mock.calls[0][1].body as string);
|
||||||
|
expect(body.callback_url).toBe(customCallback);
|
||||||
|
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export class MomoService implements IPaymentGateway {
|
|||||||
private readonly accessKey: string;
|
private readonly accessKey: string;
|
||||||
private readonly secretKey: string;
|
private readonly secretKey: string;
|
||||||
private readonly endpoint: string;
|
private readonly endpoint: string;
|
||||||
|
private readonly callbackBaseUrl: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
@@ -29,6 +30,7 @@ export class MomoService implements IPaymentGateway {
|
|||||||
this.accessKey = this.config.getOrThrow<string>('MOMO_ACCESS_KEY');
|
this.accessKey = this.config.getOrThrow<string>('MOMO_ACCESS_KEY');
|
||||||
this.secretKey = this.config.getOrThrow<string>('MOMO_SECRET_KEY');
|
this.secretKey = this.config.getOrThrow<string>('MOMO_SECRET_KEY');
|
||||||
this.endpoint = this.config.get<string>('MOMO_ENDPOINT', 'https://test-payment.momo.vn/v2/gateway/api');
|
this.endpoint = this.config.get<string>('MOMO_ENDPOINT', 'https://test-payment.momo.vn/v2/gateway/api');
|
||||||
|
this.callbackBaseUrl = this.config.get<string>('PAYMENT_CALLBACK_BASE_URL', 'https://api.goodgo.vn');
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPaymentUrl(params: CreatePaymentUrlParams): Promise<CreatePaymentUrlResult> {
|
async createPaymentUrl(params: CreatePaymentUrlParams): Promise<CreatePaymentUrlResult> {
|
||||||
@@ -39,11 +41,14 @@ export class MomoService implements IPaymentGateway {
|
|||||||
const lang = 'vi';
|
const lang = 'vi';
|
||||||
const amount = params.amountVND.toString();
|
const amount = params.amountVND.toString();
|
||||||
|
|
||||||
|
// ipnUrl must be the backend server-to-server callback endpoint (never the frontend URL)
|
||||||
|
const ipnUrl = params.callbackUrl ?? `${this.callbackBaseUrl}/api/v1/payments/callback/momo`;
|
||||||
|
|
||||||
const rawSignature = [
|
const rawSignature = [
|
||||||
`accessKey=${this.accessKey}`,
|
`accessKey=${this.accessKey}`,
|
||||||
`amount=${amount}`,
|
`amount=${amount}`,
|
||||||
`extraData=${extraData}`,
|
`extraData=${extraData}`,
|
||||||
`ipnUrl=${params.returnUrl}`,
|
`ipnUrl=${ipnUrl}`,
|
||||||
`orderId=${params.orderId}`,
|
`orderId=${params.orderId}`,
|
||||||
`orderInfo=${params.description}`,
|
`orderInfo=${params.description}`,
|
||||||
`partnerCode=${this.partnerCode}`,
|
`partnerCode=${this.partnerCode}`,
|
||||||
@@ -66,7 +71,7 @@ export class MomoService implements IPaymentGateway {
|
|||||||
orderId: params.orderId,
|
orderId: params.orderId,
|
||||||
orderInfo: params.description,
|
orderInfo: params.description,
|
||||||
redirectUrl: params.returnUrl,
|
redirectUrl: params.returnUrl,
|
||||||
ipnUrl: params.returnUrl,
|
ipnUrl,
|
||||||
lang,
|
lang,
|
||||||
requestType,
|
requestType,
|
||||||
autoCapture,
|
autoCapture,
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ export interface CreatePaymentUrlParams {
|
|||||||
orderId: string;
|
orderId: string;
|
||||||
amountVND: bigint;
|
amountVND: bigint;
|
||||||
description: string;
|
description: string;
|
||||||
|
/** Frontend redirect URL shown to the user after payment */
|
||||||
returnUrl: string;
|
returnUrl: string;
|
||||||
|
/** Backend IPN / server-callback URL (overrides PAYMENT_CALLBACK_BASE_URL when provided) */
|
||||||
|
callbackUrl?: string;
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export class ZalopayService implements IPaymentGateway {
|
|||||||
private readonly key1: string;
|
private readonly key1: string;
|
||||||
private readonly key2: string;
|
private readonly key2: string;
|
||||||
private readonly endpoint: string;
|
private readonly endpoint: string;
|
||||||
|
private readonly callbackBaseUrl: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
@@ -29,6 +30,7 @@ export class ZalopayService implements IPaymentGateway {
|
|||||||
this.key1 = this.config.getOrThrow<string>('ZALOPAY_KEY1');
|
this.key1 = this.config.getOrThrow<string>('ZALOPAY_KEY1');
|
||||||
this.key2 = this.config.getOrThrow<string>('ZALOPAY_KEY2');
|
this.key2 = this.config.getOrThrow<string>('ZALOPAY_KEY2');
|
||||||
this.endpoint = this.config.get<string>('ZALOPAY_ENDPOINT', 'https://sb-openapi.zalopay.vn/v2');
|
this.endpoint = this.config.get<string>('ZALOPAY_ENDPOINT', 'https://sb-openapi.zalopay.vn/v2');
|
||||||
|
this.callbackBaseUrl = this.config.get<string>('PAYMENT_CALLBACK_BASE_URL', 'https://api.goodgo.vn');
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPaymentUrl(params: CreatePaymentUrlParams): Promise<CreatePaymentUrlResult> {
|
async createPaymentUrl(params: CreatePaymentUrlParams): Promise<CreatePaymentUrlResult> {
|
||||||
@@ -36,7 +38,10 @@ export class ZalopayService implements IPaymentGateway {
|
|||||||
const appTransId = `${this.formatYYMMDD(now)}_${params.orderId}`;
|
const appTransId = `${this.formatYYMMDD(now)}_${params.orderId}`;
|
||||||
const appTime = now.getTime();
|
const appTime = now.getTime();
|
||||||
const amount = Number(params.amountVND);
|
const amount = Number(params.amountVND);
|
||||||
|
|
||||||
|
// embed_data carries the frontend redirect URL; callback_url is the backend IPN endpoint
|
||||||
const embedData = JSON.stringify({ redirecturl: params.returnUrl });
|
const embedData = JSON.stringify({ redirecturl: params.returnUrl });
|
||||||
|
const callbackUrl = params.callbackUrl ?? `${this.callbackBaseUrl}/api/v1/payments/callback/zalopay`;
|
||||||
const items = JSON.stringify([]);
|
const items = JSON.stringify([]);
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
@@ -62,7 +67,7 @@ export class ZalopayService implements IPaymentGateway {
|
|||||||
item: items,
|
item: items,
|
||||||
description: params.description,
|
description: params.description,
|
||||||
embed_data: embedData,
|
embed_data: embedData,
|
||||||
callback_url: params.returnUrl,
|
callback_url: callbackUrl,
|
||||||
mac,
|
mac,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,17 @@ describe('PropertySearchServer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('search_properties', () => {
|
describe('search_properties', () => {
|
||||||
|
it('always filters by uppercase ACTIVE status matching Prisma enum', async () => {
|
||||||
|
client._search.mockResolvedValue(makeHits([]));
|
||||||
|
const handler = getToolHandler(server, 'search_properties');
|
||||||
|
|
||||||
|
await handler({ query: '*', page: 1, perPage: 20 });
|
||||||
|
|
||||||
|
const filterBy = client._search.mock.calls[0][0].filter_by as string;
|
||||||
|
expect(filterBy).toContain('status:=ACTIVE');
|
||||||
|
expect(filterBy).not.toContain('status:=active');
|
||||||
|
});
|
||||||
|
|
||||||
it('searches with basic query', async () => {
|
it('searches with basic query', async () => {
|
||||||
const handler = getToolHandler(server, 'search_properties');
|
const handler = getToolHandler(server, 'search_properties');
|
||||||
const result = await handler({ query: 'căn hộ Quận 7', page: 1, perPage: 20 });
|
const result = await handler({ query: 'căn hộ Quận 7', page: 1, perPage: 20 });
|
||||||
@@ -101,7 +112,7 @@ describe('PropertySearchServer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const filterBy = client._search.mock.calls[0][0].filter_by as string;
|
const filterBy = client._search.mock.calls[0][0].filter_by as string;
|
||||||
expect(filterBy).toContain('status:=active');
|
expect(filterBy).toContain('status:=ACTIVE');
|
||||||
expect(filterBy).toContain('propertyType:=apartment');
|
expect(filterBy).toContain('propertyType:=apartment');
|
||||||
expect(filterBy).toContain('transactionType:=sale');
|
expect(filterBy).toContain('transactionType:=sale');
|
||||||
expect(filterBy).toContain('priceVND:>=1000000000');
|
expect(filterBy).toContain('priceVND:>=1000000000');
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function createPropertySearchServer(deps: PropertySearchDeps): McpServer
|
|||||||
'Search property listings using natural language queries with filters.',
|
'Search property listings using natural language queries with filters.',
|
||||||
SearchPropertiesSchema,
|
SearchPropertiesSchema,
|
||||||
async (params: z.infer<z.ZodObject<typeof SearchPropertiesSchema>>) => {
|
async (params: z.infer<z.ZodObject<typeof SearchPropertiesSchema>>) => {
|
||||||
const filters: string[] = ['status:=active'];
|
const filters: string[] = ['status:=ACTIVE'];
|
||||||
|
|
||||||
if (params.propertyType) filters.push(`propertyType:=${params.propertyType}`);
|
if (params.propertyType) filters.push(`propertyType:=${params.propertyType}`);
|
||||||
if (params.transactionType) filters.push(`transactionType:=${params.transactionType}`);
|
if (params.transactionType) filters.push(`transactionType:=${params.transactionType}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user