@foundrynorth/flux-schema 1.16.0 → 1.16.2

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/schema.js CHANGED
@@ -260,6 +260,29 @@ export const fluxProjects = pgTable("flux_projects", {
260
260
  annualRevenue: numeric("annual_revenue", { precision: 14, scale: 2 }),
261
261
  /** HubSpot type (prospect/partner/customer classification) */
262
262
  companyType: varchar("company_type"),
263
+ // -- Compass analysis properties (synced from HubSpot fn_compass_* properties) --
264
+ /** fn_compass_status — last plan analysis state (e.g. "analyzed") */
265
+ compassStatus: text("compass_status"),
266
+ /** fn_compass_plan_url — shareable URL to the Compass plan */
267
+ compassPlanUrl: text("compass_plan_url"),
268
+ /** fn_compass_plan_id — Compass plan UUID */
269
+ compassPlanId: text("compass_plan_id"),
270
+ /** fn_compass_category — inferred advertiser category */
271
+ compassCategory: text("compass_category"),
272
+ /** fn_compass_geo — inferred geographic targeting */
273
+ compassGeo: text("compass_geo"),
274
+ /** fn_compass_budget_min — minimum recommended budget */
275
+ compassBudgetMin: numeric("compass_budget_min", { precision: 14, scale: 2 }),
276
+ /** fn_compass_budget_max — maximum recommended budget */
277
+ compassBudgetMax: numeric("compass_budget_max", { precision: 14, scale: 2 }),
278
+ /** fn_compass_confidence_score — AI confidence score (0–100) */
279
+ compassConfidenceScore: integer("compass_confidence_score"),
280
+ /** fn_compass_top_products — comma-separated top recommended products */
281
+ compassTopProducts: text("compass_top_products"),
282
+ /** fn_compass_last_analyzed — ISO timestamp of last Compass analysis */
283
+ compassLastAnalyzed: timestamp("compass_last_analyzed", { withTimezone: true }),
284
+ /** fn_compass_recommended_budget — single recommended budget figure */
285
+ compassRecommendedBudget: numeric("compass_recommended_budget", { precision: 14, scale: 2 }),
263
286
  /** SLA profile override — FK to flux_sla_profiles */
264
287
  slaProfileId: text("sla_profile_id").references(() => fluxSlaProfiles.id, { onDelete: "set null" }),
265
288
  ninjacatClientId: varchar("ninjacat_client_id"),
@@ -274,6 +297,8 @@ export const fluxProjects = pgTable("flux_projects", {
274
297
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
275
298
  /** Array of monitored properties for social mention scanning */
276
299
  monitoredProperties: jsonb("monitored_properties").$type().default([]),
300
+ /** Current active onboarding ID (quick access, set on creation, cleared on completion) */
301
+ currentOnboardingId: text("current_onboarding_id"),
277
302
  }, (table) => ({
278
303
  hubspotIdIdx: uniqueIndex("flux_projects_hubspot_id_idx").on(table.hubspotCompanyId),
279
304
  primaryStrategistIdx: index("flux_projects_primary_strategist_idx").on(table.primaryStrategistId),
@@ -3769,6 +3794,7 @@ export const fluxOnboardingReasonEnum = pgEnum("flux_onboarding_reason", [
3769
3794
  "no_slack_channel",
3770
3795
  "no_template",
3771
3796
  "needs_team_assignment",
3797
+ "pending_client_onboarding",
3772
3798
  ]);
3773
3799
  /**
3774
3800
  * Tracks projects that need attention after import.
@@ -6001,4 +6027,333 @@ export const fluxVarsitySignalsRelations = relations(fluxVarsitySignals, ({ one
6001
6027
  references: [fluxVarsityAdvertisers.id],
6002
6028
  }),
6003
6029
  }));
6030
+ // =============================================================================
6031
+ // CLIENT ONBOARDING (LaunchPad)
6032
+ // =============================================================================
6033
+ export const fluxOnboardingStatusEnum = pgEnum("flux_onboarding_status", [
6034
+ "not_started",
6035
+ "collecting",
6036
+ "in_review",
6037
+ "approved",
6038
+ "complete",
6039
+ "stale",
6040
+ ]);
6041
+ export const fluxOnboardingTypeEnum = pgEnum("flux_onboarding_type", [
6042
+ "new",
6043
+ "renewal",
6044
+ "upsell",
6045
+ ]);
6046
+ export const fluxOnboardingTierEnum = pgEnum("flux_onboarding_tier", [
6047
+ "1",
6048
+ "2",
6049
+ "3",
6050
+ ]);
6051
+ export const fluxOnboardingSectionStatusEnum = pgEnum("flux_onboarding_section_status", ["not_started", "in_progress", "submitted", "approved", "needs_revision"]);
6052
+ export const fluxOnboardingSectionAudienceEnum = pgEnum("flux_onboarding_section_audience", ["client", "internal", "both"]);
6053
+ export const fluxOnboardingFileCategoryEnum = pgEnum("flux_onboarding_file_category", ["logo", "brand_guide", "creative_asset", "tracking_doc", "other"]);
6054
+ export const fluxOnboardingActivityActionEnum = pgEnum("flux_onboarding_activity_action", [
6055
+ "created",
6056
+ "magic_link_sent",
6057
+ "client_opened",
6058
+ "section_started",
6059
+ "section_submitted",
6060
+ "section_approved",
6061
+ "section_revision_requested",
6062
+ "file_uploaded",
6063
+ "reminder_sent",
6064
+ "approved",
6065
+ "completed",
6066
+ "reopened",
6067
+ "stale_detected",
6068
+ ]);
6069
+ export const fluxOnboardingQuestionTypeEnum = pgEnum("flux_onboarding_question_type", [
6070
+ "text",
6071
+ "textarea",
6072
+ "currency",
6073
+ "date_range",
6074
+ "multi_select",
6075
+ "single_select",
6076
+ "url",
6077
+ "file_upload",
6078
+ "json",
6079
+ "rating_scale",
6080
+ ]);
6081
+ /** Section keys — stable identifiers for all onboarding sections */
6082
+ export const ONBOARDING_SECTION_KEYS = [
6083
+ "brand_identity",
6084
+ "campaign_goals",
6085
+ "audience_targeting",
6086
+ "access_credentials",
6087
+ "creative_assets",
6088
+ "competitive_landscape",
6089
+ "reporting_preferences",
6090
+ "media_buying_specifics",
6091
+ "existing_history",
6092
+ ];
6093
+ // ─── flux_onboardings ────────────────────────────────────────────────────────
6094
+ /**
6095
+ * Master onboarding record — one per project per cycle.
6096
+ * Tracks the full lifecycle from kickoff through completion.
6097
+ * Supports new, renewal, and upsell flows.
6098
+ */
6099
+ export const fluxOnboardings = pgTable("flux_onboardings", {
6100
+ id: text("id")
6101
+ .primaryKey()
6102
+ .default(sql `gen_random_uuid()::text`),
6103
+ projectId: text("project_id")
6104
+ .notNull()
6105
+ .references(() => fluxProjects.id, { onDelete: "cascade" }),
6106
+ tier: fluxOnboardingTierEnum("tier").notNull(),
6107
+ type: fluxOnboardingTypeEnum("type").notNull().default("new"),
6108
+ status: fluxOnboardingStatusEnum("status").notNull().default("not_started"),
6109
+ /** Magic-link token for client access (no Clerk account needed) */
6110
+ clientToken: text("client_token")
6111
+ .unique()
6112
+ .default(sql `gen_random_uuid()::text`),
6113
+ clientTokenExpiresAt: timestamp("client_token_expires_at", {
6114
+ withTimezone: true,
6115
+ }),
6116
+ clientEmail: text("client_email"),
6117
+ clientName: text("client_name"),
6118
+ clientCompletedAt: timestamp("client_completed_at", {
6119
+ withTimezone: true,
6120
+ }),
6121
+ /** Frozen intelligence snapshot at creation time */
6122
+ prePopulatedSnapshot: jsonb("pre_populated_snapshot")
6123
+ .$type()
6124
+ .default({}),
6125
+ /** 0-100 completion score across all sections */
6126
+ completionScore: integer("completion_score").default(0).notNull(),
6127
+ /** Who approved the onboarding (strategist/AM) */
6128
+ approvedBy: text("approved_by").references(() => fluxUsers.id),
6129
+ approvedAt: timestamp("approved_at", { withTimezone: true }),
6130
+ /** For renewals/upsells — links to the prior onboarding */
6131
+ renewalParentId: text("renewal_parent_id").references(() => fluxOnboardings.id, { onDelete: "set null" }),
6132
+ /** Kickoff and target dates */
6133
+ kickoffDate: date("kickoff_date"),
6134
+ targetCompleteDate: date("target_complete_date"),
6135
+ actualCompleteDate: date("actual_complete_date"),
6136
+ /** Who started the onboarding */
6137
+ createdBy: text("created_by").references(() => fluxUsers.id),
6138
+ createdAt: timestamp("created_at", { withTimezone: true })
6139
+ .notNull()
6140
+ .defaultNow(),
6141
+ updatedAt: timestamp("updated_at", { withTimezone: true })
6142
+ .notNull()
6143
+ .defaultNow(),
6144
+ }, (table) => ({
6145
+ projectIdx: index("flux_onboardings_project_idx").on(table.projectId),
6146
+ statusIdx: index("flux_onboardings_status_idx").on(table.status),
6147
+ clientTokenIdx: uniqueIndex("flux_onboardings_client_token_idx").on(table.clientToken),
6148
+ typeIdx: index("flux_onboardings_type_idx").on(table.type),
6149
+ }));
6150
+ // ─── flux_onboarding_sections ────────────────────────────────────────────────
6151
+ /**
6152
+ * Section instances within an onboarding.
6153
+ * Each section has its own status lifecycle and approval workflow.
6154
+ * Tier visibility determines which sections appear for which tiers.
6155
+ */
6156
+ export const fluxOnboardingSections = pgTable("flux_onboarding_sections", {
6157
+ id: text("id")
6158
+ .primaryKey()
6159
+ .default(sql `gen_random_uuid()::text`),
6160
+ onboardingId: text("onboarding_id")
6161
+ .notNull()
6162
+ .references(() => fluxOnboardings.id, { onDelete: "cascade" }),
6163
+ sectionKey: text("section_key").notNull(),
6164
+ title: text("title").notNull(),
6165
+ description: text("description"),
6166
+ status: fluxOnboardingSectionStatusEnum("status")
6167
+ .notNull()
6168
+ .default("not_started"),
6169
+ audience: fluxOnboardingSectionAudienceEnum("audience")
6170
+ .notNull()
6171
+ .default("client"),
6172
+ /** Minimum tier that sees this section (1=all tiers, 2=T1+T2, 3=T1 only) */
6173
+ tierMinimum: integer("tier_minimum").notNull().default(1),
6174
+ displayOrder: integer("display_order").notNull().default(0),
6175
+ submittedAt: timestamp("submitted_at", { withTimezone: true }),
6176
+ reviewedBy: text("reviewed_by").references(() => fluxUsers.id),
6177
+ reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
6178
+ revisionNotes: text("revision_notes"),
6179
+ createdAt: timestamp("created_at", { withTimezone: true })
6180
+ .notNull()
6181
+ .defaultNow(),
6182
+ updatedAt: timestamp("updated_at", { withTimezone: true })
6183
+ .notNull()
6184
+ .defaultNow(),
6185
+ }, (table) => ({
6186
+ onboardingIdx: index("flux_onboarding_sections_onboarding_idx").on(table.onboardingId),
6187
+ sectionKeyIdx: index("flux_onboarding_sections_key_idx").on(table.onboardingId, table.sectionKey),
6188
+ }));
6189
+ // ─── flux_onboarding_responses ───────────────────────────────────────────────
6190
+ /**
6191
+ * Individual question/answer pairs within a section.
6192
+ * Tracks pre-populated vs. client-provided answers for analytics.
6193
+ * Supports multiple question types via the answer jsonb field.
6194
+ */
6195
+ export const fluxOnboardingResponses = pgTable("flux_onboarding_responses", {
6196
+ id: text("id")
6197
+ .primaryKey()
6198
+ .default(sql `gen_random_uuid()::text`),
6199
+ sectionId: text("section_id")
6200
+ .notNull()
6201
+ .references(() => fluxOnboardingSections.id, { onDelete: "cascade" }),
6202
+ questionKey: text("question_key").notNull(),
6203
+ questionText: text("question_text").notNull(),
6204
+ questionType: fluxOnboardingQuestionTypeEnum("question_type")
6205
+ .notNull()
6206
+ .default("text"),
6207
+ /** Flexible answer storage — all types serialize to jsonb */
6208
+ answer: jsonb("answer"),
6209
+ isRequired: boolean("is_required").notNull().default(false),
6210
+ /** Pre-population tracking */
6211
+ isPrePopulated: boolean("is_pre_populated").notNull().default(false),
6212
+ prePopulatedSource: text("pre_populated_source"),
6213
+ prePopulatedValue: jsonb("pre_populated_value"),
6214
+ wasModifiedByClient: boolean("was_modified_by_client")
6215
+ .notNull()
6216
+ .default(false),
6217
+ modifiedAt: timestamp("modified_at", { withTimezone: true }),
6218
+ /** Display and validation */
6219
+ displayOrder: integer("display_order").notNull().default(0),
6220
+ helpText: text("help_text"),
6221
+ placeholder: text("placeholder"),
6222
+ /** For select types: [{value, label}] */
6223
+ options: jsonb("options").$type(),
6224
+ createdAt: timestamp("created_at", { withTimezone: true })
6225
+ .notNull()
6226
+ .defaultNow(),
6227
+ updatedAt: timestamp("updated_at", { withTimezone: true })
6228
+ .notNull()
6229
+ .defaultNow(),
6230
+ }, (table) => ({
6231
+ sectionIdx: index("flux_onboarding_responses_section_idx").on(table.sectionId),
6232
+ questionKeyIdx: index("flux_onboarding_responses_question_key_idx").on(table.sectionId, table.questionKey),
6233
+ }));
6234
+ // ─── flux_onboarding_files ───────────────────────────────────────────────────
6235
+ /**
6236
+ * File uploads during onboarding — logos, brand guides, creative assets.
6237
+ * Stored in Cloudflare R2 with prefix `flux-onboarding/`.
6238
+ */
6239
+ export const fluxOnboardingFiles = pgTable("flux_onboarding_files", {
6240
+ id: text("id")
6241
+ .primaryKey()
6242
+ .default(sql `gen_random_uuid()::text`),
6243
+ onboardingId: text("onboarding_id")
6244
+ .notNull()
6245
+ .references(() => fluxOnboardings.id, { onDelete: "cascade" }),
6246
+ sectionId: text("section_id").references(() => fluxOnboardingSections.id, {
6247
+ onDelete: "set null",
6248
+ }),
6249
+ responseId: text("response_id").references(() => fluxOnboardingResponses.id, { onDelete: "set null" }),
6250
+ fileName: text("file_name").notNull(),
6251
+ fileType: text("file_type").notNull(),
6252
+ fileSizeBytes: integer("file_size_bytes").notNull(),
6253
+ r2Key: text("r2_key").notNull(),
6254
+ r2Bucket: text("r2_bucket").notNull(),
6255
+ category: fluxOnboardingFileCategoryEnum("category")
6256
+ .notNull()
6257
+ .default("other"),
6258
+ /** Client email or flux user ID */
6259
+ uploadedBy: text("uploaded_by"),
6260
+ createdAt: timestamp("created_at", { withTimezone: true })
6261
+ .notNull()
6262
+ .defaultNow(),
6263
+ }, (table) => ({
6264
+ onboardingIdx: index("flux_onboarding_files_onboarding_idx").on(table.onboardingId),
6265
+ sectionIdx: index("flux_onboarding_files_section_idx").on(table.sectionId),
6266
+ }));
6267
+ // ─── flux_onboarding_activity ────────────────────────────────────────────────
6268
+ /**
6269
+ * Audit trail for all onboarding events — who did what, when.
6270
+ * Tracks both client and internal team actions.
6271
+ */
6272
+ export const fluxOnboardingActivity = pgTable("flux_onboarding_activity", {
6273
+ id: text("id")
6274
+ .primaryKey()
6275
+ .default(sql `gen_random_uuid()::text`),
6276
+ onboardingId: text("onboarding_id")
6277
+ .notNull()
6278
+ .references(() => fluxOnboardings.id, { onDelete: "cascade" }),
6279
+ /** Client email or flux_users.id */
6280
+ actor: text("actor").notNull(),
6281
+ actorType: text("actor_type").notNull(), // "client" | "team_member" | "system"
6282
+ action: fluxOnboardingActivityActionEnum("action").notNull(),
6283
+ metadata: jsonb("metadata").$type().default({}),
6284
+ createdAt: timestamp("created_at", { withTimezone: true })
6285
+ .notNull()
6286
+ .defaultNow(),
6287
+ }, (table) => ({
6288
+ onboardingIdx: index("flux_onboarding_activity_onboarding_idx").on(table.onboardingId),
6289
+ actionIdx: index("flux_onboarding_activity_action_idx").on(table.action),
6290
+ createdAtIdx: index("flux_onboarding_activity_created_idx").on(table.createdAt),
6291
+ }));
6292
+ // ─── Onboarding Relations ────────────────────────────────────────────────────
6293
+ export const fluxOnboardingsRelations = relations(fluxOnboardings, ({ one, many }) => ({
6294
+ project: one(fluxProjects, {
6295
+ fields: [fluxOnboardings.projectId],
6296
+ references: [fluxProjects.id],
6297
+ }),
6298
+ approvedByUser: one(fluxUsers, {
6299
+ fields: [fluxOnboardings.approvedBy],
6300
+ references: [fluxUsers.id],
6301
+ relationName: "onboardingApprover",
6302
+ }),
6303
+ createdByUser: one(fluxUsers, {
6304
+ fields: [fluxOnboardings.createdBy],
6305
+ references: [fluxUsers.id],
6306
+ relationName: "onboardingCreator",
6307
+ }),
6308
+ renewalParent: one(fluxOnboardings, {
6309
+ fields: [fluxOnboardings.renewalParentId],
6310
+ references: [fluxOnboardings.id],
6311
+ relationName: "renewalChain",
6312
+ }),
6313
+ renewalChildren: many(fluxOnboardings, {
6314
+ relationName: "renewalChain",
6315
+ }),
6316
+ sections: many(fluxOnboardingSections),
6317
+ files: many(fluxOnboardingFiles),
6318
+ activity: many(fluxOnboardingActivity),
6319
+ }));
6320
+ export const fluxOnboardingSectionsRelations = relations(fluxOnboardingSections, ({ one, many }) => ({
6321
+ onboarding: one(fluxOnboardings, {
6322
+ fields: [fluxOnboardingSections.onboardingId],
6323
+ references: [fluxOnboardings.id],
6324
+ }),
6325
+ reviewedByUser: one(fluxUsers, {
6326
+ fields: [fluxOnboardingSections.reviewedBy],
6327
+ references: [fluxUsers.id],
6328
+ }),
6329
+ responses: many(fluxOnboardingResponses),
6330
+ files: many(fluxOnboardingFiles),
6331
+ }));
6332
+ export const fluxOnboardingResponsesRelations = relations(fluxOnboardingResponses, ({ one, many }) => ({
6333
+ section: one(fluxOnboardingSections, {
6334
+ fields: [fluxOnboardingResponses.sectionId],
6335
+ references: [fluxOnboardingSections.id],
6336
+ }),
6337
+ files: many(fluxOnboardingFiles),
6338
+ }));
6339
+ export const fluxOnboardingFilesRelations = relations(fluxOnboardingFiles, ({ one }) => ({
6340
+ onboarding: one(fluxOnboardings, {
6341
+ fields: [fluxOnboardingFiles.onboardingId],
6342
+ references: [fluxOnboardings.id],
6343
+ }),
6344
+ section: one(fluxOnboardingSections, {
6345
+ fields: [fluxOnboardingFiles.sectionId],
6346
+ references: [fluxOnboardingSections.id],
6347
+ }),
6348
+ response: one(fluxOnboardingResponses, {
6349
+ fields: [fluxOnboardingFiles.responseId],
6350
+ references: [fluxOnboardingResponses.id],
6351
+ }),
6352
+ }));
6353
+ export const fluxOnboardingActivityRelations = relations(fluxOnboardingActivity, ({ one }) => ({
6354
+ onboarding: one(fluxOnboardings, {
6355
+ fields: [fluxOnboardingActivity.onboardingId],
6356
+ references: [fluxOnboardings.id],
6357
+ }),
6358
+ }));
6004
6359
  //# sourceMappingURL=schema.js.map