@foundrynorth/flux-schema 1.0.0
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/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.d.ts +24371 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +3802 -0
- package/dist/schema.js.map +1 -0
- package/package.json +32 -0
package/dist/schema.js
ADDED
|
@@ -0,0 +1,3802 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FNFlux Database Schema (Drizzle ORM)
|
|
3
|
+
*
|
|
4
|
+
* All tables are prefixed with `flux_` to maintain separation from Forge tables.
|
|
5
|
+
* This schema lives in the same Neon database as Forge.
|
|
6
|
+
*
|
|
7
|
+
* CRITICAL: No stubs. All tables are real and will be created on migration.
|
|
8
|
+
*/
|
|
9
|
+
import { pgTable, varchar, text, boolean, timestamp, date, jsonb, integer, bigint, numeric, uuid, real, serial, time, pgEnum, uniqueIndex, index, } from "drizzle-orm/pg-core";
|
|
10
|
+
import { relations, sql } from "drizzle-orm";
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// ENUMS
|
|
13
|
+
// =============================================================================
|
|
14
|
+
export const fluxProjectStatusEnum = pgEnum("flux_project_status", [
|
|
15
|
+
"active",
|
|
16
|
+
"paused",
|
|
17
|
+
"archived",
|
|
18
|
+
]);
|
|
19
|
+
export const fluxEmailVisibilityEnum = pgEnum("flux_email_visibility", [
|
|
20
|
+
"private",
|
|
21
|
+
"pending_review",
|
|
22
|
+
"shared",
|
|
23
|
+
"auto_hidden",
|
|
24
|
+
]);
|
|
25
|
+
export const fluxViewTierEnum = pgEnum("flux_view_tier", [
|
|
26
|
+
"full",
|
|
27
|
+
"limited",
|
|
28
|
+
"client",
|
|
29
|
+
]);
|
|
30
|
+
export const fluxSubscriptionStatusEnum = pgEnum("flux_subscription_status", [
|
|
31
|
+
"active",
|
|
32
|
+
"pending",
|
|
33
|
+
"expired",
|
|
34
|
+
"revoked",
|
|
35
|
+
]);
|
|
36
|
+
export const fluxActivityTypeEnum = pgEnum("flux_activity_type", [
|
|
37
|
+
"email",
|
|
38
|
+
"task",
|
|
39
|
+
"ticket",
|
|
40
|
+
"pacing",
|
|
41
|
+
"slack",
|
|
42
|
+
"alert",
|
|
43
|
+
"campaign",
|
|
44
|
+
]);
|
|
45
|
+
export const fluxSourceSystemEnum = pgEnum("flux_source_system", [
|
|
46
|
+
"hubspot",
|
|
47
|
+
"ninjacat",
|
|
48
|
+
"slack",
|
|
49
|
+
"trigger",
|
|
50
|
+
"compass",
|
|
51
|
+
]);
|
|
52
|
+
export const fluxActivityVisibilityEnum = pgEnum("flux_activity_visibility", [
|
|
53
|
+
"owner_only",
|
|
54
|
+
"shared",
|
|
55
|
+
"all",
|
|
56
|
+
]);
|
|
57
|
+
export const fluxSeverityEnum = pgEnum("flux_severity", [
|
|
58
|
+
"info",
|
|
59
|
+
"warning",
|
|
60
|
+
"critical",
|
|
61
|
+
]);
|
|
62
|
+
export const fluxAlertTypeEnum = pgEnum("flux_alert_type", [
|
|
63
|
+
"pixel_down",
|
|
64
|
+
"pacing_deviation",
|
|
65
|
+
"task_overdue",
|
|
66
|
+
"ticket_stale",
|
|
67
|
+
"email_no_response",
|
|
68
|
+
"campaign_ended",
|
|
69
|
+
"custom",
|
|
70
|
+
]);
|
|
71
|
+
export const fluxPrimaryRoleEnum = pgEnum("flux_primary_role", [
|
|
72
|
+
"admin",
|
|
73
|
+
"leader",
|
|
74
|
+
"strategist",
|
|
75
|
+
"account_executive",
|
|
76
|
+
"sentinel_only",
|
|
77
|
+
"view_only",
|
|
78
|
+
]);
|
|
79
|
+
export const fluxEscalationStateEnum = pgEnum("flux_escalation_state", [
|
|
80
|
+
"critical_now",
|
|
81
|
+
"needs_owner_today",
|
|
82
|
+
"follow_up_soon",
|
|
83
|
+
"waiting_on_client",
|
|
84
|
+
"waiting_on_vendor",
|
|
85
|
+
"monitor",
|
|
86
|
+
]);
|
|
87
|
+
export const fluxEmbedTypeEnum = pgEnum("flux_embed_type", [
|
|
88
|
+
"ninjacat",
|
|
89
|
+
"iframe",
|
|
90
|
+
"custom",
|
|
91
|
+
]);
|
|
92
|
+
export const fluxTaskStatusEnum = pgEnum("flux_task_status", [
|
|
93
|
+
"backlog",
|
|
94
|
+
"in_progress",
|
|
95
|
+
"in_review",
|
|
96
|
+
"complete",
|
|
97
|
+
]);
|
|
98
|
+
export const fluxTaskPriorityEnum = pgEnum("flux_task_priority", [
|
|
99
|
+
"low",
|
|
100
|
+
"medium",
|
|
101
|
+
"high",
|
|
102
|
+
"urgent",
|
|
103
|
+
]);
|
|
104
|
+
export const fluxOwnerTypeEnum = pgEnum("flux_owner_type", [
|
|
105
|
+
"internal",
|
|
106
|
+
"external",
|
|
107
|
+
"vendor",
|
|
108
|
+
]);
|
|
109
|
+
// Secrets Management (API keys, billing, rotation tracking)
|
|
110
|
+
export const fluxSecretsProviderCategoryEnum = pgEnum("flux_secrets_provider_category", [
|
|
111
|
+
"ai",
|
|
112
|
+
"auth",
|
|
113
|
+
"database",
|
|
114
|
+
"storage",
|
|
115
|
+
"email",
|
|
116
|
+
"crm",
|
|
117
|
+
"scraping",
|
|
118
|
+
"monitoring",
|
|
119
|
+
"maps",
|
|
120
|
+
"other",
|
|
121
|
+
]);
|
|
122
|
+
export const fluxSecretsStatusEnum = pgEnum("flux_secrets_status", [
|
|
123
|
+
"active",
|
|
124
|
+
"expiring_soon",
|
|
125
|
+
"expired",
|
|
126
|
+
"revoked",
|
|
127
|
+
]);
|
|
128
|
+
export const fluxSecretsPlatformEnum = pgEnum("flux_secrets_platform", [
|
|
129
|
+
"railway",
|
|
130
|
+
"vercel",
|
|
131
|
+
"trigger",
|
|
132
|
+
"local",
|
|
133
|
+
]);
|
|
134
|
+
export const fluxSecretsSyncStatusEnum = pgEnum("flux_secrets_sync_status", [
|
|
135
|
+
"synced",
|
|
136
|
+
"pending",
|
|
137
|
+
"failed",
|
|
138
|
+
"unknown",
|
|
139
|
+
]);
|
|
140
|
+
export const fluxSecretsAuditActionEnum = pgEnum("flux_secrets_audit_action", [
|
|
141
|
+
"created",
|
|
142
|
+
"rotated",
|
|
143
|
+
"revoked",
|
|
144
|
+
"synced",
|
|
145
|
+
"sync_failed",
|
|
146
|
+
]);
|
|
147
|
+
// =============================================================================
|
|
148
|
+
// USERS (Extension table - references Clerk, not Forge users)
|
|
149
|
+
// =============================================================================
|
|
150
|
+
/**
|
|
151
|
+
* Flux user profiles - extends Clerk user identity with Flux-specific data.
|
|
152
|
+
*
|
|
153
|
+
* NOTE: Unlike Forge which uses username/password auth, Flux uses Clerk.
|
|
154
|
+
* This table stores Flux-specific user data keyed by Clerk user ID.
|
|
155
|
+
*/
|
|
156
|
+
export const fluxUsers = pgTable("flux_users", {
|
|
157
|
+
id: varchar("id")
|
|
158
|
+
.primaryKey()
|
|
159
|
+
.default(sql `gen_random_uuid()`),
|
|
160
|
+
clerkId: varchar("clerk_id").notNull().unique(),
|
|
161
|
+
email: varchar("email").notNull(),
|
|
162
|
+
name: varchar("name"),
|
|
163
|
+
primaryRole: fluxPrimaryRoleEnum("primary_role").notNull().default("strategist"),
|
|
164
|
+
isAdmin: boolean("is_admin").default(false).notNull(),
|
|
165
|
+
hasSentinelAccess: boolean("has_sentinel_access").notNull().default(false),
|
|
166
|
+
hasDashboardAccess: boolean("has_dashboard_access").notNull().default(true),
|
|
167
|
+
privacyKeywords: jsonb("privacy_keywords").default([]).$type(),
|
|
168
|
+
/** HubSpot owner ID for reverse lookup (Flux user → HubSpot owner) */
|
|
169
|
+
hubspotOwnerId: varchar("hubspot_owner_id"),
|
|
170
|
+
slackUserId: varchar("slack_user_id"),
|
|
171
|
+
notificationPreferences: jsonb("notification_preferences")
|
|
172
|
+
.default({})
|
|
173
|
+
.$type(),
|
|
174
|
+
onboardingComplete: boolean("onboarding_complete").notNull().default(true),
|
|
175
|
+
followedKeywords: jsonb("followed_keywords").default([]).$type(),
|
|
176
|
+
lastActiveAt: timestamp("last_active_at").defaultNow(),
|
|
177
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
178
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
179
|
+
}, (table) => ({
|
|
180
|
+
clerkIdIdx: uniqueIndex("flux_users_clerk_id_idx").on(table.clerkId),
|
|
181
|
+
emailIdx: index("flux_users_email_idx").on(table.email),
|
|
182
|
+
lastActiveIdx: index("flux_users_last_active_idx").on(table.lastActiveAt),
|
|
183
|
+
}));
|
|
184
|
+
// =============================================================================
|
|
185
|
+
// PROJECTS (HubSpot Companies)
|
|
186
|
+
// =============================================================================
|
|
187
|
+
export const fluxProjects = pgTable("flux_projects", {
|
|
188
|
+
id: varchar("id")
|
|
189
|
+
.primaryKey()
|
|
190
|
+
.default(sql `gen_random_uuid()`),
|
|
191
|
+
hubspotCompanyId: varchar("hubspot_company_id").unique(),
|
|
192
|
+
name: varchar("name").notNull(),
|
|
193
|
+
domain: varchar("domain"),
|
|
194
|
+
/** Account Executive — distinct role (map from HubSpot account_executive if present) */
|
|
195
|
+
accountExecutiveId: varchar("account_executive_id").references(() => fluxUsers.id),
|
|
196
|
+
/** Account Manager (HubSpot account_manager) — distinct role */
|
|
197
|
+
accountManagerId: varchar("account_manager_id").references(() => fluxUsers.id),
|
|
198
|
+
/** Primary Strategist (HubSpot strategist) */
|
|
199
|
+
primaryStrategistId: varchar("primary_strategist_id").references(() => fluxUsers.id),
|
|
200
|
+
/** Secondary Strategist (optional second strategist) */
|
|
201
|
+
secondaryStrategistId: varchar("secondary_strategist_id").references(() => fluxUsers.id),
|
|
202
|
+
/** Account Coordinator (HubSpot account_coordinator) — distinct role */
|
|
203
|
+
accountCoordinatorId: varchar("account_coordinator_id").references(() => fluxUsers.id),
|
|
204
|
+
/** Paid Search Specialist — Flux-only role (no HubSpot mapping) */
|
|
205
|
+
paidSearchSpecialistId: varchar("paid_search_specialist_id").references(() => fluxUsers.id),
|
|
206
|
+
/** Pod name — free-text label (e.g. "Pod A") */
|
|
207
|
+
pod: varchar("pod"),
|
|
208
|
+
defaultEmailVisibility: fluxEmailVisibilityEnum("default_email_visibility")
|
|
209
|
+
.default("pending_review")
|
|
210
|
+
.notNull(),
|
|
211
|
+
/** HubSpot tier_level (Tier 1 / Tier 2 / Tier 3) */
|
|
212
|
+
tierLevel: varchar("tier_level"),
|
|
213
|
+
/** HubSpot hs_ideal_customer_profile (tier_1 / tier_2 / tier_3) */
|
|
214
|
+
icpTier: varchar("icp_tier"),
|
|
215
|
+
/** HubSpot hs_is_target_account */
|
|
216
|
+
isTargetAccount: boolean("is_target_account").default(false),
|
|
217
|
+
/** Parent project for account hierarchy (HubSpot hs_parent_company_id) */
|
|
218
|
+
parentProjectId: varchar("parent_project_id").references(() => fluxProjects.id, { onDelete: "set null" }),
|
|
219
|
+
// -- HubSpot enrichment fields --
|
|
220
|
+
/** HubSpot company_status (active/inactive for Rules of Engagement) */
|
|
221
|
+
companyStatus: varchar("company_status"),
|
|
222
|
+
/** Forge creative compliance status from HubSpot fn_forge_compliance_status */
|
|
223
|
+
forgeComplianceStatus: varchar("forge_compliance_status"),
|
|
224
|
+
/** Forge fact check status from HubSpot fn_forge_fact_check_status */
|
|
225
|
+
forgeFactCheckStatus: varchar("forge_fact_check_status"),
|
|
226
|
+
/** HubSpot lifecyclestage */
|
|
227
|
+
lifecycleStage: varchar("lifecycle_stage"),
|
|
228
|
+
/** HubSpot hs_lead_status */
|
|
229
|
+
leadStatus: varchar("lead_status"),
|
|
230
|
+
/** HubSpot annualrevenue */
|
|
231
|
+
annualRevenue: numeric("annual_revenue", { precision: 14, scale: 2 }),
|
|
232
|
+
/** HubSpot type (prospect/partner/customer classification) */
|
|
233
|
+
companyType: varchar("company_type"),
|
|
234
|
+
/** SLA profile override — FK to flux_sla_profiles */
|
|
235
|
+
slaProfileId: text("sla_profile_id").references(() => fluxSlaProfiles.id, { onDelete: "set null" }),
|
|
236
|
+
ninjacatClientId: varchar("ninjacat_client_id"),
|
|
237
|
+
slackChannelId: varchar("slack_channel_id"),
|
|
238
|
+
status: fluxProjectStatusEnum("status").default("active").notNull(),
|
|
239
|
+
lastHubspotSync: timestamp("last_hubspot_sync"),
|
|
240
|
+
lastNinjacatSync: timestamp("last_ninjacat_sync"),
|
|
241
|
+
isDemo: boolean("is_demo").default(false).notNull(),
|
|
242
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
243
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
244
|
+
}, (table) => ({
|
|
245
|
+
hubspotIdIdx: uniqueIndex("flux_projects_hubspot_id_idx").on(table.hubspotCompanyId),
|
|
246
|
+
primaryStrategistIdx: index("flux_projects_primary_strategist_idx").on(table.primaryStrategistId),
|
|
247
|
+
accountManagerIdx: index("flux_projects_account_manager_idx").on(table.accountManagerId),
|
|
248
|
+
accountCoordinatorIdx: index("flux_projects_account_coordinator_idx").on(table.accountCoordinatorId),
|
|
249
|
+
paidSearchSpecialistIdx: index("flux_projects_paid_search_specialist_idx").on(table.paidSearchSpecialistId),
|
|
250
|
+
statusIdx: index("flux_projects_status_idx").on(table.status),
|
|
251
|
+
parentProjectIdx: index("flux_projects_parent_project_idx").on(table.parentProjectId),
|
|
252
|
+
}));
|
|
253
|
+
// =============================================================================
|
|
254
|
+
// TEAM TEMPLATES
|
|
255
|
+
// =============================================================================
|
|
256
|
+
export const fluxTeamTemplates = pgTable("flux_team_templates", {
|
|
257
|
+
id: varchar("id")
|
|
258
|
+
.primaryKey()
|
|
259
|
+
.default(sql `gen_random_uuid()`),
|
|
260
|
+
name: varchar("name").notNull(),
|
|
261
|
+
description: text("description"),
|
|
262
|
+
pod: varchar("pod"),
|
|
263
|
+
createdById: varchar("created_by_id")
|
|
264
|
+
.references(() => fluxUsers.id)
|
|
265
|
+
.notNull(),
|
|
266
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
267
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
268
|
+
});
|
|
269
|
+
export const fluxTeamTemplateMembers = pgTable("flux_team_template_members", {
|
|
270
|
+
id: varchar("id")
|
|
271
|
+
.primaryKey()
|
|
272
|
+
.default(sql `gen_random_uuid()`),
|
|
273
|
+
templateId: varchar("template_id")
|
|
274
|
+
.references(() => fluxTeamTemplates.id, { onDelete: "cascade" })
|
|
275
|
+
.notNull(),
|
|
276
|
+
userId: varchar("user_id").references(() => fluxUsers.id),
|
|
277
|
+
placeholderRole: varchar("placeholder_role"), // e.g., "External Vendor"
|
|
278
|
+
role: varchar("role").default("member").notNull(),
|
|
279
|
+
viewTier: fluxViewTierEnum("view_tier").default("full").notNull(),
|
|
280
|
+
displayOrder: integer("display_order").default(0).notNull(),
|
|
281
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
282
|
+
}, (table) => ({
|
|
283
|
+
templateIdx: index("flux_template_members_template_idx").on(table.templateId),
|
|
284
|
+
}));
|
|
285
|
+
/** Available pod color labels used across templates and project assignments. */
|
|
286
|
+
export const POD_OPTIONS = [
|
|
287
|
+
"Yellow",
|
|
288
|
+
"Purple",
|
|
289
|
+
"Blue",
|
|
290
|
+
"Green",
|
|
291
|
+
"Red",
|
|
292
|
+
"Orange",
|
|
293
|
+
"Pink",
|
|
294
|
+
];
|
|
295
|
+
/** Maps template role values to fluxProjects column names for apply-time writes. */
|
|
296
|
+
export const TEMPLATE_ROLE_OPTIONS = [
|
|
297
|
+
{ value: "account_executive", label: "Account Executive", projectField: "accountExecutiveId" },
|
|
298
|
+
{ value: "account_manager", label: "Account Manager", projectField: "accountManagerId" },
|
|
299
|
+
{ value: "strategist", label: "Strategist", projectField: "primaryStrategistId" },
|
|
300
|
+
{ value: "secondary_strategist", label: "Secondary Strategist", projectField: "secondaryStrategistId" },
|
|
301
|
+
{ value: "account_coordinator", label: "Account Coordinator", projectField: "accountCoordinatorId" },
|
|
302
|
+
{ value: "paid_search_specialist", label: "Paid Search Specialist", projectField: "paidSearchSpecialistId" },
|
|
303
|
+
{ value: "member", label: "Team Member", projectField: null },
|
|
304
|
+
];
|
|
305
|
+
// =============================================================================
|
|
306
|
+
// SUBSCRIPTIONS (Team Access)
|
|
307
|
+
// =============================================================================
|
|
308
|
+
export const fluxSubscriptions = pgTable("flux_subscriptions", {
|
|
309
|
+
id: varchar("id")
|
|
310
|
+
.primaryKey()
|
|
311
|
+
.default(sql `gen_random_uuid()`),
|
|
312
|
+
projectId: varchar("project_id")
|
|
313
|
+
.references(() => fluxProjects.id, { onDelete: "cascade" })
|
|
314
|
+
.notNull(),
|
|
315
|
+
userId: varchar("user_id").references(() => fluxUsers.id),
|
|
316
|
+
externalEmail: varchar("external_email"), // For non-Clerk users
|
|
317
|
+
viewTier: fluxViewTierEnum("view_tier").default("full").notNull(),
|
|
318
|
+
invitedById: varchar("invited_by_id")
|
|
319
|
+
.references(() => fluxUsers.id)
|
|
320
|
+
.notNull(),
|
|
321
|
+
appliedTemplateId: varchar("applied_template_id").references(() => fluxTeamTemplates.id),
|
|
322
|
+
expiresAt: timestamp("expires_at"),
|
|
323
|
+
status: fluxSubscriptionStatusEnum("status").default("active").notNull(),
|
|
324
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
325
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
326
|
+
}, (table) => ({
|
|
327
|
+
projectIdx: index("flux_subscriptions_project_idx").on(table.projectId),
|
|
328
|
+
userIdx: index("flux_subscriptions_user_idx").on(table.userId),
|
|
329
|
+
statusIdx: index("flux_subscriptions_status_idx").on(table.status),
|
|
330
|
+
}));
|
|
331
|
+
// =============================================================================
|
|
332
|
+
// EMAIL THREADS
|
|
333
|
+
// =============================================================================
|
|
334
|
+
export const fluxEmailThreads = pgTable("flux_email_threads", {
|
|
335
|
+
id: varchar("id")
|
|
336
|
+
.primaryKey()
|
|
337
|
+
.default(sql `gen_random_uuid()`),
|
|
338
|
+
projectId: varchar("project_id")
|
|
339
|
+
.references(() => fluxProjects.id, { onDelete: "cascade" })
|
|
340
|
+
.notNull(),
|
|
341
|
+
hubspotEmailId: varchar("hubspot_email_id").unique(),
|
|
342
|
+
subject: varchar("subject").notNull(),
|
|
343
|
+
senderEmail: varchar("sender_email").notNull(),
|
|
344
|
+
senderName: varchar("sender_name"),
|
|
345
|
+
receivedAt: timestamp("received_at").notNull(),
|
|
346
|
+
visibility: fluxEmailVisibilityEnum("visibility")
|
|
347
|
+
.default("pending_review")
|
|
348
|
+
.notNull(),
|
|
349
|
+
reviewedById: varchar("reviewed_by_id").references(() => fluxUsers.id),
|
|
350
|
+
reviewedAt: timestamp("reviewed_at"),
|
|
351
|
+
hiddenReason: varchar("hidden_reason"),
|
|
352
|
+
isDemo: boolean("is_demo").default(false).notNull(),
|
|
353
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
354
|
+
}, (table) => ({
|
|
355
|
+
projectIdx: index("flux_email_threads_project_idx").on(table.projectId),
|
|
356
|
+
visibilityIdx: index("flux_email_threads_visibility_idx").on(table.visibility),
|
|
357
|
+
hubspotIdx: uniqueIndex("flux_email_threads_hubspot_idx").on(table.hubspotEmailId),
|
|
358
|
+
}));
|
|
359
|
+
// =============================================================================
|
|
360
|
+
// ACTIVITY ITEMS (Unified Feed)
|
|
361
|
+
// =============================================================================
|
|
362
|
+
export const fluxActivityItems = pgTable("flux_activity_items", {
|
|
363
|
+
id: varchar("id")
|
|
364
|
+
.primaryKey()
|
|
365
|
+
.default(sql `gen_random_uuid()`),
|
|
366
|
+
projectId: varchar("project_id")
|
|
367
|
+
.references(() => fluxProjects.id, { onDelete: "cascade" })
|
|
368
|
+
.notNull(),
|
|
369
|
+
type: fluxActivityTypeEnum("type").notNull(),
|
|
370
|
+
sourceId: varchar("source_id").notNull(),
|
|
371
|
+
sourceSystem: fluxSourceSystemEnum("source_system").notNull(),
|
|
372
|
+
summary: text("summary").notNull(),
|
|
373
|
+
visibility: fluxActivityVisibilityEnum("visibility")
|
|
374
|
+
.default("shared")
|
|
375
|
+
.notNull(),
|
|
376
|
+
severity: fluxSeverityEnum("severity"),
|
|
377
|
+
occurredAt: timestamp("occurred_at").notNull(),
|
|
378
|
+
metadata: jsonb("metadata").default({}).$type(),
|
|
379
|
+
isDemo: boolean("is_demo").default(false).notNull(),
|
|
380
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
381
|
+
}, (table) => ({
|
|
382
|
+
projectIdx: index("flux_activity_items_project_idx").on(table.projectId),
|
|
383
|
+
typeIdx: index("flux_activity_items_type_idx").on(table.type),
|
|
384
|
+
occurredAtIdx: index("flux_activity_items_occurred_at_idx").on(table.occurredAt),
|
|
385
|
+
}));
|
|
386
|
+
// =============================================================================
|
|
387
|
+
// ALERT RULES
|
|
388
|
+
// =============================================================================
|
|
389
|
+
export const fluxAlertRules = pgTable("flux_alert_rules", {
|
|
390
|
+
id: varchar("id")
|
|
391
|
+
.primaryKey()
|
|
392
|
+
.default(sql `gen_random_uuid()`),
|
|
393
|
+
projectId: varchar("project_id").references(() => fluxProjects.id, {
|
|
394
|
+
onDelete: "cascade",
|
|
395
|
+
}),
|
|
396
|
+
createdById: varchar("created_by_id")
|
|
397
|
+
.references(() => fluxUsers.id)
|
|
398
|
+
.notNull(),
|
|
399
|
+
type: fluxAlertTypeEnum("type").notNull(),
|
|
400
|
+
name: varchar("name").notNull(),
|
|
401
|
+
config: jsonb("config").default({}).$type(),
|
|
402
|
+
notifySlack: boolean("notify_slack").default(true).notNull(),
|
|
403
|
+
notifyEmail: boolean("notify_email").default(false).notNull(),
|
|
404
|
+
notifyInApp: boolean("notify_in_app").default(true).notNull(),
|
|
405
|
+
slackChannelOverride: varchar("slack_channel_override"),
|
|
406
|
+
isActive: boolean("is_active").default(true).notNull(),
|
|
407
|
+
isDemo: boolean("is_demo").default(false).notNull(),
|
|
408
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
409
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
410
|
+
}, (table) => ({
|
|
411
|
+
projectIdx: index("flux_alert_rules_project_idx").on(table.projectId),
|
|
412
|
+
activeIdx: index("flux_alert_rules_active_idx").on(table.isActive),
|
|
413
|
+
}));
|
|
414
|
+
// =============================================================================
|
|
415
|
+
// ALERTS (Fired)
|
|
416
|
+
// =============================================================================
|
|
417
|
+
export const fluxAlerts = pgTable("flux_alerts", {
|
|
418
|
+
id: varchar("id")
|
|
419
|
+
.primaryKey()
|
|
420
|
+
.default(sql `gen_random_uuid()`),
|
|
421
|
+
projectId: varchar("project_id")
|
|
422
|
+
.references(() => fluxProjects.id, { onDelete: "cascade" })
|
|
423
|
+
.notNull(),
|
|
424
|
+
ruleId: varchar("rule_id")
|
|
425
|
+
.references(() => fluxAlertRules.id, { onDelete: "cascade" })
|
|
426
|
+
.notNull(),
|
|
427
|
+
severity: fluxSeverityEnum("severity").notNull(),
|
|
428
|
+
title: varchar("title").notNull(),
|
|
429
|
+
message: text("message").notNull(),
|
|
430
|
+
sourceData: jsonb("source_data")
|
|
431
|
+
.default({})
|
|
432
|
+
.$type(),
|
|
433
|
+
firedAt: timestamp("fired_at").defaultNow().notNull(),
|
|
434
|
+
acknowledgedAt: timestamp("acknowledged_at"),
|
|
435
|
+
acknowledgedById: varchar("acknowledged_by_id").references(() => fluxUsers.id),
|
|
436
|
+
resolvedAt: timestamp("resolved_at"),
|
|
437
|
+
slackMessageTs: varchar("slack_message_ts"),
|
|
438
|
+
isDemo: boolean("is_demo").default(false).notNull(),
|
|
439
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
440
|
+
}, (table) => ({
|
|
441
|
+
projectIdx: index("flux_alerts_project_idx").on(table.projectId),
|
|
442
|
+
firedAtIdx: index("flux_alerts_fired_at_idx").on(table.firedAt),
|
|
443
|
+
unacknowledgedIdx: index("flux_alerts_unacknowledged_idx").on(table.acknowledgedAt),
|
|
444
|
+
}));
|
|
445
|
+
// =============================================================================
|
|
446
|
+
// PROJECT ESCALATIONS
|
|
447
|
+
// =============================================================================
|
|
448
|
+
export const fluxProjectEscalations = pgTable("flux_project_escalations", {
|
|
449
|
+
id: varchar("id")
|
|
450
|
+
.primaryKey()
|
|
451
|
+
.default(sql `gen_random_uuid()`),
|
|
452
|
+
projectId: varchar("project_id")
|
|
453
|
+
.references(() => fluxProjects.id, { onDelete: "cascade" })
|
|
454
|
+
.notNull()
|
|
455
|
+
.unique(),
|
|
456
|
+
state: fluxEscalationStateEnum("state").notNull(),
|
|
457
|
+
reason: text("reason"),
|
|
458
|
+
nextFollowupAt: timestamp("next_followup_at"),
|
|
459
|
+
lastAutoRecommendationAt: timestamp("last_auto_recommendation_at"),
|
|
460
|
+
updatedById: varchar("updated_by_id")
|
|
461
|
+
.references(() => fluxUsers.id)
|
|
462
|
+
.notNull(),
|
|
463
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
464
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
465
|
+
}, (table) => ({
|
|
466
|
+
projectIdx: index("flux_project_escalations_project_idx").on(table.projectId),
|
|
467
|
+
stateIdx: index("flux_project_escalations_state_idx").on(table.state),
|
|
468
|
+
followupIdx: index("flux_project_escalations_followup_idx").on(table.nextFollowupAt),
|
|
469
|
+
}));
|
|
470
|
+
export const fluxProjectEscalationHistory = pgTable("flux_project_escalation_history", {
|
|
471
|
+
id: varchar("id")
|
|
472
|
+
.primaryKey()
|
|
473
|
+
.default(sql `gen_random_uuid()`),
|
|
474
|
+
projectId: varchar("project_id")
|
|
475
|
+
.references(() => fluxProjects.id, { onDelete: "cascade" })
|
|
476
|
+
.notNull(),
|
|
477
|
+
fromState: fluxEscalationStateEnum("from_state"),
|
|
478
|
+
toState: fluxEscalationStateEnum("to_state").notNull(),
|
|
479
|
+
reason: text("reason"),
|
|
480
|
+
changedById: varchar("changed_by_id")
|
|
481
|
+
.references(() => fluxUsers.id)
|
|
482
|
+
.notNull(),
|
|
483
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
484
|
+
}, (table) => ({
|
|
485
|
+
projectIdx: index("flux_project_escalation_history_project_idx").on(table.projectId),
|
|
486
|
+
toStateIdx: index("flux_project_escalation_history_to_state_idx").on(table.toState),
|
|
487
|
+
createdAtIdx: index("flux_project_escalation_history_created_at_idx").on(table.createdAt),
|
|
488
|
+
}));
|
|
489
|
+
// =============================================================================
|
|
490
|
+
// EMBEDDED DASHBOARDS
|
|
491
|
+
// =============================================================================
|
|
492
|
+
export const fluxEmbeddedDashboards = pgTable("flux_embedded_dashboards", {
|
|
493
|
+
id: varchar("id")
|
|
494
|
+
.primaryKey()
|
|
495
|
+
.default(sql `gen_random_uuid()`),
|
|
496
|
+
projectId: varchar("project_id")
|
|
497
|
+
.references(() => fluxProjects.id, { onDelete: "cascade" })
|
|
498
|
+
.notNull(),
|
|
499
|
+
name: varchar("name").notNull(),
|
|
500
|
+
embedType: fluxEmbedTypeEnum("embed_type").notNull(),
|
|
501
|
+
embedUrl: varchar("embed_url").notNull(),
|
|
502
|
+
displayOrder: integer("display_order").default(0).notNull(),
|
|
503
|
+
isActive: boolean("is_active").default(true).notNull(),
|
|
504
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
505
|
+
}, (table) => ({
|
|
506
|
+
projectIdx: index("flux_embedded_dashboards_project_idx").on(table.projectId),
|
|
507
|
+
}));
|
|
508
|
+
// =============================================================================
|
|
509
|
+
// AUDIT LOG
|
|
510
|
+
// =============================================================================
|
|
511
|
+
export const fluxAuditLog = pgTable("flux_audit_log", {
|
|
512
|
+
id: varchar("id")
|
|
513
|
+
.primaryKey()
|
|
514
|
+
.default(sql `gen_random_uuid()`),
|
|
515
|
+
userId: varchar("user_id")
|
|
516
|
+
.references(() => fluxUsers.id)
|
|
517
|
+
.notNull(),
|
|
518
|
+
action: varchar("action").notNull(),
|
|
519
|
+
resourceType: varchar("resource_type").notNull(),
|
|
520
|
+
resourceId: varchar("resource_id").notNull(),
|
|
521
|
+
previousValue: jsonb("previous_value").$type(),
|
|
522
|
+
newValue: jsonb("new_value").$type(),
|
|
523
|
+
ipAddress: varchar("ip_address"),
|
|
524
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
525
|
+
}, (table) => ({
|
|
526
|
+
userIdx: index("flux_audit_log_user_idx").on(table.userId),
|
|
527
|
+
resourceIdx: index("flux_audit_log_resource_idx").on(table.resourceType, table.resourceId),
|
|
528
|
+
createdAtIdx: index("flux_audit_log_created_at_idx").on(table.createdAt),
|
|
529
|
+
}));
|
|
530
|
+
// =============================================================================
|
|
531
|
+
// UNIFIED ROLE CONFIG (Cockpit user management)
|
|
532
|
+
// =============================================================================
|
|
533
|
+
export const fluxUnifiedRoleConfig = pgTable("flux_unified_role_config", {
|
|
534
|
+
id: varchar("id")
|
|
535
|
+
.primaryKey()
|
|
536
|
+
.default(sql `gen_random_uuid()`),
|
|
537
|
+
unifiedRole: varchar("unified_role").notNull(),
|
|
538
|
+
app: varchar("app").notNull(),
|
|
539
|
+
config: jsonb("config").notNull().$type(),
|
|
540
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
541
|
+
updatedBy: varchar("updated_by"),
|
|
542
|
+
}, (table) => ({
|
|
543
|
+
uniqueRoleApp: uniqueIndex("flux_unified_role_config_role_app_idx").on(table.unifiedRole, table.app),
|
|
544
|
+
}));
|
|
545
|
+
// =============================================================================
|
|
546
|
+
// STRATEGIST TELEMETRY
|
|
547
|
+
// =============================================================================
|
|
548
|
+
export const fluxStrategistEvents = pgTable("flux_strategist_events", {
|
|
549
|
+
id: varchar("id")
|
|
550
|
+
.primaryKey()
|
|
551
|
+
.default(sql `gen_random_uuid()`),
|
|
552
|
+
userId: varchar("user_id")
|
|
553
|
+
.references(() => fluxUsers.id)
|
|
554
|
+
.notNull(),
|
|
555
|
+
eventName: varchar("event_name").notNull(),
|
|
556
|
+
page: varchar("page").notNull(),
|
|
557
|
+
context: jsonb("context").default({}).$type(),
|
|
558
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
559
|
+
}, (table) => ({
|
|
560
|
+
userIdx: index("flux_strategist_events_user_idx").on(table.userId),
|
|
561
|
+
eventNameIdx: index("flux_strategist_events_event_name_idx").on(table.eventName),
|
|
562
|
+
pageIdx: index("flux_strategist_events_page_idx").on(table.page),
|
|
563
|
+
createdAtIdx: index("flux_strategist_events_created_at_idx").on(table.createdAt),
|
|
564
|
+
}));
|
|
565
|
+
// =============================================================================
|
|
566
|
+
// BUDGETS (Monthly budget records per project)
|
|
567
|
+
// =============================================================================
|
|
568
|
+
export const fluxBudgets = pgTable("flux_budgets", {
|
|
569
|
+
id: varchar("id")
|
|
570
|
+
.primaryKey()
|
|
571
|
+
.default(sql `gen_random_uuid()`),
|
|
572
|
+
projectId: varchar("project_id")
|
|
573
|
+
.references(() => fluxProjects.id, { onDelete: "cascade" })
|
|
574
|
+
.notNull(),
|
|
575
|
+
month: integer("month").notNull(), // 1-12
|
|
576
|
+
year: integer("year").notNull(),
|
|
577
|
+
budgetAmount: integer("budget_amount").notNull(), // cents
|
|
578
|
+
goalAmount: integer("goal_amount"), // revenue goal, cents
|
|
579
|
+
actualSpend: integer("actual_spend").default(0).notNull(), // cents
|
|
580
|
+
source: varchar("source").default("manual").notNull(), // "manual" / "hubspot" / "ninjacat"
|
|
581
|
+
notes: text("notes"),
|
|
582
|
+
updatedById: varchar("updated_by_id").references(() => fluxUsers.id),
|
|
583
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
584
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
585
|
+
}, (table) => ({
|
|
586
|
+
projectIdx: index("flux_budgets_project_idx").on(table.projectId),
|
|
587
|
+
periodIdx: uniqueIndex("flux_budgets_period_idx").on(table.projectId, table.year, table.month),
|
|
588
|
+
}));
|
|
589
|
+
// =============================================================================
|
|
590
|
+
// TASKS (Deliverables / Kanban items)
|
|
591
|
+
// =============================================================================
|
|
592
|
+
export const fluxTasks = pgTable("flux_tasks", {
|
|
593
|
+
id: varchar("id")
|
|
594
|
+
.primaryKey()
|
|
595
|
+
.default(sql `gen_random_uuid()`),
|
|
596
|
+
projectId: varchar("project_id")
|
|
597
|
+
.references(() => fluxProjects.id, { onDelete: "cascade" })
|
|
598
|
+
.notNull(),
|
|
599
|
+
title: varchar("title").notNull(),
|
|
600
|
+
description: text("description"),
|
|
601
|
+
status: fluxTaskStatusEnum("status").default("backlog").notNull(),
|
|
602
|
+
priority: fluxTaskPriorityEnum("priority").default("medium").notNull(),
|
|
603
|
+
assigneeId: varchar("assignee_id").references(() => fluxUsers.id),
|
|
604
|
+
createdById: varchar("created_by_id")
|
|
605
|
+
.references(() => fluxUsers.id)
|
|
606
|
+
.notNull(),
|
|
607
|
+
dueDate: timestamp("due_date"),
|
|
608
|
+
completedAt: timestamp("completed_at"),
|
|
609
|
+
displayOrder: integer("display_order").default(0).notNull(),
|
|
610
|
+
hubspotTaskId: varchar("hubspot_task_id"),
|
|
611
|
+
// Compass provenance (Feature 2)
|
|
612
|
+
compassMetadata: jsonb("compass_metadata").$type(),
|
|
613
|
+
// Risk & ownership (Feature 3)
|
|
614
|
+
blockedByTaskId: varchar("blocked_by_task_id"),
|
|
615
|
+
ownerName: text("owner_name"),
|
|
616
|
+
ownerType: fluxOwnerTypeEnum("owner_type"),
|
|
617
|
+
isInternalOnly: boolean("is_internal_only").default(false).notNull(),
|
|
618
|
+
// Notification tracking (Feature 4)
|
|
619
|
+
lastNotifiedAt: timestamp("last_notified_at"),
|
|
620
|
+
notificationCount: integer("notification_count").default(0).notNull(),
|
|
621
|
+
isDemo: boolean("is_demo").default(false).notNull(),
|
|
622
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
623
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
624
|
+
}, (table) => ({
|
|
625
|
+
projectIdx: index("flux_tasks_project_idx").on(table.projectId),
|
|
626
|
+
statusIdx: index("flux_tasks_status_idx").on(table.status),
|
|
627
|
+
assigneeIdx: index("flux_tasks_assignee_idx").on(table.assigneeId),
|
|
628
|
+
dueDateIdx: index("flux_tasks_due_date_idx").on(table.dueDate),
|
|
629
|
+
blockedByIdx: index("flux_tasks_blocked_by_idx").on(table.blockedByTaskId),
|
|
630
|
+
}));
|
|
631
|
+
// =============================================================================
|
|
632
|
+
// SECRETS MANAGEMENT (API keys, billing, rotation tracking)
|
|
633
|
+
// =============================================================================
|
|
634
|
+
/**
|
|
635
|
+
* External service/vendor accounts. Tracks billing info, payment method, dashboard URLs.
|
|
636
|
+
*/
|
|
637
|
+
export const fluxSecretsProviders = pgTable("flux_secrets_providers", {
|
|
638
|
+
id: varchar("id")
|
|
639
|
+
.primaryKey()
|
|
640
|
+
.default(sql `gen_random_uuid()`),
|
|
641
|
+
name: varchar("name").notNull(),
|
|
642
|
+
category: fluxSecretsProviderCategoryEnum("category").notNull(),
|
|
643
|
+
dashboardUrl: varchar("dashboard_url"),
|
|
644
|
+
billingUrl: varchar("billing_url"),
|
|
645
|
+
paymentMethod: varchar("payment_method"),
|
|
646
|
+
paymentExpiry: date("payment_expiry"),
|
|
647
|
+
accountEmail: varchar("account_email"),
|
|
648
|
+
accountId: varchar("account_id"),
|
|
649
|
+
pricingModel: varchar("pricing_model"),
|
|
650
|
+
monthlyBudgetUsd: integer("monthly_budget_usd"),
|
|
651
|
+
notes: text("notes"),
|
|
652
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
653
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
654
|
+
}, (table) => ({
|
|
655
|
+
categoryIdx: index("flux_secrets_providers_category_idx").on(table.category),
|
|
656
|
+
}));
|
|
657
|
+
/**
|
|
658
|
+
* Individual secrets/keys. Stores only key_prefix (first 8 chars), never full value.
|
|
659
|
+
*/
|
|
660
|
+
export const fluxSecretsRegistry = pgTable("flux_secrets_registry", {
|
|
661
|
+
id: varchar("id")
|
|
662
|
+
.primaryKey()
|
|
663
|
+
.default(sql `gen_random_uuid()`),
|
|
664
|
+
providerId: varchar("provider_id")
|
|
665
|
+
.references(() => fluxSecretsProviders.id, { onDelete: "cascade" })
|
|
666
|
+
.notNull(),
|
|
667
|
+
envVarName: varchar("env_var_name").notNull(),
|
|
668
|
+
description: varchar("description"),
|
|
669
|
+
keyPrefix: varchar("key_prefix"),
|
|
670
|
+
isRequired: boolean("is_required").default(true).notNull(),
|
|
671
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
672
|
+
expiresAt: timestamp("expires_at"),
|
|
673
|
+
lastRotatedAt: timestamp("last_rotated_at"),
|
|
674
|
+
rotationIntervalDays: integer("rotation_interval_days")
|
|
675
|
+
.default(90)
|
|
676
|
+
.notNull(),
|
|
677
|
+
status: fluxSecretsStatusEnum("status").default("active").notNull(),
|
|
678
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
679
|
+
}, (table) => ({
|
|
680
|
+
providerIdx: index("flux_secrets_registry_provider_idx").on(table.providerId),
|
|
681
|
+
statusIdx: index("flux_secrets_registry_status_idx").on(table.status),
|
|
682
|
+
}));
|
|
683
|
+
/**
|
|
684
|
+
* Maps each secret to its deployment targets (Railway services, Vercel projects).
|
|
685
|
+
*/
|
|
686
|
+
export const fluxSecretsDeployments = pgTable("flux_secrets_deployments", {
|
|
687
|
+
id: varchar("id")
|
|
688
|
+
.primaryKey()
|
|
689
|
+
.default(sql `gen_random_uuid()`),
|
|
690
|
+
secretId: varchar("secret_id")
|
|
691
|
+
.references(() => fluxSecretsRegistry.id, { onDelete: "cascade" })
|
|
692
|
+
.notNull(),
|
|
693
|
+
platform: fluxSecretsPlatformEnum("platform").notNull(),
|
|
694
|
+
projectName: varchar("project_name").notNull(),
|
|
695
|
+
projectId: varchar("project_id").notNull(),
|
|
696
|
+
platformEnvironmentId: varchar("platform_environment_id"),
|
|
697
|
+
envVarName: varchar("env_var_name").notNull(),
|
|
698
|
+
environment: varchar("environment").default("production").notNull(),
|
|
699
|
+
lastSyncedAt: timestamp("last_synced_at"),
|
|
700
|
+
syncStatus: fluxSecretsSyncStatusEnum("sync_status")
|
|
701
|
+
.default("unknown")
|
|
702
|
+
.notNull(),
|
|
703
|
+
/** Optional link to the app this deployment belongs to (set null if app deleted). */
|
|
704
|
+
appId: varchar("app_id").references(() => fluxEnvApps.id, { onDelete: "set null" }),
|
|
705
|
+
}, (table) => ({
|
|
706
|
+
secretIdx: index("flux_secrets_deployments_secret_idx").on(table.secretId),
|
|
707
|
+
platformIdx: index("flux_secrets_deployments_platform_idx").on(table.platform),
|
|
708
|
+
}));
|
|
709
|
+
/**
|
|
710
|
+
* Encrypted secret values — ciphertext stored here, decrypted via OVH KMS.
|
|
711
|
+
* Each write creates a new version (append-only).
|
|
712
|
+
*/
|
|
713
|
+
export const fluxSecretsVault = pgTable("flux_secrets_vault", {
|
|
714
|
+
id: varchar("id")
|
|
715
|
+
.primaryKey()
|
|
716
|
+
.default(sql `gen_random_uuid()`),
|
|
717
|
+
/** Vault path (e.g. foundry/fn-legacy/DATABASE_URL) */
|
|
718
|
+
vaultPath: varchar("vault_path").notNull(),
|
|
719
|
+
/** JWE-encrypted value from OVH KMS */
|
|
720
|
+
encryptedValue: text("encrypted_value").notNull(),
|
|
721
|
+
/** Auto-incrementing version per path */
|
|
722
|
+
version: integer("version").notNull().default(1),
|
|
723
|
+
/** KMS key ID used for encryption (for key rotation tracking) */
|
|
724
|
+
kmsKeyId: varchar("kms_key_id").notNull(),
|
|
725
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
726
|
+
}, (table) => ({
|
|
727
|
+
pathVersionIdx: uniqueIndex("flux_secrets_vault_path_version_idx").on(table.vaultPath, table.version),
|
|
728
|
+
pathIdx: index("flux_secrets_vault_path_idx").on(table.vaultPath),
|
|
729
|
+
}));
|
|
730
|
+
/**
|
|
731
|
+
* Immutable audit trail for rotations, syncs, and revocations.
|
|
732
|
+
*/
|
|
733
|
+
export const fluxSecretsAuditLog = pgTable("flux_secrets_audit_log", {
|
|
734
|
+
id: varchar("id")
|
|
735
|
+
.primaryKey()
|
|
736
|
+
.default(sql `gen_random_uuid()`),
|
|
737
|
+
secretId: varchar("secret_id")
|
|
738
|
+
.references(() => fluxSecretsRegistry.id, { onDelete: "cascade" })
|
|
739
|
+
.notNull(),
|
|
740
|
+
action: fluxSecretsAuditActionEnum("action").notNull(),
|
|
741
|
+
performedBy: varchar("performed_by").notNull(),
|
|
742
|
+
details: jsonb("details").$type(),
|
|
743
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
744
|
+
}, (table) => ({
|
|
745
|
+
secretIdx: index("flux_secrets_audit_log_secret_idx").on(table.secretId),
|
|
746
|
+
createdAtIdx: index("flux_secrets_audit_log_created_at_idx").on(table.createdAt),
|
|
747
|
+
}));
|
|
748
|
+
// =============================================================================
|
|
749
|
+
// ENV MANAGER (App-centric env var tracking + completeness)
|
|
750
|
+
// =============================================================================
|
|
751
|
+
/**
|
|
752
|
+
* Registered applications. Each row maps to one `env-manifest.json` file.
|
|
753
|
+
* Stores the platform config and a hash of the last imported manifest so
|
|
754
|
+
* re-imports can detect drift.
|
|
755
|
+
*/
|
|
756
|
+
export const fluxEnvApps = pgTable("flux_envmanager_apps", {
|
|
757
|
+
id: varchar("id")
|
|
758
|
+
.primaryKey()
|
|
759
|
+
.default(sql `gen_random_uuid()`),
|
|
760
|
+
/** URL-safe slug, matches the manifest's `app` field (e.g. "fn-flux"). */
|
|
761
|
+
slug: varchar("slug").notNull(),
|
|
762
|
+
/** Human-readable display name. */
|
|
763
|
+
name: varchar("name").notNull(),
|
|
764
|
+
/** Optional description from the manifest. */
|
|
765
|
+
description: varchar("description"),
|
|
766
|
+
/** Platform deployment configs keyed by platform slug. */
|
|
767
|
+
platforms: jsonb("platforms").$type(),
|
|
768
|
+
/** SHA-256 of the raw manifest JSON — used to skip no-op re-imports. */
|
|
769
|
+
manifestHash: varchar("manifest_hash"),
|
|
770
|
+
/** When the manifest was last successfully imported. */
|
|
771
|
+
manifestImportedAt: timestamp("manifest_imported_at"),
|
|
772
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
773
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
774
|
+
}, (table) => ({
|
|
775
|
+
slugIdx: uniqueIndex("flux_envmanager_apps_slug_idx").on(table.slug),
|
|
776
|
+
}));
|
|
777
|
+
/**
|
|
778
|
+
* Many-to-many join between apps and secrets. Each row says "app X needs
|
|
779
|
+
* secret Y on platforms P with sensitivity S". The `why` column captures the
|
|
780
|
+
* app-specific justification from the manifest, distinct from the secret's
|
|
781
|
+
* own `description`.
|
|
782
|
+
*/
|
|
783
|
+
export const fluxEnvAppVars = pgTable("flux_envmanager_app_vars", {
|
|
784
|
+
id: varchar("id")
|
|
785
|
+
.primaryKey()
|
|
786
|
+
.default(sql `gen_random_uuid()`),
|
|
787
|
+
/** The application that requires this secret. */
|
|
788
|
+
appId: varchar("app_id")
|
|
789
|
+
.references(() => fluxEnvApps.id, { onDelete: "cascade" })
|
|
790
|
+
.notNull(),
|
|
791
|
+
/** The secret being required. */
|
|
792
|
+
secretId: varchar("secret_id")
|
|
793
|
+
.references(() => fluxSecretsRegistry.id, { onDelete: "cascade" })
|
|
794
|
+
.notNull(),
|
|
795
|
+
/** Subset of the app's platforms that consume this var. */
|
|
796
|
+
platforms: jsonb("platforms").$type(),
|
|
797
|
+
/** Whether the app fails to start without this var. */
|
|
798
|
+
required: boolean("required").default(true).notNull(),
|
|
799
|
+
/** Whether the value is a secret (true) or a public config (false). */
|
|
800
|
+
sensitive: boolean("sensitive").default(true).notNull(),
|
|
801
|
+
/** Default value — only meaningful when sensitive=false. */
|
|
802
|
+
defaultValue: varchar("default_value"),
|
|
803
|
+
/** App-specific explanation from the manifest's `why` field. */
|
|
804
|
+
why: varchar("why"),
|
|
805
|
+
/** Optional OVH Secret Manager vault path override. Null = use default convention. */
|
|
806
|
+
vaultPath: varchar("vault_path"),
|
|
807
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
808
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
809
|
+
}, (table) => ({
|
|
810
|
+
appIdx: index("flux_envmanager_app_vars_app_idx").on(table.appId),
|
|
811
|
+
secretIdx: index("flux_envmanager_app_vars_secret_idx").on(table.secretId),
|
|
812
|
+
appSecretIdx: uniqueIndex("flux_envmanager_app_vars_app_secret_idx").on(table.appId, table.secretId),
|
|
813
|
+
}));
|
|
814
|
+
// ---------------------------------------------------------------------------
|
|
815
|
+
// Admin Settings (feature flags)
|
|
816
|
+
// ---------------------------------------------------------------------------
|
|
817
|
+
export const fluxAdminSettings = pgTable("flux_admin_settings", {
|
|
818
|
+
id: varchar("id").primaryKey().default(sql `gen_random_uuid()`),
|
|
819
|
+
key: text("key").notNull().unique(),
|
|
820
|
+
value: jsonb("value").notNull().default(false),
|
|
821
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
822
|
+
updatedBy: text("updated_by"),
|
|
823
|
+
});
|
|
824
|
+
// =============================================================================
|
|
825
|
+
// RELATIONS
|
|
826
|
+
// =============================================================================
|
|
827
|
+
export const fluxUsersRelations = relations(fluxUsers, ({ many }) => ({
|
|
828
|
+
ownedProjects: many(fluxProjects, { relationName: "accountExecutive" }),
|
|
829
|
+
accountManagerProjects: many(fluxProjects, {
|
|
830
|
+
relationName: "accountManager",
|
|
831
|
+
}),
|
|
832
|
+
primaryProjects: many(fluxProjects, { relationName: "primaryStrategist" }),
|
|
833
|
+
secondaryProjects: many(fluxProjects, {
|
|
834
|
+
relationName: "secondaryStrategist",
|
|
835
|
+
}),
|
|
836
|
+
accountCoordinatorProjects: many(fluxProjects, {
|
|
837
|
+
relationName: "accountCoordinator",
|
|
838
|
+
}),
|
|
839
|
+
paidSearchSpecialistProjects: many(fluxProjects, {
|
|
840
|
+
relationName: "paidSearchSpecialist",
|
|
841
|
+
}),
|
|
842
|
+
subscriptions: many(fluxSubscriptions),
|
|
843
|
+
createdTemplates: many(fluxTeamTemplates),
|
|
844
|
+
auditLogs: many(fluxAuditLog),
|
|
845
|
+
assignedTasks: many(fluxTasks, { relationName: "taskAssignee" }),
|
|
846
|
+
createdTasks: many(fluxTasks, { relationName: "taskCreator" }),
|
|
847
|
+
updatedBudgets: many(fluxBudgets),
|
|
848
|
+
}));
|
|
849
|
+
export const fluxProjectsRelations = relations(fluxProjects, ({ one, many }) => ({
|
|
850
|
+
parentProject: one(fluxProjects, {
|
|
851
|
+
fields: [fluxProjects.parentProjectId],
|
|
852
|
+
references: [fluxProjects.id],
|
|
853
|
+
relationName: "projectHierarchy",
|
|
854
|
+
}),
|
|
855
|
+
childProjects: many(fluxProjects, {
|
|
856
|
+
relationName: "projectHierarchy",
|
|
857
|
+
}),
|
|
858
|
+
accountExecutive: one(fluxUsers, {
|
|
859
|
+
fields: [fluxProjects.accountExecutiveId],
|
|
860
|
+
references: [fluxUsers.id],
|
|
861
|
+
relationName: "accountExecutive",
|
|
862
|
+
}),
|
|
863
|
+
accountManager: one(fluxUsers, {
|
|
864
|
+
fields: [fluxProjects.accountManagerId],
|
|
865
|
+
references: [fluxUsers.id],
|
|
866
|
+
relationName: "accountManager",
|
|
867
|
+
}),
|
|
868
|
+
primaryStrategist: one(fluxUsers, {
|
|
869
|
+
fields: [fluxProjects.primaryStrategistId],
|
|
870
|
+
references: [fluxUsers.id],
|
|
871
|
+
relationName: "primaryStrategist",
|
|
872
|
+
}),
|
|
873
|
+
secondaryStrategist: one(fluxUsers, {
|
|
874
|
+
fields: [fluxProjects.secondaryStrategistId],
|
|
875
|
+
references: [fluxUsers.id],
|
|
876
|
+
relationName: "secondaryStrategist",
|
|
877
|
+
}),
|
|
878
|
+
accountCoordinator: one(fluxUsers, {
|
|
879
|
+
fields: [fluxProjects.accountCoordinatorId],
|
|
880
|
+
references: [fluxUsers.id],
|
|
881
|
+
relationName: "accountCoordinator",
|
|
882
|
+
}),
|
|
883
|
+
paidSearchSpecialist: one(fluxUsers, {
|
|
884
|
+
fields: [fluxProjects.paidSearchSpecialistId],
|
|
885
|
+
references: [fluxUsers.id],
|
|
886
|
+
relationName: "paidSearchSpecialist",
|
|
887
|
+
}),
|
|
888
|
+
subscriptions: many(fluxSubscriptions),
|
|
889
|
+
emailThreads: many(fluxEmailThreads),
|
|
890
|
+
activityItems: many(fluxActivityItems),
|
|
891
|
+
alertRules: many(fluxAlertRules),
|
|
892
|
+
alerts: many(fluxAlerts),
|
|
893
|
+
embeddedDashboards: many(fluxEmbeddedDashboards),
|
|
894
|
+
budgets: many(fluxBudgets),
|
|
895
|
+
tasks: many(fluxTasks),
|
|
896
|
+
escalation: many(fluxProjectEscalations),
|
|
897
|
+
escalationHistory: many(fluxProjectEscalationHistory),
|
|
898
|
+
}));
|
|
899
|
+
export const fluxTeamTemplatesRelations = relations(fluxTeamTemplates, ({ one, many }) => ({
|
|
900
|
+
createdBy: one(fluxUsers, {
|
|
901
|
+
fields: [fluxTeamTemplates.createdById],
|
|
902
|
+
references: [fluxUsers.id],
|
|
903
|
+
}),
|
|
904
|
+
members: many(fluxTeamTemplateMembers),
|
|
905
|
+
}));
|
|
906
|
+
export const fluxTeamTemplateMembersRelations = relations(fluxTeamTemplateMembers, ({ one }) => ({
|
|
907
|
+
template: one(fluxTeamTemplates, {
|
|
908
|
+
fields: [fluxTeamTemplateMembers.templateId],
|
|
909
|
+
references: [fluxTeamTemplates.id],
|
|
910
|
+
}),
|
|
911
|
+
user: one(fluxUsers, {
|
|
912
|
+
fields: [fluxTeamTemplateMembers.userId],
|
|
913
|
+
references: [fluxUsers.id],
|
|
914
|
+
}),
|
|
915
|
+
}));
|
|
916
|
+
export const fluxSubscriptionsRelations = relations(fluxSubscriptions, ({ one }) => ({
|
|
917
|
+
project: one(fluxProjects, {
|
|
918
|
+
fields: [fluxSubscriptions.projectId],
|
|
919
|
+
references: [fluxProjects.id],
|
|
920
|
+
}),
|
|
921
|
+
user: one(fluxUsers, {
|
|
922
|
+
fields: [fluxSubscriptions.userId],
|
|
923
|
+
references: [fluxUsers.id],
|
|
924
|
+
}),
|
|
925
|
+
invitedBy: one(fluxUsers, {
|
|
926
|
+
fields: [fluxSubscriptions.invitedById],
|
|
927
|
+
references: [fluxUsers.id],
|
|
928
|
+
}),
|
|
929
|
+
appliedTemplate: one(fluxTeamTemplates, {
|
|
930
|
+
fields: [fluxSubscriptions.appliedTemplateId],
|
|
931
|
+
references: [fluxTeamTemplates.id],
|
|
932
|
+
}),
|
|
933
|
+
}));
|
|
934
|
+
export const fluxEmailThreadsRelations = relations(fluxEmailThreads, ({ one }) => ({
|
|
935
|
+
project: one(fluxProjects, {
|
|
936
|
+
fields: [fluxEmailThreads.projectId],
|
|
937
|
+
references: [fluxProjects.id],
|
|
938
|
+
}),
|
|
939
|
+
reviewedBy: one(fluxUsers, {
|
|
940
|
+
fields: [fluxEmailThreads.reviewedById],
|
|
941
|
+
references: [fluxUsers.id],
|
|
942
|
+
}),
|
|
943
|
+
}));
|
|
944
|
+
export const fluxActivityItemsRelations = relations(fluxActivityItems, ({ one }) => ({
|
|
945
|
+
project: one(fluxProjects, {
|
|
946
|
+
fields: [fluxActivityItems.projectId],
|
|
947
|
+
references: [fluxProjects.id],
|
|
948
|
+
}),
|
|
949
|
+
}));
|
|
950
|
+
export const fluxAlertRulesRelations = relations(fluxAlertRules, ({ one, many }) => ({
|
|
951
|
+
project: one(fluxProjects, {
|
|
952
|
+
fields: [fluxAlertRules.projectId],
|
|
953
|
+
references: [fluxProjects.id],
|
|
954
|
+
}),
|
|
955
|
+
createdBy: one(fluxUsers, {
|
|
956
|
+
fields: [fluxAlertRules.createdById],
|
|
957
|
+
references: [fluxUsers.id],
|
|
958
|
+
}),
|
|
959
|
+
alerts: many(fluxAlerts),
|
|
960
|
+
}));
|
|
961
|
+
export const fluxAlertsRelations = relations(fluxAlerts, ({ one }) => ({
|
|
962
|
+
project: one(fluxProjects, {
|
|
963
|
+
fields: [fluxAlerts.projectId],
|
|
964
|
+
references: [fluxProjects.id],
|
|
965
|
+
}),
|
|
966
|
+
rule: one(fluxAlertRules, {
|
|
967
|
+
fields: [fluxAlerts.ruleId],
|
|
968
|
+
references: [fluxAlertRules.id],
|
|
969
|
+
}),
|
|
970
|
+
acknowledgedBy: one(fluxUsers, {
|
|
971
|
+
fields: [fluxAlerts.acknowledgedById],
|
|
972
|
+
references: [fluxUsers.id],
|
|
973
|
+
}),
|
|
974
|
+
}));
|
|
975
|
+
export const fluxProjectEscalationsRelations = relations(fluxProjectEscalations, ({ one }) => ({
|
|
976
|
+
project: one(fluxProjects, {
|
|
977
|
+
fields: [fluxProjectEscalations.projectId],
|
|
978
|
+
references: [fluxProjects.id],
|
|
979
|
+
}),
|
|
980
|
+
updatedBy: one(fluxUsers, {
|
|
981
|
+
fields: [fluxProjectEscalations.updatedById],
|
|
982
|
+
references: [fluxUsers.id],
|
|
983
|
+
}),
|
|
984
|
+
}));
|
|
985
|
+
export const fluxProjectEscalationHistoryRelations = relations(fluxProjectEscalationHistory, ({ one }) => ({
|
|
986
|
+
project: one(fluxProjects, {
|
|
987
|
+
fields: [fluxProjectEscalationHistory.projectId],
|
|
988
|
+
references: [fluxProjects.id],
|
|
989
|
+
}),
|
|
990
|
+
changedBy: one(fluxUsers, {
|
|
991
|
+
fields: [fluxProjectEscalationHistory.changedById],
|
|
992
|
+
references: [fluxUsers.id],
|
|
993
|
+
}),
|
|
994
|
+
}));
|
|
995
|
+
export const fluxEmbeddedDashboardsRelations = relations(fluxEmbeddedDashboards, ({ one }) => ({
|
|
996
|
+
project: one(fluxProjects, {
|
|
997
|
+
fields: [fluxEmbeddedDashboards.projectId],
|
|
998
|
+
references: [fluxProjects.id],
|
|
999
|
+
}),
|
|
1000
|
+
}));
|
|
1001
|
+
export const fluxAuditLogRelations = relations(fluxAuditLog, ({ one }) => ({
|
|
1002
|
+
user: one(fluxUsers, {
|
|
1003
|
+
fields: [fluxAuditLog.userId],
|
|
1004
|
+
references: [fluxUsers.id],
|
|
1005
|
+
}),
|
|
1006
|
+
}));
|
|
1007
|
+
export const fluxStrategistEventsRelations = relations(fluxStrategistEvents, ({ one }) => ({
|
|
1008
|
+
user: one(fluxUsers, {
|
|
1009
|
+
fields: [fluxStrategistEvents.userId],
|
|
1010
|
+
references: [fluxUsers.id],
|
|
1011
|
+
}),
|
|
1012
|
+
}));
|
|
1013
|
+
export const fluxBudgetsRelations = relations(fluxBudgets, ({ one }) => ({
|
|
1014
|
+
project: one(fluxProjects, {
|
|
1015
|
+
fields: [fluxBudgets.projectId],
|
|
1016
|
+
references: [fluxProjects.id],
|
|
1017
|
+
}),
|
|
1018
|
+
updatedBy: one(fluxUsers, {
|
|
1019
|
+
fields: [fluxBudgets.updatedById],
|
|
1020
|
+
references: [fluxUsers.id],
|
|
1021
|
+
}),
|
|
1022
|
+
}));
|
|
1023
|
+
export const fluxTasksRelations = relations(fluxTasks, ({ one }) => ({
|
|
1024
|
+
project: one(fluxProjects, {
|
|
1025
|
+
fields: [fluxTasks.projectId],
|
|
1026
|
+
references: [fluxProjects.id],
|
|
1027
|
+
}),
|
|
1028
|
+
assignee: one(fluxUsers, {
|
|
1029
|
+
fields: [fluxTasks.assigneeId],
|
|
1030
|
+
references: [fluxUsers.id],
|
|
1031
|
+
relationName: "taskAssignee",
|
|
1032
|
+
}),
|
|
1033
|
+
createdBy: one(fluxUsers, {
|
|
1034
|
+
fields: [fluxTasks.createdById],
|
|
1035
|
+
references: [fluxUsers.id],
|
|
1036
|
+
relationName: "taskCreator",
|
|
1037
|
+
}),
|
|
1038
|
+
blockedBy: one(fluxTasks, {
|
|
1039
|
+
fields: [fluxTasks.blockedByTaskId],
|
|
1040
|
+
references: [fluxTasks.id],
|
|
1041
|
+
relationName: "taskBlockedBy",
|
|
1042
|
+
}),
|
|
1043
|
+
}));
|
|
1044
|
+
export const fluxSecretsProvidersRelations = relations(fluxSecretsProviders, ({ many }) => ({
|
|
1045
|
+
secrets: many(fluxSecretsRegistry),
|
|
1046
|
+
}));
|
|
1047
|
+
export const fluxSecretsRegistryRelations = relations(fluxSecretsRegistry, ({ one, many }) => ({
|
|
1048
|
+
provider: one(fluxSecretsProviders, {
|
|
1049
|
+
fields: [fluxSecretsRegistry.providerId],
|
|
1050
|
+
references: [fluxSecretsProviders.id],
|
|
1051
|
+
}),
|
|
1052
|
+
deployments: many(fluxSecretsDeployments),
|
|
1053
|
+
auditLog: many(fluxSecretsAuditLog),
|
|
1054
|
+
appVars: many(fluxEnvAppVars),
|
|
1055
|
+
}));
|
|
1056
|
+
export const fluxSecretsDeploymentsRelations = relations(fluxSecretsDeployments, ({ one }) => ({
|
|
1057
|
+
secret: one(fluxSecretsRegistry, {
|
|
1058
|
+
fields: [fluxSecretsDeployments.secretId],
|
|
1059
|
+
references: [fluxSecretsRegistry.id],
|
|
1060
|
+
}),
|
|
1061
|
+
app: one(fluxEnvApps, {
|
|
1062
|
+
fields: [fluxSecretsDeployments.appId],
|
|
1063
|
+
references: [fluxEnvApps.id],
|
|
1064
|
+
}),
|
|
1065
|
+
}));
|
|
1066
|
+
export const fluxSecretsVaultRelations = relations(fluxSecretsVault, () => ({}));
|
|
1067
|
+
export const fluxSecretsAuditLogRelations = relations(fluxSecretsAuditLog, ({ one }) => ({
|
|
1068
|
+
secret: one(fluxSecretsRegistry, {
|
|
1069
|
+
fields: [fluxSecretsAuditLog.secretId],
|
|
1070
|
+
references: [fluxSecretsRegistry.id],
|
|
1071
|
+
}),
|
|
1072
|
+
}));
|
|
1073
|
+
export const fluxEnvAppsRelations = relations(fluxEnvApps, ({ many }) => ({
|
|
1074
|
+
appVars: many(fluxEnvAppVars),
|
|
1075
|
+
}));
|
|
1076
|
+
export const fluxEnvAppVarsRelations = relations(fluxEnvAppVars, ({ one }) => ({
|
|
1077
|
+
app: one(fluxEnvApps, {
|
|
1078
|
+
fields: [fluxEnvAppVars.appId],
|
|
1079
|
+
references: [fluxEnvApps.id],
|
|
1080
|
+
}),
|
|
1081
|
+
secret: one(fluxSecretsRegistry, {
|
|
1082
|
+
fields: [fluxEnvAppVars.secretId],
|
|
1083
|
+
references: [fluxSecretsRegistry.id],
|
|
1084
|
+
}),
|
|
1085
|
+
}));
|
|
1086
|
+
// =============================================================================
|
|
1087
|
+
// BRAND NARRATIVES — Team-level messaging baselines for drift detection
|
|
1088
|
+
// =============================================================================
|
|
1089
|
+
export const fluxSentinelTeamEnum = pgEnum("flux_sentinel_team", [
|
|
1090
|
+
"pr_comms",
|
|
1091
|
+
"newsroom",
|
|
1092
|
+
"agency",
|
|
1093
|
+
"philanthropy",
|
|
1094
|
+
"audience",
|
|
1095
|
+
]);
|
|
1096
|
+
// =============================================================================
|
|
1097
|
+
// SENTINEL CAMPAIGNS — Group keywords by PR initiative
|
|
1098
|
+
// =============================================================================
|
|
1099
|
+
export const fluxSentinelCampaigns = pgTable("flux_sentinel_campaigns", {
|
|
1100
|
+
id: text("id")
|
|
1101
|
+
.primaryKey()
|
|
1102
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1103
|
+
name: text("name").notNull(),
|
|
1104
|
+
description: text("description"),
|
|
1105
|
+
startDate: timestamp("start_date", { withTimezone: true }),
|
|
1106
|
+
endDate: timestamp("end_date", { withTimezone: true }),
|
|
1107
|
+
isActive: boolean("is_active").default(true).notNull(),
|
|
1108
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1109
|
+
.defaultNow()
|
|
1110
|
+
.notNull(),
|
|
1111
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1112
|
+
.defaultNow()
|
|
1113
|
+
.notNull(),
|
|
1114
|
+
}, (table) => ({
|
|
1115
|
+
activeIdx: index("flux_sentinel_campaigns_active_idx").on(table.isActive),
|
|
1116
|
+
nameIdx: index("flux_sentinel_campaigns_name_idx").on(table.name),
|
|
1117
|
+
}));
|
|
1118
|
+
// =============================================================================
|
|
1119
|
+
// SENTINEL KEY MESSAGES — Trackable messages for pull-through measurement
|
|
1120
|
+
// =============================================================================
|
|
1121
|
+
export const fluxSentinelMessages = pgTable("flux_sentinel_messages", {
|
|
1122
|
+
id: text("id")
|
|
1123
|
+
.primaryKey()
|
|
1124
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1125
|
+
team: fluxSentinelTeamEnum("team"),
|
|
1126
|
+
label: text("label").notNull(),
|
|
1127
|
+
description: text("description"),
|
|
1128
|
+
isActive: boolean("is_active").default(true).notNull(),
|
|
1129
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1130
|
+
.defaultNow()
|
|
1131
|
+
.notNull(),
|
|
1132
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1133
|
+
.defaultNow()
|
|
1134
|
+
.notNull(),
|
|
1135
|
+
}, (table) => ({
|
|
1136
|
+
activeIdx: index("flux_sentinel_messages_active_idx").on(table.isActive),
|
|
1137
|
+
teamIdx: index("flux_sentinel_messages_team_idx").on(table.team),
|
|
1138
|
+
}));
|
|
1139
|
+
export const fluxBrandNarratives = pgTable("flux_brand_narratives", {
|
|
1140
|
+
id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
|
|
1141
|
+
team: fluxSentinelTeamEnum("team").notNull(),
|
|
1142
|
+
narrative: text("narrative").notNull(),
|
|
1143
|
+
isActive: boolean("is_active").default(true).notNull(),
|
|
1144
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
1145
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
1146
|
+
}, (table) => ({
|
|
1147
|
+
teamIdx: index("flux_brand_narratives_team_idx").on(table.team),
|
|
1148
|
+
activeIdx: index("flux_brand_narratives_active_idx").on(table.isActive),
|
|
1149
|
+
}));
|
|
1150
|
+
// =============================================================================
|
|
1151
|
+
// SENTINEL DIGEST REPORTS — Shareable pre-computed digests
|
|
1152
|
+
// =============================================================================
|
|
1153
|
+
export const fluxSentinelDigestReports = pgTable("flux_sentinel_digest_reports", {
|
|
1154
|
+
id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
|
|
1155
|
+
shareToken: text("share_token").notNull().unique(),
|
|
1156
|
+
period: text("period").notNull(),
|
|
1157
|
+
generatedAt: timestamp("generated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
1158
|
+
dateFrom: timestamp("date_from", { withTimezone: true }).notNull(),
|
|
1159
|
+
dateTo: timestamp("date_to", { withTimezone: true }).notNull(),
|
|
1160
|
+
reportData: jsonb("report_data").notNull().$type(),
|
|
1161
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
1162
|
+
}, (table) => ({
|
|
1163
|
+
shareTokenIdx: uniqueIndex("flux_sentinel_digest_reports_token_idx").on(table.shareToken),
|
|
1164
|
+
generatedAtIdx: index("flux_sentinel_digest_reports_generated_at_idx").on(table.generatedAt),
|
|
1165
|
+
}));
|
|
1166
|
+
// =============================================================================
|
|
1167
|
+
// DONOR SENTINEL — Philanthropy Prospect Discovery & Qualification
|
|
1168
|
+
// =============================================================================
|
|
1169
|
+
export const fluxDonorLeadStageEnum = pgEnum("flux_donor_lead_stage", [
|
|
1170
|
+
"discovered",
|
|
1171
|
+
"researching",
|
|
1172
|
+
"qualified",
|
|
1173
|
+
"priority",
|
|
1174
|
+
"archived",
|
|
1175
|
+
]);
|
|
1176
|
+
export const fluxDonorEntityTypeEnum = pgEnum("flux_donor_entity_type", [
|
|
1177
|
+
"foundation",
|
|
1178
|
+
"individual",
|
|
1179
|
+
"corporate",
|
|
1180
|
+
"daf",
|
|
1181
|
+
]);
|
|
1182
|
+
export const fluxDonorSignalTypeEnum = pgEnum("flux_donor_signal_type", [
|
|
1183
|
+
"grant_announcement",
|
|
1184
|
+
"leadership_change",
|
|
1185
|
+
"rfi",
|
|
1186
|
+
"board_appointment",
|
|
1187
|
+
"mission_statement",
|
|
1188
|
+
"donation",
|
|
1189
|
+
"partnership",
|
|
1190
|
+
"general",
|
|
1191
|
+
]);
|
|
1192
|
+
/**
|
|
1193
|
+
* Donor prospects — entity-centric records consolidating multiple signals.
|
|
1194
|
+
* One person/org across many articles = one prospect.
|
|
1195
|
+
*/
|
|
1196
|
+
export const fluxDonorProspects = pgTable("flux_donor_prospects", {
|
|
1197
|
+
id: text("id")
|
|
1198
|
+
.primaryKey()
|
|
1199
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1200
|
+
name: text("name").notNull(),
|
|
1201
|
+
entityType: fluxDonorEntityTypeEnum("entity_type"),
|
|
1202
|
+
website: text("website"),
|
|
1203
|
+
location: text("location"),
|
|
1204
|
+
focusAreas: text("focus_areas")
|
|
1205
|
+
.array()
|
|
1206
|
+
.default(sql `'{}'::text[]`)
|
|
1207
|
+
.notNull(),
|
|
1208
|
+
leadStage: fluxDonorLeadStageEnum("lead_stage")
|
|
1209
|
+
.default("discovered")
|
|
1210
|
+
.notNull(),
|
|
1211
|
+
score: integer("score").default(0).notNull(),
|
|
1212
|
+
scoreFactors: jsonb("score_factors")
|
|
1213
|
+
.default({})
|
|
1214
|
+
.$type(),
|
|
1215
|
+
notes: text("notes"),
|
|
1216
|
+
contactEmail: text("contact_email"),
|
|
1217
|
+
contactPhone: text("contact_phone"),
|
|
1218
|
+
linkedinUrl: text("linkedin_url"),
|
|
1219
|
+
slackChannelId: text("slack_channel_id"),
|
|
1220
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1221
|
+
.defaultNow()
|
|
1222
|
+
.notNull(),
|
|
1223
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1224
|
+
.defaultNow()
|
|
1225
|
+
.notNull(),
|
|
1226
|
+
}, (table) => ({
|
|
1227
|
+
nameIdx: index("flux_donor_prospects_name_idx").on(table.name),
|
|
1228
|
+
leadStageIdx: index("flux_donor_prospects_lead_stage_idx").on(table.leadStage),
|
|
1229
|
+
scoreIdx: index("flux_donor_prospects_score_idx").on(table.score),
|
|
1230
|
+
entityTypeIdx: index("flux_donor_prospects_entity_type_idx").on(table.entityType),
|
|
1231
|
+
createdAtIdx: index("flux_donor_prospects_created_at_idx").on(table.createdAt),
|
|
1232
|
+
}));
|
|
1233
|
+
/**
|
|
1234
|
+
* Donor signals — article/evidence records linking to prospects.
|
|
1235
|
+
* Mirrors flux_media_mentions shape but with donor-specific fields.
|
|
1236
|
+
*/
|
|
1237
|
+
export const fluxDonorSignals = pgTable("flux_donor_signals", {
|
|
1238
|
+
id: text("id")
|
|
1239
|
+
.primaryKey()
|
|
1240
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1241
|
+
prospectId: text("prospect_id").references(() => fluxDonorProspects.id, {
|
|
1242
|
+
onDelete: "set null",
|
|
1243
|
+
}),
|
|
1244
|
+
keywordId: text("keyword_id")
|
|
1245
|
+
.references(() => fluxSentinelKeywords.id, { onDelete: "cascade" })
|
|
1246
|
+
.notNull(),
|
|
1247
|
+
url: text("url").notNull().unique(),
|
|
1248
|
+
title: text("title").notNull(),
|
|
1249
|
+
author: text("author"),
|
|
1250
|
+
publishedAt: timestamp("published_at", { withTimezone: true }),
|
|
1251
|
+
sourceDomain: text("source_domain"),
|
|
1252
|
+
snippet: text("snippet"),
|
|
1253
|
+
sourceType: text("source_type").default("article").notNull(),
|
|
1254
|
+
signalType: fluxDonorSignalTypeEnum("signal_type")
|
|
1255
|
+
.default("general")
|
|
1256
|
+
.notNull(),
|
|
1257
|
+
relevanceScore: real("relevance_score"),
|
|
1258
|
+
analysisRationale: text("analysis_rationale"),
|
|
1259
|
+
suggestedAction: text("suggested_action"),
|
|
1260
|
+
sentiment: real("sentiment"),
|
|
1261
|
+
extractedEntities: jsonb("extracted_entities")
|
|
1262
|
+
.default([])
|
|
1263
|
+
.$type(),
|
|
1264
|
+
slackMessageTs: text("slack_message_ts"),
|
|
1265
|
+
notifiedAt: timestamp("notified_at", { withTimezone: true }),
|
|
1266
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1267
|
+
.defaultNow()
|
|
1268
|
+
.notNull(),
|
|
1269
|
+
}, (table) => ({
|
|
1270
|
+
urlIdx: uniqueIndex("flux_donor_signals_url_idx").on(table.url),
|
|
1271
|
+
prospectIdx: index("flux_donor_signals_prospect_idx").on(table.prospectId),
|
|
1272
|
+
keywordIdx: index("flux_donor_signals_keyword_idx").on(table.keywordId),
|
|
1273
|
+
signalTypeIdx: index("flux_donor_signals_signal_type_idx").on(table.signalType),
|
|
1274
|
+
createdAtIdx: index("flux_donor_signals_created_at_idx").on(table.createdAt),
|
|
1275
|
+
}));
|
|
1276
|
+
/**
|
|
1277
|
+
* Donor scan run log — tracks each donor pipeline cycle for observability.
|
|
1278
|
+
*/
|
|
1279
|
+
export const fluxDonorScanRuns = pgTable("flux_donor_scan_runs", {
|
|
1280
|
+
id: text("id")
|
|
1281
|
+
.primaryKey()
|
|
1282
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1283
|
+
runAt: timestamp("run_at", { withTimezone: true }).defaultNow().notNull(),
|
|
1284
|
+
triggeredBy: text("triggered_by").notNull(),
|
|
1285
|
+
keywordsScanned: integer("keywords_scanned").default(0).notNull(),
|
|
1286
|
+
signalsFound: integer("signals_found").default(0).notNull(),
|
|
1287
|
+
signalsNew: integer("signals_new").default(0).notNull(),
|
|
1288
|
+
prospectsCreated: integer("prospects_created").default(0).notNull(),
|
|
1289
|
+
alertsSent: integer("alerts_sent").default(0).notNull(),
|
|
1290
|
+
durationMs: integer("duration_ms"),
|
|
1291
|
+
error: text("error"),
|
|
1292
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1293
|
+
.defaultNow()
|
|
1294
|
+
.notNull(),
|
|
1295
|
+
}, (table) => ({
|
|
1296
|
+
runAtIdx: index("flux_donor_scan_runs_run_at_idx").on(table.runAt),
|
|
1297
|
+
}));
|
|
1298
|
+
// Donor relations
|
|
1299
|
+
export const fluxDonorProspectsRelations = relations(fluxDonorProspects, ({ many }) => ({
|
|
1300
|
+
signals: many(fluxDonorSignals),
|
|
1301
|
+
}));
|
|
1302
|
+
export const fluxDonorSignalsRelations = relations(fluxDonorSignals, ({ one }) => ({
|
|
1303
|
+
prospect: one(fluxDonorProspects, {
|
|
1304
|
+
fields: [fluxDonorSignals.prospectId],
|
|
1305
|
+
references: [fluxDonorProspects.id],
|
|
1306
|
+
}),
|
|
1307
|
+
keyword: one(fluxSentinelKeywords, {
|
|
1308
|
+
fields: [fluxDonorSignals.keywordId],
|
|
1309
|
+
references: [fluxSentinelKeywords.id],
|
|
1310
|
+
}),
|
|
1311
|
+
}));
|
|
1312
|
+
// =============================================================================
|
|
1313
|
+
// LEGACY EVENT INGEST (tracking table for events from Compass)
|
|
1314
|
+
// =============================================================================
|
|
1315
|
+
export const fluxLegacyEventIngest = pgTable("flux_legacy_event_ingest", {
|
|
1316
|
+
id: text("id").primaryKey(),
|
|
1317
|
+
eventId: text("event_id").notNull(),
|
|
1318
|
+
eventType: text("event_type").notNull(),
|
|
1319
|
+
idempotencyKey: text("idempotency_key").notNull().unique(),
|
|
1320
|
+
emittedAt: timestamp("emitted_at", { withTimezone: true }).notNull(),
|
|
1321
|
+
actorUserId: text("actor_user_id"),
|
|
1322
|
+
actorUsername: text("actor_username"),
|
|
1323
|
+
payload: jsonb("payload").notNull(),
|
|
1324
|
+
receivedAt: timestamp("received_at", { withTimezone: true })
|
|
1325
|
+
.defaultNow()
|
|
1326
|
+
.notNull(),
|
|
1327
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1328
|
+
.defaultNow()
|
|
1329
|
+
.notNull(),
|
|
1330
|
+
}, (table) => ({
|
|
1331
|
+
typeTimeIdx: index("idx_flux_legacy_event_ingest_type_time").on(table.eventType, table.emittedAt),
|
|
1332
|
+
}));
|
|
1333
|
+
// =============================================================================
|
|
1334
|
+
// COMPASS SNAPSHOTS (Projected state from Compass events)
|
|
1335
|
+
// =============================================================================
|
|
1336
|
+
export const fluxCompassOrderStatusEnum = pgEnum("flux_compass_order_status", [
|
|
1337
|
+
"draft",
|
|
1338
|
+
"pending_approval",
|
|
1339
|
+
"approved",
|
|
1340
|
+
"sent",
|
|
1341
|
+
"active",
|
|
1342
|
+
"completed",
|
|
1343
|
+
]);
|
|
1344
|
+
export const fluxCompassHoldStatusEnum = pgEnum("flux_compass_hold_status", [
|
|
1345
|
+
"tentative",
|
|
1346
|
+
"confirmed",
|
|
1347
|
+
"released",
|
|
1348
|
+
"expired",
|
|
1349
|
+
]);
|
|
1350
|
+
/**
|
|
1351
|
+
* Current state of all Compass media orders.
|
|
1352
|
+
* Upserted on every order.created / order.status_changed event.
|
|
1353
|
+
*/
|
|
1354
|
+
export const fluxCompassOrderSnapshots = pgTable("flux_compass_order_snapshots", {
|
|
1355
|
+
id: text("id")
|
|
1356
|
+
.primaryKey()
|
|
1357
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1358
|
+
compassOrderId: text("compass_order_id").notNull().unique(),
|
|
1359
|
+
projectId: text("project_id").references(() => fluxProjects.id, {
|
|
1360
|
+
onDelete: "set null",
|
|
1361
|
+
}),
|
|
1362
|
+
hubspotDealId: text("hubspot_deal_id"),
|
|
1363
|
+
hubspotCompanyId: text("hubspot_company_id"),
|
|
1364
|
+
clientName: text("client_name"),
|
|
1365
|
+
status: fluxCompassOrderStatusEnum("status").default("draft").notNull(),
|
|
1366
|
+
totalInvestment: numeric("total_investment"),
|
|
1367
|
+
ownerEmail: text("owner_email"),
|
|
1368
|
+
ownerName: text("owner_name"),
|
|
1369
|
+
lastStatusChangeAt: timestamp("last_status_change_at", {
|
|
1370
|
+
withTimezone: true,
|
|
1371
|
+
}),
|
|
1372
|
+
lastEventAt: timestamp("last_event_at", { withTimezone: true })
|
|
1373
|
+
.defaultNow()
|
|
1374
|
+
.notNull(),
|
|
1375
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1376
|
+
.defaultNow()
|
|
1377
|
+
.notNull(),
|
|
1378
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1379
|
+
.defaultNow()
|
|
1380
|
+
.notNull(),
|
|
1381
|
+
}, (table) => ({
|
|
1382
|
+
compassOrderIdx: uniqueIndex("flux_compass_order_snapshots_order_idx").on(table.compassOrderId),
|
|
1383
|
+
projectIdx: index("flux_compass_order_snapshots_project_idx").on(table.projectId),
|
|
1384
|
+
statusIdx: index("flux_compass_order_snapshots_status_idx").on(table.status),
|
|
1385
|
+
ownerIdx: index("flux_compass_order_snapshots_owner_idx").on(table.ownerEmail),
|
|
1386
|
+
}));
|
|
1387
|
+
/**
|
|
1388
|
+
* Current state of Compass inventory holds.
|
|
1389
|
+
* Upserted on every hold.created / hold.confirmed / hold.released event.
|
|
1390
|
+
*/
|
|
1391
|
+
export const fluxCompassHoldSnapshots = pgTable("flux_compass_hold_snapshots", {
|
|
1392
|
+
id: text("id")
|
|
1393
|
+
.primaryKey()
|
|
1394
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1395
|
+
compassHoldId: text("compass_hold_id").notNull().unique(),
|
|
1396
|
+
compassOrderId: text("compass_order_id"),
|
|
1397
|
+
projectId: text("project_id").references(() => fluxProjects.id, {
|
|
1398
|
+
onDelete: "set null",
|
|
1399
|
+
}),
|
|
1400
|
+
productId: text("product_id"),
|
|
1401
|
+
holdDate: date("hold_date"),
|
|
1402
|
+
holdEndDate: date("hold_end_date"),
|
|
1403
|
+
status: fluxCompassHoldStatusEnum("status").default("tentative").notNull(),
|
|
1404
|
+
clientName: text("client_name"),
|
|
1405
|
+
lastEventAt: timestamp("last_event_at", { withTimezone: true })
|
|
1406
|
+
.defaultNow()
|
|
1407
|
+
.notNull(),
|
|
1408
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1409
|
+
.defaultNow()
|
|
1410
|
+
.notNull(),
|
|
1411
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1412
|
+
.defaultNow()
|
|
1413
|
+
.notNull(),
|
|
1414
|
+
}, (table) => ({
|
|
1415
|
+
compassHoldIdx: uniqueIndex("flux_compass_hold_snapshots_hold_idx").on(table.compassHoldId),
|
|
1416
|
+
orderIdx: index("flux_compass_hold_snapshots_order_idx").on(table.compassOrderId),
|
|
1417
|
+
statusIdx: index("flux_compass_hold_snapshots_status_idx").on(table.status),
|
|
1418
|
+
}));
|
|
1419
|
+
// =============================================================================
|
|
1420
|
+
// DELIVERY MONITORING (GAM Campaign Delivery Tracking)
|
|
1421
|
+
// =============================================================================
|
|
1422
|
+
export const fluxDeliveryReportConfig = pgTable("flux_delivery_report_config", {
|
|
1423
|
+
id: text("id")
|
|
1424
|
+
.primaryKey()
|
|
1425
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1426
|
+
enabled: boolean("enabled").default(true).notNull(),
|
|
1427
|
+
scheduleCadence: text("schedule_cadence").default("daily").notNull(),
|
|
1428
|
+
scheduleTimeUtc: time("schedule_time_utc").default("08:00:00").notNull(),
|
|
1429
|
+
scheduleDays: integer("schedule_days").array(),
|
|
1430
|
+
warningThresholdPercent: numeric("warning_threshold_percent")
|
|
1431
|
+
.default("85")
|
|
1432
|
+
.notNull(),
|
|
1433
|
+
criticalThresholdPercent: numeric("critical_threshold_percent")
|
|
1434
|
+
.default("70")
|
|
1435
|
+
.notNull(),
|
|
1436
|
+
daysUntilEndEscalate: integer("days_until_end_escalate").default(7).notNull(),
|
|
1437
|
+
campaignTagFilters: text("campaign_tag_filters").array(),
|
|
1438
|
+
advertiserNamePatterns: text("advertiser_name_patterns").array(),
|
|
1439
|
+
minBudgetFilter: numeric("min_budget_filter"),
|
|
1440
|
+
emailRecipients: text("email_recipients").array(),
|
|
1441
|
+
slackChannel: text("slack_channel").default("").notNull(),
|
|
1442
|
+
slackChannelId: text("slack_channel_id"),
|
|
1443
|
+
emailFormat: text("email_format").default("csv").notNull(),
|
|
1444
|
+
includeRecommendations: boolean("include_recommendations")
|
|
1445
|
+
.default(true)
|
|
1446
|
+
.notNull(),
|
|
1447
|
+
hubspotDealCustomField: text("hubspot_deal_custom_field"),
|
|
1448
|
+
fallbackNameMatching: boolean("fallback_name_matching")
|
|
1449
|
+
.default(true)
|
|
1450
|
+
.notNull(),
|
|
1451
|
+
postMortemGoalPercent: numeric("post_mortem_goal_percent").default("95"),
|
|
1452
|
+
endingSoonDays: integer("ending_soon_days").default(7),
|
|
1453
|
+
endingThisMonthDays: integer("ending_this_month_days").default(14),
|
|
1454
|
+
recentlyEndedDays: integer("recently_ended_days").default(14),
|
|
1455
|
+
dmEscalationThreshold: text("dm_escalation_threshold").default("critical"),
|
|
1456
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1457
|
+
.defaultNow()
|
|
1458
|
+
.notNull(),
|
|
1459
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1460
|
+
.defaultNow()
|
|
1461
|
+
.notNull(),
|
|
1462
|
+
});
|
|
1463
|
+
export const fluxDeliveryReports = pgTable("flux_delivery_reports", {
|
|
1464
|
+
id: text("id")
|
|
1465
|
+
.primaryKey()
|
|
1466
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1467
|
+
runAt: timestamp("run_at", { withTimezone: true }).defaultNow().notNull(),
|
|
1468
|
+
triggeredBy: text("triggered_by").notNull(),
|
|
1469
|
+
totalCampaigns: integer("total_campaigns").default(0).notNull(),
|
|
1470
|
+
atRiskCount: integer("at_risk_count").default(0).notNull(),
|
|
1471
|
+
criticalCount: integer("critical_count").default(0).notNull(),
|
|
1472
|
+
postMortemCount: integer("post_mortem_count").default(0).notNull(),
|
|
1473
|
+
configSnapshot: jsonb("config_snapshot"),
|
|
1474
|
+
errorMessage: text("error_message"),
|
|
1475
|
+
status: text("status").default("success").notNull(),
|
|
1476
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1477
|
+
.defaultNow()
|
|
1478
|
+
.notNull(),
|
|
1479
|
+
}, (table) => ({
|
|
1480
|
+
runAtIdx: index("idx_flux_delivery_reports_run_at").on(table.runAt),
|
|
1481
|
+
}));
|
|
1482
|
+
export const fluxDeliveryLineItems = pgTable("flux_delivery_line_items", {
|
|
1483
|
+
id: text("id")
|
|
1484
|
+
.primaryKey()
|
|
1485
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1486
|
+
reportId: text("report_id")
|
|
1487
|
+
.references(() => fluxDeliveryReports.id, { onDelete: "cascade" })
|
|
1488
|
+
.notNull(),
|
|
1489
|
+
projectId: text("project_id").references(() => fluxProjects.id, {
|
|
1490
|
+
onDelete: "set null",
|
|
1491
|
+
}),
|
|
1492
|
+
gamOrderId: text("gam_order_id").notNull(),
|
|
1493
|
+
gamLineItemId: text("gam_line_item_id").notNull(),
|
|
1494
|
+
advertiserName: text("advertiser_name").notNull(),
|
|
1495
|
+
lineItemName: text("line_item_name").notNull(),
|
|
1496
|
+
orderName: text("order_name").notNull(),
|
|
1497
|
+
impressionsContracted: bigint("impressions_contracted", { mode: "number" })
|
|
1498
|
+
.notNull(),
|
|
1499
|
+
impressionsDelivered: bigint("impressions_delivered", { mode: "number" })
|
|
1500
|
+
.notNull(),
|
|
1501
|
+
deliveryPercent: numeric("delivery_percent").notNull(),
|
|
1502
|
+
startDate: date("start_date").notNull(),
|
|
1503
|
+
endDate: date("end_date").notNull(),
|
|
1504
|
+
daysRemaining: integer("days_remaining"),
|
|
1505
|
+
riskLevel: text("risk_level"),
|
|
1506
|
+
recommendedAction: text("recommended_action"),
|
|
1507
|
+
mappingMethod: text("mapping_method"),
|
|
1508
|
+
mappingConfidence: numeric("mapping_confidence"),
|
|
1509
|
+
timeHorizon: text("time_horizon"),
|
|
1510
|
+
postMortemStatus: text("post_mortem_status"),
|
|
1511
|
+
ownerUserId: text("owner_user_id"),
|
|
1512
|
+
ownerSlackId: text("owner_slack_id"),
|
|
1513
|
+
ownerName: text("owner_name"),
|
|
1514
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1515
|
+
.defaultNow()
|
|
1516
|
+
.notNull(),
|
|
1517
|
+
}, (table) => ({
|
|
1518
|
+
reportIdx: index("idx_flux_delivery_line_items_report").on(table.reportId),
|
|
1519
|
+
projectIdx: index("idx_flux_delivery_line_items_project").on(table.projectId),
|
|
1520
|
+
riskIdx: index("idx_flux_delivery_line_items_risk").on(table.riskLevel),
|
|
1521
|
+
uniqueLineItem: uniqueIndex("flux_delivery_line_items_unique").on(table.reportId, table.gamLineItemId),
|
|
1522
|
+
}));
|
|
1523
|
+
export const fluxDeliveryProjectMappings = pgTable("flux_delivery_project_mappings", {
|
|
1524
|
+
id: text("id")
|
|
1525
|
+
.primaryKey()
|
|
1526
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1527
|
+
advertiserNamePattern: text("advertiser_name_pattern").notNull().unique(),
|
|
1528
|
+
projectId: text("project_id")
|
|
1529
|
+
.references(() => fluxProjects.id, { onDelete: "cascade" })
|
|
1530
|
+
.notNull(),
|
|
1531
|
+
createdBy: text("created_by").notNull(),
|
|
1532
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1533
|
+
.defaultNow()
|
|
1534
|
+
.notNull(),
|
|
1535
|
+
});
|
|
1536
|
+
// =============================================================================
|
|
1537
|
+
// ZIP TARGETING (ZIP radius resolution + platform export)
|
|
1538
|
+
// =============================================================================
|
|
1539
|
+
/** US ZIP centroid reference data used for radius resolution. */
|
|
1540
|
+
export const fluxZipGeo = pgTable("flux_zip_geo", {
|
|
1541
|
+
zip: text("zip").primaryKey(),
|
|
1542
|
+
lat: numeric("lat", { precision: 10, scale: 6 }).notNull(),
|
|
1543
|
+
lng: numeric("lng", { precision: 10, scale: 6 }).notNull(),
|
|
1544
|
+
city: text("city"),
|
|
1545
|
+
state: text("state"),
|
|
1546
|
+
countryCode: text("country_code").default("US").notNull(),
|
|
1547
|
+
population: integer("population"),
|
|
1548
|
+
households: integer("households"),
|
|
1549
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1550
|
+
.defaultNow()
|
|
1551
|
+
.notNull(),
|
|
1552
|
+
}, (table) => ({
|
|
1553
|
+
stateIdx: index("flux_zip_geo_state_idx").on(table.state),
|
|
1554
|
+
countryIdx: index("flux_zip_geo_country_idx").on(table.countryCode),
|
|
1555
|
+
}));
|
|
1556
|
+
/** Google geo-target constants for ZIP/postal code exports. */
|
|
1557
|
+
export const fluxGoogleGeoTargets = pgTable("flux_google_geo_targets", {
|
|
1558
|
+
criterionId: text("criterion_id").primaryKey(),
|
|
1559
|
+
resourceName: text("resource_name"),
|
|
1560
|
+
targetType: text("target_type"),
|
|
1561
|
+
canonicalName: text("canonical_name"),
|
|
1562
|
+
postalCode: text("postal_code"),
|
|
1563
|
+
countryCode: text("country_code").default("US").notNull(),
|
|
1564
|
+
status: text("status"),
|
|
1565
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1566
|
+
.defaultNow()
|
|
1567
|
+
.notNull(),
|
|
1568
|
+
}, (table) => ({
|
|
1569
|
+
postalIdx: index("flux_google_geo_targets_postal_idx").on(table.postalCode, table.countryCode),
|
|
1570
|
+
statusIdx: index("flux_google_geo_targets_status_idx").on(table.status),
|
|
1571
|
+
}));
|
|
1572
|
+
/** Saved ZIP radius target sets by project/ticket. */
|
|
1573
|
+
export const fluxZipTargetSets = pgTable("flux_zip_target_sets", {
|
|
1574
|
+
id: text("id")
|
|
1575
|
+
.primaryKey()
|
|
1576
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1577
|
+
projectId: text("project_id")
|
|
1578
|
+
.references(() => fluxProjects.id, { onDelete: "cascade" })
|
|
1579
|
+
.notNull(),
|
|
1580
|
+
fulfillmentTicketId: text("fulfillment_ticket_id").references(() => fluxFulfillmentTickets.id, { onDelete: "set null" }),
|
|
1581
|
+
mode: text("mode").notNull().default("radius"),
|
|
1582
|
+
label: text("label"),
|
|
1583
|
+
centerZip: text("center_zip").notNull(),
|
|
1584
|
+
radiusMiles: numeric("radius_miles", { precision: 8, scale: 2 }).notNull(),
|
|
1585
|
+
centerLat: numeric("center_lat", { precision: 10, scale: 6 }).notNull(),
|
|
1586
|
+
centerLng: numeric("center_lng", { precision: 10, scale: 6 }).notNull(),
|
|
1587
|
+
zipCount: integer("zip_count").notNull(),
|
|
1588
|
+
zipCodes: jsonb("zip_codes").$type().notNull(),
|
|
1589
|
+
boundaryGeoJson: jsonb("boundary_geojson"),
|
|
1590
|
+
createdById: text("created_by_id")
|
|
1591
|
+
.references(() => fluxUsers.id)
|
|
1592
|
+
.notNull(),
|
|
1593
|
+
isActive: boolean("is_active").default(true).notNull(),
|
|
1594
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1595
|
+
.defaultNow()
|
|
1596
|
+
.notNull(),
|
|
1597
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1598
|
+
.defaultNow()
|
|
1599
|
+
.notNull(),
|
|
1600
|
+
}, (table) => ({
|
|
1601
|
+
projectIdx: index("flux_zip_target_sets_project_idx").on(table.projectId),
|
|
1602
|
+
ticketIdx: index("flux_zip_target_sets_ticket_idx").on(table.fulfillmentTicketId),
|
|
1603
|
+
createdAtIdx: index("flux_zip_target_sets_created_at_idx").on(table.createdAt),
|
|
1604
|
+
}));
|
|
1605
|
+
/** Export history for auditability. */
|
|
1606
|
+
export const fluxZipTargetExports = pgTable("flux_zip_target_exports", {
|
|
1607
|
+
id: text("id")
|
|
1608
|
+
.primaryKey()
|
|
1609
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1610
|
+
targetSetId: text("target_set_id")
|
|
1611
|
+
.references(() => fluxZipTargetSets.id, { onDelete: "cascade" })
|
|
1612
|
+
.notNull(),
|
|
1613
|
+
platform: text("platform").notNull(),
|
|
1614
|
+
rowCount: integer("row_count").notNull(),
|
|
1615
|
+
exportedById: text("exported_by_id")
|
|
1616
|
+
.references(() => fluxUsers.id)
|
|
1617
|
+
.notNull(),
|
|
1618
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1619
|
+
.defaultNow()
|
|
1620
|
+
.notNull(),
|
|
1621
|
+
}, (table) => ({
|
|
1622
|
+
targetSetIdx: index("flux_zip_target_exports_target_set_idx").on(table.targetSetId),
|
|
1623
|
+
platformIdx: index("flux_zip_target_exports_platform_idx").on(table.platform),
|
|
1624
|
+
createdAtIdx: index("flux_zip_target_exports_created_at_idx").on(table.createdAt),
|
|
1625
|
+
}));
|
|
1626
|
+
// =============================================================================
|
|
1627
|
+
// SENTINEL INVITES — Invite-only access for sentinel-only users
|
|
1628
|
+
// =============================================================================
|
|
1629
|
+
export const fluxSentinelInvites = pgTable("flux_sentinel_invites", {
|
|
1630
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
1631
|
+
email: varchar("email", { length: 255 }).notNull(),
|
|
1632
|
+
invitedBy: varchar("invited_by").references(() => fluxUsers.id),
|
|
1633
|
+
token: varchar("token", { length: 64 }).notNull().unique(),
|
|
1634
|
+
acceptedAt: timestamp("accepted_at"),
|
|
1635
|
+
expiresAt: timestamp("expires_at")
|
|
1636
|
+
.notNull()
|
|
1637
|
+
.default(sql `NOW() + INTERVAL '7 days'`),
|
|
1638
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
1639
|
+
}, (table) => ({
|
|
1640
|
+
tokenIdx: index("idx_sentinel_invites_token").on(table.token),
|
|
1641
|
+
emailIdx: index("idx_sentinel_invites_email").on(table.email),
|
|
1642
|
+
}));
|
|
1643
|
+
export const fluxSentinelInvitesRelations = relations(fluxSentinelInvites, ({ one }) => ({
|
|
1644
|
+
inviter: one(fluxUsers, {
|
|
1645
|
+
fields: [fluxSentinelInvites.invitedBy],
|
|
1646
|
+
references: [fluxUsers.id],
|
|
1647
|
+
}),
|
|
1648
|
+
}));
|
|
1649
|
+
// =============================================================================
|
|
1650
|
+
// PR SENTINEL — Media Monitoring & Crisis Detection
|
|
1651
|
+
// =============================================================================
|
|
1652
|
+
export const fluxSentinelSeverityEnum = pgEnum("flux_sentinel_severity", [
|
|
1653
|
+
"routine",
|
|
1654
|
+
"notable",
|
|
1655
|
+
"urgent",
|
|
1656
|
+
"crisis",
|
|
1657
|
+
]);
|
|
1658
|
+
/**
|
|
1659
|
+
* Configurable search keywords/topics for the PR Sentinel.
|
|
1660
|
+
* Each keyword group can target specific Slack channels for alerts.
|
|
1661
|
+
*/
|
|
1662
|
+
export const fluxSentinelKeywords = pgTable("flux_sentinel_keywords", {
|
|
1663
|
+
id: text("id")
|
|
1664
|
+
.primaryKey()
|
|
1665
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1666
|
+
label: text("label").notNull(),
|
|
1667
|
+
query: text("query").notNull(),
|
|
1668
|
+
isActive: boolean("is_active").default(true).notNull(),
|
|
1669
|
+
searchSocial: boolean("search_social").default(false).notNull(),
|
|
1670
|
+
socialPlatforms: text("social_platforms")
|
|
1671
|
+
.array()
|
|
1672
|
+
.default(sql `'{}'::text[]`)
|
|
1673
|
+
.notNull(),
|
|
1674
|
+
slackChannelId: text("slack_channel_id"),
|
|
1675
|
+
team: fluxSentinelTeamEnum("team"),
|
|
1676
|
+
/** Keyword role: self (brand), competitor, or topic */
|
|
1677
|
+
keywordRole: text("keyword_role").default("self").notNull(),
|
|
1678
|
+
/** For competitor keywords — the competitor's name */
|
|
1679
|
+
competitorName: text("competitor_name"),
|
|
1680
|
+
/** Optional campaign grouping */
|
|
1681
|
+
campaignId: text("campaign_id").references(() => fluxSentinelCampaigns.id, {
|
|
1682
|
+
onDelete: "set null",
|
|
1683
|
+
}),
|
|
1684
|
+
/** Trigify workflow configuration tracking */
|
|
1685
|
+
trigifyConfigured: boolean("trigify_configured").default(false).notNull(),
|
|
1686
|
+
trigifyConfiguredAt: timestamp("trigify_configured_at", { withTimezone: true }),
|
|
1687
|
+
trigifyConfiguredBy: text("trigify_configured_by"),
|
|
1688
|
+
/** AI-generated Trigify query: { andKeywords, orKeywords, notKeywords, goal, platform, generatedFrom } */
|
|
1689
|
+
trigifyQuery: jsonb("trigify_query"),
|
|
1690
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1691
|
+
.defaultNow()
|
|
1692
|
+
.notNull(),
|
|
1693
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1694
|
+
.defaultNow()
|
|
1695
|
+
.notNull(),
|
|
1696
|
+
}, (table) => ({
|
|
1697
|
+
activeIdx: index("flux_sentinel_keywords_active_idx").on(table.isActive),
|
|
1698
|
+
roleIdx: index("flux_sentinel_keywords_role_idx").on(table.keywordRole),
|
|
1699
|
+
}));
|
|
1700
|
+
/**
|
|
1701
|
+
* Discovered media mentions. Deduplication by URL.
|
|
1702
|
+
* Stores Exa search results with LLM-derived severity classification.
|
|
1703
|
+
*/
|
|
1704
|
+
export const fluxMediaMentions = pgTable("flux_media_mentions", {
|
|
1705
|
+
id: text("id")
|
|
1706
|
+
.primaryKey()
|
|
1707
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1708
|
+
keywordId: text("keyword_id")
|
|
1709
|
+
.references(() => fluxSentinelKeywords.id, { onDelete: "cascade" })
|
|
1710
|
+
.notNull(),
|
|
1711
|
+
url: text("url").notNull().unique(),
|
|
1712
|
+
title: text("title").notNull(),
|
|
1713
|
+
author: text("author"),
|
|
1714
|
+
publishedAt: timestamp("published_at", { withTimezone: true }),
|
|
1715
|
+
sourceDomain: text("source_domain"),
|
|
1716
|
+
snippet: text("snippet"),
|
|
1717
|
+
severity: fluxSentinelSeverityEnum("severity").default("routine").notNull(),
|
|
1718
|
+
analysisRationale: text("analysis_rationale"),
|
|
1719
|
+
suggestedAction: text("suggested_action"),
|
|
1720
|
+
isMisinformation: boolean("is_misinformation").default(false).notNull(),
|
|
1721
|
+
sentiment: real("sentiment"),
|
|
1722
|
+
sourceType: text("source_type").default("article").notNull(),
|
|
1723
|
+
thumbnailUrl: text("thumbnail_url"),
|
|
1724
|
+
thumbnailCredit: text("thumbnail_credit"),
|
|
1725
|
+
narrativeDrift: boolean("narrative_drift").default(false).notNull(),
|
|
1726
|
+
driftSummary: text("drift_summary"),
|
|
1727
|
+
slackMessageTs: text("slack_message_ts"),
|
|
1728
|
+
notifiedAt: timestamp("notified_at", { withTimezone: true }),
|
|
1729
|
+
/** Per-article message pull-through scores from LLM analysis */
|
|
1730
|
+
messageScores: jsonb("message_scores").$type(),
|
|
1731
|
+
/** Social engagement metrics from Trigify (likes, comments, shares, views) */
|
|
1732
|
+
engagement: jsonb("engagement").$type(),
|
|
1733
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1734
|
+
.defaultNow()
|
|
1735
|
+
.notNull(),
|
|
1736
|
+
}, (table) => ({
|
|
1737
|
+
urlIdx: uniqueIndex("flux_media_mentions_url_idx").on(table.url),
|
|
1738
|
+
keywordIdx: index("flux_media_mentions_keyword_idx").on(table.keywordId),
|
|
1739
|
+
severityIdx: index("flux_media_mentions_severity_idx").on(table.severity),
|
|
1740
|
+
sourceTypeIdx: index("flux_media_mentions_source_type_idx").on(table.sourceType),
|
|
1741
|
+
createdAtIdx: index("flux_media_mentions_created_at_idx").on(table.createdAt),
|
|
1742
|
+
}));
|
|
1743
|
+
/**
|
|
1744
|
+
* Cached journalist contact info from Hunter.io lookups.
|
|
1745
|
+
* Avoids redundant API calls for known journalists.
|
|
1746
|
+
*/
|
|
1747
|
+
export const fluxJournalistContacts = pgTable("flux_journalist_contacts", {
|
|
1748
|
+
id: text("id")
|
|
1749
|
+
.primaryKey()
|
|
1750
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1751
|
+
name: text("name").notNull(),
|
|
1752
|
+
email: text("email"),
|
|
1753
|
+
domain: text("domain"),
|
|
1754
|
+
twitterHandle: text("twitter_handle"),
|
|
1755
|
+
linkedinUrl: text("linkedin_url"),
|
|
1756
|
+
confidence: integer("confidence"),
|
|
1757
|
+
lastVerifiedAt: timestamp("last_verified_at", { withTimezone: true }),
|
|
1758
|
+
twitterFollowers: integer("twitter_followers"),
|
|
1759
|
+
twitterBio: text("twitter_bio"),
|
|
1760
|
+
twitterVerified: boolean("twitter_verified"),
|
|
1761
|
+
twitterProfileImage: text("twitter_profile_image"),
|
|
1762
|
+
socialEnrichedAt: timestamp("social_enriched_at", { withTimezone: true }),
|
|
1763
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1764
|
+
.defaultNow()
|
|
1765
|
+
.notNull(),
|
|
1766
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1767
|
+
.defaultNow()
|
|
1768
|
+
.notNull(),
|
|
1769
|
+
}, (table) => ({
|
|
1770
|
+
nameIdx: index("flux_journalist_contacts_name_idx").on(table.name),
|
|
1771
|
+
domainIdx: index("flux_journalist_contacts_domain_idx").on(table.domain),
|
|
1772
|
+
nameDomainUnique: uniqueIndex("flux_journalist_contacts_name_domain_idx").on(table.name, table.domain),
|
|
1773
|
+
}));
|
|
1774
|
+
/**
|
|
1775
|
+
* Sentinel run log — tracks each scan cycle for observability.
|
|
1776
|
+
*/
|
|
1777
|
+
export const fluxSentinelRuns = pgTable("flux_sentinel_runs", {
|
|
1778
|
+
id: text("id")
|
|
1779
|
+
.primaryKey()
|
|
1780
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1781
|
+
runAt: timestamp("run_at", { withTimezone: true }).defaultNow().notNull(),
|
|
1782
|
+
triggeredBy: text("triggered_by").notNull(),
|
|
1783
|
+
keywordsScanned: integer("keywords_scanned").default(0).notNull(),
|
|
1784
|
+
mentionsFound: integer("mentions_found").default(0).notNull(),
|
|
1785
|
+
mentionsNew: integer("mentions_new").default(0).notNull(),
|
|
1786
|
+
alertsSent: integer("alerts_sent").default(0).notNull(),
|
|
1787
|
+
errorMessage: text("error_message"),
|
|
1788
|
+
durationMs: integer("duration_ms"),
|
|
1789
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1790
|
+
.defaultNow()
|
|
1791
|
+
.notNull(),
|
|
1792
|
+
}, (table) => ({
|
|
1793
|
+
runAtIdx: index("flux_sentinel_runs_run_at_idx").on(table.runAt),
|
|
1794
|
+
}));
|
|
1795
|
+
// Sentinel relations
|
|
1796
|
+
export const fluxSentinelKeywordsRelations = relations(fluxSentinelKeywords, ({ one, many }) => ({
|
|
1797
|
+
mentions: many(fluxMediaMentions),
|
|
1798
|
+
donorSignals: many(fluxDonorSignals),
|
|
1799
|
+
campaign: one(fluxSentinelCampaigns, {
|
|
1800
|
+
fields: [fluxSentinelKeywords.campaignId],
|
|
1801
|
+
references: [fluxSentinelCampaigns.id],
|
|
1802
|
+
}),
|
|
1803
|
+
}));
|
|
1804
|
+
export const fluxSentinelCampaignsRelations = relations(fluxSentinelCampaigns, ({ many }) => ({
|
|
1805
|
+
keywords: many(fluxSentinelKeywords),
|
|
1806
|
+
}));
|
|
1807
|
+
// =============================================================================
|
|
1808
|
+
// SENTINEL INCIDENTS — Escalation & Response Tracking
|
|
1809
|
+
// =============================================================================
|
|
1810
|
+
export const fluxSentinelIncidentStatusEnum = pgEnum("flux_sentinel_incident_status", ["new", "investigating", "responding", "resolved", "closed"]);
|
|
1811
|
+
export const fluxSentinelIncidentPriorityEnum = pgEnum("flux_sentinel_incident_priority", ["low", "medium", "high", "critical"]);
|
|
1812
|
+
/**
|
|
1813
|
+
* Sentinel incidents — escalated media mentions tracked as response efforts.
|
|
1814
|
+
*/
|
|
1815
|
+
export const fluxSentinelIncidents = pgTable("flux_sentinel_incidents", {
|
|
1816
|
+
id: text("id")
|
|
1817
|
+
.primaryKey()
|
|
1818
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1819
|
+
mentionId: text("mention_id").references(() => fluxMediaMentions.id, {
|
|
1820
|
+
onDelete: "set null",
|
|
1821
|
+
}),
|
|
1822
|
+
title: text("title").notNull(),
|
|
1823
|
+
status: fluxSentinelIncidentStatusEnum("status").default("new").notNull(),
|
|
1824
|
+
priority: fluxSentinelIncidentPriorityEnum("priority")
|
|
1825
|
+
.default("medium")
|
|
1826
|
+
.notNull(),
|
|
1827
|
+
ownerSlackId: text("owner_slack_id"),
|
|
1828
|
+
ownerName: text("owner_name"),
|
|
1829
|
+
notes: text("notes"),
|
|
1830
|
+
slackChannelId: text("slack_channel_id"),
|
|
1831
|
+
slackMessageTs: text("slack_message_ts"),
|
|
1832
|
+
resolvedAt: timestamp("resolved_at", { withTimezone: true }),
|
|
1833
|
+
closedAt: timestamp("closed_at", { withTimezone: true }),
|
|
1834
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1835
|
+
.defaultNow()
|
|
1836
|
+
.notNull(),
|
|
1837
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1838
|
+
.defaultNow()
|
|
1839
|
+
.notNull(),
|
|
1840
|
+
}, (table) => ({
|
|
1841
|
+
mentionIdx: index("flux_sentinel_incidents_mention_idx").on(table.mentionId),
|
|
1842
|
+
statusIdx: index("flux_sentinel_incidents_status_idx").on(table.status),
|
|
1843
|
+
priorityIdx: index("flux_sentinel_incidents_priority_idx").on(table.priority),
|
|
1844
|
+
createdAtIdx: index("flux_sentinel_incidents_created_at_idx").on(table.createdAt),
|
|
1845
|
+
}));
|
|
1846
|
+
/**
|
|
1847
|
+
* Incident tasks — checklist items for tracking response actions.
|
|
1848
|
+
*/
|
|
1849
|
+
export const fluxSentinelIncidentTasks = pgTable("flux_sentinel_incident_tasks", {
|
|
1850
|
+
id: text("id")
|
|
1851
|
+
.primaryKey()
|
|
1852
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1853
|
+
incidentId: text("incident_id")
|
|
1854
|
+
.notNull()
|
|
1855
|
+
.references(() => fluxSentinelIncidents.id, { onDelete: "cascade" }),
|
|
1856
|
+
title: text("title").notNull(),
|
|
1857
|
+
assigneeName: text("assignee_name"),
|
|
1858
|
+
dueDate: timestamp("due_date", { withTimezone: true }),
|
|
1859
|
+
isComplete: boolean("is_complete").default(false).notNull(),
|
|
1860
|
+
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
1861
|
+
displayOrder: integer("display_order").default(0).notNull(),
|
|
1862
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1863
|
+
.defaultNow()
|
|
1864
|
+
.notNull(),
|
|
1865
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1866
|
+
.defaultNow()
|
|
1867
|
+
.notNull(),
|
|
1868
|
+
}, (table) => ({
|
|
1869
|
+
incidentIdx: index("flux_sentinel_incident_tasks_incident_idx").on(table.incidentId),
|
|
1870
|
+
completeIdx: index("flux_sentinel_incident_tasks_complete_idx").on(table.isComplete),
|
|
1871
|
+
}));
|
|
1872
|
+
/**
|
|
1873
|
+
* Incident activity log — audit trail for all incident actions.
|
|
1874
|
+
*/
|
|
1875
|
+
export const fluxSentinelIncidentActivity = pgTable("flux_sentinel_incident_activity", {
|
|
1876
|
+
id: text("id")
|
|
1877
|
+
.primaryKey()
|
|
1878
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1879
|
+
incidentId: text("incident_id")
|
|
1880
|
+
.notNull()
|
|
1881
|
+
.references(() => fluxSentinelIncidents.id, { onDelete: "cascade" }),
|
|
1882
|
+
action: text("action").notNull(),
|
|
1883
|
+
actorName: text("actor_name"),
|
|
1884
|
+
details: jsonb("details"),
|
|
1885
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1886
|
+
.defaultNow()
|
|
1887
|
+
.notNull(),
|
|
1888
|
+
}, (table) => ({
|
|
1889
|
+
incidentIdx: index("flux_sentinel_incident_activity_incident_idx").on(table.incidentId),
|
|
1890
|
+
createdAtIdx: index("flux_sentinel_incident_activity_created_at_idx").on(table.createdAt),
|
|
1891
|
+
}));
|
|
1892
|
+
// Sentinel incident relations
|
|
1893
|
+
export const fluxSentinelIncidentsRelations = relations(fluxSentinelIncidents, ({ one, many }) => ({
|
|
1894
|
+
mention: one(fluxMediaMentions, {
|
|
1895
|
+
fields: [fluxSentinelIncidents.mentionId],
|
|
1896
|
+
references: [fluxMediaMentions.id],
|
|
1897
|
+
}),
|
|
1898
|
+
tasks: many(fluxSentinelIncidentTasks),
|
|
1899
|
+
activity: many(fluxSentinelIncidentActivity),
|
|
1900
|
+
}));
|
|
1901
|
+
export const fluxSentinelIncidentTasksRelations = relations(fluxSentinelIncidentTasks, ({ one }) => ({
|
|
1902
|
+
incident: one(fluxSentinelIncidents, {
|
|
1903
|
+
fields: [fluxSentinelIncidentTasks.incidentId],
|
|
1904
|
+
references: [fluxSentinelIncidents.id],
|
|
1905
|
+
}),
|
|
1906
|
+
}));
|
|
1907
|
+
export const fluxSentinelIncidentActivityRelations = relations(fluxSentinelIncidentActivity, ({ one }) => ({
|
|
1908
|
+
incident: one(fluxSentinelIncidents, {
|
|
1909
|
+
fields: [fluxSentinelIncidentActivity.incidentId],
|
|
1910
|
+
references: [fluxSentinelIncidents.id],
|
|
1911
|
+
}),
|
|
1912
|
+
}));
|
|
1913
|
+
// =============================================================================
|
|
1914
|
+
// SENTINEL PITCH TRACKING — Outreach & Relationship History
|
|
1915
|
+
// =============================================================================
|
|
1916
|
+
export const fluxSentinelPitchStatusEnum = pgEnum("flux_sentinel_pitch_status", [
|
|
1917
|
+
"draft",
|
|
1918
|
+
"sent",
|
|
1919
|
+
"opened",
|
|
1920
|
+
"replied",
|
|
1921
|
+
"declined",
|
|
1922
|
+
"no_response",
|
|
1923
|
+
]);
|
|
1924
|
+
/**
|
|
1925
|
+
* Pitches — logged outreach to journalists.
|
|
1926
|
+
* Links to journalist contacts, media mentions, and media lists.
|
|
1927
|
+
*/
|
|
1928
|
+
export const fluxSentinelPitches = pgTable("flux_sentinel_pitches", {
|
|
1929
|
+
id: text("id")
|
|
1930
|
+
.primaryKey()
|
|
1931
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1932
|
+
journalistId: text("journalist_id").references(() => fluxJournalistContacts.id, { onDelete: "set null" }),
|
|
1933
|
+
journalistName: text("journalist_name").notNull(),
|
|
1934
|
+
journalistEmail: text("journalist_email"),
|
|
1935
|
+
subject: text("subject").notNull(),
|
|
1936
|
+
body: text("body"),
|
|
1937
|
+
status: fluxSentinelPitchStatusEnum("status").default("draft").notNull(),
|
|
1938
|
+
sentAt: timestamp("sent_at", { withTimezone: true }),
|
|
1939
|
+
sentBySlackId: text("sent_by_slack_id").notNull(),
|
|
1940
|
+
sentByName: text("sent_by_name"),
|
|
1941
|
+
followUpAt: timestamp("follow_up_at", { withTimezone: true }),
|
|
1942
|
+
lastStatusAt: timestamp("last_status_at", { withTimezone: true }),
|
|
1943
|
+
mentionId: text("mention_id").references(() => fluxMediaMentions.id, {
|
|
1944
|
+
onDelete: "set null",
|
|
1945
|
+
}),
|
|
1946
|
+
mediaListId: text("media_list_id").references(() => fluxSentinelMediaLists.id, { onDelete: "set null" }),
|
|
1947
|
+
notes: text("notes"),
|
|
1948
|
+
zeroClickHeadline: text("zero_click_headline"),
|
|
1949
|
+
zeroClickBullets: text("zero_click_bullets").array(),
|
|
1950
|
+
zeroClickQuote: text("zero_click_quote"),
|
|
1951
|
+
slackChannelId: text("slack_channel_id"),
|
|
1952
|
+
slackMessageTs: text("slack_message_ts"),
|
|
1953
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1954
|
+
.defaultNow()
|
|
1955
|
+
.notNull(),
|
|
1956
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
1957
|
+
.defaultNow()
|
|
1958
|
+
.notNull(),
|
|
1959
|
+
}, (table) => ({
|
|
1960
|
+
journalistIdx: index("flux_sentinel_pitches_journalist_idx").on(table.journalistId),
|
|
1961
|
+
statusIdx: index("flux_sentinel_pitches_status_idx").on(table.status),
|
|
1962
|
+
sentByIdx: index("flux_sentinel_pitches_sent_by_idx").on(table.sentBySlackId),
|
|
1963
|
+
followUpIdx: index("flux_sentinel_pitches_follow_up_idx").on(table.followUpAt),
|
|
1964
|
+
createdAtIdx: index("flux_sentinel_pitches_created_at_idx").on(table.createdAt),
|
|
1965
|
+
}));
|
|
1966
|
+
/**
|
|
1967
|
+
* Pitch activity log — audit trail for all pitch actions.
|
|
1968
|
+
*/
|
|
1969
|
+
export const fluxSentinelPitchActivity = pgTable("flux_sentinel_pitch_activity", {
|
|
1970
|
+
id: text("id")
|
|
1971
|
+
.primaryKey()
|
|
1972
|
+
.default(sql `gen_random_uuid()::text`),
|
|
1973
|
+
pitchId: text("pitch_id")
|
|
1974
|
+
.notNull()
|
|
1975
|
+
.references(() => fluxSentinelPitches.id, { onDelete: "cascade" }),
|
|
1976
|
+
action: text("action").notNull(),
|
|
1977
|
+
actorName: text("actor_name"),
|
|
1978
|
+
details: jsonb("details"),
|
|
1979
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
1980
|
+
.defaultNow()
|
|
1981
|
+
.notNull(),
|
|
1982
|
+
}, (table) => ({
|
|
1983
|
+
pitchIdx: index("flux_sentinel_pitch_activity_pitch_idx").on(table.pitchId),
|
|
1984
|
+
createdAtIdx: index("flux_sentinel_pitch_activity_created_at_idx").on(table.createdAt),
|
|
1985
|
+
}));
|
|
1986
|
+
// Sentinel pitch relations
|
|
1987
|
+
export const fluxSentinelPitchesRelations = relations(fluxSentinelPitches, ({ one, many }) => ({
|
|
1988
|
+
journalist: one(fluxJournalistContacts, {
|
|
1989
|
+
fields: [fluxSentinelPitches.journalistId],
|
|
1990
|
+
references: [fluxJournalistContacts.id],
|
|
1991
|
+
}),
|
|
1992
|
+
mention: one(fluxMediaMentions, {
|
|
1993
|
+
fields: [fluxSentinelPitches.mentionId],
|
|
1994
|
+
references: [fluxMediaMentions.id],
|
|
1995
|
+
}),
|
|
1996
|
+
mediaList: one(fluxSentinelMediaLists, {
|
|
1997
|
+
fields: [fluxSentinelPitches.mediaListId],
|
|
1998
|
+
references: [fluxSentinelMediaLists.id],
|
|
1999
|
+
}),
|
|
2000
|
+
activity: many(fluxSentinelPitchActivity),
|
|
2001
|
+
}));
|
|
2002
|
+
export const fluxSentinelPitchActivityRelations = relations(fluxSentinelPitchActivity, ({ one }) => ({
|
|
2003
|
+
pitch: one(fluxSentinelPitches, {
|
|
2004
|
+
fields: [fluxSentinelPitchActivity.pitchId],
|
|
2005
|
+
references: [fluxSentinelPitches.id],
|
|
2006
|
+
}),
|
|
2007
|
+
}));
|
|
2008
|
+
// =============================================================================
|
|
2009
|
+
// SENTINEL MEDIA LISTS — Organized Journalist Groups
|
|
2010
|
+
// =============================================================================
|
|
2011
|
+
/**
|
|
2012
|
+
* Media lists — curated groups of journalists for organized outreach.
|
|
2013
|
+
*/
|
|
2014
|
+
export const fluxSentinelMediaLists = pgTable("flux_sentinel_media_lists", {
|
|
2015
|
+
id: text("id")
|
|
2016
|
+
.primaryKey()
|
|
2017
|
+
.default(sql `gen_random_uuid()::text`),
|
|
2018
|
+
name: text("name").notNull(),
|
|
2019
|
+
description: text("description"),
|
|
2020
|
+
createdBySlackId: text("created_by_slack_id").notNull(),
|
|
2021
|
+
createdByName: text("created_by_name"),
|
|
2022
|
+
isShared: boolean("is_shared").default(true).notNull(),
|
|
2023
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2024
|
+
.defaultNow()
|
|
2025
|
+
.notNull(),
|
|
2026
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
2027
|
+
.defaultNow()
|
|
2028
|
+
.notNull(),
|
|
2029
|
+
}, (table) => ({
|
|
2030
|
+
nameIdx: index("flux_sentinel_media_lists_name_idx").on(table.name),
|
|
2031
|
+
createdAtIdx: index("flux_sentinel_media_lists_created_at_idx").on(table.createdAt),
|
|
2032
|
+
}));
|
|
2033
|
+
/**
|
|
2034
|
+
* Media list members — journalists belonging to a media list.
|
|
2035
|
+
*/
|
|
2036
|
+
export const fluxSentinelMediaListMembers = pgTable("flux_sentinel_media_list_members", {
|
|
2037
|
+
id: text("id")
|
|
2038
|
+
.primaryKey()
|
|
2039
|
+
.default(sql `gen_random_uuid()::text`),
|
|
2040
|
+
listId: text("list_id")
|
|
2041
|
+
.notNull()
|
|
2042
|
+
.references(() => fluxSentinelMediaLists.id, { onDelete: "cascade" }),
|
|
2043
|
+
journalistId: text("journalist_id")
|
|
2044
|
+
.notNull()
|
|
2045
|
+
.references(() => fluxJournalistContacts.id, { onDelete: "cascade" }),
|
|
2046
|
+
addedByName: text("added_by_name"),
|
|
2047
|
+
notes: text("notes"),
|
|
2048
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2049
|
+
.defaultNow()
|
|
2050
|
+
.notNull(),
|
|
2051
|
+
}, (table) => ({
|
|
2052
|
+
listIdx: index("flux_sentinel_media_list_members_list_idx").on(table.listId),
|
|
2053
|
+
journalistIdx: index("flux_sentinel_media_list_members_journalist_idx").on(table.journalistId),
|
|
2054
|
+
uniqueMember: uniqueIndex("flux_sentinel_media_list_members_unique_idx").on(table.listId, table.journalistId),
|
|
2055
|
+
}));
|
|
2056
|
+
// Media list relations
|
|
2057
|
+
export const fluxSentinelMediaListsRelations = relations(fluxSentinelMediaLists, ({ many }) => ({
|
|
2058
|
+
members: many(fluxSentinelMediaListMembers),
|
|
2059
|
+
pitches: many(fluxSentinelPitches),
|
|
2060
|
+
}));
|
|
2061
|
+
export const fluxSentinelMediaListMembersRelations = relations(fluxSentinelMediaListMembers, ({ one }) => ({
|
|
2062
|
+
list: one(fluxSentinelMediaLists, {
|
|
2063
|
+
fields: [fluxSentinelMediaListMembers.listId],
|
|
2064
|
+
references: [fluxSentinelMediaLists.id],
|
|
2065
|
+
}),
|
|
2066
|
+
journalist: one(fluxJournalistContacts, {
|
|
2067
|
+
fields: [fluxSentinelMediaListMembers.journalistId],
|
|
2068
|
+
references: [fluxJournalistContacts.id],
|
|
2069
|
+
}),
|
|
2070
|
+
}));
|
|
2071
|
+
// Media mention relations (placed after all tables to avoid forward references)
|
|
2072
|
+
export const fluxMediaMentionsRelations = relations(fluxMediaMentions, ({ one, many }) => ({
|
|
2073
|
+
keyword: one(fluxSentinelKeywords, {
|
|
2074
|
+
fields: [fluxMediaMentions.keywordId],
|
|
2075
|
+
references: [fluxSentinelKeywords.id],
|
|
2076
|
+
}),
|
|
2077
|
+
incidents: many(fluxSentinelIncidents),
|
|
2078
|
+
pitches: many(fluxSentinelPitches),
|
|
2079
|
+
}));
|
|
2080
|
+
export const fluxJournalistContactsRelations = relations(fluxJournalistContacts, ({ many }) => ({
|
|
2081
|
+
pitches: many(fluxSentinelPitches),
|
|
2082
|
+
listMemberships: many(fluxSentinelMediaListMembers),
|
|
2083
|
+
}));
|
|
2084
|
+
// =============================================================================
|
|
2085
|
+
// FEEDBACK & FEATURE REQUESTS
|
|
2086
|
+
// =============================================================================
|
|
2087
|
+
export const fluxFeedbackCategoryEnum = pgEnum("flux_feedback_category", [
|
|
2088
|
+
"bug",
|
|
2089
|
+
"feature_request",
|
|
2090
|
+
"roadmap_vote",
|
|
2091
|
+
"general",
|
|
2092
|
+
]);
|
|
2093
|
+
export const fluxFeedbackStatusEnum = pgEnum("flux_feedback_status", [
|
|
2094
|
+
"new",
|
|
2095
|
+
"acknowledged",
|
|
2096
|
+
"planned",
|
|
2097
|
+
"in_progress",
|
|
2098
|
+
"completed",
|
|
2099
|
+
"declined",
|
|
2100
|
+
]);
|
|
2101
|
+
/**
|
|
2102
|
+
* User feedback and feature requests — submitted via /ideas slash command.
|
|
2103
|
+
* General-purpose: covers bugs, feature requests, roadmap votes, and general input.
|
|
2104
|
+
*/
|
|
2105
|
+
export const fluxFeedback = pgTable("flux_feedback", {
|
|
2106
|
+
id: text("id")
|
|
2107
|
+
.primaryKey()
|
|
2108
|
+
.default(sql `gen_random_uuid()::text`),
|
|
2109
|
+
slackUserId: text("slack_user_id").notNull(),
|
|
2110
|
+
slackUserName: text("slack_user_name"),
|
|
2111
|
+
category: fluxFeedbackCategoryEnum("category").default("general").notNull(),
|
|
2112
|
+
title: text("title").notNull(),
|
|
2113
|
+
description: text("description"),
|
|
2114
|
+
status: fluxFeedbackStatusEnum("status").default("new").notNull(),
|
|
2115
|
+
adminNotes: text("admin_notes"),
|
|
2116
|
+
slackChannelId: text("slack_channel_id"),
|
|
2117
|
+
slackMessageTs: text("slack_message_ts"),
|
|
2118
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2119
|
+
.defaultNow()
|
|
2120
|
+
.notNull(),
|
|
2121
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
2122
|
+
.defaultNow()
|
|
2123
|
+
.notNull(),
|
|
2124
|
+
}, (table) => ({
|
|
2125
|
+
categoryIdx: index("flux_feedback_category_idx").on(table.category),
|
|
2126
|
+
statusIdx: index("flux_feedback_status_idx").on(table.status),
|
|
2127
|
+
createdAtIdx: index("flux_feedback_created_at_idx").on(table.createdAt),
|
|
2128
|
+
}));
|
|
2129
|
+
// =============================================================================
|
|
2130
|
+
// FULFILLMENT PIPELINE — HubSpot Fulfillment Ticket Projections
|
|
2131
|
+
// =============================================================================
|
|
2132
|
+
/**
|
|
2133
|
+
* Fulfillment pipeline stage enum — mirrors HubSpot pipeline 1230947033 stages.
|
|
2134
|
+
* Used by the fulfillment board, webhook handler, and SLA tracking.
|
|
2135
|
+
*/
|
|
2136
|
+
export const fluxFulfillmentStageEnum = pgEnum("flux_fulfillment_stage", [
|
|
2137
|
+
"pending_form",
|
|
2138
|
+
"submitted",
|
|
2139
|
+
"pending_creative",
|
|
2140
|
+
"pending_fulfillment",
|
|
2141
|
+
"ready",
|
|
2142
|
+
"running",
|
|
2143
|
+
"paused",
|
|
2144
|
+
"completed",
|
|
2145
|
+
"cancelled",
|
|
2146
|
+
"change_required",
|
|
2147
|
+
"billing_change_required",
|
|
2148
|
+
]);
|
|
2149
|
+
/**
|
|
2150
|
+
* Fulfillment team enum — the operational team responsible for the ticket.
|
|
2151
|
+
*/
|
|
2152
|
+
export const fluxFulfillmentTeamEnum = pgEnum("flux_fulfillment_team", [
|
|
2153
|
+
"creative_team",
|
|
2154
|
+
"campaign_manager",
|
|
2155
|
+
"media_buyer",
|
|
2156
|
+
]);
|
|
2157
|
+
/**
|
|
2158
|
+
* Fulfillment tickets — projected from HubSpot Fulfillment Pipeline (1230947033).
|
|
2159
|
+
*
|
|
2160
|
+
* Each row represents a fulfillment ticket synced from HubSpot via periodic sync
|
|
2161
|
+
* or real-time webhook. HubSpot is the system of record; this table is a read
|
|
2162
|
+
* projection enriched with stage history for SLA tracking and board display.
|
|
2163
|
+
*
|
|
2164
|
+
* Associated with a flux_project via the company → project mapping.
|
|
2165
|
+
*/
|
|
2166
|
+
export const fluxFulfillmentTickets = pgTable("flux_fulfillment_tickets", {
|
|
2167
|
+
id: text("id")
|
|
2168
|
+
.primaryKey()
|
|
2169
|
+
.default(sql `gen_random_uuid()::text`),
|
|
2170
|
+
/** HubSpot ticket ID — unique constraint ensures no duplicate projections */
|
|
2171
|
+
hubspotTicketId: text("hubspot_ticket_id").notNull(),
|
|
2172
|
+
/** FK to flux_projects — resolved from HubSpot company association */
|
|
2173
|
+
projectId: text("project_id").references(() => fluxProjects.id, {
|
|
2174
|
+
onDelete: "cascade",
|
|
2175
|
+
}),
|
|
2176
|
+
// -- Core ticket fields --
|
|
2177
|
+
/** Ticket name / subject line */
|
|
2178
|
+
subject: text("subject").notNull(),
|
|
2179
|
+
/** Ticket description / content */
|
|
2180
|
+
content: text("content"),
|
|
2181
|
+
/** Current pipeline stage — enum for type-safe queries */
|
|
2182
|
+
pipelineStage: fluxFulfillmentStageEnum("pipeline_stage")
|
|
2183
|
+
.notNull()
|
|
2184
|
+
.default("pending_form"),
|
|
2185
|
+
/** HubSpot numeric pipeline stage ID (e.g. "2056920807") for API round-trips */
|
|
2186
|
+
hubspotStageId: text("hubspot_stage_id"),
|
|
2187
|
+
// -- Team & Ownership --
|
|
2188
|
+
/** Operational team: creative_team, campaign_manager, media_buyer */
|
|
2189
|
+
fulfillmentTeam: fluxFulfillmentTeamEnum("fulfillment_team"),
|
|
2190
|
+
/** HubSpot owner ID for the fulfillment assignee */
|
|
2191
|
+
fulfillmentOwnerId: text("fulfillment_owner_id"),
|
|
2192
|
+
/** HubSpot owner ID for the sales owner on the ticket */
|
|
2193
|
+
salesOwnerId: text("sales_owner_id"),
|
|
2194
|
+
/** HubSpot owner ID for the trafficking assignee */
|
|
2195
|
+
traffickingOwnerId: text("trafficking_owner_id"),
|
|
2196
|
+
// -- Account Team --
|
|
2197
|
+
/** Account Executive owner ID */
|
|
2198
|
+
accountExecutiveId: text("account_executive_id"),
|
|
2199
|
+
/** Account Manager owner ID */
|
|
2200
|
+
accountManagerId: text("account_manager_id"),
|
|
2201
|
+
/** Media Strategist owner ID */
|
|
2202
|
+
mediaStrategistId: text("media_strategist_id"),
|
|
2203
|
+
// -- Campaign Details --
|
|
2204
|
+
/** Campaign type (e.g. Affiliate Marketing, Automotive) */
|
|
2205
|
+
campaignType: text("campaign_type"),
|
|
2206
|
+
/** Ad type */
|
|
2207
|
+
adType: text("ad_type"),
|
|
2208
|
+
/** Product name from the associated line item */
|
|
2209
|
+
productName: text("product_name"),
|
|
2210
|
+
/** Product code / SKU */
|
|
2211
|
+
productCode: text("product_code"),
|
|
2212
|
+
// -- Deal Type --
|
|
2213
|
+
/** Deal type from HubSpot (e.g., "newbusiness", "existingbusiness") */
|
|
2214
|
+
dealType: text("deal_type"),
|
|
2215
|
+
// -- Creative Status --
|
|
2216
|
+
/** "true" = Needs Creative, "false" = Camera Ready */
|
|
2217
|
+
creativeRequest: text("creative_request"),
|
|
2218
|
+
/** "Camera Ready" | "Needs Creative" */
|
|
2219
|
+
creativeProvided: text("creative_provided"),
|
|
2220
|
+
// -- Change Request --
|
|
2221
|
+
/** Change type if this is a change request: Date, Pause, Cancel, Budget, Extension, etc. */
|
|
2222
|
+
changeType: text("change_type"),
|
|
2223
|
+
/** Whether this is a change request */
|
|
2224
|
+
isChangeRequest: boolean("is_change_request").default(false),
|
|
2225
|
+
/** Date the change was requested */
|
|
2226
|
+
changeRequestDate: timestamp("change_request_date", {
|
|
2227
|
+
withTimezone: true,
|
|
2228
|
+
}),
|
|
2229
|
+
// -- Dates --
|
|
2230
|
+
/** Campaign start date */
|
|
2231
|
+
startDate: date("start_date"),
|
|
2232
|
+
/** Campaign end date */
|
|
2233
|
+
endDate: date("end_date"),
|
|
2234
|
+
/** Cancel date if applicable */
|
|
2235
|
+
cancelDate: date("cancel_date"),
|
|
2236
|
+
// -- Investment --
|
|
2237
|
+
/** Monthly investment amount in dollars */
|
|
2238
|
+
monthlyInvestment: numeric("monthly_investment", {
|
|
2239
|
+
precision: 12,
|
|
2240
|
+
scale: 2,
|
|
2241
|
+
}),
|
|
2242
|
+
/** Total investment amount in dollars */
|
|
2243
|
+
totalInvestment: numeric("total_investment", { precision: 12, scale: 2 }),
|
|
2244
|
+
// -- Line Item Link --
|
|
2245
|
+
/** HubSpot line item ID — bridges to Compass order data */
|
|
2246
|
+
lineItemId: text("line_item_id"),
|
|
2247
|
+
// -- Jotform --
|
|
2248
|
+
/** Jotform status: Incomplete | Complete */
|
|
2249
|
+
jotformStatus: text("jotform_status"),
|
|
2250
|
+
// -- Stage Tracking (for SLA) --
|
|
2251
|
+
/** When the ticket entered its current stage (from HubSpot hs_v2_date_entered_current_stage) */
|
|
2252
|
+
stageEnteredAt: timestamp("stage_entered_at", { withTimezone: true }),
|
|
2253
|
+
/**
|
|
2254
|
+
* Full stage history as JSONB array. Each entry:
|
|
2255
|
+
* { stage: string, enteredAt: string, exitedAt?: string, durationSeconds?: number }
|
|
2256
|
+
*/
|
|
2257
|
+
stageHistory: jsonb("stage_history").default(sql `'[]'::jsonb`),
|
|
2258
|
+
// -- Compass Order Context --
|
|
2259
|
+
/** Deep link URL to the Compass order that generated this ticket */
|
|
2260
|
+
compassOrderUrl: text("compass_order_url"),
|
|
2261
|
+
/** Compass product family code (e.g., "SEM", "Display", "Social") */
|
|
2262
|
+
compassProductFamily: text("compass_product_family"),
|
|
2263
|
+
/** Compass product code */
|
|
2264
|
+
compassProductCode: text("compass_product_code"),
|
|
2265
|
+
/** Impressions from Compass order line item */
|
|
2266
|
+
compassImpressions: text("compass_impressions"),
|
|
2267
|
+
/** Creative source from Compass order */
|
|
2268
|
+
compassCreativeSource: text("compass_creative_source"),
|
|
2269
|
+
/** Targeting details from Compass order */
|
|
2270
|
+
compassTargeting: text("compass_targeting"),
|
|
2271
|
+
/** HubSpot deal ID associated with this ticket's line item */
|
|
2272
|
+
dealId: text("deal_id"),
|
|
2273
|
+
/** Deal name for display */
|
|
2274
|
+
dealName: text("deal_name"),
|
|
2275
|
+
/** Total order value from the deal */
|
|
2276
|
+
dealOrderTotal: numeric("deal_order_total", { precision: 12, scale: 2 }),
|
|
2277
|
+
/** Compass order status (e.g., "signed", "draft", "amended") */
|
|
2278
|
+
compassOrderStatus: text("compass_order_status"),
|
|
2279
|
+
// -- Native HubSpot SLA --
|
|
2280
|
+
/** HubSpot SLA status for first response (Overdue, Due Soon, Active SLA, etc.) */
|
|
2281
|
+
hubspotSlaFirstResponse: varchar("hubspot_sla_first_response"),
|
|
2282
|
+
/** HubSpot SLA status for time to close */
|
|
2283
|
+
hubspotSlaClose: varchar("hubspot_sla_close"),
|
|
2284
|
+
/** HubSpot SLA status for next response */
|
|
2285
|
+
hubspotSlaNextResponse: varchar("hubspot_sla_next_response"),
|
|
2286
|
+
// -- Additional HubSpot Properties --
|
|
2287
|
+
/** Date creative was requested */
|
|
2288
|
+
creativeRequestDate: timestamp("creative_request_date", { withTimezone: true }),
|
|
2289
|
+
/** Whether creative upload is completed */
|
|
2290
|
+
uploadCreativeCompleted: text("upload_creative_completed"),
|
|
2291
|
+
/** Content in progress status */
|
|
2292
|
+
contentInProgress: text("content_in_progress"),
|
|
2293
|
+
/** Naviga campaign ID */
|
|
2294
|
+
navigaCampaignId: text("na_campaign_id"),
|
|
2295
|
+
/** Platform campaign name */
|
|
2296
|
+
platformCampaignName: text("platform_campaign_name"),
|
|
2297
|
+
// -- Additional operational fields --
|
|
2298
|
+
/** Trafficking request status */
|
|
2299
|
+
traffickingRequest: text("trafficking_request"),
|
|
2300
|
+
/** When trafficking was requested */
|
|
2301
|
+
traffickingRequestDate: timestamp("trafficking_request_date", { withTimezone: true }),
|
|
2302
|
+
/** When ticket was accepted */
|
|
2303
|
+
acceptedDate: timestamp("accepted_date", { withTimezone: true }),
|
|
2304
|
+
/** Urgency flag for triage prioritization */
|
|
2305
|
+
isUrgent: boolean("is_urgent").default(false),
|
|
2306
|
+
/** Internal flags */
|
|
2307
|
+
flag: text("flag"),
|
|
2308
|
+
/** PO number for billing */
|
|
2309
|
+
purchaseOrder: text("purchase_order"),
|
|
2310
|
+
/** Creative brief lifecycle status */
|
|
2311
|
+
creativeBriefStatus: text("creative_brief_status"),
|
|
2312
|
+
/** Which operations center handles this */
|
|
2313
|
+
operationsCenter: text("operations_center"),
|
|
2314
|
+
// -- HubSpot Metadata --
|
|
2315
|
+
/** Ticket priority from HubSpot */
|
|
2316
|
+
priority: text("priority"),
|
|
2317
|
+
/** HubSpot URL for quick navigation */
|
|
2318
|
+
hubspotUrl: text("hubspot_url"),
|
|
2319
|
+
/** Original ticket creation date in HubSpot */
|
|
2320
|
+
hubspotCreatedAt: timestamp("hubspot_created_at", { withTimezone: true }),
|
|
2321
|
+
/** Last modification date in HubSpot */
|
|
2322
|
+
hubspotUpdatedAt: timestamp("hubspot_updated_at", { withTimezone: true }),
|
|
2323
|
+
// -- SLA Profile Override --
|
|
2324
|
+
/** SLA profile override — FK to flux_sla_profiles (takes precedence over project profile) */
|
|
2325
|
+
slaProfileId: text("sla_profile_id").references(() => fluxSlaProfiles.id, { onDelete: "set null" }),
|
|
2326
|
+
/** Source of the SLA override: admin or compass_sla_exception */
|
|
2327
|
+
slaOverrideSource: text("sla_override_source"),
|
|
2328
|
+
/** When the override expires (null = permanent) */
|
|
2329
|
+
slaOverrideExpiresAt: timestamp("sla_override_expires_at", { withTimezone: true }),
|
|
2330
|
+
// -- Local tracking --
|
|
2331
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2332
|
+
.defaultNow()
|
|
2333
|
+
.notNull(),
|
|
2334
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
2335
|
+
.defaultNow()
|
|
2336
|
+
.notNull(),
|
|
2337
|
+
}, (table) => ({
|
|
2338
|
+
/** Unique constraint on HubSpot ticket ID — prevents duplicate projections */
|
|
2339
|
+
hubspotTicketIdx: uniqueIndex("flux_fulfillment_tickets_hs_id_idx").on(table.hubspotTicketId),
|
|
2340
|
+
/** Filter by project for project-level board */
|
|
2341
|
+
projectIdx: index("flux_fulfillment_tickets_project_idx").on(table.projectId),
|
|
2342
|
+
/** Filter by pipeline stage for board columns */
|
|
2343
|
+
stageIdx: index("flux_fulfillment_tickets_stage_idx").on(table.pipelineStage),
|
|
2344
|
+
/** Filter by team for team-based views */
|
|
2345
|
+
teamIdx: index("flux_fulfillment_tickets_team_idx").on(table.fulfillmentTeam),
|
|
2346
|
+
/** Filter change requests */
|
|
2347
|
+
changeRequestIdx: index("flux_fulfillment_tickets_change_request_idx").on(table.isChangeRequest),
|
|
2348
|
+
/** Sort by start date for timeline views */
|
|
2349
|
+
startDateIdx: index("flux_fulfillment_tickets_start_date_idx").on(table.startDate),
|
|
2350
|
+
/** SLA tracking — find tickets by stage entry time */
|
|
2351
|
+
stageEnteredIdx: index("flux_fulfillment_tickets_stage_entered_idx").on(table.stageEnteredAt),
|
|
2352
|
+
}));
|
|
2353
|
+
/** Fulfillment ticket relations */
|
|
2354
|
+
export const fluxFulfillmentTicketsRelations = relations(fluxFulfillmentTickets, ({ one }) => ({
|
|
2355
|
+
project: one(fluxProjects, {
|
|
2356
|
+
fields: [fluxFulfillmentTickets.projectId],
|
|
2357
|
+
references: [fluxProjects.id],
|
|
2358
|
+
}),
|
|
2359
|
+
}));
|
|
2360
|
+
// =============================================================================
|
|
2361
|
+
// FULFILLMENT TASK TEMPLATES
|
|
2362
|
+
// =============================================================================
|
|
2363
|
+
/**
|
|
2364
|
+
* Fulfillment Task Templates — auto-generate tasks when tickets transition stages.
|
|
2365
|
+
*
|
|
2366
|
+
* Each template defines a task that should be created when a fulfillment ticket
|
|
2367
|
+
* enters a specific pipeline stage. Templates can be customized per stage, with
|
|
2368
|
+
* default titles that support {subject} placeholder interpolation.
|
|
2369
|
+
*/
|
|
2370
|
+
export const fluxFulfillmentTaskTemplates = pgTable("flux_fulfillment_task_templates", {
|
|
2371
|
+
id: varchar("id")
|
|
2372
|
+
.primaryKey()
|
|
2373
|
+
.default(sql `gen_random_uuid()`),
|
|
2374
|
+
/** Pipeline stage that triggers this task template */
|
|
2375
|
+
triggerStage: fluxFulfillmentStageEnum("trigger_stage").notNull(),
|
|
2376
|
+
/** Task title template — supports {subject} placeholder */
|
|
2377
|
+
titleTemplate: text("title_template").notNull(),
|
|
2378
|
+
/** Task description template */
|
|
2379
|
+
descriptionTemplate: text("description_template"),
|
|
2380
|
+
/** Default priority for the generated task */
|
|
2381
|
+
priority: text("priority").default("medium").notNull(),
|
|
2382
|
+
/** Role hint for assignment (e.g., "creative_team", "strategist", "ae") */
|
|
2383
|
+
assigneeRole: text("assignee_role"),
|
|
2384
|
+
/** Days until due from stage entry */
|
|
2385
|
+
dueDaysFromTransition: integer("due_days_from_transition").default(2).notNull(),
|
|
2386
|
+
/** Whether this template is active */
|
|
2387
|
+
isActive: boolean("is_active").default(true).notNull(),
|
|
2388
|
+
/** Display order for admin management */
|
|
2389
|
+
displayOrder: integer("display_order").default(0).notNull(),
|
|
2390
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2391
|
+
.defaultNow()
|
|
2392
|
+
.notNull(),
|
|
2393
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
2394
|
+
.defaultNow()
|
|
2395
|
+
.notNull(),
|
|
2396
|
+
}, (table) => ({
|
|
2397
|
+
stageIdx: index("flux_fulfillment_task_templates_stage_idx").on(table.triggerStage),
|
|
2398
|
+
}));
|
|
2399
|
+
// =============================================================================
|
|
2400
|
+
// FULFILLMENT EVENT OUTBOX
|
|
2401
|
+
// =============================================================================
|
|
2402
|
+
/**
|
|
2403
|
+
* Delivery status for fulfillment events sent from Flux to Forge.
|
|
2404
|
+
* Mirrors Forge's execution_event_outbox delivery lifecycle.
|
|
2405
|
+
*/
|
|
2406
|
+
export const fluxFulfillmentEventDeliveryEnum = pgEnum("flux_fulfillment_event_delivery", ["pending", "delivered", "failed", "dead_letter"]);
|
|
2407
|
+
/**
|
|
2408
|
+
* Outbox table for fulfillment events emitted by Flux to Forge.
|
|
2409
|
+
*
|
|
2410
|
+
* When a fulfillment ticket enters a stage that requires action in Forge
|
|
2411
|
+
* (e.g., pending_creative), an event is queued here. A delivery worker
|
|
2412
|
+
* reads pending events, signs them with HMAC-SHA256, and POSTs to Forge's
|
|
2413
|
+
* /api/fulfillment-events endpoint.
|
|
2414
|
+
*
|
|
2415
|
+
* Uses the same FORGE_EXECUTION_EVENT_SHARED_SECRET as the Forge → Flux
|
|
2416
|
+
* direction for symmetric HMAC verification.
|
|
2417
|
+
*/
|
|
2418
|
+
export const fluxFulfillmentEventOutbox = pgTable("flux_fulfillment_event_outbox", {
|
|
2419
|
+
id: varchar("id")
|
|
2420
|
+
.primaryKey()
|
|
2421
|
+
.default(sql `gen_random_uuid()`),
|
|
2422
|
+
/** Unique event identifier for deduplication */
|
|
2423
|
+
eventId: varchar("event_id").notNull(),
|
|
2424
|
+
/** Event type (e.g., "fulfillment.creative_needed") */
|
|
2425
|
+
eventType: text("event_type").notNull(),
|
|
2426
|
+
/** HubSpot ticket ID this event relates to */
|
|
2427
|
+
hubspotTicketId: text("hubspot_ticket_id").notNull(),
|
|
2428
|
+
/** Flux fulfillment ticket UUID */
|
|
2429
|
+
fulfillmentTicketId: text("fulfillment_ticket_id").notNull(),
|
|
2430
|
+
/** Idempotency key for dedup on the receiver side */
|
|
2431
|
+
idempotencyKey: text("idempotency_key").notNull().unique(),
|
|
2432
|
+
/** ISO timestamp when the event was emitted */
|
|
2433
|
+
emittedAt: timestamp("emitted_at", { withTimezone: true })
|
|
2434
|
+
.defaultNow()
|
|
2435
|
+
.notNull(),
|
|
2436
|
+
/** Full event payload as JSONB */
|
|
2437
|
+
payload: jsonb("payload").notNull(),
|
|
2438
|
+
/** Delivery status lifecycle: pending → delivered | failed → dead_letter */
|
|
2439
|
+
deliveryStatus: fluxFulfillmentEventDeliveryEnum("delivery_status")
|
|
2440
|
+
.default("pending")
|
|
2441
|
+
.notNull(),
|
|
2442
|
+
/** Number of delivery attempts so far */
|
|
2443
|
+
attemptCount: integer("attempt_count").default(0).notNull(),
|
|
2444
|
+
/** When to next attempt delivery (for backoff) */
|
|
2445
|
+
nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true })
|
|
2446
|
+
.defaultNow()
|
|
2447
|
+
.notNull(),
|
|
2448
|
+
/** Timestamp of successful delivery */
|
|
2449
|
+
deliveredAt: timestamp("delivered_at", { withTimezone: true }),
|
|
2450
|
+
/** Last error message from delivery attempt */
|
|
2451
|
+
lastError: text("last_error"),
|
|
2452
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2453
|
+
.defaultNow()
|
|
2454
|
+
.notNull(),
|
|
2455
|
+
}, (table) => ({
|
|
2456
|
+
pendingIdx: index("flux_fulfillment_event_outbox_pending_idx").on(table.deliveryStatus, table.nextAttemptAt),
|
|
2457
|
+
ticketIdx: index("flux_fulfillment_event_outbox_ticket_idx").on(table.hubspotTicketId),
|
|
2458
|
+
}));
|
|
2459
|
+
/** Relations for fulfillment event outbox (standalone — no foreign keys) */
|
|
2460
|
+
export const fluxFulfillmentEventOutboxRelations = relations(fluxFulfillmentEventOutbox, () => ({}));
|
|
2461
|
+
// =============================================================================
|
|
2462
|
+
// PROGRAMMATIC SHELL — Enums
|
|
2463
|
+
// =============================================================================
|
|
2464
|
+
export const fluxProgrammaticShellStatusEnum = pgEnum("flux_programmatic_shell_status", [
|
|
2465
|
+
"draft",
|
|
2466
|
+
"edited",
|
|
2467
|
+
"vendor_locked",
|
|
2468
|
+
"activated",
|
|
2469
|
+
"completed",
|
|
2470
|
+
]);
|
|
2471
|
+
export const fluxProgrammaticDecisionTypeEnum = pgEnum("flux_programmatic_decision_type", [
|
|
2472
|
+
"vendor_select",
|
|
2473
|
+
"recommendation_accept",
|
|
2474
|
+
"recommendation_edit",
|
|
2475
|
+
"recommendation_dismiss",
|
|
2476
|
+
"activation",
|
|
2477
|
+
]);
|
|
2478
|
+
export const fluxProgrammaticFeedbackActionEnum = pgEnum("flux_programmatic_feedback_action", [
|
|
2479
|
+
"accept",
|
|
2480
|
+
"edit",
|
|
2481
|
+
"dismiss",
|
|
2482
|
+
]);
|
|
2483
|
+
// =============================================================================
|
|
2484
|
+
// PROGRAMMATIC SHELLS
|
|
2485
|
+
// =============================================================================
|
|
2486
|
+
/**
|
|
2487
|
+
* flux_programmatic_shells — Draft campaign shells prefilled from Compass order context.
|
|
2488
|
+
* Created when a programmatic fulfillment ticket enters the pipeline.
|
|
2489
|
+
* No DSP campaigns are created until vendor_locked status.
|
|
2490
|
+
*/
|
|
2491
|
+
export const fluxProgrammaticShells = pgTable("flux_programmatic_shells", {
|
|
2492
|
+
id: varchar("id")
|
|
2493
|
+
.primaryKey()
|
|
2494
|
+
.default(sql `gen_random_uuid()`),
|
|
2495
|
+
/** FK to the fulfillment ticket that spawned this shell (1:1 relationship) */
|
|
2496
|
+
fulfillmentTicketId: text("fulfillment_ticket_id")
|
|
2497
|
+
.references(() => fluxFulfillmentTickets.id, { onDelete: "cascade" })
|
|
2498
|
+
.notNull()
|
|
2499
|
+
.unique(),
|
|
2500
|
+
/** HubSpot deal ID for order-level correlation */
|
|
2501
|
+
dealId: text("deal_id"),
|
|
2502
|
+
/** HubSpot line item ID for precise routing */
|
|
2503
|
+
lineItemId: text("line_item_id"),
|
|
2504
|
+
/** Shell lifecycle status */
|
|
2505
|
+
status: fluxProgrammaticShellStatusEnum("status")
|
|
2506
|
+
.notNull()
|
|
2507
|
+
.default("draft"),
|
|
2508
|
+
/** Whether a vendor has been selected */
|
|
2509
|
+
vendorSelected: boolean("vendor_selected").default(false).notNull(),
|
|
2510
|
+
/** Selected vendor identifier (e.g., "ttd", "datasys") */
|
|
2511
|
+
selectedVendor: text("selected_vendor"),
|
|
2512
|
+
/** Raw lookup response from fn-legacy POST /api/programmatic/lookup */
|
|
2513
|
+
lookupData: jsonb("lookup_data").$type(),
|
|
2514
|
+
/**
|
|
2515
|
+
* Prefilled campaign parameters from order context + lookup.
|
|
2516
|
+
* TTD-specific fields:
|
|
2517
|
+
* - channel: DISPLAY | VIDEO | DOOH
|
|
2518
|
+
* - funnelLocation: AWARENESS | CONSIDERATION
|
|
2519
|
+
* - allocationType: MAXIMUM | MINIMUM
|
|
2520
|
+
* - budgetInAdvertiserCurrency: total budget
|
|
2521
|
+
* - dailyTargetInAdvertiserCurrency: daily pacing target
|
|
2522
|
+
* - baseBidCPMInAdvertiserCurrency: base bid
|
|
2523
|
+
* - maxBidCPMInAdvertiserCurrency: max bid
|
|
2524
|
+
* - seedId: TTD seed ID for campaign creation
|
|
2525
|
+
* - flightStart/flightEnd: ISO dates
|
|
2526
|
+
* - targetingGeo/targetingAudience/targetingDevice: JSONB segments
|
|
2527
|
+
*/
|
|
2528
|
+
prefillData: jsonb("prefill_data").$type(),
|
|
2529
|
+
/** TTD campaign ID (set after activation) */
|
|
2530
|
+
ttdCampaignId: text("ttd_campaign_id"),
|
|
2531
|
+
/** TTD ad group ID (set after activation) */
|
|
2532
|
+
ttdAdGroupId: text("ttd_ad_group_id"),
|
|
2533
|
+
/** Correlation ID for audit trail across fn-legacy/fn-flux/fn-forge */
|
|
2534
|
+
correlationId: text("correlation_id"),
|
|
2535
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2536
|
+
.defaultNow()
|
|
2537
|
+
.notNull(),
|
|
2538
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
2539
|
+
.defaultNow()
|
|
2540
|
+
.notNull(),
|
|
2541
|
+
}, (table) => ({
|
|
2542
|
+
fulfillmentTicketIdx: index("flux_prog_shells_ticket_idx").on(table.fulfillmentTicketId),
|
|
2543
|
+
dealIdx: index("flux_prog_shells_deal_idx").on(table.dealId),
|
|
2544
|
+
statusIdx: index("flux_prog_shells_status_idx").on(table.status),
|
|
2545
|
+
ttdCampaignIdx: index("flux_prog_shells_ttd_campaign_idx").on(table.ttdCampaignId),
|
|
2546
|
+
}));
|
|
2547
|
+
/**
|
|
2548
|
+
* flux_programmatic_shell_versions — Snapshot history for shell changes.
|
|
2549
|
+
* Each edit creates a new version for auditability.
|
|
2550
|
+
*/
|
|
2551
|
+
export const fluxProgrammaticShellVersions = pgTable("flux_programmatic_shell_versions", {
|
|
2552
|
+
id: varchar("id")
|
|
2553
|
+
.primaryKey()
|
|
2554
|
+
.default(sql `gen_random_uuid()`),
|
|
2555
|
+
shellId: varchar("shell_id")
|
|
2556
|
+
.references(() => fluxProgrammaticShells.id, { onDelete: "cascade" })
|
|
2557
|
+
.notNull(),
|
|
2558
|
+
version: integer("version").notNull(),
|
|
2559
|
+
snapshot: jsonb("snapshot").notNull().$type(),
|
|
2560
|
+
changedBy: text("changed_by"),
|
|
2561
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2562
|
+
.defaultNow()
|
|
2563
|
+
.notNull(),
|
|
2564
|
+
}, (table) => ({
|
|
2565
|
+
shellIdx: index("flux_prog_shell_versions_shell_idx").on(table.shellId),
|
|
2566
|
+
shellVersionUniq: uniqueIndex("flux_prog_shell_versions_shell_version_uniq").on(table.shellId, table.version),
|
|
2567
|
+
}));
|
|
2568
|
+
/**
|
|
2569
|
+
* flux_programmatic_decisions — Audit trail of buyer decisions on shells.
|
|
2570
|
+
* Tracks vendor selection, recommendation acceptance, and activation.
|
|
2571
|
+
*/
|
|
2572
|
+
export const fluxProgrammaticDecisions = pgTable("flux_programmatic_decisions", {
|
|
2573
|
+
id: varchar("id")
|
|
2574
|
+
.primaryKey()
|
|
2575
|
+
.default(sql `gen_random_uuid()`),
|
|
2576
|
+
shellId: varchar("shell_id")
|
|
2577
|
+
.references(() => fluxProgrammaticShells.id, { onDelete: "cascade" })
|
|
2578
|
+
.notNull(),
|
|
2579
|
+
decisionType: fluxProgrammaticDecisionTypeEnum("decision_type").notNull(),
|
|
2580
|
+
decisionData: jsonb("decision_data").$type(),
|
|
2581
|
+
decidedBy: text("decided_by"),
|
|
2582
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2583
|
+
.defaultNow()
|
|
2584
|
+
.notNull(),
|
|
2585
|
+
}, (table) => ({
|
|
2586
|
+
shellIdx: index("flux_prog_decisions_shell_idx").on(table.shellId),
|
|
2587
|
+
typeIdx: index("flux_prog_decisions_type_idx").on(table.decisionType),
|
|
2588
|
+
}));
|
|
2589
|
+
/**
|
|
2590
|
+
* flux_programmatic_recommendation_feedback — Buyer feedback on recommendations.
|
|
2591
|
+
* Used for ranking calibration and recommendation quality improvement.
|
|
2592
|
+
*/
|
|
2593
|
+
export const fluxProgrammaticRecommendationFeedback = pgTable("flux_programmatic_recommendation_feedback", {
|
|
2594
|
+
id: varchar("id")
|
|
2595
|
+
.primaryKey()
|
|
2596
|
+
.default(sql `gen_random_uuid()`),
|
|
2597
|
+
shellId: varchar("shell_id")
|
|
2598
|
+
.references(() => fluxProgrammaticShells.id, { onDelete: "cascade" })
|
|
2599
|
+
.notNull(),
|
|
2600
|
+
recommendationId: text("recommendation_id").notNull(),
|
|
2601
|
+
action: fluxProgrammaticFeedbackActionEnum("action").notNull(),
|
|
2602
|
+
editedValues: jsonb("edited_values").$type(),
|
|
2603
|
+
createdBy: text("created_by"),
|
|
2604
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2605
|
+
.defaultNow()
|
|
2606
|
+
.notNull(),
|
|
2607
|
+
}, (table) => ({
|
|
2608
|
+
shellIdx: index("flux_prog_rec_feedback_shell_idx").on(table.shellId),
|
|
2609
|
+
recIdx: index("flux_prog_rec_feedback_rec_idx").on(table.recommendationId),
|
|
2610
|
+
}));
|
|
2611
|
+
// =============================================================================
|
|
2612
|
+
// PROJECT CONTACTS (HubSpot Contacts)
|
|
2613
|
+
// =============================================================================
|
|
2614
|
+
export const fluxProjectContacts = pgTable("flux_project_contacts", {
|
|
2615
|
+
id: text("id")
|
|
2616
|
+
.primaryKey()
|
|
2617
|
+
.default(sql `gen_random_uuid()::text`),
|
|
2618
|
+
projectId: text("project_id")
|
|
2619
|
+
.notNull()
|
|
2620
|
+
.references(() => fluxProjects.id, { onDelete: "cascade" }),
|
|
2621
|
+
hubspotContactId: text("hubspot_contact_id").notNull(),
|
|
2622
|
+
firstName: text("first_name"),
|
|
2623
|
+
lastName: text("last_name"),
|
|
2624
|
+
email: text("email"),
|
|
2625
|
+
phone: text("phone"),
|
|
2626
|
+
jobTitle: text("job_title"),
|
|
2627
|
+
isPrimary: boolean("is_primary").default(false),
|
|
2628
|
+
lastActivityDate: timestamp("last_activity_date", { withTimezone: true }),
|
|
2629
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2630
|
+
.defaultNow()
|
|
2631
|
+
.notNull(),
|
|
2632
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
2633
|
+
.defaultNow()
|
|
2634
|
+
.notNull(),
|
|
2635
|
+
}, (table) => ({
|
|
2636
|
+
projectIdx: index("flux_project_contacts_project_idx").on(table.projectId),
|
|
2637
|
+
emailIdx: index("flux_project_contacts_email_idx").on(table.email),
|
|
2638
|
+
uniqueContactPerProject: uniqueIndex("flux_project_contacts_unique_idx").on(table.projectId, table.hubspotContactId),
|
|
2639
|
+
}));
|
|
2640
|
+
// =============================================================================
|
|
2641
|
+
// HUBSPOT SYNC LOG (Operational Visibility)
|
|
2642
|
+
// =============================================================================
|
|
2643
|
+
export const fluxHubspotSyncLog = pgTable("flux_hubspot_sync_log", {
|
|
2644
|
+
id: text("id")
|
|
2645
|
+
.primaryKey()
|
|
2646
|
+
.default(sql `gen_random_uuid()::text`),
|
|
2647
|
+
syncType: varchar("sync_type").notNull(),
|
|
2648
|
+
objectType: varchar("object_type"),
|
|
2649
|
+
status: varchar("status").notNull().default("success"),
|
|
2650
|
+
recordsProcessed: integer("records_processed").default(0),
|
|
2651
|
+
errorMessage: text("error_message"),
|
|
2652
|
+
durationMs: integer("duration_ms"),
|
|
2653
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2654
|
+
.defaultNow()
|
|
2655
|
+
.notNull(),
|
|
2656
|
+
}, (table) => ({
|
|
2657
|
+
typeIdx: index("flux_hubspot_sync_log_type_idx").on(table.syncType),
|
|
2658
|
+
createdIdx: index("flux_hubspot_sync_log_created_idx").on(table.createdAt),
|
|
2659
|
+
}));
|
|
2660
|
+
// =============================================================================
|
|
2661
|
+
// DEAL PIPELINE & FORECASTING
|
|
2662
|
+
// =============================================================================
|
|
2663
|
+
export const fluxConfidenceTierEnum = pgEnum("flux_confidence_tier", [
|
|
2664
|
+
"commit",
|
|
2665
|
+
"likely",
|
|
2666
|
+
"upside",
|
|
2667
|
+
"longshot",
|
|
2668
|
+
]);
|
|
2669
|
+
export const fluxCloseOutcomeEnum = pgEnum("flux_close_outcome", [
|
|
2670
|
+
"won",
|
|
2671
|
+
"lost",
|
|
2672
|
+
]);
|
|
2673
|
+
/**
|
|
2674
|
+
* flux_deal_pipeline — One row per HubSpot deal in the open pipeline.
|
|
2675
|
+
* Unified view merging HubSpot deal data with Compass order/margin data
|
|
2676
|
+
* and behavioral confidence scoring.
|
|
2677
|
+
*/
|
|
2678
|
+
export const fluxDealPipeline = pgTable("flux_deal_pipeline", {
|
|
2679
|
+
id: text("id")
|
|
2680
|
+
.primaryKey()
|
|
2681
|
+
.default(sql `gen_random_uuid()::text`),
|
|
2682
|
+
hubspotDealId: text("hubspot_deal_id").notNull().unique(),
|
|
2683
|
+
dealName: text("deal_name").notNull(),
|
|
2684
|
+
dealStage: text("deal_stage"),
|
|
2685
|
+
dealStageProbability: real("deal_stage_probability"),
|
|
2686
|
+
amount: numeric("amount"),
|
|
2687
|
+
closeDate: date("close_date"),
|
|
2688
|
+
hubspotOwnerId: text("hubspot_owner_id"),
|
|
2689
|
+
ownerName: text("owner_name"),
|
|
2690
|
+
ownerEmail: text("owner_email"),
|
|
2691
|
+
hubspotCompanyId: text("hubspot_company_id"),
|
|
2692
|
+
projectId: text("project_id").references(() => fluxProjects.id, {
|
|
2693
|
+
onDelete: "set null",
|
|
2694
|
+
}),
|
|
2695
|
+
/** Deal type from HubSpot (e.g., "newbusiness", "existingbusiness") */
|
|
2696
|
+
dealType: text("deal_type"),
|
|
2697
|
+
// Compass linkage
|
|
2698
|
+
compassPlanId: text("compass_plan_id"),
|
|
2699
|
+
compassOrderId: text("compass_order_id"),
|
|
2700
|
+
compassOrderStatus: text("compass_order_status"),
|
|
2701
|
+
totalInvestment: numeric("total_investment"),
|
|
2702
|
+
marginPercent: real("margin_percent"),
|
|
2703
|
+
productMix: jsonb("product_mix").$type(),
|
|
2704
|
+
// Confidence scoring
|
|
2705
|
+
confidenceScore: real("confidence_score"),
|
|
2706
|
+
confidenceTier: fluxConfidenceTierEnum("confidence_tier"),
|
|
2707
|
+
scoreFactors: jsonb("score_factors").$type(),
|
|
2708
|
+
// Forecast windows
|
|
2709
|
+
forecast30d: numeric("forecast_30d"),
|
|
2710
|
+
forecast60d: numeric("forecast_60d"),
|
|
2711
|
+
forecast90d: numeric("forecast_90d"),
|
|
2712
|
+
marginForecast30d: numeric("margin_forecast_30d"),
|
|
2713
|
+
marginForecast60d: numeric("margin_forecast_60d"),
|
|
2714
|
+
marginForecast90d: numeric("margin_forecast_90d"),
|
|
2715
|
+
predictedMarginPct: real("predicted_margin_pct"),
|
|
2716
|
+
// Rep override
|
|
2717
|
+
overrideScore: real("override_score"),
|
|
2718
|
+
overrideReason: text("override_reason"),
|
|
2719
|
+
overrideBy: text("override_by"),
|
|
2720
|
+
overrideAt: timestamp("override_at", { withTimezone: true }),
|
|
2721
|
+
overrideExpiresAt: timestamp("override_expires_at", {
|
|
2722
|
+
withTimezone: true,
|
|
2723
|
+
}),
|
|
2724
|
+
// Stage velocity tracking
|
|
2725
|
+
/** JSONB map of stage name → ISO timestamp when deal entered that stage */
|
|
2726
|
+
stageEntryDates: jsonb("stage_entry_dates")
|
|
2727
|
+
.default(sql `'{}'::jsonb`)
|
|
2728
|
+
.$type(),
|
|
2729
|
+
// -- HubSpot enrichment fields --
|
|
2730
|
+
/** HubSpot AI deal score (hs_deal_score) */
|
|
2731
|
+
hubspotDealScore: real("hubspot_deal_score"),
|
|
2732
|
+
/** HubSpot stall detection (hs_is_stalled) */
|
|
2733
|
+
isStalled: boolean("is_stalled").default(false),
|
|
2734
|
+
/** Reason deal was won */
|
|
2735
|
+
closedWonReason: text("closed_won_reason"),
|
|
2736
|
+
/** Reason deal was lost */
|
|
2737
|
+
closedLostReason: text("closed_lost_reason"),
|
|
2738
|
+
/** Next step from HubSpot (hs_next_step) */
|
|
2739
|
+
nextStep: text("next_step"),
|
|
2740
|
+
/** Naviga campaign ID for billing reconciliation */
|
|
2741
|
+
navigaCampaignId: text("naviga_campaign_id"),
|
|
2742
|
+
/** Compass order URL (from fn_compass_order_url on deal) */
|
|
2743
|
+
compassOrderUrl: text("compass_order_url"),
|
|
2744
|
+
/** Compass order total value */
|
|
2745
|
+
compassOrderTotal: numeric("compass_order_total", { precision: 12, scale: 2 }),
|
|
2746
|
+
// Compass lifecycle timestamps
|
|
2747
|
+
compassApprovedAt: timestamp("compass_approved_at", { withTimezone: true }),
|
|
2748
|
+
compassSubmittedAt: timestamp("compass_submitted_at", { withTimezone: true }),
|
|
2749
|
+
compassActivatedAt: timestamp("compass_activated_at", { withTimezone: true }),
|
|
2750
|
+
compassCompletedAt: timestamp("compass_completed_at", { withTimezone: true }),
|
|
2751
|
+
compassSentAt: timestamp("compass_sent_at", { withTimezone: true }),
|
|
2752
|
+
// Compass financial
|
|
2753
|
+
compassTotalMarginDollars: numeric("compass_total_margin_dollars", { precision: 12, scale: 2 }),
|
|
2754
|
+
compassWeightedMarginPct: real("compass_weighted_margin_pct"),
|
|
2755
|
+
compassTotalMediaBudget: numeric("compass_total_media_budget", { precision: 12, scale: 2 }),
|
|
2756
|
+
// Compass amendment tracking
|
|
2757
|
+
compassAmendmentCount: integer("compass_amendment_count").default(0),
|
|
2758
|
+
compassOrderVersion: integer("compass_order_version").default(1),
|
|
2759
|
+
compassLastAmendment: text("compass_last_amendment"),
|
|
2760
|
+
compassLastAmendmentDate: timestamp("compass_last_amendment_date", { withTimezone: true }),
|
|
2761
|
+
// Compass risk & context
|
|
2762
|
+
compassPendingRateApprovals: integer("compass_pending_rate_approvals").default(0),
|
|
2763
|
+
compassReturnReason: text("compass_return_reason"),
|
|
2764
|
+
compassProductFamilies: text("compass_product_families"),
|
|
2765
|
+
compassPlanMode: text("compass_plan_mode"),
|
|
2766
|
+
compassFlightDates: text("compass_flight_dates"),
|
|
2767
|
+
compassLocation: text("compass_location"),
|
|
2768
|
+
compassOrderName: text("compass_order_name"),
|
|
2769
|
+
// Activity tracking
|
|
2770
|
+
lastActivityDate: timestamp("last_activity_date", { withTimezone: true }),
|
|
2771
|
+
daysInCurrentStage: integer("days_in_current_stage"),
|
|
2772
|
+
// Close tracking
|
|
2773
|
+
isClosed: boolean("is_closed").default(false).notNull(),
|
|
2774
|
+
closeOutcome: fluxCloseOutcomeEnum("close_outcome"),
|
|
2775
|
+
closedAt: timestamp("closed_at", { withTimezone: true }),
|
|
2776
|
+
// HubSpot metadata
|
|
2777
|
+
hubspotCreatedAt: timestamp("hubspot_created_at", { withTimezone: true }),
|
|
2778
|
+
hubspotUpdatedAt: timestamp("hubspot_updated_at", { withTimezone: true }),
|
|
2779
|
+
// Timestamps
|
|
2780
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2781
|
+
.defaultNow()
|
|
2782
|
+
.notNull(),
|
|
2783
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
2784
|
+
.defaultNow()
|
|
2785
|
+
.notNull(),
|
|
2786
|
+
}, (table) => ({
|
|
2787
|
+
hubspotDealIdx: uniqueIndex("flux_deal_pipeline_hubspot_deal_idx").on(table.hubspotDealId),
|
|
2788
|
+
companyIdx: index("flux_deal_pipeline_company_idx").on(table.hubspotCompanyId),
|
|
2789
|
+
projectIdx: index("flux_deal_pipeline_project_idx").on(table.projectId),
|
|
2790
|
+
stageIdx: index("flux_deal_pipeline_stage_idx").on(table.dealStage),
|
|
2791
|
+
tierIdx: index("flux_deal_pipeline_tier_idx").on(table.confidenceTier),
|
|
2792
|
+
closeDateIdx: index("flux_deal_pipeline_close_date_idx").on(table.closeDate),
|
|
2793
|
+
ownerIdx: index("flux_deal_pipeline_owner_idx").on(table.hubspotOwnerId),
|
|
2794
|
+
closedIdx: index("flux_deal_pipeline_closed_idx").on(table.isClosed),
|
|
2795
|
+
}));
|
|
2796
|
+
/**
|
|
2797
|
+
* flux_forecast_snapshots — Weekly point-in-time pipeline snapshots for accuracy tracking.
|
|
2798
|
+
* Unique per (deal_id, snapshot_date) to support idempotent weekly runs.
|
|
2799
|
+
*/
|
|
2800
|
+
export const fluxForecastSnapshots = pgTable("flux_forecast_snapshots", {
|
|
2801
|
+
id: text("id")
|
|
2802
|
+
.primaryKey()
|
|
2803
|
+
.default(sql `gen_random_uuid()::text`),
|
|
2804
|
+
dealId: text("deal_id")
|
|
2805
|
+
.references(() => fluxDealPipeline.id, { onDelete: "cascade" })
|
|
2806
|
+
.notNull(),
|
|
2807
|
+
hubspotDealId: text("hubspot_deal_id").notNull(),
|
|
2808
|
+
snapshotDate: date("snapshot_date").notNull(),
|
|
2809
|
+
dealName: text("deal_name"),
|
|
2810
|
+
amount: numeric("amount"),
|
|
2811
|
+
confidenceScore: real("confidence_score"),
|
|
2812
|
+
confidenceTier: fluxConfidenceTierEnum("confidence_tier"),
|
|
2813
|
+
forecast30d: numeric("forecast_30d"),
|
|
2814
|
+
forecast60d: numeric("forecast_60d"),
|
|
2815
|
+
forecast90d: numeric("forecast_90d"),
|
|
2816
|
+
marginForecast30d: numeric("margin_forecast_30d"),
|
|
2817
|
+
marginForecast60d: numeric("margin_forecast_60d"),
|
|
2818
|
+
marginForecast90d: numeric("margin_forecast_90d"),
|
|
2819
|
+
predictedMarginPct: real("predicted_margin_pct"),
|
|
2820
|
+
dealStage: text("deal_stage"),
|
|
2821
|
+
closeDate: date("close_date"),
|
|
2822
|
+
ownerEmail: text("owner_email"),
|
|
2823
|
+
overrideActive: boolean("override_active").default(false),
|
|
2824
|
+
// Backfilled from Snowflake billing actuals
|
|
2825
|
+
actualBilled: numeric("actual_billed"),
|
|
2826
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2827
|
+
.defaultNow()
|
|
2828
|
+
.notNull(),
|
|
2829
|
+
}, (table) => ({
|
|
2830
|
+
dealSnapshotIdx: uniqueIndex("flux_forecast_snapshots_deal_date_idx").on(table.dealId, table.snapshotDate),
|
|
2831
|
+
snapshotDateIdx: index("flux_forecast_snapshots_date_idx").on(table.snapshotDate),
|
|
2832
|
+
hubspotDealIdx: index("flux_forecast_snapshots_hs_deal_idx").on(table.hubspotDealId),
|
|
2833
|
+
}));
|
|
2834
|
+
/**
|
|
2835
|
+
* flux_billing_actuals — Naviga billing data from Snowflake.
|
|
2836
|
+
* Keyed by advertiser + billing period for forecast accuracy tracking.
|
|
2837
|
+
*/
|
|
2838
|
+
export const fluxBillingActuals = pgTable("flux_billing_actuals", {
|
|
2839
|
+
id: text("id")
|
|
2840
|
+
.primaryKey()
|
|
2841
|
+
.default(sql `gen_random_uuid()::text`),
|
|
2842
|
+
advertiserName: text("advertiser_name").notNull(),
|
|
2843
|
+
advertiserId: text("advertiser_id"),
|
|
2844
|
+
hubspotCompanyId: text("hubspot_company_id"),
|
|
2845
|
+
billingPeriod: date("billing_period").notNull(),
|
|
2846
|
+
totalBilled: numeric("total_billed").notNull(),
|
|
2847
|
+
totalRevenue: numeric("total_revenue"),
|
|
2848
|
+
productBreakdown: jsonb("product_breakdown").$type(),
|
|
2849
|
+
source: text("source").default("snowflake").notNull(),
|
|
2850
|
+
syncedAt: timestamp("synced_at", { withTimezone: true })
|
|
2851
|
+
.defaultNow()
|
|
2852
|
+
.notNull(),
|
|
2853
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2854
|
+
.defaultNow()
|
|
2855
|
+
.notNull(),
|
|
2856
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
2857
|
+
.defaultNow()
|
|
2858
|
+
.notNull(),
|
|
2859
|
+
}, (table) => ({
|
|
2860
|
+
advertiserPeriodIdx: uniqueIndex("flux_billing_actuals_advertiser_period_idx").on(table.advertiserName, table.billingPeriod),
|
|
2861
|
+
hubspotCompanyIdx: index("flux_billing_actuals_company_idx").on(table.hubspotCompanyId),
|
|
2862
|
+
periodIdx: index("flux_billing_actuals_period_idx").on(table.billingPeriod),
|
|
2863
|
+
}));
|
|
2864
|
+
/**
|
|
2865
|
+
* flux_product_family_benchmarks — Benchmark margin % by product family.
|
|
2866
|
+
* Used by deal scoring for margin prediction when Compass order margin is missing.
|
|
2867
|
+
* Seed from fn-legacy strib_product_families.benchmark_margin_percent or manual entry.
|
|
2868
|
+
*/
|
|
2869
|
+
export const fluxProductFamilyBenchmarks = pgTable("flux_product_family_benchmarks", {
|
|
2870
|
+
familyCode: varchar("family_code", { length: 64 }).primaryKey(),
|
|
2871
|
+
benchmarkMarginPercent: real("benchmark_margin_percent").notNull(),
|
|
2872
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
2873
|
+
.notNull()
|
|
2874
|
+
.defaultNow(),
|
|
2875
|
+
});
|
|
2876
|
+
// ─── AE Close Rates (materialized from closed deals) ─────────────────────
|
|
2877
|
+
export const fluxAeCloseRates = pgTable("flux_ae_close_rates", {
|
|
2878
|
+
id: serial("id").primaryKey(),
|
|
2879
|
+
hubspotOwnerId: text("hubspot_owner_id").notNull(),
|
|
2880
|
+
ownerName: text("owner_name"),
|
|
2881
|
+
ownerEmail: text("owner_email"),
|
|
2882
|
+
sizeTier: text("size_tier").notNull(),
|
|
2883
|
+
dealsWon: integer("deals_won").notNull().default(0),
|
|
2884
|
+
dealsLost: integer("deals_lost").notNull().default(0),
|
|
2885
|
+
dealsTotal: integer("deals_total").notNull().default(0),
|
|
2886
|
+
closeRate: real("close_rate").notNull().default(0),
|
|
2887
|
+
avgDealSize: numeric("avg_deal_size", { precision: 12, scale: 2 }),
|
|
2888
|
+
avgDaysToClose: real("avg_days_to_close"),
|
|
2889
|
+
windowStart: date("window_start").notNull(),
|
|
2890
|
+
windowEnd: date("window_end").notNull(),
|
|
2891
|
+
overrideCount: integer("override_count").notNull().default(0),
|
|
2892
|
+
overrideAvgScore: real("override_avg_score"),
|
|
2893
|
+
overrideActualWinRate: real("override_actual_win_rate"),
|
|
2894
|
+
overrideAccuracyDelta: real("override_accuracy_delta"),
|
|
2895
|
+
computedAt: timestamp("computed_at", { withTimezone: true })
|
|
2896
|
+
.notNull()
|
|
2897
|
+
.defaultNow(),
|
|
2898
|
+
}, (table) => [
|
|
2899
|
+
uniqueIndex("flux_ae_close_rates_owner_tier_idx").on(table.hubspotOwnerId, table.sizeTier),
|
|
2900
|
+
index("flux_ae_close_rates_owner_idx").on(table.hubspotOwnerId),
|
|
2901
|
+
]);
|
|
2902
|
+
// =============================================================================
|
|
2903
|
+
// ASSISTANT CONVERSATIONS
|
|
2904
|
+
// =============================================================================
|
|
2905
|
+
/**
|
|
2906
|
+
* flux_assistant_conversations — Stores AI assistant conversation history.
|
|
2907
|
+
* Each conversation belongs to a specific surface (e.g., "fulfillment") and user.
|
|
2908
|
+
* Messages stored as JSONB array of { role, content, toolCalls? } objects.
|
|
2909
|
+
*/
|
|
2910
|
+
export const fluxAssistantConversations = pgTable("flux_assistant_conversations", {
|
|
2911
|
+
id: varchar("id")
|
|
2912
|
+
.primaryKey()
|
|
2913
|
+
.default(sql `gen_random_uuid()`),
|
|
2914
|
+
clerkUserId: varchar("clerk_user_id").notNull(),
|
|
2915
|
+
userEmail: varchar("user_email").notNull(),
|
|
2916
|
+
surfaceId: varchar("surface_id").notNull(),
|
|
2917
|
+
title: text("title").notNull().default("New conversation"),
|
|
2918
|
+
messages: jsonb("messages").$type().notNull().default(sql `'[]'::jsonb`),
|
|
2919
|
+
pageContext: jsonb("page_context"),
|
|
2920
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2921
|
+
.defaultNow()
|
|
2922
|
+
.notNull(),
|
|
2923
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
2924
|
+
.defaultNow()
|
|
2925
|
+
.notNull(),
|
|
2926
|
+
}, (table) => ({
|
|
2927
|
+
userSurfaceIdx: index("flux_assistant_conv_user_surface_idx").on(table.clerkUserId, table.surfaceId),
|
|
2928
|
+
}));
|
|
2929
|
+
// =============================================================================
|
|
2930
|
+
// CLIENT SENTINEL — Tier 1 Client Intelligence Monitoring
|
|
2931
|
+
// =============================================================================
|
|
2932
|
+
export const fluxClientSignalTypeEnum = pgEnum("flux_client_signal_type", [
|
|
2933
|
+
"ma_activity",
|
|
2934
|
+
"leadership_change",
|
|
2935
|
+
"earnings",
|
|
2936
|
+
"layoffs",
|
|
2937
|
+
"expansion",
|
|
2938
|
+
"legal",
|
|
2939
|
+
"partnership",
|
|
2940
|
+
"general",
|
|
2941
|
+
]);
|
|
2942
|
+
/**
|
|
2943
|
+
* Client watches — per-client monitoring configuration.
|
|
2944
|
+
* Created automatically from HubSpot Tier 1 sync or manually via Slack commands.
|
|
2945
|
+
*/
|
|
2946
|
+
export const fluxClientWatches = pgTable("flux_client_watches", {
|
|
2947
|
+
id: text("id")
|
|
2948
|
+
.primaryKey()
|
|
2949
|
+
.default(sql `gen_random_uuid()::text`),
|
|
2950
|
+
projectId: text("project_id").references(() => fluxProjects.id, {
|
|
2951
|
+
onDelete: "set null",
|
|
2952
|
+
}),
|
|
2953
|
+
hubspotCompanyId: text("hubspot_company_id"),
|
|
2954
|
+
companyName: text("company_name").notNull(),
|
|
2955
|
+
searchTerms: text("search_terms")
|
|
2956
|
+
.array()
|
|
2957
|
+
.notNull(),
|
|
2958
|
+
slackChannelId: text("slack_channel_id"),
|
|
2959
|
+
isActive: boolean("is_active").default(true).notNull(),
|
|
2960
|
+
syncedFromHubspot: boolean("synced_from_hubspot").default(false).notNull(),
|
|
2961
|
+
lastSyncedAt: timestamp("last_synced_at", { withTimezone: true }),
|
|
2962
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
2963
|
+
.defaultNow()
|
|
2964
|
+
.notNull(),
|
|
2965
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
2966
|
+
.defaultNow()
|
|
2967
|
+
.notNull(),
|
|
2968
|
+
}, (table) => ({
|
|
2969
|
+
companyNameIdx: index("flux_client_watches_company_name_idx").on(table.companyName),
|
|
2970
|
+
hubspotCompanyIdx: index("flux_client_watches_hubspot_company_idx").on(table.hubspotCompanyId),
|
|
2971
|
+
isActiveIdx: index("flux_client_watches_is_active_idx").on(table.isActive),
|
|
2972
|
+
createdAtIdx: index("flux_client_watches_created_at_idx").on(table.createdAt),
|
|
2973
|
+
}));
|
|
2974
|
+
/**
|
|
2975
|
+
* Client signals — discovered business intelligence for watched clients.
|
|
2976
|
+
*/
|
|
2977
|
+
export const fluxClientSignals = pgTable("flux_client_signals", {
|
|
2978
|
+
id: text("id")
|
|
2979
|
+
.primaryKey()
|
|
2980
|
+
.default(sql `gen_random_uuid()::text`),
|
|
2981
|
+
watchId: text("watch_id")
|
|
2982
|
+
.references(() => fluxClientWatches.id, { onDelete: "cascade" })
|
|
2983
|
+
.notNull(),
|
|
2984
|
+
url: text("url").notNull().unique(),
|
|
2985
|
+
title: text("title").notNull(),
|
|
2986
|
+
author: text("author"),
|
|
2987
|
+
publishedAt: timestamp("published_at", { withTimezone: true }),
|
|
2988
|
+
sourceDomain: text("source_domain"),
|
|
2989
|
+
snippet: text("snippet"),
|
|
2990
|
+
sourceType: text("source_type").default("article").notNull(),
|
|
2991
|
+
signalType: fluxClientSignalTypeEnum("signal_type")
|
|
2992
|
+
.default("general")
|
|
2993
|
+
.notNull(),
|
|
2994
|
+
relevanceScore: real("relevance_score"),
|
|
2995
|
+
analysisRationale: text("analysis_rationale"),
|
|
2996
|
+
suggestedAction: text("suggested_action"),
|
|
2997
|
+
sentiment: real("sentiment"),
|
|
2998
|
+
impactAssessment: text("impact_assessment"),
|
|
2999
|
+
slackMessageTs: text("slack_message_ts"),
|
|
3000
|
+
notifiedAt: timestamp("notified_at", { withTimezone: true }),
|
|
3001
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
3002
|
+
.defaultNow()
|
|
3003
|
+
.notNull(),
|
|
3004
|
+
}, (table) => ({
|
|
3005
|
+
urlIdx: uniqueIndex("flux_client_signals_url_idx").on(table.url),
|
|
3006
|
+
watchIdx: index("flux_client_signals_watch_idx").on(table.watchId),
|
|
3007
|
+
signalTypeIdx: index("flux_client_signals_signal_type_idx").on(table.signalType),
|
|
3008
|
+
createdAtIdx: index("flux_client_signals_created_at_idx").on(table.createdAt),
|
|
3009
|
+
}));
|
|
3010
|
+
/**
|
|
3011
|
+
* Client scan run log — tracks each client pipeline cycle for observability.
|
|
3012
|
+
*/
|
|
3013
|
+
export const fluxClientScanRuns = pgTable("flux_client_scan_runs", {
|
|
3014
|
+
id: text("id")
|
|
3015
|
+
.primaryKey()
|
|
3016
|
+
.default(sql `gen_random_uuid()::text`),
|
|
3017
|
+
runAt: timestamp("run_at", { withTimezone: true }).defaultNow().notNull(),
|
|
3018
|
+
triggeredBy: text("triggered_by").notNull(),
|
|
3019
|
+
watchesScanned: integer("watches_scanned").default(0).notNull(),
|
|
3020
|
+
signalsFound: integer("signals_found").default(0).notNull(),
|
|
3021
|
+
signalsNew: integer("signals_new").default(0).notNull(),
|
|
3022
|
+
alertsSent: integer("alerts_sent").default(0).notNull(),
|
|
3023
|
+
durationMs: integer("duration_ms"),
|
|
3024
|
+
error: text("error"),
|
|
3025
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
3026
|
+
.defaultNow()
|
|
3027
|
+
.notNull(),
|
|
3028
|
+
}, (table) => ({
|
|
3029
|
+
runAtIdx: index("flux_client_scan_runs_run_at_idx").on(table.runAt),
|
|
3030
|
+
}));
|
|
3031
|
+
// Client Sentinel relations
|
|
3032
|
+
export const fluxClientWatchesRelations = relations(fluxClientWatches, ({ one, many }) => ({
|
|
3033
|
+
project: one(fluxProjects, {
|
|
3034
|
+
fields: [fluxClientWatches.projectId],
|
|
3035
|
+
references: [fluxProjects.id],
|
|
3036
|
+
}),
|
|
3037
|
+
signals: many(fluxClientSignals),
|
|
3038
|
+
}));
|
|
3039
|
+
export const fluxClientSignalsRelations = relations(fluxClientSignals, ({ one }) => ({
|
|
3040
|
+
watch: one(fluxClientWatches, {
|
|
3041
|
+
fields: [fluxClientSignals.watchId],
|
|
3042
|
+
references: [fluxClientWatches.id],
|
|
3043
|
+
}),
|
|
3044
|
+
}));
|
|
3045
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3046
|
+
// Bad Ad Reporter
|
|
3047
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3048
|
+
export const fluxBadadTypeEnum = pgEnum("flux_badad_type", [
|
|
3049
|
+
"redirect",
|
|
3050
|
+
"popup",
|
|
3051
|
+
"inappropriate_content",
|
|
3052
|
+
"slow_resource_heavy",
|
|
3053
|
+
"auto_playing_media",
|
|
3054
|
+
"phishing_malware",
|
|
3055
|
+
"other",
|
|
3056
|
+
]);
|
|
3057
|
+
export const fluxBadadSeverityEnum = pgEnum("flux_badad_severity", [
|
|
3058
|
+
"annoying",
|
|
3059
|
+
"disruptive",
|
|
3060
|
+
"dangerous",
|
|
3061
|
+
]);
|
|
3062
|
+
export const fluxBadadReportStatusEnum = pgEnum("flux_badad_report_status", [
|
|
3063
|
+
"new",
|
|
3064
|
+
"investigating",
|
|
3065
|
+
"blocked",
|
|
3066
|
+
"escalated",
|
|
3067
|
+
"resolved",
|
|
3068
|
+
]);
|
|
3069
|
+
export const fluxBadadReports = pgTable("flux_badad_reports", {
|
|
3070
|
+
id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
|
|
3071
|
+
adType: fluxBadadTypeEnum("ad_type").notNull(),
|
|
3072
|
+
severity: fluxBadadSeverityEnum("severity").notNull().default("annoying"),
|
|
3073
|
+
status: fluxBadadReportStatusEnum("status").notNull().default("new"),
|
|
3074
|
+
pageUrl: text("page_url").notNull(),
|
|
3075
|
+
description: text("description"),
|
|
3076
|
+
browserDevice: text("browser_device"),
|
|
3077
|
+
advertiserName: text("advertiser_name"),
|
|
3078
|
+
clickThroughUrl: text("click_through_url"),
|
|
3079
|
+
adPlacement: text("ad_placement"),
|
|
3080
|
+
adDebugInfo: text("ad_debug_info"),
|
|
3081
|
+
reporterSlackId: text("reporter_slack_id").notNull(),
|
|
3082
|
+
reporterName: text("reporter_name"),
|
|
3083
|
+
ownerSlackId: text("owner_slack_id"),
|
|
3084
|
+
ownerName: text("owner_name"),
|
|
3085
|
+
reportCount: integer("report_count").notNull().default(1),
|
|
3086
|
+
slackChannelId: text("slack_channel_id"),
|
|
3087
|
+
slackMessageTs: text("slack_message_ts"),
|
|
3088
|
+
escalationNetwork: text("escalation_network"),
|
|
3089
|
+
escalationNotes: text("escalation_notes"),
|
|
3090
|
+
resolutionNotes: text("resolution_notes"),
|
|
3091
|
+
resolvedAt: timestamp("resolved_at", { withTimezone: true }),
|
|
3092
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3093
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3094
|
+
});
|
|
3095
|
+
export const fluxBadadActivity = pgTable("flux_badad_activity", {
|
|
3096
|
+
id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
|
|
3097
|
+
reportId: text("report_id")
|
|
3098
|
+
.notNull()
|
|
3099
|
+
.references(() => fluxBadadReports.id, { onDelete: "cascade" }),
|
|
3100
|
+
action: text("action").notNull(),
|
|
3101
|
+
actorName: text("actor_name"),
|
|
3102
|
+
actorSlackId: text("actor_slack_id"),
|
|
3103
|
+
details: jsonb("details"),
|
|
3104
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3105
|
+
});
|
|
3106
|
+
export const fluxBadadReportsRelations = relations(fluxBadadReports, ({ many }) => ({
|
|
3107
|
+
activity: many(fluxBadadActivity),
|
|
3108
|
+
}));
|
|
3109
|
+
export const fluxBadadActivityRelations = relations(fluxBadadActivity, ({ one }) => ({
|
|
3110
|
+
report: one(fluxBadadReports, {
|
|
3111
|
+
fields: [fluxBadadActivity.reportId],
|
|
3112
|
+
references: [fluxBadadReports.id],
|
|
3113
|
+
}),
|
|
3114
|
+
}));
|
|
3115
|
+
// =============================================================================
|
|
3116
|
+
// PROJECT ONBOARDING QUEUE
|
|
3117
|
+
// =============================================================================
|
|
3118
|
+
export const fluxOnboardingReasonEnum = pgEnum("flux_onboarding_reason", [
|
|
3119
|
+
"no_team",
|
|
3120
|
+
"no_slack_channel",
|
|
3121
|
+
"no_template",
|
|
3122
|
+
]);
|
|
3123
|
+
/**
|
|
3124
|
+
* Tracks projects that need attention after import.
|
|
3125
|
+
* Entries are created when a project is imported with missing team assignments,
|
|
3126
|
+
* no Slack channel, or other incomplete setup. Auto-resolved when conditions are met.
|
|
3127
|
+
*/
|
|
3128
|
+
export const fluxProjectOnboardingQueue = pgTable("flux_project_onboarding_queue", {
|
|
3129
|
+
id: varchar("id")
|
|
3130
|
+
.primaryKey()
|
|
3131
|
+
.default(sql `gen_random_uuid()`),
|
|
3132
|
+
projectId: varchar("project_id")
|
|
3133
|
+
.notNull()
|
|
3134
|
+
.references(() => fluxProjects.id),
|
|
3135
|
+
reason: fluxOnboardingReasonEnum("reason").notNull(),
|
|
3136
|
+
metadata: jsonb("metadata").default({}).$type(),
|
|
3137
|
+
resolvedAt: timestamp("resolved_at"),
|
|
3138
|
+
resolvedBy: varchar("resolved_by").references(() => fluxUsers.id),
|
|
3139
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
3140
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
3141
|
+
}, (table) => ({
|
|
3142
|
+
projectIdx: index("flux_onboarding_queue_project_idx").on(table.projectId),
|
|
3143
|
+
unresolvedIdx: index("flux_onboarding_queue_unresolved_idx")
|
|
3144
|
+
.on(table.resolvedAt)
|
|
3145
|
+
.where(sql `resolved_at IS NULL`),
|
|
3146
|
+
}));
|
|
3147
|
+
export const fluxProjectOnboardingQueueRelations = relations(fluxProjectOnboardingQueue, ({ one }) => ({
|
|
3148
|
+
project: one(fluxProjects, {
|
|
3149
|
+
fields: [fluxProjectOnboardingQueue.projectId],
|
|
3150
|
+
references: [fluxProjects.id],
|
|
3151
|
+
}),
|
|
3152
|
+
resolvedByUser: one(fluxUsers, {
|
|
3153
|
+
fields: [fluxProjectOnboardingQueue.resolvedBy],
|
|
3154
|
+
references: [fluxUsers.id],
|
|
3155
|
+
}),
|
|
3156
|
+
}));
|
|
3157
|
+
// =============================================================================
|
|
3158
|
+
// SLA CONFIGURATION
|
|
3159
|
+
// =============================================================================
|
|
3160
|
+
/**
|
|
3161
|
+
* Per-tier SLA thresholds (all values in business days).
|
|
3162
|
+
* Tiers: Default (fallback), Tier 1, Tier 2, Tier 3.
|
|
3163
|
+
* NUMERIC(4,1) allows half-day precision (e.g. 0.5 for the Ready stage).
|
|
3164
|
+
*/
|
|
3165
|
+
export const fluxSlaConfig = pgTable("flux_sla_config", {
|
|
3166
|
+
id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
|
|
3167
|
+
tier: text("tier").notNull().unique(),
|
|
3168
|
+
pendingFormDays: numeric("pending_form_days", { precision: 4, scale: 1 }),
|
|
3169
|
+
submittedDays: numeric("submitted_days", { precision: 4, scale: 1 }),
|
|
3170
|
+
pendingCreativeDays: numeric("pending_creative_days", { precision: 4, scale: 1 }),
|
|
3171
|
+
pendingFulfillmentDays: numeric("pending_fulfillment_days", { precision: 4, scale: 1 }),
|
|
3172
|
+
readyDays: numeric("ready_days", { precision: 4, scale: 1 }),
|
|
3173
|
+
endToEndDays: numeric("end_to_end_days", { precision: 4, scale: 1 }),
|
|
3174
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3175
|
+
updatedBy: text("updated_by"),
|
|
3176
|
+
});
|
|
3177
|
+
// ---------------------------------------------------------------------------
|
|
3178
|
+
// SLA Profiles (flexible JSONB thresholds, replaces fixed-column config)
|
|
3179
|
+
// ---------------------------------------------------------------------------
|
|
3180
|
+
/**
|
|
3181
|
+
* SLA profiles with JSONB thresholds — supports named profiles for tiers,
|
|
3182
|
+
* per-client overrides, and Compass SLA exceptions.
|
|
3183
|
+
*
|
|
3184
|
+
* Resolution chain: ticket override → project profile → default profile → hardcoded.
|
|
3185
|
+
*/
|
|
3186
|
+
export const fluxSlaProfiles = pgTable("flux_sla_profiles", {
|
|
3187
|
+
id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
|
|
3188
|
+
name: text("name").notNull().unique(),
|
|
3189
|
+
description: text("description"),
|
|
3190
|
+
/** Exactly one profile should be marked default (enforced by partial unique index in migration) */
|
|
3191
|
+
isDefault: boolean("is_default").notNull().default(false),
|
|
3192
|
+
/**
|
|
3193
|
+
* Stage thresholds in business days as JSONB:
|
|
3194
|
+
* { pending_form: 1, submitted: 2, pending_creative: 2, pending_fulfillment: 1, ready: 0.5, end_to_end: 5 }
|
|
3195
|
+
*/
|
|
3196
|
+
thresholds: jsonb("thresholds").notNull().default(sql `'{}'::jsonb`),
|
|
3197
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3198
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3199
|
+
updatedBy: text("updated_by"),
|
|
3200
|
+
});
|
|
3201
|
+
/** Known SLA stage slugs for threshold validation */
|
|
3202
|
+
export const SLA_STAGE_SLUGS = [
|
|
3203
|
+
"pending_form",
|
|
3204
|
+
"submitted",
|
|
3205
|
+
"pending_creative",
|
|
3206
|
+
"pending_fulfillment",
|
|
3207
|
+
"ready",
|
|
3208
|
+
"end_to_end",
|
|
3209
|
+
];
|
|
3210
|
+
// ============================================================================
|
|
3211
|
+
// Agreement Tracking
|
|
3212
|
+
// ============================================================================
|
|
3213
|
+
/**
|
|
3214
|
+
* Agreement tier: contract (annual commitment), io (per-order), approval (lightweight)
|
|
3215
|
+
*/
|
|
3216
|
+
export const fluxAgreementTierEnum = pgEnum("flux_agreement_tier", [
|
|
3217
|
+
"contract",
|
|
3218
|
+
"io",
|
|
3219
|
+
"approval",
|
|
3220
|
+
]);
|
|
3221
|
+
/**
|
|
3222
|
+
* Agreement lifecycle status
|
|
3223
|
+
*/
|
|
3224
|
+
export const fluxAgreementStatusEnum = pgEnum("flux_agreement_status", [
|
|
3225
|
+
"draft",
|
|
3226
|
+
"pending_internal",
|
|
3227
|
+
"pending_client",
|
|
3228
|
+
"pending_countersign",
|
|
3229
|
+
"active",
|
|
3230
|
+
"amended",
|
|
3231
|
+
"expired",
|
|
3232
|
+
"voided",
|
|
3233
|
+
]);
|
|
3234
|
+
/**
|
|
3235
|
+
* Signer role in the signing workflow
|
|
3236
|
+
*/
|
|
3237
|
+
export const fluxSignerRoleEnum = pgEnum("flux_signer_role", [
|
|
3238
|
+
"internal_approver",
|
|
3239
|
+
"client_signer",
|
|
3240
|
+
"counter_signer",
|
|
3241
|
+
]);
|
|
3242
|
+
/**
|
|
3243
|
+
* Individual signer status
|
|
3244
|
+
*/
|
|
3245
|
+
export const fluxSignerStatusEnum = pgEnum("flux_signer_status", [
|
|
3246
|
+
"pending",
|
|
3247
|
+
"signed",
|
|
3248
|
+
"declined",
|
|
3249
|
+
"delegated",
|
|
3250
|
+
]);
|
|
3251
|
+
/**
|
|
3252
|
+
* Agreements — tracks contracts, IOs, and approvals across their lifecycle.
|
|
3253
|
+
* Links to projects, deals, fulfillment tickets, and Compass entities.
|
|
3254
|
+
*/
|
|
3255
|
+
export const fluxAgreements = pgTable("flux_agreements", {
|
|
3256
|
+
id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
|
|
3257
|
+
tier: fluxAgreementTierEnum("tier").notNull(),
|
|
3258
|
+
status: fluxAgreementStatusEnum("status").notNull().default("draft"),
|
|
3259
|
+
projectId: text("project_id").references(() => fluxProjects.id),
|
|
3260
|
+
dealId: text("deal_id").references(() => fluxDealPipeline.id),
|
|
3261
|
+
hubspotDealId: text("hubspot_deal_id"),
|
|
3262
|
+
fulfillmentTicketId: text("fulfillment_ticket_id").references(() => fluxFulfillmentTickets.id),
|
|
3263
|
+
compassContractId: text("compass_contract_id"),
|
|
3264
|
+
compassOrderId: text("compass_order_id"),
|
|
3265
|
+
title: text("title").notNull(),
|
|
3266
|
+
description: text("description"),
|
|
3267
|
+
commitmentAmount: numeric("commitment_amount", { precision: 14, scale: 2 }),
|
|
3268
|
+
tierCode: text("tier_code"),
|
|
3269
|
+
currency: text("currency").default("USD"),
|
|
3270
|
+
effectiveDate: date("effective_date"),
|
|
3271
|
+
expirationDate: date("expiration_date"),
|
|
3272
|
+
version: integer("version").notNull().default(1),
|
|
3273
|
+
previousVersionId: text("previous_version_id").references(() => fluxAgreements.id),
|
|
3274
|
+
amendmentReason: text("amendment_reason"),
|
|
3275
|
+
documentUrl: text("document_url"),
|
|
3276
|
+
documentFingerprint: text("document_fingerprint"),
|
|
3277
|
+
documentFileName: text("document_file_name"),
|
|
3278
|
+
esignProvider: text("esign_provider"),
|
|
3279
|
+
esignEnvelopeId: text("esign_envelope_id"),
|
|
3280
|
+
esignCallbackUrl: text("esign_callback_url"),
|
|
3281
|
+
esignStatus: text("esign_status"),
|
|
3282
|
+
hubspotPropertySyncedAt: timestamp("hubspot_property_synced_at", {
|
|
3283
|
+
withTimezone: true,
|
|
3284
|
+
}),
|
|
3285
|
+
slackChannelId: text("slack_channel_id"),
|
|
3286
|
+
slackMessageTs: text("slack_message_ts"),
|
|
3287
|
+
healthScore: integer("health_score"),
|
|
3288
|
+
complianceStatus: text("compliance_status"),
|
|
3289
|
+
riskIndicators: jsonb("risk_indicators").default([]),
|
|
3290
|
+
metadata: jsonb("metadata").default({}),
|
|
3291
|
+
createdById: text("created_by_id").references(() => fluxUsers.id),
|
|
3292
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
3293
|
+
.notNull()
|
|
3294
|
+
.defaultNow(),
|
|
3295
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
3296
|
+
.notNull()
|
|
3297
|
+
.defaultNow(),
|
|
3298
|
+
});
|
|
3299
|
+
export const fluxAgreementsRelations = relations(fluxAgreements, ({ one, many }) => ({
|
|
3300
|
+
project: one(fluxProjects, {
|
|
3301
|
+
fields: [fluxAgreements.projectId],
|
|
3302
|
+
references: [fluxProjects.id],
|
|
3303
|
+
}),
|
|
3304
|
+
deal: one(fluxDealPipeline, {
|
|
3305
|
+
fields: [fluxAgreements.dealId],
|
|
3306
|
+
references: [fluxDealPipeline.id],
|
|
3307
|
+
}),
|
|
3308
|
+
fulfillmentTicket: one(fluxFulfillmentTickets, {
|
|
3309
|
+
fields: [fluxAgreements.fulfillmentTicketId],
|
|
3310
|
+
references: [fluxFulfillmentTickets.id],
|
|
3311
|
+
}),
|
|
3312
|
+
previousVersion: one(fluxAgreements, {
|
|
3313
|
+
fields: [fluxAgreements.previousVersionId],
|
|
3314
|
+
references: [fluxAgreements.id],
|
|
3315
|
+
}),
|
|
3316
|
+
createdBy: one(fluxUsers, {
|
|
3317
|
+
fields: [fluxAgreements.createdById],
|
|
3318
|
+
references: [fluxUsers.id],
|
|
3319
|
+
}),
|
|
3320
|
+
signers: many(fluxAgreementSigners),
|
|
3321
|
+
activity: many(fluxAgreementActivity),
|
|
3322
|
+
}));
|
|
3323
|
+
/**
|
|
3324
|
+
* Agreement signers — tracks each signer in the signing workflow.
|
|
3325
|
+
* Supports delegation and e-signature provider integration.
|
|
3326
|
+
*/
|
|
3327
|
+
export const fluxAgreementSigners = pgTable("flux_agreement_signers", {
|
|
3328
|
+
id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
|
|
3329
|
+
agreementId: text("agreement_id")
|
|
3330
|
+
.notNull()
|
|
3331
|
+
.references(() => fluxAgreements.id, { onDelete: "cascade" }),
|
|
3332
|
+
role: fluxSignerRoleEnum("role").notNull(),
|
|
3333
|
+
status: fluxSignerStatusEnum("status").notNull().default("pending"),
|
|
3334
|
+
displayOrder: integer("display_order").notNull().default(0),
|
|
3335
|
+
fluxUserId: text("flux_user_id").references(() => fluxUsers.id),
|
|
3336
|
+
contactEmail: text("contact_email"),
|
|
3337
|
+
contactName: text("contact_name"),
|
|
3338
|
+
hubspotContactId: text("hubspot_contact_id"),
|
|
3339
|
+
signedAt: timestamp("signed_at", { withTimezone: true }),
|
|
3340
|
+
signedVia: text("signed_via"),
|
|
3341
|
+
signedIpAddress: text("signed_ip_address"),
|
|
3342
|
+
attestationNote: text("attestation_note"),
|
|
3343
|
+
delegatedToId: text("delegated_to_id").references(() => fluxAgreementSigners.id),
|
|
3344
|
+
delegatedReason: text("delegated_reason"),
|
|
3345
|
+
esignRecipientId: text("esign_recipient_id"),
|
|
3346
|
+
esignSignedAt: timestamp("esign_signed_at", { withTimezone: true }),
|
|
3347
|
+
declinedAt: timestamp("declined_at", { withTimezone: true }),
|
|
3348
|
+
declinedReason: text("declined_reason"),
|
|
3349
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
3350
|
+
.notNull()
|
|
3351
|
+
.defaultNow(),
|
|
3352
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
3353
|
+
.notNull()
|
|
3354
|
+
.defaultNow(),
|
|
3355
|
+
});
|
|
3356
|
+
export const fluxAgreementSignersRelations = relations(fluxAgreementSigners, ({ one }) => ({
|
|
3357
|
+
agreement: one(fluxAgreements, {
|
|
3358
|
+
fields: [fluxAgreementSigners.agreementId],
|
|
3359
|
+
references: [fluxAgreements.id],
|
|
3360
|
+
}),
|
|
3361
|
+
fluxUser: one(fluxUsers, {
|
|
3362
|
+
fields: [fluxAgreementSigners.fluxUserId],
|
|
3363
|
+
references: [fluxUsers.id],
|
|
3364
|
+
}),
|
|
3365
|
+
delegatedTo: one(fluxAgreementSigners, {
|
|
3366
|
+
fields: [fluxAgreementSigners.delegatedToId],
|
|
3367
|
+
references: [fluxAgreementSigners.id],
|
|
3368
|
+
}),
|
|
3369
|
+
}));
|
|
3370
|
+
/**
|
|
3371
|
+
* Agreement activity — immutable audit log for agreement lifecycle events.
|
|
3372
|
+
* No updatedAt column — entries are append-only.
|
|
3373
|
+
*/
|
|
3374
|
+
export const fluxAgreementActivity = pgTable("flux_agreement_activity", {
|
|
3375
|
+
id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
|
|
3376
|
+
agreementId: text("agreement_id")
|
|
3377
|
+
.notNull()
|
|
3378
|
+
.references(() => fluxAgreements.id, { onDelete: "cascade" }),
|
|
3379
|
+
action: text("action").notNull(),
|
|
3380
|
+
actorId: text("actor_id").references(() => fluxUsers.id),
|
|
3381
|
+
actorName: text("actor_name"),
|
|
3382
|
+
actorEmail: text("actor_email"),
|
|
3383
|
+
details: jsonb("details").default({}),
|
|
3384
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
3385
|
+
.notNull()
|
|
3386
|
+
.defaultNow(),
|
|
3387
|
+
});
|
|
3388
|
+
export const fluxAgreementActivityRelations = relations(fluxAgreementActivity, ({ one }) => ({
|
|
3389
|
+
agreement: one(fluxAgreements, {
|
|
3390
|
+
fields: [fluxAgreementActivity.agreementId],
|
|
3391
|
+
references: [fluxAgreements.id],
|
|
3392
|
+
}),
|
|
3393
|
+
actor: one(fluxUsers, {
|
|
3394
|
+
fields: [fluxAgreementActivity.actorId],
|
|
3395
|
+
references: [fluxUsers.id],
|
|
3396
|
+
}),
|
|
3397
|
+
}));
|
|
3398
|
+
// ============================================================================
|
|
3399
|
+
// E-Sign Configuration
|
|
3400
|
+
// ============================================================================
|
|
3401
|
+
/**
|
|
3402
|
+
* Per-tier e-sign configuration — controls which Firma template to use,
|
|
3403
|
+
* default VP signers for auto-assignment, and signing mode (template vs embedded).
|
|
3404
|
+
*/
|
|
3405
|
+
export const fluxEsignConfig = pgTable("flux_esign_config", {
|
|
3406
|
+
id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
|
|
3407
|
+
tier: fluxAgreementTierEnum("tier").notNull().unique(),
|
|
3408
|
+
firmaTemplateId: text("firma_template_id"),
|
|
3409
|
+
defaultVpSignerIds: text("default_vp_signer_ids")
|
|
3410
|
+
.array()
|
|
3411
|
+
.notNull()
|
|
3412
|
+
.default(sql `'{}'`),
|
|
3413
|
+
autoSend: boolean("auto_send").notNull().default(true),
|
|
3414
|
+
/** 'template' = full doc generation from Firma template; 'embedded' = lightweight iframe signing */
|
|
3415
|
+
signingMode: text("signing_mode").notNull().default("template"),
|
|
3416
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
3417
|
+
.notNull()
|
|
3418
|
+
.defaultNow(),
|
|
3419
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
3420
|
+
.notNull()
|
|
3421
|
+
.defaultNow(),
|
|
3422
|
+
});
|
|
3423
|
+
// =============================================================================
|
|
3424
|
+
// TAPCLICKS ARCHIVE (historical data from legacy TapClicks system)
|
|
3425
|
+
// =============================================================================
|
|
3426
|
+
/**
|
|
3427
|
+
* Historical orders from TapClicks Orders & Workflow.
|
|
3428
|
+
* One-time import of ~27K orders (last 12 months).
|
|
3429
|
+
* Indexed search columns + raw_json JSONB for full detail.
|
|
3430
|
+
*/
|
|
3431
|
+
export const fluxTapclicksOrders = pgTable("flux_tapclicks_orders", {
|
|
3432
|
+
id: serial("id").primaryKey(),
|
|
3433
|
+
tapId: integer("tap_id").notNull().unique(),
|
|
3434
|
+
orderName: text("order_name"),
|
|
3435
|
+
clientName: text("client_name"),
|
|
3436
|
+
clientTapId: integer("client_tap_id"),
|
|
3437
|
+
salesRep: text("sales_rep"),
|
|
3438
|
+
status: text("status"),
|
|
3439
|
+
workflowStep: text("workflow_step"),
|
|
3440
|
+
orderType: text("order_type"),
|
|
3441
|
+
totalBudget: numeric("total_budget"),
|
|
3442
|
+
startDate: date("start_date"),
|
|
3443
|
+
endDate: date("end_date"),
|
|
3444
|
+
dfpOrderId: text("dfp_order_id"),
|
|
3445
|
+
lineItemCount: integer("line_item_count"),
|
|
3446
|
+
rawJson: jsonb("raw_json").notNull(),
|
|
3447
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
3448
|
+
.notNull()
|
|
3449
|
+
.defaultNow(),
|
|
3450
|
+
tapCreatedAt: timestamp("tap_created_at", { withTimezone: true }),
|
|
3451
|
+
tapUpdatedAt: timestamp("tap_updated_at", { withTimezone: true }),
|
|
3452
|
+
}, (table) => ({
|
|
3453
|
+
tapIdIdx: uniqueIndex("flux_tapclicks_orders_tap_id_idx").on(table.tapId),
|
|
3454
|
+
clientNameIdx: index("flux_tapclicks_orders_client_name_idx").on(table.clientName),
|
|
3455
|
+
salesRepIdx: index("flux_tapclicks_orders_sales_rep_idx").on(table.salesRep),
|
|
3456
|
+
statusIdx: index("flux_tapclicks_orders_status_idx").on(table.status),
|
|
3457
|
+
startDateIdx: index("flux_tapclicks_orders_start_date_idx").on(table.startDate),
|
|
3458
|
+
endDateIdx: index("flux_tapclicks_orders_end_date_idx").on(table.endDate),
|
|
3459
|
+
}));
|
|
3460
|
+
/**
|
|
3461
|
+
* Historical line items from TapClicks.
|
|
3462
|
+
* Each order contains ~5-10 line items with product, rate, targeting, and GAM details.
|
|
3463
|
+
*/
|
|
3464
|
+
export const fluxTapclicksLineItems = pgTable("flux_tapclicks_line_items", {
|
|
3465
|
+
id: serial("id").primaryKey(),
|
|
3466
|
+
tapId: integer("tap_id").notNull().unique(),
|
|
3467
|
+
tapOrderId: integer("tap_order_id").notNull(),
|
|
3468
|
+
campaignName: text("campaign_name"),
|
|
3469
|
+
productName: text("product_name"),
|
|
3470
|
+
subProductName: text("sub_product_name"),
|
|
3471
|
+
status: text("status"),
|
|
3472
|
+
workflowStep: text("workflow_step"),
|
|
3473
|
+
startDate: date("start_date"),
|
|
3474
|
+
endDate: date("end_date"),
|
|
3475
|
+
rate: numeric("rate"),
|
|
3476
|
+
floorRate: numeric("floor_rate"),
|
|
3477
|
+
rateType: text("rate_type"),
|
|
3478
|
+
impressions: integer("impressions"),
|
|
3479
|
+
totalCost: numeric("total_cost"),
|
|
3480
|
+
creativeOptions: text("creative_options"),
|
|
3481
|
+
dfpLineItemId: text("dfp_line_item_id"),
|
|
3482
|
+
namingConvention: text("naming_convention"),
|
|
3483
|
+
rawJson: jsonb("raw_json").notNull(),
|
|
3484
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
3485
|
+
.notNull()
|
|
3486
|
+
.defaultNow(),
|
|
3487
|
+
tapCreatedAt: timestamp("tap_created_at", { withTimezone: true }),
|
|
3488
|
+
tapUpdatedAt: timestamp("tap_updated_at", { withTimezone: true }),
|
|
3489
|
+
}, (table) => ({
|
|
3490
|
+
tapIdIdx: uniqueIndex("flux_tapclicks_line_items_tap_id_idx").on(table.tapId),
|
|
3491
|
+
tapOrderIdIdx: index("flux_tapclicks_line_items_order_idx").on(table.tapOrderId),
|
|
3492
|
+
productNameIdx: index("flux_tapclicks_line_items_product_idx").on(table.productName),
|
|
3493
|
+
statusIdx: index("flux_tapclicks_line_items_status_idx").on(table.status),
|
|
3494
|
+
startDateIdx: index("flux_tapclicks_line_items_start_date_idx").on(table.startDate),
|
|
3495
|
+
}));
|
|
3496
|
+
/**
|
|
3497
|
+
* Historical clients/advertisers from TapClicks.
|
|
3498
|
+
*/
|
|
3499
|
+
export const fluxTapclicksClients = pgTable("flux_tapclicks_clients", {
|
|
3500
|
+
id: serial("id").primaryKey(),
|
|
3501
|
+
tapId: integer("tap_id").notNull().unique(),
|
|
3502
|
+
companyName: text("company_name"),
|
|
3503
|
+
crmId: text("crm_id"),
|
|
3504
|
+
billingId: text("billing_id"),
|
|
3505
|
+
iotoolStatus: text("iotool_status"),
|
|
3506
|
+
city: text("city"),
|
|
3507
|
+
state: text("state"),
|
|
3508
|
+
rawJson: jsonb("raw_json").notNull(),
|
|
3509
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
3510
|
+
.notNull()
|
|
3511
|
+
.defaultNow(),
|
|
3512
|
+
}, (table) => ({
|
|
3513
|
+
tapIdIdx: uniqueIndex("flux_tapclicks_clients_tap_id_idx").on(table.tapId),
|
|
3514
|
+
companyNameIdx: index("flux_tapclicks_clients_company_name_idx").on(table.companyName),
|
|
3515
|
+
iotoolStatusIdx: index("flux_tapclicks_clients_status_idx").on(table.iotoolStatus),
|
|
3516
|
+
}));
|
|
3517
|
+
// =============================================================================
|
|
3518
|
+
// TAPCLICKS → COMPASS PREFILL PIPELINE
|
|
3519
|
+
// =============================================================================
|
|
3520
|
+
/**
|
|
3521
|
+
* Admin-managed mapping of TapClicks product names → Compass product codes.
|
|
3522
|
+
* Used by the preparePrefillToken tool to translate line items.
|
|
3523
|
+
*/
|
|
3524
|
+
export const fluxTapclicksProductMap = pgTable("flux_tapclicks_product_map", {
|
|
3525
|
+
id: serial("id").primaryKey(),
|
|
3526
|
+
tapclicksProductName: text("tapclicks_product_name").notNull().unique(),
|
|
3527
|
+
stribProductCode: text("strib_product_code").notNull(),
|
|
3528
|
+
stribProductId: text("strib_product_id"),
|
|
3529
|
+
stribProductName: text("strib_product_name").notNull(),
|
|
3530
|
+
pricingModel: text("pricing_model").notNull(),
|
|
3531
|
+
notes: text("notes"),
|
|
3532
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
3533
|
+
.notNull()
|
|
3534
|
+
.defaultNow(),
|
|
3535
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
3536
|
+
.notNull()
|
|
3537
|
+
.defaultNow(),
|
|
3538
|
+
});
|
|
3539
|
+
/**
|
|
3540
|
+
* Short-lived prefill tokens for the TapClicks → Compass order bridge.
|
|
3541
|
+
* UUID PK, 1-hour TTL. Compass reads the payload via CORS-enabled GET endpoint.
|
|
3542
|
+
*/
|
|
3543
|
+
export const fluxOrderPrefillTokens = pgTable("flux_order_prefill_tokens", {
|
|
3544
|
+
id: text("id")
|
|
3545
|
+
.primaryKey()
|
|
3546
|
+
.default(sql `gen_random_uuid()::text`),
|
|
3547
|
+
createdByUserId: text("created_by_user_id").notNull(),
|
|
3548
|
+
clientName: text("client_name").notNull(),
|
|
3549
|
+
hubspotCompanyId: text("hubspot_company_id"),
|
|
3550
|
+
flightStart: text("flight_start"),
|
|
3551
|
+
flightEnd: text("flight_end"),
|
|
3552
|
+
geo: text("geo"),
|
|
3553
|
+
lineItems: jsonb("line_items").notNull(),
|
|
3554
|
+
metadata: jsonb("metadata"),
|
|
3555
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
3556
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
3557
|
+
.notNull()
|
|
3558
|
+
.defaultNow(),
|
|
3559
|
+
}, (table) => ({
|
|
3560
|
+
expiresAtIdx: index("flux_order_prefill_tokens_expires_at_idx").on(table.expiresAt),
|
|
3561
|
+
}));
|
|
3562
|
+
// =============================================================================
|
|
3563
|
+
// AI USAGE TRACKING
|
|
3564
|
+
// =============================================================================
|
|
3565
|
+
/**
|
|
3566
|
+
* AI usage log — tracks AI provider API calls for cost visibility
|
|
3567
|
+
* and sliding-window rate limiting.
|
|
3568
|
+
*
|
|
3569
|
+
* Streaming endpoints insert a placeholder row pre-stream (for rate limit counting),
|
|
3570
|
+
* then update with final token counts in onFinish. Non-streaming endpoints do
|
|
3571
|
+
* a single insert after the response is parsed.
|
|
3572
|
+
*/
|
|
3573
|
+
export const fluxAiUsage = pgTable("flux_ai_usage", {
|
|
3574
|
+
id: text("id")
|
|
3575
|
+
.primaryKey()
|
|
3576
|
+
.default(sql `gen_random_uuid()::text`),
|
|
3577
|
+
userId: text("user_id").notNull(),
|
|
3578
|
+
endpoint: text("endpoint").notNull(),
|
|
3579
|
+
model: text("model").notNull(),
|
|
3580
|
+
inputTokens: integer("input_tokens"),
|
|
3581
|
+
outputTokens: integer("output_tokens"),
|
|
3582
|
+
totalTokens: integer("total_tokens"),
|
|
3583
|
+
/** Cost in microdollars (millionths of USD) — avoids floating-point drift */
|
|
3584
|
+
estimatedCostUsdMicro: integer("estimated_cost_usd_micro"),
|
|
3585
|
+
durationMs: integer("duration_ms"),
|
|
3586
|
+
metadata: jsonb("metadata"),
|
|
3587
|
+
sourceApp: text("source_app").notNull().default("fn-flux"),
|
|
3588
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
3589
|
+
.notNull()
|
|
3590
|
+
.defaultNow(),
|
|
3591
|
+
}, (table) => ({
|
|
3592
|
+
userCreatedAtIdx: index("flux_ai_usage_user_created_at_idx").on(table.userId, table.createdAt),
|
|
3593
|
+
endpointCreatedAtIdx: index("flux_ai_usage_endpoint_created_at_idx").on(table.endpoint, table.createdAt),
|
|
3594
|
+
sourceAppCreatedAtIdx: index("flux_ai_usage_source_app_idx").on(table.sourceApp, table.createdAt),
|
|
3595
|
+
}));
|
|
3596
|
+
// ---------------------------------------------------------------------------
|
|
3597
|
+
// TapClicks Services — registry of all connected data services
|
|
3598
|
+
// ---------------------------------------------------------------------------
|
|
3599
|
+
export const fluxTapclicksServices = pgTable("flux_tapclicks_services", {
|
|
3600
|
+
id: serial("id").primaryKey(),
|
|
3601
|
+
serviceId: text("service_id").notNull().unique(),
|
|
3602
|
+
serviceName: text("service_name").notNull(),
|
|
3603
|
+
platform: text("platform"),
|
|
3604
|
+
category: text("category"),
|
|
3605
|
+
impressionColumn: text("impression_column"),
|
|
3606
|
+
clickColumn: text("click_column"),
|
|
3607
|
+
spendColumn: text("spend_column"),
|
|
3608
|
+
adUnitColumn: text("ad_unit_column"),
|
|
3609
|
+
advertiserColumn: text("advertiser_column"),
|
|
3610
|
+
campaignColumn: text("campaign_column"),
|
|
3611
|
+
dateColumn: text("date_column"),
|
|
3612
|
+
isActive: boolean("is_active").notNull().default(false),
|
|
3613
|
+
syncFrequency: text("sync_frequency").default("daily").notNull(),
|
|
3614
|
+
lastSyncAt: timestamp("last_sync_at", { withTimezone: true }),
|
|
3615
|
+
lastSyncStatus: text("last_sync_status"),
|
|
3616
|
+
lastSyncError: text("last_sync_error"),
|
|
3617
|
+
lastSyncRowCount: integer("last_sync_row_count"),
|
|
3618
|
+
columnMetadata: jsonb("column_metadata"),
|
|
3619
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3620
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3621
|
+
}, (table) => ({
|
|
3622
|
+
serviceIdIdx: uniqueIndex("flux_tapclicks_services_service_id_idx").on(table.serviceId),
|
|
3623
|
+
platformIdx: index("flux_tapclicks_services_platform_idx").on(table.platform),
|
|
3624
|
+
categoryIdx: index("flux_tapclicks_services_category_idx").on(table.category),
|
|
3625
|
+
isActiveIdx: index("flux_tapclicks_services_is_active_idx").on(table.isActive),
|
|
3626
|
+
}));
|
|
3627
|
+
// ---------------------------------------------------------------------------
|
|
3628
|
+
// TapClicks Delivery — daily delivery snapshots from all platforms
|
|
3629
|
+
// ---------------------------------------------------------------------------
|
|
3630
|
+
export const fluxTapclicksDelivery = pgTable("flux_tapclicks_delivery", {
|
|
3631
|
+
id: serial("id").primaryKey(),
|
|
3632
|
+
serviceId: text("service_id").notNull(),
|
|
3633
|
+
serviceName: text("service_name"),
|
|
3634
|
+
platform: text("platform"),
|
|
3635
|
+
campaignId: text("campaign_id"),
|
|
3636
|
+
campaignName: text("campaign_name"),
|
|
3637
|
+
adUnit: text("ad_unit"),
|
|
3638
|
+
advertiser: text("advertiser"),
|
|
3639
|
+
date: date("date").notNull(),
|
|
3640
|
+
impressions: integer("impressions").default(0),
|
|
3641
|
+
clicks: integer("clicks"),
|
|
3642
|
+
spend: numeric("spend", { precision: 12, scale: 2 }),
|
|
3643
|
+
ctr: numeric("ctr", { precision: 8, scale: 6 }),
|
|
3644
|
+
rawData: jsonb("raw_data"),
|
|
3645
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3646
|
+
}, (table) => ({
|
|
3647
|
+
serviceIdIdx: index("flux_tapclicks_delivery_service_id_idx").on(table.serviceId),
|
|
3648
|
+
campaignIdIdx: index("flux_tapclicks_delivery_campaign_id_idx").on(table.campaignId),
|
|
3649
|
+
dateIdx: index("flux_tapclicks_delivery_date_idx").on(table.date),
|
|
3650
|
+
advertiserIdx: index("flux_tapclicks_delivery_advertiser_idx").on(table.advertiser),
|
|
3651
|
+
platformIdx: index("flux_tapclicks_delivery_platform_idx").on(table.platform),
|
|
3652
|
+
serviceDateUniqueIdx: uniqueIndex("flux_tapclicks_delivery_unique_idx").on(table.serviceId, table.campaignId, table.adUnit, table.date),
|
|
3653
|
+
}));
|
|
3654
|
+
// ---------------------------------------------------------------------------
|
|
3655
|
+
// TapClicks Pacing — computed pacing status per active line item
|
|
3656
|
+
// ---------------------------------------------------------------------------
|
|
3657
|
+
export const fluxTapclicksPacing = pgTable("flux_tapclicks_pacing", {
|
|
3658
|
+
id: serial("id").primaryKey(),
|
|
3659
|
+
lineItemTapId: integer("line_item_tap_id"),
|
|
3660
|
+
orderTapId: integer("order_tap_id"),
|
|
3661
|
+
serviceId: text("service_id"),
|
|
3662
|
+
clientName: text("client_name"),
|
|
3663
|
+
campaignName: text("campaign_name"),
|
|
3664
|
+
productName: text("product_name"),
|
|
3665
|
+
advertiser: text("advertiser"),
|
|
3666
|
+
startDate: date("start_date"),
|
|
3667
|
+
endDate: date("end_date"),
|
|
3668
|
+
impressionGoal: integer("impression_goal"),
|
|
3669
|
+
deliveredImpressions: integer("delivered_impressions").notNull().default(0),
|
|
3670
|
+
expectedImpressions: integer("expected_impressions"),
|
|
3671
|
+
pacingPercent: numeric("pacing_percent", { precision: 8, scale: 2 }),
|
|
3672
|
+
pacingStatus: text("pacing_status"),
|
|
3673
|
+
dailyVelocity: numeric("daily_velocity", { precision: 12, scale: 2 }),
|
|
3674
|
+
velocityTrend: text("velocity_trend"),
|
|
3675
|
+
daysRemaining: integer("days_remaining"),
|
|
3676
|
+
requiredDailyRate: integer("required_daily_rate"),
|
|
3677
|
+
projectedEndImpressions: integer("projected_end_impressions"),
|
|
3678
|
+
lastDeliveryDate: date("last_delivery_date"),
|
|
3679
|
+
lastAlertAt: timestamp("last_alert_at", { withTimezone: true }),
|
|
3680
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3681
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3682
|
+
}, (table) => ({
|
|
3683
|
+
lineItemTapIdIdx: uniqueIndex("flux_tapclicks_pacing_line_item_idx").on(table.lineItemTapId),
|
|
3684
|
+
orderTapIdIdx: index("flux_tapclicks_pacing_order_idx").on(table.orderTapId),
|
|
3685
|
+
pacingStatusIdx: index("flux_tapclicks_pacing_status_idx").on(table.pacingStatus),
|
|
3686
|
+
clientNameIdx: index("flux_tapclicks_pacing_client_idx").on(table.clientName),
|
|
3687
|
+
endDateIdx: index("flux_tapclicks_pacing_end_date_idx").on(table.endDate),
|
|
3688
|
+
}));
|
|
3689
|
+
// =============================================================================
|
|
3690
|
+
// CREATIVE–DELIVERY CORRELATION (Initiative 5: True North Intelligence)
|
|
3691
|
+
// =============================================================================
|
|
3692
|
+
export const fluxCreativeDeliveryCorrelation = pgTable("flux_creative_delivery_correlation", {
|
|
3693
|
+
id: serial("id").primaryKey(),
|
|
3694
|
+
correlationType: text("correlation_type").notNull(),
|
|
3695
|
+
segmentKey: text("segment_key").notNull(),
|
|
3696
|
+
sampleSize: integer("sample_size").notNull(),
|
|
3697
|
+
avgFillRate: numeric("avg_fill_rate", { precision: 8, scale: 4 }),
|
|
3698
|
+
avgReadinessScore: numeric("avg_readiness_score", { precision: 6, scale: 2 }),
|
|
3699
|
+
correlation: numeric("correlation", { precision: 6, scale: 4 }),
|
|
3700
|
+
insight: text("insight"),
|
|
3701
|
+
metadata: jsonb("metadata"),
|
|
3702
|
+
computedAt: timestamp("computed_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3703
|
+
}, (table) => ({
|
|
3704
|
+
typeSegmentIdx: uniqueIndex("flux_creative_delivery_corr_type_segment_idx").on(table.correlationType, table.segmentKey),
|
|
3705
|
+
typeIdx: index("flux_creative_delivery_corr_type_idx").on(table.correlationType),
|
|
3706
|
+
computedAtIdx: index("flux_creative_delivery_corr_computed_idx").on(table.computedAt),
|
|
3707
|
+
}));
|
|
3708
|
+
// =============================================================================
|
|
3709
|
+
// GAM COMPLIANCE AUDIT — audience segment targeting compliance per line item
|
|
3710
|
+
// =============================================================================
|
|
3711
|
+
export const fluxComplianceAudit = pgTable("flux_compliance_audit", {
|
|
3712
|
+
id: serial("id").primaryKey(),
|
|
3713
|
+
lineItemId: text("line_item_id").notNull(),
|
|
3714
|
+
lineItemName: text("line_item_name"),
|
|
3715
|
+
orderId: text("order_id"),
|
|
3716
|
+
orderName: text("order_name"),
|
|
3717
|
+
lineItemStatus: text("line_item_status"),
|
|
3718
|
+
audienceSegmentId: text("audience_segment_id").notNull(),
|
|
3719
|
+
isCompliant: boolean("is_compliant").notNull(),
|
|
3720
|
+
isExempt: boolean("is_exempt").notNull().default(false),
|
|
3721
|
+
exemptReason: text("exempt_reason"),
|
|
3722
|
+
auditedAt: timestamp("audited_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3723
|
+
}, (table) => ({
|
|
3724
|
+
lineItemAuditIdx: uniqueIndex("flux_compliance_audit_li_seg_idx").on(table.lineItemId, table.audienceSegmentId),
|
|
3725
|
+
auditedAtIdx: index("flux_compliance_audit_date_idx").on(table.auditedAt),
|
|
3726
|
+
complianceIdx: index("flux_compliance_audit_compliant_idx").on(table.isCompliant),
|
|
3727
|
+
}));
|
|
3728
|
+
// =============================================================================
|
|
3729
|
+
// WEBHOOK EVENT DEDUP — DB-backed idempotency for HubSpot (and other) webhooks
|
|
3730
|
+
// =============================================================================
|
|
3731
|
+
export const fluxWebhookEvents = pgTable("flux_webhook_events", {
|
|
3732
|
+
eventId: varchar("event_id").primaryKey(),
|
|
3733
|
+
source: varchar("source", { length: 32 }).notNull(),
|
|
3734
|
+
processedAt: timestamp("processed_at", { withTimezone: true })
|
|
3735
|
+
.notNull()
|
|
3736
|
+
.defaultNow(),
|
|
3737
|
+
}, (table) => ({
|
|
3738
|
+
sourceProcessedIdx: index("flux_webhook_events_source_processed_idx").on(table.source, table.processedAt),
|
|
3739
|
+
}));
|
|
3740
|
+
// =============================================================================
|
|
3741
|
+
// ASSISTANT TELEMETRY — tracks docs/assistant interactions for learning loops
|
|
3742
|
+
// =============================================================================
|
|
3743
|
+
export const fluxAssistantTelemetry = pgTable("flux_assistant_telemetry", {
|
|
3744
|
+
id: varchar("id")
|
|
3745
|
+
.primaryKey()
|
|
3746
|
+
.default(sql `gen_random_uuid()`),
|
|
3747
|
+
clerkUserId: varchar("clerk_user_id").notNull(),
|
|
3748
|
+
eventType: varchar("event_type", { length: 64 }).notNull(),
|
|
3749
|
+
surfaceId: varchar("surface_id", { length: 64 }),
|
|
3750
|
+
query: text("query"),
|
|
3751
|
+
resultId: text("result_id"),
|
|
3752
|
+
metadata: jsonb("metadata"),
|
|
3753
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
3754
|
+
.notNull()
|
|
3755
|
+
.defaultNow(),
|
|
3756
|
+
}, (table) => ({
|
|
3757
|
+
userEventIdx: index("flux_assistant_telemetry_user_event_idx").on(table.clerkUserId, table.eventType),
|
|
3758
|
+
surfaceCreatedIdx: index("flux_assistant_telemetry_surface_created_idx").on(table.surfaceId, table.createdAt),
|
|
3759
|
+
}));
|
|
3760
|
+
// =============================================================================
|
|
3761
|
+
// COCKPIT: CRON RUN TRACKING
|
|
3762
|
+
// =============================================================================
|
|
3763
|
+
export const fluxCronRuns = pgTable("flux_cron_runs", {
|
|
3764
|
+
id: varchar("id", { length: 36 })
|
|
3765
|
+
.primaryKey()
|
|
3766
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
3767
|
+
cronPath: varchar("cron_path", { length: 255 }).notNull(),
|
|
3768
|
+
app: varchar("app", { length: 50 }).notNull().default("fn-flux"),
|
|
3769
|
+
startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3770
|
+
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
3771
|
+
status: varchar("status", { length: 20 }).notNull().default("running"),
|
|
3772
|
+
durationMs: integer("duration_ms"),
|
|
3773
|
+
recordsProcessed: integer("records_processed"),
|
|
3774
|
+
error: text("error"),
|
|
3775
|
+
metadata: jsonb("metadata"),
|
|
3776
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3777
|
+
}, (table) => ({
|
|
3778
|
+
pathStartedIdx: index("idx_flux_cron_runs_path_started").on(table.cronPath, table.startedAt),
|
|
3779
|
+
statusIdx: index("idx_flux_cron_runs_status").on(table.status),
|
|
3780
|
+
}));
|
|
3781
|
+
// =============================================================================
|
|
3782
|
+
// TRIGGER.DEV: TASK RUN LOG
|
|
3783
|
+
// =============================================================================
|
|
3784
|
+
export const fluxTriggerRunLog = pgTable("flux_trigger_run_log", {
|
|
3785
|
+
id: varchar("id", { length: 36 })
|
|
3786
|
+
.primaryKey()
|
|
3787
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
3788
|
+
taskIdentifier: varchar("task_identifier", { length: 100 }).notNull(),
|
|
3789
|
+
runId: varchar("run_id", { length: 100 }).notNull(),
|
|
3790
|
+
status: varchar("status", { length: 30 }).notNull(),
|
|
3791
|
+
startedAt: timestamp("started_at", { withTimezone: true }),
|
|
3792
|
+
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
3793
|
+
durationMs: integer("duration_ms"),
|
|
3794
|
+
resultSummary: jsonb("result_summary"),
|
|
3795
|
+
errorMessage: text("error_message"),
|
|
3796
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
3797
|
+
}, (table) => ({
|
|
3798
|
+
taskStartedIdx: index("idx_flux_trigger_run_log_task_started").on(table.taskIdentifier, table.startedAt),
|
|
3799
|
+
runIdIdx: index("idx_flux_trigger_run_log_run_id").on(table.runId),
|
|
3800
|
+
statusIdx: index("idx_flux_trigger_run_log_status").on(table.status),
|
|
3801
|
+
}));
|
|
3802
|
+
//# sourceMappingURL=schema.js.map
|