@foundrynorth/compass-schema 1.0.10 → 1.0.11
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 +3 -2
- package/src/analyzeTypes.ts +1072 -0
- package/src/index.ts +12 -0
- package/src/relations.ts +111 -0
- package/src/schema.ts +7958 -0
|
@@ -0,0 +1,1072 @@
|
|
|
1
|
+
// shared/analyzeTypes.ts
|
|
2
|
+
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SERVICE AREA TYPES
|
|
7
|
+
* For home service businesses (HVAC, plumbing, electrical, etc.),
|
|
8
|
+
* competition is based on service area overlap rather than physical distance.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type BusinessType = 'retail' | 'restaurant' | 'service_business' | 'other';
|
|
12
|
+
|
|
13
|
+
export interface ServiceArea {
|
|
14
|
+
zips?: string[];
|
|
15
|
+
cities?: string[];
|
|
16
|
+
namedRegions?: string[];
|
|
17
|
+
radiusMiles?: number;
|
|
18
|
+
coverageScore?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ServiceAreaOverlap {
|
|
22
|
+
overlapZips: string[];
|
|
23
|
+
overlapCities: string[];
|
|
24
|
+
overlapRatio: number;
|
|
25
|
+
competitorTier: 'primary' | 'secondary' | 'peripheral';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* CANONICAL COMPETITOR TYPE
|
|
30
|
+
* Single source of truth for competitor representation across the platform.
|
|
31
|
+
* Use this type for:
|
|
32
|
+
* - Enrichment persistence
|
|
33
|
+
* - Prompt construction
|
|
34
|
+
* - UI display on competitor cards
|
|
35
|
+
*
|
|
36
|
+
* This type normalizes competitors from all sources (Google Places, DataForSEO, Perplexity)
|
|
37
|
+
* into a consistent shape for the entire application.
|
|
38
|
+
*/
|
|
39
|
+
export interface CanonicalCompetitor {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
domain?: string;
|
|
43
|
+
placeId?: string;
|
|
44
|
+
location?: {
|
|
45
|
+
lat: number;
|
|
46
|
+
lng: number;
|
|
47
|
+
city?: string;
|
|
48
|
+
state?: string;
|
|
49
|
+
address?: string;
|
|
50
|
+
};
|
|
51
|
+
category?: string;
|
|
52
|
+
sources: string[];
|
|
53
|
+
metrics?: {
|
|
54
|
+
searchShare?: number;
|
|
55
|
+
estClicks?: number;
|
|
56
|
+
estSpend?: number;
|
|
57
|
+
rating?: number;
|
|
58
|
+
reviewCount?: number;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Chain/multi-location support
|
|
62
|
+
isChain?: boolean;
|
|
63
|
+
chainId?: string;
|
|
64
|
+
chainName?: string;
|
|
65
|
+
chainType?: 'national' | 'regional' | 'local';
|
|
66
|
+
locations?: ChainLocation[];
|
|
67
|
+
locationCount?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Selection mode for multi-location chains:
|
|
70
|
+
* - 'single': Only this specific location
|
|
71
|
+
* - 'dma': All locations in the same DMA as prospect
|
|
72
|
+
* - 'overlap': All locations overlapping with prospect's service area
|
|
73
|
+
* - 'all': All locations nationwide (legacy 'chain' mode)
|
|
74
|
+
*/
|
|
75
|
+
selectionMode?: ChainSelectionMode;
|
|
76
|
+
// Which locations were selected (place_ids)
|
|
77
|
+
selectedLocationIds?: string[];
|
|
78
|
+
// Counts by selection mode (populated by worker when DMA data available)
|
|
79
|
+
locationsByMode?: {
|
|
80
|
+
dma?: number; // Count of locations in prospect's DMA
|
|
81
|
+
overlap?: number; // Count of locations overlapping prospect service area
|
|
82
|
+
all: number; // Total location count
|
|
83
|
+
};
|
|
84
|
+
aggregatedMetrics?: {
|
|
85
|
+
totalReviews?: number;
|
|
86
|
+
avgRating?: number;
|
|
87
|
+
serviceAreaUnion?: ServiceArea;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Chain selection mode for multi-location businesses
|
|
93
|
+
* Determines how many locations to include in analysis
|
|
94
|
+
*/
|
|
95
|
+
export type ChainSelectionMode = 'single' | 'dma' | 'overlap' | 'all';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Individual location within a chain
|
|
99
|
+
* Used when a competitor or prospect has multiple locations in the market
|
|
100
|
+
*/
|
|
101
|
+
export interface ChainLocation {
|
|
102
|
+
placeId: string;
|
|
103
|
+
name: string;
|
|
104
|
+
address?: string;
|
|
105
|
+
city?: string;
|
|
106
|
+
state?: string;
|
|
107
|
+
lat?: number;
|
|
108
|
+
lng?: number;
|
|
109
|
+
distance_km?: number;
|
|
110
|
+
rating?: number;
|
|
111
|
+
reviewCount?: number;
|
|
112
|
+
isPrimary?: boolean;
|
|
113
|
+
// DMA/geographic filtering (populated by worker when available)
|
|
114
|
+
dmaCode?: string; // DMA code (e.g., "613" for Minneapolis)
|
|
115
|
+
dmaName?: string; // DMA name (e.g., "Minneapolis-St. Paul")
|
|
116
|
+
inProspectDma?: boolean; // Is this location in the same DMA as prospect?
|
|
117
|
+
overlapsProspect?: boolean; // Does this location's service area overlap prospect?
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* COMPETITOR CREATIVE TYPES
|
|
122
|
+
* Unified interface for ad creatives from Google Ads and Facebook Ads
|
|
123
|
+
* Used for UI display and media planning prompts
|
|
124
|
+
*/
|
|
125
|
+
export interface CompetitorCreative {
|
|
126
|
+
source: 'google_ads' | 'facebook_ads';
|
|
127
|
+
platform: 'google' | 'facebook' | 'instagram' | 'audience_network';
|
|
128
|
+
accountName?: string;
|
|
129
|
+
accountId?: string;
|
|
130
|
+
headline?: string;
|
|
131
|
+
primaryText?: string;
|
|
132
|
+
description?: string;
|
|
133
|
+
cta?: string;
|
|
134
|
+
ctaType?: string;
|
|
135
|
+
landingUrl?: string;
|
|
136
|
+
imageUrl?: string;
|
|
137
|
+
imageUrls?: string[] | Array<{ original: string; resized: string }>;
|
|
138
|
+
videoUrl?: string;
|
|
139
|
+
format?: string; // responsive search, display, video, carousel, etc
|
|
140
|
+
impressions?: number | null;
|
|
141
|
+
region?: string | null;
|
|
142
|
+
startDate?: string | null;
|
|
143
|
+
endDate?: string | null;
|
|
144
|
+
pageProfileUrl?: string;
|
|
145
|
+
pageProfilePicture?: string;
|
|
146
|
+
pageLikeCount?: number;
|
|
147
|
+
pageCategories?: string[];
|
|
148
|
+
adLibraryUrl?: string;
|
|
149
|
+
sourceUrl?: string;
|
|
150
|
+
raw?: unknown; // keep raw payload for debugging
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Facebook Ad structure (from curious_coder/facebook-ads-library-scraper)
|
|
155
|
+
* Extended to include all fields from the actual API response
|
|
156
|
+
*/
|
|
157
|
+
export interface FacebookAdCreative {
|
|
158
|
+
adId?: string;
|
|
159
|
+
adArchiveId?: string;
|
|
160
|
+
adImage?: string;
|
|
161
|
+
adImages?: Array<{ original: string; resized: string }>;
|
|
162
|
+
adCopy?: string;
|
|
163
|
+
headline?: string;
|
|
164
|
+
callToAction?: string;
|
|
165
|
+
ctaType?: string;
|
|
166
|
+
ctaLink?: string;
|
|
167
|
+
publisherPlatforms?: string[];
|
|
168
|
+
startDate?: string;
|
|
169
|
+
endDate?: string;
|
|
170
|
+
status?: string;
|
|
171
|
+
pageName?: string;
|
|
172
|
+
pageProfileUrl?: string;
|
|
173
|
+
pageProfilePicture?: string;
|
|
174
|
+
pageLikeCount?: number;
|
|
175
|
+
pageCategories?: string[];
|
|
176
|
+
displayFormat?: string;
|
|
177
|
+
adLibraryUrl?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Summary of creatives for a competitor
|
|
182
|
+
*/
|
|
183
|
+
export interface CompetitorCreativeSummary {
|
|
184
|
+
googleAdsCount: number;
|
|
185
|
+
facebookAdsCount: number;
|
|
186
|
+
totalCount: number;
|
|
187
|
+
creatives: CompetitorCreative[];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* CRITICAL CHANGE: Known competitors are now REQUIRED (1-5)
|
|
192
|
+
* Discovery is OPTIONAL and defaults to OFF
|
|
193
|
+
*/
|
|
194
|
+
export const KnownCompetitorSchema = z.object({
|
|
195
|
+
name: z.string()
|
|
196
|
+
.min(2, 'Competitor name must be at least 2 characters')
|
|
197
|
+
.max(100, 'Competitor name too long'),
|
|
198
|
+
|
|
199
|
+
placeId: z.string().optional(),
|
|
200
|
+
// If user already has the Google Place ID (e.g., from previous search)
|
|
201
|
+
|
|
202
|
+
notes: z.string().max(200).optional(),
|
|
203
|
+
// Optional context: "Main competitor", "They dominate paid search", etc.
|
|
204
|
+
|
|
205
|
+
address: z.string().optional(),
|
|
206
|
+
// User can provide address to help disambiguation
|
|
207
|
+
|
|
208
|
+
website: z.string()
|
|
209
|
+
.optional()
|
|
210
|
+
.transform((val) => {
|
|
211
|
+
// Allow flexible URL input - normalize to full URL
|
|
212
|
+
if (!val || val.trim() === '') return undefined;
|
|
213
|
+
const trimmed = val.trim();
|
|
214
|
+
|
|
215
|
+
// Already has protocol
|
|
216
|
+
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
217
|
+
return trimmed;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Add https:// if missing
|
|
221
|
+
return `https://${trimmed}`;
|
|
222
|
+
})
|
|
223
|
+
// If user knows the website, helps with validation
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
export const AnalyzePlanInputSchema = z.object({
|
|
227
|
+
// =================================================
|
|
228
|
+
// PROSPECT INFORMATION (mostly unchanged)
|
|
229
|
+
// =================================================
|
|
230
|
+
name: z.string().optional(),
|
|
231
|
+
placeId: z.string().optional(),
|
|
232
|
+
|
|
233
|
+
// Legacy/alternative field names for compatibility
|
|
234
|
+
brand: z.string().optional(), // Alternative to 'name'
|
|
235
|
+
businessName: z.string().optional(), // Alternative to 'name'
|
|
236
|
+
|
|
237
|
+
city: z.string().optional(),
|
|
238
|
+
state: z.string().optional(),
|
|
239
|
+
geo: z.string().optional(),
|
|
240
|
+
targetGeo: z.string().optional(), // Alternative to 'geo'
|
|
241
|
+
lat: z.number().optional(),
|
|
242
|
+
lng: z.number().optional(),
|
|
243
|
+
|
|
244
|
+
// Chain detection and area selection
|
|
245
|
+
chainDetected: z.boolean().optional(),
|
|
246
|
+
selectedAreas: z.array(z.string()).optional(), // Selected geographic areas for chain analysis
|
|
247
|
+
|
|
248
|
+
businessDescription: z.string()
|
|
249
|
+
.optional()
|
|
250
|
+
.default('')
|
|
251
|
+
.transform((val) => val?.trim() || ''),
|
|
252
|
+
// businessDescription is OPTIONAL - category and placeId provide sufficient context for analysis
|
|
253
|
+
|
|
254
|
+
url: z.string()
|
|
255
|
+
.optional()
|
|
256
|
+
.transform((val) => {
|
|
257
|
+
// Allow flexible URL input - normalize to full URL
|
|
258
|
+
if (!val || val.trim() === '') return undefined;
|
|
259
|
+
const trimmed = val.trim();
|
|
260
|
+
|
|
261
|
+
// Already has protocol
|
|
262
|
+
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
263
|
+
return trimmed;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Add https:// if missing
|
|
267
|
+
return `https://${trimmed}`;
|
|
268
|
+
}),
|
|
269
|
+
|
|
270
|
+
// =================================================
|
|
271
|
+
// COMPETITOR INFORMATION (NEW PRIMARY INPUT)
|
|
272
|
+
// =================================================
|
|
273
|
+
knownCompetitors: z.array(KnownCompetitorSchema)
|
|
274
|
+
.min(1, 'Provide at least 1 known competitor')
|
|
275
|
+
.max(5, 'Maximum 5 competitors allowed')
|
|
276
|
+
.refine(
|
|
277
|
+
(competitors) => {
|
|
278
|
+
const names = competitors.map(c => c.name.toLowerCase().trim());
|
|
279
|
+
return new Set(names).size === names.length;
|
|
280
|
+
},
|
|
281
|
+
'Duplicate competitor names not allowed'
|
|
282
|
+
),
|
|
283
|
+
|
|
284
|
+
// =================================================
|
|
285
|
+
// OPTIONAL AI DISCOVERY
|
|
286
|
+
// =================================================
|
|
287
|
+
discoverAdditional: z.boolean().default(false),
|
|
288
|
+
// Should we find 1-2 more competitors?
|
|
289
|
+
|
|
290
|
+
maxAdditionalCompetitors: z.number()
|
|
291
|
+
.min(0)
|
|
292
|
+
.max(3)
|
|
293
|
+
.default(1),
|
|
294
|
+
// How many additional to find (if discovery enabled)
|
|
295
|
+
|
|
296
|
+
// =================================================
|
|
297
|
+
// ACTIVE MARKET SELECTION (NEW)
|
|
298
|
+
// =================================================
|
|
299
|
+
marketId: z.string().optional(),
|
|
300
|
+
// The active market ID for location bias (e.g., 'mkt_twin-cities')
|
|
301
|
+
|
|
302
|
+
marketCenter: z.object({
|
|
303
|
+
lat: z.number(),
|
|
304
|
+
lng: z.number()
|
|
305
|
+
}).optional(),
|
|
306
|
+
// Market center coordinates for location-biased API calls
|
|
307
|
+
|
|
308
|
+
// =================================================
|
|
309
|
+
// SERVICE AREA CONFIGURATION (NEW)
|
|
310
|
+
// =================================================
|
|
311
|
+
serviceAreaConfig: z.object({
|
|
312
|
+
type: z.enum(['physical', 'hq_radius', 'metro', 'custom_zips']).default('physical'),
|
|
313
|
+
radiusMiles: z.number().min(5).max(150).optional(),
|
|
314
|
+
zipCodes: z.array(z.string()).optional(),
|
|
315
|
+
}).optional(),
|
|
316
|
+
// Configuration details: physical (single location), hq_radius (X miles from HQ),
|
|
317
|
+
// metro (entire metro area), custom_zips (specific zip codes)
|
|
318
|
+
|
|
319
|
+
// =================================================
|
|
320
|
+
// PROSPECT CHAIN/MULTI-LOCATION CONFIGURATION
|
|
321
|
+
// =================================================
|
|
322
|
+
prospectPlaceId: z.string().optional(),
|
|
323
|
+
// Google Place ID for the prospect (if chain, this is the selected location)
|
|
324
|
+
|
|
325
|
+
prospectBrandName: z.string().optional(),
|
|
326
|
+
// Canonical brand name for chains (e.g., "Caribou Coffee" instead of "Caribou Coffee - Minneapolis")
|
|
327
|
+
// Used by worker for location enumeration. For non-chains, this may equal the display name.
|
|
328
|
+
|
|
329
|
+
prospectChainMode: z.enum(['single', 'dma', 'overlap', 'all']).optional(),
|
|
330
|
+
// How to handle prospect if they are a multi-location chain:
|
|
331
|
+
// - 'single': Analyze only the selected location
|
|
332
|
+
// - 'dma': Analyze all locations in the prospect's DMA
|
|
333
|
+
// - 'overlap': Analyze locations overlapping with active market
|
|
334
|
+
// - 'all': Analyze all locations (default legacy behavior)
|
|
335
|
+
|
|
336
|
+
prospectLocationCount: z.number().optional(),
|
|
337
|
+
// Total locations for the prospect chain (for UI display)
|
|
338
|
+
|
|
339
|
+
// Option D: user-selected locations from multi-result search (worker uses these instead of enumerating)
|
|
340
|
+
prospectLocations: z.array(z.object({
|
|
341
|
+
placeId: z.string(),
|
|
342
|
+
name: z.string().optional(),
|
|
343
|
+
address: z.string().optional(),
|
|
344
|
+
city: z.string().optional(),
|
|
345
|
+
state: z.string().optional(),
|
|
346
|
+
lat: z.number().optional(),
|
|
347
|
+
lng: z.number().optional(),
|
|
348
|
+
})).optional(),
|
|
349
|
+
|
|
350
|
+
// =================================================
|
|
351
|
+
// OPTIONAL METADATA
|
|
352
|
+
// =================================================
|
|
353
|
+
budget: z.object({
|
|
354
|
+
min: z.number().positive(),
|
|
355
|
+
max: z.number().positive()
|
|
356
|
+
}).optional(),
|
|
357
|
+
|
|
358
|
+
planTotalBudget: z.number().positive().optional(),
|
|
359
|
+
planDurationMonths: z.number().int().positive().optional(),
|
|
360
|
+
|
|
361
|
+
goals: z.array(z.string()).optional(),
|
|
362
|
+
|
|
363
|
+
// User override for business category (takes precedence over Google Places)
|
|
364
|
+
category: z.string().optional(),
|
|
365
|
+
|
|
366
|
+
// Analysis depth - controls how deep the enrichment and planning phases go
|
|
367
|
+
analysisDepth: z.enum(['standard', 'deep']).optional().default('deep'),
|
|
368
|
+
|
|
369
|
+
// =================================================
|
|
370
|
+
// HUBSPOT PRE-LINKED COMPANY (from frontend typeahead)
|
|
371
|
+
// =================================================
|
|
372
|
+
hubspotCompanyId: z.string().optional(),
|
|
373
|
+
hubspotUrl: z.string().optional(),
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
export type KnownCompetitor = z.infer<typeof KnownCompetitorSchema>;
|
|
377
|
+
export type AnalyzePlanInput = z.infer<typeof AnalyzePlanInputSchema>;
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Validation state after processing
|
|
381
|
+
*/
|
|
382
|
+
export interface ValidatedCompetitor extends KnownCompetitor {
|
|
383
|
+
// Google Places validation results
|
|
384
|
+
validated: boolean | 'needs_clarification';
|
|
385
|
+
status: 'confirmed' | 'not_found' | 'multiple_matches';
|
|
386
|
+
confidence: number; // 0-100
|
|
387
|
+
|
|
388
|
+
// If validated successfully
|
|
389
|
+
placeId?: string;
|
|
390
|
+
verifiedName?: string; // Official name from Google
|
|
391
|
+
verifiedAddress?: string;
|
|
392
|
+
geometry?: {
|
|
393
|
+
lat: number;
|
|
394
|
+
lng: number;
|
|
395
|
+
};
|
|
396
|
+
category?: string;
|
|
397
|
+
rating?: number;
|
|
398
|
+
reviewCount?: number;
|
|
399
|
+
|
|
400
|
+
// If multiple matches found
|
|
401
|
+
options?: Array<{
|
|
402
|
+
placeId: string;
|
|
403
|
+
name: string;
|
|
404
|
+
address: string;
|
|
405
|
+
distance?: number; // km from prospect
|
|
406
|
+
}>;
|
|
407
|
+
|
|
408
|
+
// If not found
|
|
409
|
+
suggestion?: string;
|
|
410
|
+
|
|
411
|
+
// Source tracking
|
|
412
|
+
source: 'user_provided' | 'ai_suggested';
|
|
413
|
+
originalName?: string; // What user typed vs. what Google says
|
|
414
|
+
|
|
415
|
+
// Service area support (for HVAC, plumbing, etc.)
|
|
416
|
+
businessType?: BusinessType;
|
|
417
|
+
serviceArea?: ServiceArea | null;
|
|
418
|
+
serviceAreaOverlap?: ServiceAreaOverlap | null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Base business profile (minimal required fields)
|
|
423
|
+
*/
|
|
424
|
+
export interface BaseBusinessProfile {
|
|
425
|
+
name: string;
|
|
426
|
+
placeId: string | null;
|
|
427
|
+
address: string;
|
|
428
|
+
category?: string;
|
|
429
|
+
rating?: number;
|
|
430
|
+
reviewCount?: number;
|
|
431
|
+
phone?: string;
|
|
432
|
+
website?: string;
|
|
433
|
+
seeded?: boolean;
|
|
434
|
+
source?: string;
|
|
435
|
+
validationBypassed?: boolean;
|
|
436
|
+
bypassedAI?: boolean;
|
|
437
|
+
lat?: number;
|
|
438
|
+
lng?: number;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Enrichment augmentation data (additional fields added during enrichment)
|
|
443
|
+
*/
|
|
444
|
+
export interface EnrichmentAugmentation {
|
|
445
|
+
digitalMaturity?: number;
|
|
446
|
+
maturityStage?: string;
|
|
447
|
+
strengths?: string[];
|
|
448
|
+
gaps?: string[];
|
|
449
|
+
competitiveContext?: any;
|
|
450
|
+
comparativeInsights?: any;
|
|
451
|
+
[key: string]: any; // Allow additional enrichment fields
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Validated business (base profile + enrichment data)
|
|
456
|
+
*/
|
|
457
|
+
export interface ValidatedBusiness extends BaseBusinessProfile, Partial<EnrichmentAugmentation> {
|
|
458
|
+
validated: boolean;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Fully enriched business (all enrichment fields guaranteed)
|
|
463
|
+
*/
|
|
464
|
+
export interface FullyEnrichedBusiness extends ValidatedBusiness {
|
|
465
|
+
digitalMaturity: number;
|
|
466
|
+
maturityStage: string;
|
|
467
|
+
strengths: string[];
|
|
468
|
+
gaps: string[];
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Enrichment result structure
|
|
473
|
+
*/
|
|
474
|
+
export interface EnrichedBusiness {
|
|
475
|
+
// Core identity
|
|
476
|
+
placeId: string;
|
|
477
|
+
name: string;
|
|
478
|
+
address: string;
|
|
479
|
+
city: string;
|
|
480
|
+
state: string;
|
|
481
|
+
lat: number;
|
|
482
|
+
lng: number;
|
|
483
|
+
category: string;
|
|
484
|
+
|
|
485
|
+
// Service area support (for HVAC, plumbing, etc.)
|
|
486
|
+
businessType?: BusinessType;
|
|
487
|
+
serviceArea?: ServiceArea | null;
|
|
488
|
+
serviceAreaOverlap?: ServiceAreaOverlap | null;
|
|
489
|
+
|
|
490
|
+
// Basic metrics
|
|
491
|
+
rating: number;
|
|
492
|
+
reviewCount: number;
|
|
493
|
+
|
|
494
|
+
// DEEP enrichment data
|
|
495
|
+
metaAds: {
|
|
496
|
+
present: boolean;
|
|
497
|
+
activeCount: number;
|
|
498
|
+
totalSeen: number;
|
|
499
|
+
oldestAdDate: string | null;
|
|
500
|
+
impressionsRange: string | null;
|
|
501
|
+
|
|
502
|
+
// NEW: Creative analysis
|
|
503
|
+
creative?: {
|
|
504
|
+
themes: string[]; // ["discount", "quality", "emergency service"]
|
|
505
|
+
messaging: string[]; // Key phrases from ad copy
|
|
506
|
+
visualStyle: string; // "professional" | "playful" | "urgent"
|
|
507
|
+
offers: string[]; // ["20% off", "free estimate", "same-day"]
|
|
508
|
+
ctaTypes: string[]; // ["Learn More", "Book Now", "Call Now"]
|
|
509
|
+
// Actual ad data from Facebook Ads Library (via Apify)
|
|
510
|
+
ads?: Array<{
|
|
511
|
+
adId?: string;
|
|
512
|
+
adImage?: string; // URL to ad creative image
|
|
513
|
+
adCopy?: string; // Full ad body text
|
|
514
|
+
headline?: string; // Ad headline
|
|
515
|
+
callToAction?: string; // CTA button text
|
|
516
|
+
publisherPlatforms?: string[]; // ["Facebook", "Instagram"]
|
|
517
|
+
startDate?: string; // When ad started running
|
|
518
|
+
status?: string; // "active" | "inactive"
|
|
519
|
+
}>;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// NEW: Audience insights
|
|
523
|
+
audience?: {
|
|
524
|
+
platforms: string[]; // ["Facebook", "Instagram"]
|
|
525
|
+
ageRanges: string[]; // ["25-34", "35-44"]
|
|
526
|
+
genders: string[]; // ["All", "Male", "Female"]
|
|
527
|
+
interests: string[]; // Inferred from creative themes
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// NEW: Spend estimation
|
|
531
|
+
estimatedSpend?: {
|
|
532
|
+
monthly: number;
|
|
533
|
+
confidence: 'low' | 'medium' | 'high';
|
|
534
|
+
basis: string; // How we calculated it
|
|
535
|
+
};
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// Website analysis
|
|
539
|
+
website: {
|
|
540
|
+
url: string | null;
|
|
541
|
+
accessible: boolean;
|
|
542
|
+
|
|
543
|
+
// NEW: SEO audit
|
|
544
|
+
seo?: {
|
|
545
|
+
titleTag: string;
|
|
546
|
+
metaDescription: string;
|
|
547
|
+
h1Tags: string[];
|
|
548
|
+
contentWordCount: number;
|
|
549
|
+
keywordDensity: { [keyword: string]: number };
|
|
550
|
+
|
|
551
|
+
// Technical SEO
|
|
552
|
+
mobileSpeed: number; // Google PageSpeed score 0-100
|
|
553
|
+
desktopSpeed: number;
|
|
554
|
+
coreWebVitals: boolean;
|
|
555
|
+
httpsEnabled: boolean;
|
|
556
|
+
structuredData: string[]; // ["LocalBusiness", "Organization"]
|
|
557
|
+
|
|
558
|
+
// Local SEO
|
|
559
|
+
nabConsistency: boolean; // Name/Address/Phone match
|
|
560
|
+
localKeywords: string[];
|
|
561
|
+
locationPages: number;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// NEW: Conversion optimization
|
|
565
|
+
conversion?: {
|
|
566
|
+
hasBooking: boolean;
|
|
567
|
+
hasContactForm: boolean;
|
|
568
|
+
hasPhoneNumber: boolean;
|
|
569
|
+
hasChatWidget: boolean;
|
|
570
|
+
ctaCount: number;
|
|
571
|
+
ctaTypes: string[];
|
|
572
|
+
};
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// Existing fields
|
|
576
|
+
technologies: string[];
|
|
577
|
+
sophistication: 'basic' | 'intermediate' | 'advanced';
|
|
578
|
+
hasEcommerce: boolean;
|
|
579
|
+
hasAnalytics: boolean;
|
|
580
|
+
hasBlog: boolean;
|
|
581
|
+
mobileOptimized: boolean;
|
|
582
|
+
loadTime: number;
|
|
583
|
+
|
|
584
|
+
// Social media
|
|
585
|
+
social: {
|
|
586
|
+
facebook?: {
|
|
587
|
+
url: string;
|
|
588
|
+
followers: number;
|
|
589
|
+
postsPerWeek: number;
|
|
590
|
+
engagementRate: number;
|
|
591
|
+
lastPostDate: string;
|
|
592
|
+
};
|
|
593
|
+
instagram?: {
|
|
594
|
+
url: string;
|
|
595
|
+
followers: number;
|
|
596
|
+
postsPerWeek: number;
|
|
597
|
+
engagementRate: number;
|
|
598
|
+
lastPostDate: string;
|
|
599
|
+
};
|
|
600
|
+
linkedin?: {
|
|
601
|
+
url: string;
|
|
602
|
+
followers: number;
|
|
603
|
+
};
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// Google Ads from Transparency Center (Creative Vault)
|
|
607
|
+
googleAds?: {
|
|
608
|
+
active: boolean;
|
|
609
|
+
adCount: number;
|
|
610
|
+
advertiserName?: string;
|
|
611
|
+
lastScrapedAt?: Date;
|
|
612
|
+
fromCache?: boolean;
|
|
613
|
+
ads?: Array<{
|
|
614
|
+
id: string;
|
|
615
|
+
adType: 'image' | 'video' | 'text';
|
|
616
|
+
imageUrl?: string;
|
|
617
|
+
videoUrl?: string;
|
|
618
|
+
headline?: string;
|
|
619
|
+
description?: string;
|
|
620
|
+
landingPage?: string;
|
|
621
|
+
advertiser?: string;
|
|
622
|
+
format?: string;
|
|
623
|
+
lastShownDate?: string;
|
|
624
|
+
regions?: string[];
|
|
625
|
+
}>;
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// Unified creatives array (normalized from Google Ads + Facebook Ads)
|
|
629
|
+
// This is the primary field for UI display and media planning prompts
|
|
630
|
+
creatives?: CompetitorCreative[];
|
|
631
|
+
|
|
632
|
+
// Strategic analysis (from GPT-4o)
|
|
633
|
+
strategy: {
|
|
634
|
+
channelPresence: {
|
|
635
|
+
[channel: string]: {
|
|
636
|
+
active: boolean;
|
|
637
|
+
evidence: string;
|
|
638
|
+
investmentLevel: 'low' | 'medium' | 'high';
|
|
639
|
+
};
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
messaging: {
|
|
643
|
+
primaryThemes: string[];
|
|
644
|
+
positioning: string;
|
|
645
|
+
tone: 'professional' | 'casual' | 'playful' | 'authoritative';
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
sophistication: {
|
|
649
|
+
level: 'beginner' | 'intermediate' | 'advanced';
|
|
650
|
+
signals: string[];
|
|
651
|
+
investmentLevel: 'low' | 'medium' | 'high';
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
strengths: string[];
|
|
655
|
+
gaps: string[];
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// Digital maturity score
|
|
659
|
+
digitalMaturity: number; // 0-100
|
|
660
|
+
maturityStage: 'Foundation' | 'Growth' | 'Advanced';
|
|
661
|
+
|
|
662
|
+
// Source tracking
|
|
663
|
+
source: 'prospect' | 'user_provided' | 'ai_suggested';
|
|
664
|
+
validated: boolean;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ============================================================================
|
|
668
|
+
// PLANNING CONTEXT DTO
|
|
669
|
+
// Single structured object for media plan LLM prompts
|
|
670
|
+
// Consolidates prospect intelligence, competitor data, and benchmarks
|
|
671
|
+
// ============================================================================
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Prospect context for media planning
|
|
675
|
+
* Contains essential business information extracted from enrichment
|
|
676
|
+
*/
|
|
677
|
+
export interface PlanningProspect {
|
|
678
|
+
name: string;
|
|
679
|
+
category: string;
|
|
680
|
+
serviceAreaDescription: string;
|
|
681
|
+
websiteSummary?: string;
|
|
682
|
+
keyValueProps?: string[];
|
|
683
|
+
currentChannels?: string[];
|
|
684
|
+
weaknesses?: string[];
|
|
685
|
+
gaps?: string[];
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Competitor context for media planning
|
|
690
|
+
* Summarized competitive intelligence including creative themes
|
|
691
|
+
*/
|
|
692
|
+
export interface PlanningCompetitor {
|
|
693
|
+
name: string;
|
|
694
|
+
category: string;
|
|
695
|
+
serviceAreaDescription?: string;
|
|
696
|
+
adSpendLevel?: 'low' | 'medium' | 'high';
|
|
697
|
+
googleAdsThemes?: string[];
|
|
698
|
+
metaAdsThemes?: string[];
|
|
699
|
+
landingPagePatterns?: string[];
|
|
700
|
+
activeChannels?: string[];
|
|
701
|
+
investmentLevel?: 'low' | 'medium' | 'high';
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Performance benchmarks for a product/channel
|
|
706
|
+
* Used to inform realistic KPI expectations
|
|
707
|
+
*/
|
|
708
|
+
export interface PlanningBenchmark {
|
|
709
|
+
productCode: string;
|
|
710
|
+
mappedIndustry: string;
|
|
711
|
+
cpm?: number;
|
|
712
|
+
ctr?: number;
|
|
713
|
+
cvr?: number;
|
|
714
|
+
viewability?: number;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* PlanningContext DTO
|
|
719
|
+
* Single structured object passed to media plan LLM
|
|
720
|
+
*
|
|
721
|
+
* This replaces scattered string concatenation with a clean JSON structure
|
|
722
|
+
* that the model can reference systematically when generating plans.
|
|
723
|
+
*/
|
|
724
|
+
export interface PlanningContext {
|
|
725
|
+
prospect: PlanningProspect;
|
|
726
|
+
competitors: PlanningCompetitor[];
|
|
727
|
+
performanceBenchmarks: PlanningBenchmark[];
|
|
728
|
+
|
|
729
|
+
// Market analysis
|
|
730
|
+
competitorCount: number;
|
|
731
|
+
advertisingIntensity: 'low' | 'medium' | 'high';
|
|
732
|
+
marketGaps: string[];
|
|
733
|
+
|
|
734
|
+
// Creative strategy summary (from Google/Meta ads analysis)
|
|
735
|
+
creativeStrategySummary?: string;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ============================================================================
|
|
739
|
+
// LOCAL MARKET SCORE TYPES
|
|
740
|
+
// Simplified 0-100 score for sales conversations
|
|
741
|
+
// ============================================================================
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Local Market Score - single 0-100 number for quick comparison
|
|
745
|
+
* Distills the 5-pillar Local Authority Score into a sales-friendly format
|
|
746
|
+
*/
|
|
747
|
+
export interface LocalMarketScore {
|
|
748
|
+
/** Single 0-100 score for quick comparison */
|
|
749
|
+
score: number;
|
|
750
|
+
/** Classification for sales conversations */
|
|
751
|
+
tier: 'Leader' | 'Competitive' | 'Emerging' | 'At Risk';
|
|
752
|
+
/** Color code for UI (red, orange, yellow, gray) */
|
|
753
|
+
tierColor: 'red' | 'orange' | 'yellow' | 'gray';
|
|
754
|
+
/** One-line summary for sales pitch */
|
|
755
|
+
summary: string;
|
|
756
|
+
/** Component breakdown for details */
|
|
757
|
+
breakdown: {
|
|
758
|
+
localPack: { score: number; maxScore: 40; label: string };
|
|
759
|
+
reviews: { score: number; maxScore: 30; label: string };
|
|
760
|
+
gbpProfile: { score: number; maxScore: 20; label: string };
|
|
761
|
+
digital: { score: number; maxScore: 10; label: string };
|
|
762
|
+
};
|
|
763
|
+
/** Calculated timestamp */
|
|
764
|
+
calculatedAt: string;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ============================================================================
|
|
768
|
+
// REVENUE IMPACT TYPES
|
|
769
|
+
// Connect local search performance to dollars
|
|
770
|
+
// ============================================================================
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Revenue Impact - quantifies the dollar value of local search position
|
|
774
|
+
*/
|
|
775
|
+
export interface RevenueImpact {
|
|
776
|
+
/** Monthly revenue prospect is currently capturing */
|
|
777
|
+
currentMonthlyRevenue: number;
|
|
778
|
+
/** Monthly revenue the market leader captures */
|
|
779
|
+
leaderMonthlyRevenue: number;
|
|
780
|
+
/** Monthly revenue gap (what prospect is missing) */
|
|
781
|
+
monthlyRevenueGap: number;
|
|
782
|
+
/** Annual revenue opportunity */
|
|
783
|
+
annualOpportunity: number;
|
|
784
|
+
/** Benchmarks used for calculation */
|
|
785
|
+
benchmarks: {
|
|
786
|
+
avgTicket: number;
|
|
787
|
+
conversionRate: number;
|
|
788
|
+
category: string;
|
|
789
|
+
};
|
|
790
|
+
/** Position-based CTR breakdown */
|
|
791
|
+
positionAnalysis: {
|
|
792
|
+
prospectCTR: number;
|
|
793
|
+
leaderCTR: number;
|
|
794
|
+
ctrGap: number;
|
|
795
|
+
};
|
|
796
|
+
/** Funnel metrics for transparency */
|
|
797
|
+
funnel: {
|
|
798
|
+
monthlySearchVolume: number;
|
|
799
|
+
prospectClicks: number;
|
|
800
|
+
prospectConversions: number;
|
|
801
|
+
leaderClicks: number;
|
|
802
|
+
leaderConversions: number;
|
|
803
|
+
};
|
|
804
|
+
/** Sales-ready talking points */
|
|
805
|
+
insights: {
|
|
806
|
+
headline: string;
|
|
807
|
+
comparison: string;
|
|
808
|
+
opportunity: string;
|
|
809
|
+
};
|
|
810
|
+
/** Calculation timestamp */
|
|
811
|
+
calculatedAt: string;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// ============================================================================
|
|
815
|
+
// COMPETITIVE NARRATIVE TYPES
|
|
816
|
+
// Wins vs Losses storytelling for sales conversations
|
|
817
|
+
// ============================================================================
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Single comparison point in a head-to-head analysis
|
|
821
|
+
*/
|
|
822
|
+
export interface WinLossItem {
|
|
823
|
+
/** What's being compared */
|
|
824
|
+
metric: string;
|
|
825
|
+
/** Prospect's value/status */
|
|
826
|
+
prospectValue: string | number;
|
|
827
|
+
/** Competitor's value/status */
|
|
828
|
+
competitorValue: string | number;
|
|
829
|
+
/** Whether prospect wins this comparison */
|
|
830
|
+
prospectWins: boolean;
|
|
831
|
+
/** Impact level for prioritization */
|
|
832
|
+
impact: 'high' | 'medium' | 'low';
|
|
833
|
+
/** One-sentence insight for sales pitch */
|
|
834
|
+
insight: string;
|
|
835
|
+
/** Actionable recommendation */
|
|
836
|
+
action?: string;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Head-to-head comparison with a single competitor
|
|
841
|
+
*/
|
|
842
|
+
export interface HeadToHeadComparison {
|
|
843
|
+
/** Competitor being compared */
|
|
844
|
+
competitorName: string;
|
|
845
|
+
/** List of wins for prospect */
|
|
846
|
+
wins: WinLossItem[];
|
|
847
|
+
/** List of losses for prospect */
|
|
848
|
+
losses: WinLossItem[];
|
|
849
|
+
/** Overall assessment */
|
|
850
|
+
summary: {
|
|
851
|
+
winCount: number;
|
|
852
|
+
lossCount: number;
|
|
853
|
+
verdict: 'prospect_leads' | 'competitor_leads' | 'close_match';
|
|
854
|
+
headline: string;
|
|
855
|
+
};
|
|
856
|
+
/** Priority actions based on losses */
|
|
857
|
+
priorityActions: string[];
|
|
858
|
+
/** Calculated at timestamp */
|
|
859
|
+
calculatedAt: string;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Full competitive narrative across all competitors
|
|
864
|
+
*/
|
|
865
|
+
export interface CompetitiveNarrative {
|
|
866
|
+
/** Prospect name */
|
|
867
|
+
prospectName: string;
|
|
868
|
+
/** Head-to-head comparisons with each competitor */
|
|
869
|
+
headToHead: HeadToHeadComparison[];
|
|
870
|
+
/** Market-level summary */
|
|
871
|
+
marketSummary: {
|
|
872
|
+
/** Where prospect wins vs the market */
|
|
873
|
+
marketWins: string[];
|
|
874
|
+
/** Where prospect loses vs the market */
|
|
875
|
+
marketLosses: string[];
|
|
876
|
+
/** Overall market position */
|
|
877
|
+
position: 'leader' | 'competitive' | 'challenger' | 'at_risk';
|
|
878
|
+
/** Executive summary paragraph */
|
|
879
|
+
executiveSummary: string;
|
|
880
|
+
};
|
|
881
|
+
/** Top 3 actions to take */
|
|
882
|
+
topActions: Array<{
|
|
883
|
+
action: string;
|
|
884
|
+
impact: string;
|
|
885
|
+
competitor: string;
|
|
886
|
+
}>;
|
|
887
|
+
/** Calculated at timestamp */
|
|
888
|
+
calculatedAt: string;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// ============================================================================
|
|
892
|
+
// INTELLIGENCE DATA — Aggregated enrichment outputs from fn-v2 pipeline
|
|
893
|
+
// Written to plans.intelligenceData JSONB column
|
|
894
|
+
// ============================================================================
|
|
895
|
+
|
|
896
|
+
/** Prospect enrichment from business lookup + website analysis */
|
|
897
|
+
export interface IntelligenceProspect extends PlanningProspect {
|
|
898
|
+
url?: string;
|
|
899
|
+
placeId?: string;
|
|
900
|
+
address?: string;
|
|
901
|
+
city?: string;
|
|
902
|
+
state?: string;
|
|
903
|
+
phone?: string;
|
|
904
|
+
rating?: number;
|
|
905
|
+
reviewCount?: number;
|
|
906
|
+
techStack?: string[];
|
|
907
|
+
digitalMaturityScore?: number;
|
|
908
|
+
pixelsDetected?: string[];
|
|
909
|
+
socialProfiles?: Record<string, string>;
|
|
910
|
+
/** Nielsen DMA code for market intelligence lookups */
|
|
911
|
+
dmaCode?: string;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** Ad intelligence for a single competitor or prospect */
|
|
915
|
+
export interface AdIntelligenceEntry {
|
|
916
|
+
entityName: string;
|
|
917
|
+
googleAdsCount?: number;
|
|
918
|
+
googleAdsThemes?: string[];
|
|
919
|
+
metaAdsCount?: number;
|
|
920
|
+
metaAdsThemes?: string[];
|
|
921
|
+
estimatedMonthlySpend?: number;
|
|
922
|
+
activeChannels?: string[];
|
|
923
|
+
creativeExamples?: Array<{ platform: string; headline?: string; description?: string; imageUrl?: string }>;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** Pixel / tracking detection results */
|
|
927
|
+
export interface PixelDetectionResult {
|
|
928
|
+
url: string;
|
|
929
|
+
pixelsFound: string[];
|
|
930
|
+
analyticsTools: string[];
|
|
931
|
+
adPlatforms: string[];
|
|
932
|
+
tagManagers: string[];
|
|
933
|
+
remarketingActive: boolean;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** Domain SEO overview */
|
|
937
|
+
export interface DomainOverviewResult {
|
|
938
|
+
domain: string;
|
|
939
|
+
organicKeywords?: number;
|
|
940
|
+
organicTraffic?: number;
|
|
941
|
+
domainAuthority?: number;
|
|
942
|
+
backlinks?: number;
|
|
943
|
+
topKeywords?: Array<{ keyword: string; position: number; volume: number }>;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/** SEO keyword/ranking analysis (distinct from domain-level overview) */
|
|
947
|
+
export interface SeoAnalysisResult {
|
|
948
|
+
domain: string;
|
|
949
|
+
totalKeywordsTracked?: number;
|
|
950
|
+
keywordsInTop10?: number;
|
|
951
|
+
keywordsInTop3?: number;
|
|
952
|
+
organicTraffic?: number;
|
|
953
|
+
topKeywords?: Array<{ keyword: string; position: number; volume: number; difficulty?: number }>;
|
|
954
|
+
contentGaps?: string[];
|
|
955
|
+
technicalIssues?: string[];
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/** Landing page audit results */
|
|
959
|
+
export interface LandingPageAuditResult {
|
|
960
|
+
url: string;
|
|
961
|
+
mobileScore?: number;
|
|
962
|
+
desktopScore?: number;
|
|
963
|
+
loadTimeMs?: number;
|
|
964
|
+
hasCtaAboveFold?: boolean;
|
|
965
|
+
hasForm?: boolean;
|
|
966
|
+
hasPhoneNumber?: boolean;
|
|
967
|
+
issues?: string[];
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/** Social media presence data */
|
|
971
|
+
export interface SocialDataResult {
|
|
972
|
+
profiles: Array<{
|
|
973
|
+
platform: string;
|
|
974
|
+
url?: string;
|
|
975
|
+
followers?: number;
|
|
976
|
+
postsPerWeek?: number;
|
|
977
|
+
engagementRate?: number;
|
|
978
|
+
}>;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/** Strategy brief output */
|
|
982
|
+
export interface StrategyBriefResult {
|
|
983
|
+
executiveSummary: string;
|
|
984
|
+
marketPosition: string;
|
|
985
|
+
recommendedChannels: Array<{
|
|
986
|
+
channel: string;
|
|
987
|
+
productCode?: string;
|
|
988
|
+
rationale: string;
|
|
989
|
+
priority: 'high' | 'medium' | 'low';
|
|
990
|
+
}>;
|
|
991
|
+
competitiveGaps: string[];
|
|
992
|
+
targetAudience?: string;
|
|
993
|
+
messagingThemes?: string[];
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/** Market context from market intelligence tables */
|
|
997
|
+
export interface MarketContextResult {
|
|
998
|
+
seasonality?: {
|
|
999
|
+
type: string;
|
|
1000
|
+
score: number;
|
|
1001
|
+
peakMonths: string[];
|
|
1002
|
+
monthlyMultipliers: Record<string, number>;
|
|
1003
|
+
};
|
|
1004
|
+
benchmarks?: {
|
|
1005
|
+
avgTicket: number;
|
|
1006
|
+
conversionRate: number;
|
|
1007
|
+
marketSaturationScore?: number;
|
|
1008
|
+
yoyGrowthRate?: number;
|
|
1009
|
+
};
|
|
1010
|
+
marketScore?: {
|
|
1011
|
+
overallScore: number;
|
|
1012
|
+
timingScore: number;
|
|
1013
|
+
competitionScore: number;
|
|
1014
|
+
economicScore?: number;
|
|
1015
|
+
recommendation: string;
|
|
1016
|
+
rationale?: string;
|
|
1017
|
+
};
|
|
1018
|
+
competitiveLandscape?: {
|
|
1019
|
+
totalBusinesses?: number;
|
|
1020
|
+
advertisingPenetration?: number;
|
|
1021
|
+
marketConcentration?: string;
|
|
1022
|
+
opportunityScore?: number;
|
|
1023
|
+
};
|
|
1024
|
+
economicIndicators?: {
|
|
1025
|
+
consumerConfidenceIndex?: number;
|
|
1026
|
+
unemploymentRate?: number;
|
|
1027
|
+
medianHouseholdIncome?: number;
|
|
1028
|
+
economicHealthScore?: number;
|
|
1029
|
+
economicHealthTrend?: string;
|
|
1030
|
+
};
|
|
1031
|
+
searchVolume?: {
|
|
1032
|
+
monthlySearchVolume?: number;
|
|
1033
|
+
topKeywords?: Array<{ keyword: string; volume: number }>;
|
|
1034
|
+
yoyChangePct?: number;
|
|
1035
|
+
trendDirection?: string;
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* IntelligenceData — the single structured blob written to plans.intelligenceData.
|
|
1041
|
+
*
|
|
1042
|
+
* fn-v2 analyze-location.ts writes this after all enrichment tools complete.
|
|
1043
|
+
* All fields are optional — data fills in progressively as tools complete.
|
|
1044
|
+
*/
|
|
1045
|
+
export interface IntelligenceData {
|
|
1046
|
+
/** Enriched prospect profile */
|
|
1047
|
+
prospect?: IntelligenceProspect;
|
|
1048
|
+
/** Enriched competitor profiles */
|
|
1049
|
+
competitors?: PlanningCompetitor[];
|
|
1050
|
+
/** Ad intelligence (prospect + competitors) */
|
|
1051
|
+
adIntelligence?: AdIntelligenceEntry[];
|
|
1052
|
+
/** Pixel / tracking detection for prospect site */
|
|
1053
|
+
pixelDetection?: PixelDetectionResult;
|
|
1054
|
+
/** Domain SEO overview for prospect */
|
|
1055
|
+
domainOverview?: DomainOverviewResult;
|
|
1056
|
+
/** Landing page audit for prospect */
|
|
1057
|
+
landingPageAudit?: LandingPageAuditResult;
|
|
1058
|
+
/** SEO keyword/ranking analysis */
|
|
1059
|
+
seoData?: SeoAnalysisResult;
|
|
1060
|
+
/** Social media presence */
|
|
1061
|
+
socialData?: SocialDataResult;
|
|
1062
|
+
/** Strategy brief from LLM analysis */
|
|
1063
|
+
strategyBrief?: StrategyBriefResult;
|
|
1064
|
+
/** Market intelligence (seasonality, benchmarks, scores) */
|
|
1065
|
+
marketContext?: MarketContextResult;
|
|
1066
|
+
|
|
1067
|
+
/** Metadata */
|
|
1068
|
+
enrichedAt?: string;
|
|
1069
|
+
enrichmentVersion?: string;
|
|
1070
|
+
toolsCompleted?: string[];
|
|
1071
|
+
toolsFailed?: string[];
|
|
1072
|
+
}
|