@rubytech/create-maxy 1.0.507 → 1.0.508
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -1
- package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts +18 -0
- package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/mcp-stderr-tee/dist/index.js +45 -0
- package/payload/platform/lib/mcp-stderr-tee/dist/index.js.map +1 -0
- package/payload/platform/lib/mcp-stderr-tee/src/index.ts +51 -0
- package/payload/platform/lib/mcp-stderr-tee/tsconfig.json +8 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/mcp/dist/index.js +2 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js +2 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/index.js +2 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/index.js +2 -0
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js +2 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/replicate/mcp/dist/index.js +2 -0
- package/payload/platform/plugins/replicate/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/index.js +2 -0
- package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/index.js +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/waitlist/mcp/dist/index.js +2 -0
- package/payload/platform/plugins/waitlist/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js +5 -0
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/index.js +2 -0
- package/payload/platform/plugins/workflows/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/index.js +2 -33
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.js +110 -18
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.js +34 -6
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.js +59 -16
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.js +5 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.js +4 -2
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.js +6 -2
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-info.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-info.js +16 -4
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-info.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.js +63 -18
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.js +13 -3
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/index.ts +3 -35
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/customer-preferences.ts +6 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/feedback.ts +6 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-enquiry.ts +8 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match-batch.ts +5 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match-request.ts +5 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match.ts +6 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-detail.ts +203 -21
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-search.ts +99 -12
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-detail.ts +95 -20
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-listed.ts +27 -6
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-request.ts +5 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-search.ts +20 -8
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/supplier.ts +9 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/team-availability.ts +12 -2
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/team-info.ts +50 -9
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-create.ts +5 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-detail.ts +124 -23
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-search.ts +25 -7
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-update.ts +5 -0
- package/payload/server/server.js +122 -14
|
@@ -2,6 +2,14 @@ import { loopGet, loopPost, loopPut, withTeamKey } from "../lib/loop-api.js";
|
|
|
2
2
|
|
|
3
3
|
type EnquiryAction = "seller-enquiry" | "autoresponder-get" | "autoresponder-answers" | "autoresponder-details" | "autoresponder-refer";
|
|
4
4
|
|
|
5
|
+
// ─── Loop API V2: Marketing Enquiry Endpoints ──────────────────
|
|
6
|
+
// POST /marketing/enquiries/seller — submit seller enquiry
|
|
7
|
+
// GET /marketing/enquiries/auto-responder/{id}/{key} — read enquiry
|
|
8
|
+
// PUT .../auto-responder/{id}/answers/{key} — submit answers
|
|
9
|
+
// PUT .../auto-responder/{id}/details/{key} — update details
|
|
10
|
+
// PUT .../auto-responder/{id}/refer/{key} — refer enquiry
|
|
11
|
+
// Note: auto-responder uses personCode + key (UUID), not personId
|
|
12
|
+
// ────────────────────────────────────────────────────────────────
|
|
5
13
|
interface LoopBooleanResponse {
|
|
6
14
|
success?: boolean;
|
|
7
15
|
[key: string]: unknown;
|
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
loopPut,
|
|
5
5
|
} from "../lib/loop-api.js";
|
|
6
6
|
|
|
7
|
+
// ─── Loop API V2: PUT /marketing/matching/other-matches ─────────
|
|
8
|
+
// Request body: array of property IDs
|
|
9
|
+
// Response: array of matching property summaries
|
|
10
|
+
// Schema not verified via curl (depends on matching state)
|
|
11
|
+
// ────────────────────────────────────────────────────────────────
|
|
7
12
|
interface LoopMatchingSummary {
|
|
8
13
|
id?: number;
|
|
9
14
|
address?: string;
|
|
@@ -3,6 +3,11 @@ import { loopPost, withTeamKey } from "../lib/loop-api.js";
|
|
|
3
3
|
type Department = "sales" | "lettings";
|
|
4
4
|
type MatchAction = "viewing" | "information" | "callback";
|
|
5
5
|
|
|
6
|
+
// ─── Loop API V2: POST /marketing/matching/{id}/{action} ────────
|
|
7
|
+
// Actions: viewing, information, callback
|
|
8
|
+
// Request body: { name?, email?, phone?, message? }
|
|
9
|
+
// Response: boolean success
|
|
10
|
+
// ────────────────────────────────────────────────────────────────
|
|
6
11
|
interface LoopBooleanResponse {
|
|
7
12
|
success?: boolean;
|
|
8
13
|
[key: string]: unknown;
|
|
@@ -4,6 +4,12 @@ import {
|
|
|
4
4
|
loopGet,
|
|
5
5
|
} from "../lib/loop-api.js";
|
|
6
6
|
|
|
7
|
+
// ─── Loop API V2: /marketing/matching/{id} ─────────────────────
|
|
8
|
+
// Returned HTTP 500 for test property 895155 — may require
|
|
9
|
+
// publishedToMatching: true. Exact response schema not verified.
|
|
10
|
+
// Fields below are from Swagger docs; [key: string]: unknown
|
|
11
|
+
// ensures any actual response fields are accessible.
|
|
12
|
+
// ────────────────────────────────────────────────────────────────
|
|
7
13
|
interface LoopMatchingProperty {
|
|
8
14
|
id?: number;
|
|
9
15
|
address?: string;
|
package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-detail.ts
CHANGED
|
@@ -4,17 +4,111 @@ import {
|
|
|
4
4
|
loopGet,
|
|
5
5
|
} from "../lib/loop-api.js";
|
|
6
6
|
|
|
7
|
+
// ─── Loop API V2: /people/{id} ─────────────────────────────────
|
|
8
|
+
// Fields: id, title, firstName, lastName, contactNotes,
|
|
9
|
+
// primaryPhone, primaryPhoneType, primaryEmail, primaryEmailType,
|
|
10
|
+
// dateCreated, houseNameOrNumber, houseSecondaryNameOrNumber,
|
|
11
|
+
// street, locality, town, district, county, postcode,
|
|
12
|
+
// buyerProfiles[], sellerProfiles[], renterIds[], landlordIds[],
|
|
13
|
+
// gdpr*, communication*
|
|
14
|
+
// ────────────────────────────────────────────────────────────────
|
|
7
15
|
interface LoopPersonDetail {
|
|
8
|
-
id
|
|
16
|
+
id: number;
|
|
17
|
+
title?: string;
|
|
9
18
|
firstName?: string;
|
|
10
19
|
lastName?: string;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
contactNotes?: string;
|
|
21
|
+
primaryPhone?: string;
|
|
22
|
+
primaryPhoneType?: string;
|
|
23
|
+
primaryEmail?: string;
|
|
24
|
+
primaryEmailType?: string;
|
|
25
|
+
dateCreated?: string;
|
|
26
|
+
houseNameOrNumber?: string;
|
|
27
|
+
houseSecondaryNameOrNumber?: string;
|
|
28
|
+
street?: string;
|
|
29
|
+
locality?: string;
|
|
30
|
+
town?: string;
|
|
31
|
+
district?: string;
|
|
32
|
+
county?: string;
|
|
16
33
|
postcode?: string;
|
|
17
|
-
|
|
34
|
+
buyerProfiles?: LoopBuyerProfileRef[];
|
|
35
|
+
sellerProfiles?: LoopSellerProfileRef[];
|
|
36
|
+
renterIds?: number[];
|
|
37
|
+
landlordIds?: number[];
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface LoopBuyerProfileRef {
|
|
42
|
+
id: number;
|
|
43
|
+
buyerGroupName?: string;
|
|
44
|
+
maxPrice?: number | null;
|
|
45
|
+
minBeds?: number;
|
|
46
|
+
viewingCount?: number;
|
|
47
|
+
offerCount?: number;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface LoopSellerProfileRef {
|
|
52
|
+
id: number;
|
|
53
|
+
sellerGroupName?: string;
|
|
54
|
+
propertyId?: number;
|
|
55
|
+
propertyAddress?: string;
|
|
56
|
+
price?: number;
|
|
57
|
+
propertyStatus?: string;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Loop API V2: /people/buyers/{id} ──────────────────────────
|
|
62
|
+
// Fields: id, buyerGroupName, buyerIsGroup, maxPrice, minBeds,
|
|
63
|
+
// position, requirementNotes, sellingPosition, purchaseReason,
|
|
64
|
+
// financiallyVerified, mortgageOffered, insuranceOffered,
|
|
65
|
+
// people[] (nested PersonSummary), propertyTypes[], searchAreas[],
|
|
66
|
+
// viewings[], offers[]
|
|
67
|
+
// ────────────────────────────────────────────────────────────────
|
|
68
|
+
interface LoopBuyerDetail {
|
|
69
|
+
id: number;
|
|
70
|
+
buyerGroupName?: string;
|
|
71
|
+
buyerIsGroup?: boolean;
|
|
72
|
+
maxPrice?: number | null;
|
|
73
|
+
minBeds?: number;
|
|
74
|
+
position?: string;
|
|
75
|
+
requirementNotes?: string;
|
|
76
|
+
sellingPosition?: string | null;
|
|
77
|
+
purchaseReason?: string | null;
|
|
78
|
+
financiallyVerified?: string | null;
|
|
79
|
+
mortgageOffered?: string | null;
|
|
80
|
+
insuranceOffered?: string | null;
|
|
81
|
+
people?: LoopNestedPerson[];
|
|
82
|
+
propertyTypes?: string[];
|
|
83
|
+
searchAreas?: string[];
|
|
84
|
+
viewings?: Record<string, unknown>[];
|
|
85
|
+
offers?: Record<string, unknown>[];
|
|
86
|
+
[key: string]: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Loop API V2: /people/sellers/{id} ─────────────────────────
|
|
90
|
+
// Fields: id, sellerGroupName, sellerIsGroup,
|
|
91
|
+
// people[] (nested PersonSummary), property (nested PropertySummary)
|
|
92
|
+
// ────────────────────────────────────────────────────────────────
|
|
93
|
+
interface LoopSellerDetail {
|
|
94
|
+
id: number;
|
|
95
|
+
sellerGroupName?: string;
|
|
96
|
+
sellerIsGroup?: boolean;
|
|
97
|
+
people?: LoopNestedPerson[];
|
|
98
|
+
property?: Record<string, unknown>;
|
|
99
|
+
[key: string]: unknown;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface LoopNestedPerson {
|
|
103
|
+
id: number;
|
|
104
|
+
title?: string;
|
|
105
|
+
firstName?: string;
|
|
106
|
+
lastName?: string;
|
|
107
|
+
contactNotes?: string;
|
|
108
|
+
primaryPhone?: string;
|
|
109
|
+
primaryPhoneType?: string;
|
|
110
|
+
primaryEmail?: string;
|
|
111
|
+
primaryEmailType?: string;
|
|
18
112
|
[key: string]: unknown;
|
|
19
113
|
}
|
|
20
114
|
|
|
@@ -28,14 +122,14 @@ export async function peopleDetail(params: {
|
|
|
28
122
|
}): Promise<string> {
|
|
29
123
|
const { accountId, personId, role, teamName } = params;
|
|
30
124
|
|
|
31
|
-
const result = await aggregateAcrossTeams<
|
|
125
|
+
const result = await aggregateAcrossTeams<Record<string, unknown>>(
|
|
32
126
|
accountId,
|
|
33
127
|
"people",
|
|
34
128
|
"loop-people-detail",
|
|
35
129
|
async (apiKey, team) => {
|
|
36
130
|
const basePath = role ? `/people/${role}` : "/people";
|
|
37
131
|
const path = `${basePath}/${personId}`;
|
|
38
|
-
const data = await loopGet<
|
|
132
|
+
const data = await loopGet<Record<string, unknown>>(apiKey, path, "loop-people-detail", team);
|
|
39
133
|
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
40
134
|
return [data];
|
|
41
135
|
}
|
|
@@ -46,18 +140,106 @@ export async function peopleDetail(params: {
|
|
|
46
140
|
|
|
47
141
|
return formatAggregationResult(
|
|
48
142
|
result,
|
|
49
|
-
(
|
|
50
|
-
const name = [p.firstName, p.lastName].filter(Boolean).join(" ") || "Unknown";
|
|
51
|
-
const lines = [`**${name}**`];
|
|
52
|
-
if (p.type) lines.push(`Type: ${p.type}`);
|
|
53
|
-
if (p.email) lines.push(`Email: ${p.email}`);
|
|
54
|
-
if (p.phone) lines.push(`Phone: ${p.phone}`);
|
|
55
|
-
if (p.mobile) lines.push(`Mobile: ${p.mobile}`);
|
|
56
|
-
if (p.address) lines.push(`Address: ${p.address}`);
|
|
57
|
-
if (p.postcode) lines.push(`Postcode: ${p.postcode}`);
|
|
58
|
-
if (p.notes) lines.push(`Notes: ${p.notes}`);
|
|
59
|
-
return lines.join("\n");
|
|
60
|
-
},
|
|
143
|
+
(record) => formatDetailRecord(record, role),
|
|
61
144
|
"person details"
|
|
62
145
|
);
|
|
63
146
|
}
|
|
147
|
+
|
|
148
|
+
function formatDetailRecord(record: Record<string, unknown>, role?: PeopleRole): string {
|
|
149
|
+
if (role === "buyers") {
|
|
150
|
+
const p = record as unknown as LoopBuyerDetail;
|
|
151
|
+
const lines = [`**${p.buyerGroupName ?? "Unknown"}** [ID: ${p.id}]`];
|
|
152
|
+
if (p.maxPrice != null) lines.push(`Max price: £${p.maxPrice.toLocaleString("en-GB")}`);
|
|
153
|
+
if (p.minBeds) lines.push(`Min beds: ${p.minBeds}`);
|
|
154
|
+
if (p.position && p.position !== "none") lines.push(`Position: ${p.position}`);
|
|
155
|
+
if (p.sellingPosition) lines.push(`Selling position: ${p.sellingPosition}`);
|
|
156
|
+
if (p.purchaseReason) lines.push(`Purchase reason: ${p.purchaseReason}`);
|
|
157
|
+
if (p.financiallyVerified) lines.push(`Financially verified: ${p.financiallyVerified}`);
|
|
158
|
+
if (p.mortgageOffered) lines.push(`Mortgage: ${p.mortgageOffered}`);
|
|
159
|
+
if (p.requirementNotes) lines.push(`Requirements: ${p.requirementNotes}`);
|
|
160
|
+
if (p.propertyTypes?.length) lines.push(`Property types: ${p.propertyTypes.join(", ")}`);
|
|
161
|
+
if (p.searchAreas?.length) lines.push(`Search areas: ${p.searchAreas.join(", ")}`);
|
|
162
|
+
if (p.viewings?.length) lines.push(`Viewings: ${p.viewings.length}`);
|
|
163
|
+
if (p.offers?.length) lines.push(`Offers: ${p.offers.length}`);
|
|
164
|
+
|
|
165
|
+
if (p.people?.length) {
|
|
166
|
+
lines.push(`\n**Contacts (${p.people.length}):**`);
|
|
167
|
+
for (const person of p.people) {
|
|
168
|
+
const name = [person.firstName, person.lastName].filter(Boolean).join(" ");
|
|
169
|
+
const phone = person.primaryPhone ?? "";
|
|
170
|
+
const email = person.primaryEmail ?? "";
|
|
171
|
+
const contact = [phone, email].filter(Boolean).join(" | ");
|
|
172
|
+
lines.push(` - ${name} [ID: ${person.id}]${contact ? ` — ${contact}` : ""}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (role === "sellers" || role === "landlords") {
|
|
179
|
+
const p = record as unknown as LoopSellerDetail;
|
|
180
|
+
const lines = [`**${p.sellerGroupName ?? "Unknown"}** [ID: ${p.id}]`];
|
|
181
|
+
|
|
182
|
+
if (p.property) {
|
|
183
|
+
const prop = p.property;
|
|
184
|
+
if (prop.propertyAddress) lines.push(`Property: ${prop.propertyAddress}`);
|
|
185
|
+
if (prop.price != null) lines.push(`Price: £${Number(prop.price).toLocaleString("en-GB")}`);
|
|
186
|
+
if (prop.status) lines.push(`Status: ${prop.status}`);
|
|
187
|
+
if (prop.propertyType) lines.push(`Type: ${prop.propertyType}`);
|
|
188
|
+
if (prop.propertyId || prop.id) lines.push(`Property ID: ${prop.propertyId ?? prop.id}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (p.people?.length) {
|
|
192
|
+
lines.push(`\n**Contacts (${p.people.length}):**`);
|
|
193
|
+
for (const person of p.people) {
|
|
194
|
+
const name = [person.firstName, person.lastName].filter(Boolean).join(" ");
|
|
195
|
+
const phone = person.primaryPhone ?? "";
|
|
196
|
+
const email = person.primaryEmail ?? "";
|
|
197
|
+
const contact = [phone, email].filter(Boolean).join(" | ");
|
|
198
|
+
lines.push(` - ${name} [ID: ${person.id}]${contact ? ` — ${contact}` : ""}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Generic /people/{id} — PersonDetail schema
|
|
205
|
+
const p = record as unknown as LoopPersonDetail;
|
|
206
|
+
const title = p.title?.trim() ? `${p.title.trim()} ` : "";
|
|
207
|
+
const name = [p.firstName, p.lastName].filter(Boolean).join(" ") || "Unknown";
|
|
208
|
+
const lines = [`**${title}${name}** [ID: ${p.id}]`];
|
|
209
|
+
|
|
210
|
+
if (p.primaryEmail) lines.push(`Email: ${p.primaryEmail}`);
|
|
211
|
+
if (p.primaryPhone) lines.push(`Phone: ${p.primaryPhone}`);
|
|
212
|
+
if (p.dateCreated) lines.push(`Created: ${p.dateCreated.split("T")[0]}`);
|
|
213
|
+
|
|
214
|
+
// Build address from components
|
|
215
|
+
const addrParts = [
|
|
216
|
+
p.houseSecondaryNameOrNumber,
|
|
217
|
+
p.houseNameOrNumber,
|
|
218
|
+
p.street,
|
|
219
|
+
p.locality,
|
|
220
|
+
p.town,
|
|
221
|
+
p.postcode,
|
|
222
|
+
].filter(Boolean);
|
|
223
|
+
if (addrParts.length) lines.push(`Address: ${addrParts.join(", ")}`);
|
|
224
|
+
|
|
225
|
+
if (p.contactNotes) lines.push(`Notes: ${p.contactNotes}`);
|
|
226
|
+
|
|
227
|
+
if (p.buyerProfiles?.length) {
|
|
228
|
+
lines.push(`\n**Buyer profiles (${p.buyerProfiles.length}):**`);
|
|
229
|
+
for (const bp of p.buyerProfiles) {
|
|
230
|
+
const bpName = bp.buyerGroupName ?? `Buyer ${bp.id}`;
|
|
231
|
+
const price = bp.maxPrice != null ? ` max £${bp.maxPrice.toLocaleString("en-GB")}` : "";
|
|
232
|
+
lines.push(` - ${bpName} [ID: ${bp.id}]${price}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (p.sellerProfiles?.length) {
|
|
236
|
+
lines.push(`\n**Seller profiles (${p.sellerProfiles.length}):**`);
|
|
237
|
+
for (const sp of p.sellerProfiles) {
|
|
238
|
+
const spName = sp.sellerGroupName ?? `Seller ${sp.id}`;
|
|
239
|
+
const addr = sp.propertyAddress ? ` — ${sp.propertyAddress}` : "";
|
|
240
|
+
lines.push(` - ${spName} [ID: ${sp.id}]${addr}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return lines.join("\n");
|
|
245
|
+
}
|
package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-search.ts
CHANGED
|
@@ -4,13 +4,69 @@ import {
|
|
|
4
4
|
loopGet,
|
|
5
5
|
} from "../lib/loop-api.js";
|
|
6
6
|
|
|
7
|
+
// ─── Loop API V2: /people ──────────────────────────────────────
|
|
8
|
+
// Fields: id, title, firstName, lastName, contactNotes,
|
|
9
|
+
// primaryPhone, primaryPhoneType, primaryPhoneNotes,
|
|
10
|
+
// primaryEmail, primaryEmailType, primaryEmailNotes,
|
|
11
|
+
// gdprGeneralMarketing, gdprPropertyMatching, gdprThirdParties,
|
|
12
|
+
// communicationCall, communicationEmail, communicationPost, communicationText
|
|
13
|
+
// ────────────────────────────────────────────────────────────────
|
|
7
14
|
interface LoopPersonSummary {
|
|
8
|
-
id
|
|
15
|
+
id: number;
|
|
16
|
+
title?: string;
|
|
9
17
|
firstName?: string;
|
|
10
18
|
lastName?: string;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
contactNotes?: string;
|
|
20
|
+
primaryPhone?: string;
|
|
21
|
+
primaryPhoneType?: string;
|
|
22
|
+
primaryEmail?: string;
|
|
23
|
+
primaryEmailType?: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── Loop API V2: /people/buyers ───────────────────────────────
|
|
28
|
+
// Fields: id, buyerGroupName, buyerIsGroup, maxPrice, minBeds,
|
|
29
|
+
// position, sellingPosition, purchaseReason, financiallyVerified,
|
|
30
|
+
// mortgageOffered, insuranceOffered, offerCount, acceptedOfferCount,
|
|
31
|
+
// viewingCount, upcomingViewingCount
|
|
32
|
+
// ────────────────────────────────────────────────────────────────
|
|
33
|
+
interface LoopBuyerSummary {
|
|
34
|
+
id: number;
|
|
35
|
+
buyerGroupName?: string;
|
|
36
|
+
buyerIsGroup?: boolean;
|
|
37
|
+
maxPrice?: number | null;
|
|
38
|
+
minBeds?: number;
|
|
39
|
+
position?: string;
|
|
40
|
+
sellingPosition?: string | null;
|
|
41
|
+
purchaseReason?: string | null;
|
|
42
|
+
financiallyVerified?: string | null;
|
|
43
|
+
mortgageOffered?: string | null;
|
|
44
|
+
insuranceOffered?: string | null;
|
|
45
|
+
offerCount?: number;
|
|
46
|
+
acceptedOfferCount?: number;
|
|
47
|
+
viewingCount?: number;
|
|
48
|
+
upcomingViewingCount?: number;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Loop API V2: /people/sellers ──────────────────────────────
|
|
53
|
+
// Fields: id, sellerGroupName, sellerIsGroup, propertyId,
|
|
54
|
+
// propertyStatus, propertyType, propertyAddress, price,
|
|
55
|
+
// offerCount, acceptedOfferCount, viewingCount, upcomingViewingCount
|
|
56
|
+
// ────────────────────────────────────────────────────────────────
|
|
57
|
+
interface LoopSellerSummary {
|
|
58
|
+
id: number;
|
|
59
|
+
sellerGroupName?: string;
|
|
60
|
+
sellerIsGroup?: boolean;
|
|
61
|
+
propertyId?: number;
|
|
62
|
+
propertyStatus?: string;
|
|
63
|
+
propertyType?: string;
|
|
64
|
+
propertyAddress?: string;
|
|
65
|
+
price?: number;
|
|
66
|
+
offerCount?: number;
|
|
67
|
+
acceptedOfferCount?: number;
|
|
68
|
+
viewingCount?: number;
|
|
69
|
+
upcomingViewingCount?: number;
|
|
14
70
|
[key: string]: unknown;
|
|
15
71
|
}
|
|
16
72
|
|
|
@@ -41,7 +97,9 @@ export async function peopleSearch(params: {
|
|
|
41
97
|
teamName, limit,
|
|
42
98
|
} = params;
|
|
43
99
|
|
|
44
|
-
|
|
100
|
+
// Role-specific endpoints return different schemas — use a generic record
|
|
101
|
+
// type for aggregation, then cast per-role in the formatter.
|
|
102
|
+
const result = await aggregateAcrossTeams<Record<string, unknown>>(
|
|
45
103
|
accountId,
|
|
46
104
|
"people",
|
|
47
105
|
"loop-people-search",
|
|
@@ -73,7 +131,7 @@ export async function peopleSearch(params: {
|
|
|
73
131
|
const query = qp.length > 0 ? `?${qp.join("&")}` : "";
|
|
74
132
|
const basePath = role ? `/people/${role}` : "/people";
|
|
75
133
|
const path = `${basePath}${query}`;
|
|
76
|
-
const data = await loopGet<
|
|
134
|
+
const data = await loopGet<Record<string, unknown>[]>(apiKey, path, "loop-people-search", team);
|
|
77
135
|
return Array.isArray(data) ? data : [];
|
|
78
136
|
},
|
|
79
137
|
{ teamName, limitPerTeam: limit ?? 50, limitTotal: limit ?? 200 }
|
|
@@ -82,12 +140,41 @@ export async function peopleSearch(params: {
|
|
|
82
140
|
const entityName = role ?? "people";
|
|
83
141
|
return formatAggregationResult(
|
|
84
142
|
result,
|
|
85
|
-
(
|
|
86
|
-
const name = [p.firstName, p.lastName].filter(Boolean).join(" ") || "Unknown";
|
|
87
|
-
const contact = p.email ?? p.phone ?? "";
|
|
88
|
-
const type = p.type ? ` (${p.type})` : "";
|
|
89
|
-
return `- ${name}${contact ? ` <${contact}>` : ""}${type}`;
|
|
90
|
-
},
|
|
143
|
+
(record) => formatPersonRecord(record, role),
|
|
91
144
|
entityName
|
|
92
145
|
);
|
|
93
146
|
}
|
|
147
|
+
|
|
148
|
+
function formatPersonRecord(record: Record<string, unknown>, role?: PeopleRole): string {
|
|
149
|
+
if (role === "buyers") {
|
|
150
|
+
const p = record as unknown as LoopBuyerSummary;
|
|
151
|
+
const name = p.buyerGroupName ?? "Unknown";
|
|
152
|
+
const id = p.id != null ? ` [ID: ${p.id}]` : "";
|
|
153
|
+
const price = p.maxPrice != null ? ` — max £${p.maxPrice.toLocaleString("en-GB")}` : "";
|
|
154
|
+
const beds = p.minBeds ? ` ${p.minBeds}+ beds` : "";
|
|
155
|
+
const position = p.position && p.position !== "none" ? ` (${p.position})` : "";
|
|
156
|
+
const viewings = p.viewingCount ? ` | ${p.viewingCount} viewing${p.viewingCount !== 1 ? "s" : ""}` : "";
|
|
157
|
+
const offers = p.offerCount ? ` | ${p.offerCount} offer${p.offerCount !== 1 ? "s" : ""}` : "";
|
|
158
|
+
return `- ${name}${id}${price}${beds}${position}${viewings}${offers}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (role === "sellers" || role === "landlords") {
|
|
162
|
+
const p = record as unknown as LoopSellerSummary;
|
|
163
|
+
const name = p.sellerGroupName ?? "Unknown";
|
|
164
|
+
const id = p.id != null ? ` [ID: ${p.id}]` : "";
|
|
165
|
+
const addr = p.propertyAddress ? ` — ${p.propertyAddress}` : "";
|
|
166
|
+
const price = p.price != null ? ` £${p.price.toLocaleString("en-GB")}` : "";
|
|
167
|
+
const status = p.propertyStatus ? ` [${p.propertyStatus}]` : "";
|
|
168
|
+
const viewings = p.viewingCount ? ` | ${p.viewingCount} viewing${p.viewingCount !== 1 ? "s" : ""}` : "";
|
|
169
|
+
const offers = p.offerCount ? ` | ${p.offerCount} offer${p.offerCount !== 1 ? "s" : ""}` : "";
|
|
170
|
+
return `- ${name}${id}${addr}${price}${status}${viewings}${offers}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Generic /people or /people/renters — uses PersonSummary schema
|
|
174
|
+
const p = record as unknown as LoopPersonSummary;
|
|
175
|
+
const name = [p.firstName, p.lastName].filter(Boolean).join(" ") || "Unknown";
|
|
176
|
+
const id = p.id != null ? ` [ID: ${p.id}]` : "";
|
|
177
|
+
const contact = p.primaryEmail ?? p.primaryPhone ?? "";
|
|
178
|
+
const title = p.title?.trim() ? `${p.title.trim()} ` : "";
|
|
179
|
+
return `- ${title}${name}${id}${contact ? ` <${contact}>` : ""}`;
|
|
180
|
+
}
|
|
@@ -4,20 +4,55 @@ import {
|
|
|
4
4
|
loopGet,
|
|
5
5
|
} from "../lib/loop-api.js";
|
|
6
6
|
|
|
7
|
+
// ─── Loop API V2: /property/residential/{dept}/{id} ────────────
|
|
8
|
+
// Fields: id, refId, address_DisplayAddress, address_HouseNameOrNumber,
|
|
9
|
+
// address_HouseSecondaryNameOrNumber, address_Street, address_Locality,
|
|
10
|
+
// address_Town, address_Postcode, status, propertyType, dateCreated,
|
|
11
|
+
// dateInstructed, dateLaunched, dateExchanged, dateCompleted,
|
|
12
|
+
// dateWithdrawn, shortDescription, fullDescription, bathrooms,
|
|
13
|
+
// bedrooms, receptionRooms, price, priceQualifier, latitude,
|
|
14
|
+
// longitude, parking, outsideSpace, councilTaxBand, floorArea,
|
|
15
|
+
// tenure, serviceCharge, groundRent, features[], images[],
|
|
16
|
+
// marketingFlag_*, creatingAgentId
|
|
17
|
+
// ────────────────────────────────────────────────────────────────
|
|
7
18
|
interface LoopPropertyDetail {
|
|
8
|
-
id
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
id: number;
|
|
20
|
+
refId?: string;
|
|
21
|
+
address_DisplayAddress?: string | null;
|
|
22
|
+
address_HouseNameOrNumber?: string;
|
|
23
|
+
address_HouseSecondaryNameOrNumber?: string;
|
|
24
|
+
address_Street?: string;
|
|
25
|
+
address_Locality?: string | null;
|
|
26
|
+
address_Town?: string;
|
|
27
|
+
address_Postcode?: string;
|
|
11
28
|
status?: string;
|
|
12
|
-
|
|
13
|
-
|
|
29
|
+
propertyType?: string;
|
|
30
|
+
dateCreated?: string;
|
|
31
|
+
dateInstructed?: string | null;
|
|
32
|
+
dateLaunched?: string | null;
|
|
33
|
+
dateExchanged?: string | null;
|
|
34
|
+
dateCompleted?: string | null;
|
|
35
|
+
dateWithdrawn?: string | null;
|
|
36
|
+
shortDescription?: string;
|
|
37
|
+
fullDescription?: string;
|
|
14
38
|
bathrooms?: number;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
39
|
+
bedrooms?: number;
|
|
40
|
+
receptionRooms?: number;
|
|
41
|
+
price?: number;
|
|
42
|
+
priceQualifier?: string;
|
|
43
|
+
latitude?: number;
|
|
44
|
+
longitude?: number;
|
|
45
|
+
parking?: string;
|
|
46
|
+
outsideSpace?: string;
|
|
47
|
+
councilTaxBand?: string;
|
|
20
48
|
floorArea?: number;
|
|
49
|
+
tenure?: string;
|
|
50
|
+
serviceCharge?: number;
|
|
51
|
+
groundRent?: number;
|
|
52
|
+
features?: string[];
|
|
53
|
+
images?: { url: string; mimeType?: string }[];
|
|
54
|
+
creatingAgentId?: string;
|
|
55
|
+
isDirectViewingsEnabled?: boolean;
|
|
21
56
|
[key: string]: unknown;
|
|
22
57
|
}
|
|
23
58
|
|
|
@@ -52,17 +87,57 @@ export async function propertyDetail(params: {
|
|
|
52
87
|
return formatAggregationResult(
|
|
53
88
|
result,
|
|
54
89
|
(p) => {
|
|
55
|
-
|
|
56
|
-
|
|
90
|
+
// Build display address from components
|
|
91
|
+
const addr = p.address_DisplayAddress
|
|
92
|
+
?? ([
|
|
93
|
+
p.address_HouseSecondaryNameOrNumber,
|
|
94
|
+
p.address_HouseNameOrNumber,
|
|
95
|
+
p.address_Street,
|
|
96
|
+
p.address_Town,
|
|
97
|
+
p.address_Postcode,
|
|
98
|
+
].filter(Boolean).join(", ") || "Unknown address");
|
|
99
|
+
|
|
100
|
+
const lines = [`**${addr}** [ID: ${p.id}]`];
|
|
101
|
+
if (p.price != null) {
|
|
102
|
+
const qualifier = p.priceQualifier && p.priceQualifier !== "none" ? ` (${p.priceQualifier})` : "";
|
|
103
|
+
lines.push(`Price: £${p.price.toLocaleString("en-GB")}${qualifier}`);
|
|
104
|
+
}
|
|
57
105
|
if (p.status) lines.push(`Status: ${p.status}`);
|
|
58
|
-
if (p.
|
|
59
|
-
if (p.bedrooms) lines.push(`Bedrooms: ${p.bedrooms}`);
|
|
60
|
-
if (p.bathrooms) lines.push(`Bathrooms: ${p.bathrooms}`);
|
|
61
|
-
if (p.
|
|
62
|
-
if (p.
|
|
63
|
-
if (p.
|
|
64
|
-
if (p.
|
|
65
|
-
if (p.
|
|
106
|
+
if (p.propertyType) lines.push(`Type: ${p.propertyType}`);
|
|
107
|
+
if (p.bedrooms != null) lines.push(`Bedrooms: ${p.bedrooms}`);
|
|
108
|
+
if (p.bathrooms != null) lines.push(`Bathrooms: ${p.bathrooms}`);
|
|
109
|
+
if (p.receptionRooms != null) lines.push(`Reception rooms: ${p.receptionRooms}`);
|
|
110
|
+
if (p.floorArea) lines.push(`Floor area: ${p.floorArea} sq ft`);
|
|
111
|
+
if (p.tenure && p.tenure !== "None") lines.push(`Tenure: ${p.tenure}`);
|
|
112
|
+
if (p.councilTaxBand) lines.push(`Council Tax: Band ${p.councilTaxBand}`);
|
|
113
|
+
if (p.parking) lines.push(`Parking: ${p.parking}`);
|
|
114
|
+
if (p.outsideSpace) lines.push(`Outside: ${p.outsideSpace}`);
|
|
115
|
+
if (p.serviceCharge) lines.push(`Service charge: £${p.serviceCharge}`);
|
|
116
|
+
if (p.groundRent) lines.push(`Ground rent: £${p.groundRent}`);
|
|
117
|
+
|
|
118
|
+
// Key dates
|
|
119
|
+
const dates: string[] = [];
|
|
120
|
+
if (p.dateInstructed) dates.push(`Instructed: ${p.dateInstructed.split("T")[0]}`);
|
|
121
|
+
if (p.dateLaunched) dates.push(`Launched: ${p.dateLaunched.split("T")[0]}`);
|
|
122
|
+
if (p.dateExchanged) dates.push(`Exchanged: ${p.dateExchanged.split("T")[0]}`);
|
|
123
|
+
if (p.dateCompleted) dates.push(`Completed: ${p.dateCompleted.split("T")[0]}`);
|
|
124
|
+
if (p.dateWithdrawn) dates.push(`Withdrawn: ${p.dateWithdrawn.split("T")[0]}`);
|
|
125
|
+
if (dates.length) lines.push(dates.join(" | "));
|
|
126
|
+
|
|
127
|
+
if (p.features?.length) {
|
|
128
|
+
lines.push(`Features: ${p.features.join(", ")}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (p.images?.length) {
|
|
132
|
+
lines.push(`Images: ${p.images.length} photo${p.images.length !== 1 ? "s" : ""}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Short description — strip HTML tags for readability
|
|
136
|
+
if (p.shortDescription) {
|
|
137
|
+
const clean = p.shortDescription.replace(/<[^>]+>/g, "").trim();
|
|
138
|
+
if (clean) lines.push(`\n${clean}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
66
141
|
return lines.join("\n");
|
|
67
142
|
},
|
|
68
143
|
"property details"
|
|
@@ -4,14 +4,31 @@ import {
|
|
|
4
4
|
loopGet,
|
|
5
5
|
} from "../lib/loop-api.js";
|
|
6
6
|
|
|
7
|
+
// ─── Loop API V2: /property/residential/{dept}/listed/{channel} ─
|
|
8
|
+
// Returns full property detail enriched with listing metadata:
|
|
9
|
+
// listingId, propertyId, propertyRefId, channel, displayAddress,
|
|
10
|
+
// propertyAddress, address_* fields, status, propertyType,
|
|
11
|
+
// dateLaunched, price, bedrooms, bathrooms, receptionRooms,
|
|
12
|
+
// shortDescription, features[], images[]
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────
|
|
7
14
|
interface LoopPropertyListing {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
listingId?: number;
|
|
16
|
+
propertyId?: number;
|
|
17
|
+
propertyRefId?: string;
|
|
18
|
+
channel?: string;
|
|
19
|
+
propertyAddress?: string;
|
|
20
|
+
displayAddress?: string;
|
|
11
21
|
status?: string;
|
|
12
|
-
|
|
22
|
+
propertyType?: string;
|
|
23
|
+
dateLaunched?: string;
|
|
24
|
+
price?: number;
|
|
25
|
+
priceQualifier?: string;
|
|
13
26
|
bedrooms?: number;
|
|
14
|
-
|
|
27
|
+
bathrooms?: number;
|
|
28
|
+
receptionRooms?: number;
|
|
29
|
+
shortDescription?: string;
|
|
30
|
+
features?: string[];
|
|
31
|
+
images?: { url: string }[];
|
|
15
32
|
[key: string]: unknown;
|
|
16
33
|
}
|
|
17
34
|
|
|
@@ -57,10 +74,14 @@ export async function propertyListed(params: {
|
|
|
57
74
|
return formatAggregationResult(
|
|
58
75
|
result,
|
|
59
76
|
(p) => {
|
|
77
|
+
const addr = p.displayAddress ?? p.propertyAddress ?? "Unknown address";
|
|
78
|
+
const id = p.propertyId != null ? ` [ID: ${p.propertyId}]` : "";
|
|
60
79
|
const price = p.price ? ` — £${p.price.toLocaleString("en-GB")}` : "";
|
|
61
80
|
const beds = p.bedrooms ? ` ${p.bedrooms}bed` : "";
|
|
81
|
+
const propType = p.propertyType ? ` (${p.propertyType})` : "";
|
|
62
82
|
const status = p.status ? ` [${p.status}]` : "";
|
|
63
|
-
|
|
83
|
+
const photos = p.images?.length ? ` ${p.images.length} photo${p.images.length !== 1 ? "s" : ""}` : "";
|
|
84
|
+
return `- ${addr}${id}${price}${beds}${propType}${status}${photos}`;
|
|
64
85
|
},
|
|
65
86
|
`${channel} listings`
|
|
66
87
|
);
|
|
@@ -3,6 +3,11 @@ import { loopPost, withTeamKey } from "../lib/loop-api.js";
|
|
|
3
3
|
type Department = "sales" | "lettings";
|
|
4
4
|
type RequestAction = "viewing" | "call-back" | "information";
|
|
5
5
|
|
|
6
|
+
// ─── Loop API V2: POST /property/residential/{dept}/{id}/{action}
|
|
7
|
+
// Actions: viewing, call-back, information
|
|
8
|
+
// Request body: { name?, email?, phone?, message? }
|
|
9
|
+
// Response: boolean success
|
|
10
|
+
// ────────────────────────────────────────────────────────────────
|
|
6
11
|
interface LoopBooleanResponse {
|
|
7
12
|
success?: boolean;
|
|
8
13
|
[key: string]: unknown;
|