feat(analytics): integrate AI/ML services — AVM endpoint, moderation pipeline, market index cron
- Add AiServiceClient HTTP client for Python FastAPI AI service with timeout and fallback - Add HttpAVMService that calls Python AVM endpoint, falls back to PrismaAVMService on failure - Add ListingCreatedModerationHandler: auto-flags suspicious listings via AI moderation on create - Add MarketIndexCronService: daily cron job aggregating market stats per district/city/type - Wire ScheduleModule and new providers into AnalyticsModule and AppModule - Add unit tests for AiServiceClient, HttpAVMService, and moderation handler (all passing) Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
106
e2e/api/inquiries.spec.ts
Normal file
106
e2e/api/inquiries.spec.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { test, expect, createListing, registerUser } from '../fixtures';
|
||||
|
||||
test.describe('Inquiries API', () => {
|
||||
test('POST /inquiries — creates inquiry for a listing', async ({
|
||||
request,
|
||||
authedRequest,
|
||||
testTokens,
|
||||
}) => {
|
||||
const { listing } = await createListing(request, testTokens.accessToken);
|
||||
|
||||
const res = await authedRequest.post('/inquiries', {
|
||||
data: {
|
||||
listingId: listing.listingId,
|
||||
message: 'Tôi muốn xem căn hộ này',
|
||||
phone: '0901234567',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status()).toBe(201);
|
||||
const body = await res.json();
|
||||
expect(body).toHaveProperty('inquiryId');
|
||||
expect(body.listingId).toBe(listing.listingId);
|
||||
});
|
||||
|
||||
test('POST /inquiries — rejects without auth', async ({ request }) => {
|
||||
const res = await request.post('/inquiries', {
|
||||
data: {
|
||||
listingId: 'nonexistent',
|
||||
message: 'Test inquiry',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status()).toBe(401);
|
||||
});
|
||||
|
||||
test('GET /inquiries/listing/:id — returns inquiries for a listing', async ({
|
||||
request,
|
||||
authedRequest,
|
||||
testTokens,
|
||||
}) => {
|
||||
const { listing } = await createListing(request, testTokens.accessToken);
|
||||
|
||||
// Create an inquiry first
|
||||
await authedRequest.post('/inquiries', {
|
||||
data: {
|
||||
listingId: listing.listingId,
|
||||
message: 'Inquiry E2E test',
|
||||
},
|
||||
});
|
||||
|
||||
const res = await authedRequest.get(`/inquiries/listing/${listing.listingId}`);
|
||||
|
||||
expect(res.status()).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body).toHaveProperty('data');
|
||||
expect(body).toHaveProperty('total');
|
||||
expect(body.data.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Leads API', () => {
|
||||
test('POST /leads — creates a lead (agent only)', async ({
|
||||
request,
|
||||
authedRequest,
|
||||
testTokens,
|
||||
}) => {
|
||||
const { listing } = await createListing(request, testTokens.accessToken);
|
||||
|
||||
const res = await authedRequest.post('/leads', {
|
||||
data: {
|
||||
listingId: listing.listingId,
|
||||
buyerName: 'Nguyễn Văn A',
|
||||
buyerPhone: '0912345678',
|
||||
source: 'WEBSITE',
|
||||
notes: 'Khách quan tâm căn hộ Q1',
|
||||
},
|
||||
});
|
||||
|
||||
// May fail if user is not AGENT role — that's expected behavior
|
||||
if (res.status() === 201) {
|
||||
const body = await res.json();
|
||||
expect(body).toHaveProperty('leadId');
|
||||
} else {
|
||||
// Non-agent users get 403
|
||||
expect(res.status()).toBe(403);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Agent Dashboard API', () => {
|
||||
test('GET /agents/dashboard — returns stats for agent', async ({
|
||||
authedRequest,
|
||||
}) => {
|
||||
const res = await authedRequest.get('/agents/dashboard');
|
||||
|
||||
// May return 403 if test user is not an agent
|
||||
if (res.status() === 200) {
|
||||
const body = await res.json();
|
||||
expect(body).toHaveProperty('qualityScore');
|
||||
expect(body).toHaveProperty('totalLeads');
|
||||
expect(body).toHaveProperty('totalInquiries');
|
||||
} else {
|
||||
expect(res.status()).toBe(403);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user