@foundrynorth/compass-schema 1.0.20 → 1.0.22

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/src/schema.ts CHANGED
@@ -105,6 +105,12 @@ import {
105
105
  import { createInsertSchema } from "drizzle-zod";
106
106
  import { z } from "zod";
107
107
  import type { IntelligenceData } from "./analyzeTypes.js";
108
+ import {
109
+ ACTIVITY_EVENT_TYPES,
110
+ ACTIVITY_SEVERITIES,
111
+ ACTIVITY_SOURCE_SYSTEMS,
112
+ PENDING_ACTION_TYPES,
113
+ } from "./types/activity-feed.js";
108
114
 
109
115
  export const users = pgTable("users", {
110
116
  id: varchar("id")
@@ -124,6 +130,38 @@ export type User = typeof users.$inferSelect;
124
130
 
125
131
  export const avatarRequestStatusEnum = pgEnum("avatar_request_status", ["pending", "processed", "rejected"]);
126
132
 
133
+ // Activity feed enums (order activity feed + pending actions)
134
+ export const activityEventTypeEnum = pgEnum(
135
+ "activity_event_type",
136
+ ACTIVITY_EVENT_TYPES as unknown as [string, ...string[]],
137
+ );
138
+
139
+ export const activitySeverityEnum = pgEnum(
140
+ "activity_severity",
141
+ ACTIVITY_SEVERITIES as unknown as [string, ...string[]],
142
+ );
143
+
144
+ export const activitySourceSystemEnum = pgEnum(
145
+ "activity_source_system",
146
+ ACTIVITY_SOURCE_SYSTEMS as unknown as [string, ...string[]],
147
+ );
148
+
149
+ export const pendingActionTypeEnum = pgEnum(
150
+ "pending_action_type",
151
+ PENDING_ACTION_TYPES as unknown as [string, ...string[]],
152
+ );
153
+
154
+ export const amendmentReasonEnum = pgEnum("amendment_reason", [
155
+ "budget_adjustment",
156
+ "flight_date_change",
157
+ "product_swap",
158
+ "add_line_items",
159
+ "remove_line_items",
160
+ "rate_renegotiation",
161
+ "client_request",
162
+ "other",
163
+ ]);
164
+
127
165
  // Vendor enums (must be declared before clerkUsers which references them)
128
166
  export const vendorFulfillmentStatusEnum = pgEnum("vendor_fulfillment_status", [
129
167
  "assigned",
@@ -208,6 +246,19 @@ export const userProfiles = pgTable("user_profiles", {
208
246
  photoUrl: text("photo_url"), // Profile photo (can sync from Clerk or custom upload)
209
247
  agencyAvatarUrl: text("agency_avatar_url"), // Hand-sketched avatar for proposals (Fal.ai)
210
248
  lastAvatarGeneratedAt: timestamp("last_avatar_generated_at"), // Rate limiting: non-admins can only generate 1/day
249
+ proposalPortraitRenderUrl: text("proposal_portrait_render_url"),
250
+ proposalPortraitCutoutUrl: text("proposal_portrait_cutout_url"),
251
+ proposalPortraitStatus: text("proposal_portrait_status"),
252
+ proposalPortraitPromptVersion: text("proposal_portrait_prompt_version"),
253
+ proposalPortraitSeed: integer("proposal_portrait_seed"),
254
+ proposalPortraitGeneratedAt: timestamp("proposal_portrait_generated_at"),
255
+ proposalPortraitSourcePhotoHash: text("proposal_portrait_source_photo_hash"),
256
+ proposalPortraitPreviewRenderUrl: text("proposal_portrait_preview_render_url"),
257
+ proposalPortraitPreviewCutoutUrl: text("proposal_portrait_preview_cutout_url"),
258
+ proposalPortraitPreviewPromptVersion: text("proposal_portrait_preview_prompt_version"),
259
+ proposalPortraitPreviewSeed: integer("proposal_portrait_preview_seed"),
260
+ proposalPortraitPreviewGeneratedAt: timestamp("proposal_portrait_preview_generated_at"),
261
+ proposalPortraitPreviewSourcePhotoHash: text("proposal_portrait_preview_source_photo_hash"),
211
262
 
212
263
  // Active Market Preference (overrides partner default)
213
264
  preferredMarketId: varchar("preferred_market_id"), // User's preferred market for planning
@@ -247,6 +298,19 @@ export const externalTeamMembers = pgTable("external_team_members", {
247
298
  title: text("title"),
248
299
  photoUrl: text("photo_url"),
249
300
  agencyAvatarUrl: text("agency_avatar_url"),
301
+ proposalPortraitRenderUrl: text("proposal_portrait_render_url"),
302
+ proposalPortraitCutoutUrl: text("proposal_portrait_cutout_url"),
303
+ proposalPortraitStatus: text("proposal_portrait_status"),
304
+ proposalPortraitPromptVersion: text("proposal_portrait_prompt_version"),
305
+ proposalPortraitSeed: integer("proposal_portrait_seed"),
306
+ proposalPortraitGeneratedAt: timestamp("proposal_portrait_generated_at"),
307
+ proposalPortraitSourcePhotoHash: text("proposal_portrait_source_photo_hash"),
308
+ proposalPortraitPreviewRenderUrl: text("proposal_portrait_preview_render_url"),
309
+ proposalPortraitPreviewCutoutUrl: text("proposal_portrait_preview_cutout_url"),
310
+ proposalPortraitPreviewPromptVersion: text("proposal_portrait_preview_prompt_version"),
311
+ proposalPortraitPreviewSeed: integer("proposal_portrait_preview_seed"),
312
+ proposalPortraitPreviewGeneratedAt: timestamp("proposal_portrait_preview_generated_at"),
313
+ proposalPortraitPreviewSourcePhotoHash: text("proposal_portrait_preview_source_photo_hash"),
250
314
  linkedinUrl: text("linkedin_url"),
251
315
  displayOrder: integer("display_order").default(0),
252
316
  createdAt: timestamp("created_at").notNull().defaultNow(),
@@ -534,6 +598,35 @@ export const competitorSchema = z.object({
534
598
  lng: z.number(),
535
599
  })
536
600
  .optional(), // Detailed location information
601
+
602
+ // Ad library data from Apify deep enrichment
603
+ ads: z
604
+ .object({
605
+ facebook: z
606
+ .array(
607
+ z.object({
608
+ adImage: z.string().optional(),
609
+ headline: z.string().optional(),
610
+ adCopy: z.string().optional(),
611
+ callToAction: z.string().optional(),
612
+ publisherPlatforms: z.array(z.string()).optional(),
613
+ startDate: z.string().optional(),
614
+ })
615
+ )
616
+ .optional(),
617
+ google: z
618
+ .array(
619
+ z.object({
620
+ headline: z.string().optional(),
621
+ description: z.string().optional(),
622
+ landingPage: z.string().optional(),
623
+ adType: z.string().optional(),
624
+ imageUrl: z.string().optional(),
625
+ })
626
+ )
627
+ .optional(),
628
+ })
629
+ .optional(),
537
630
  });
538
631
 
539
632
  export type Competitor = z.infer<typeof competitorSchema>;
@@ -1029,7 +1122,7 @@ export const plans = pgTable("plans", {
1029
1122
  contractId: uuid("contract_id"), // FK → contracts.id (nullable — links to annual contract)
1030
1123
 
1031
1124
  // Workflow mode: determines which UI sections and features are available
1032
- // 'research_backed' = full AnalyzeAndPlan flow (default, all existing plans)
1125
+ // 'research_backed' = discovery mode creates plan, runs analysis in background
1033
1126
  // 'direct_config' = configure products directly without research
1034
1127
  // 'aor_pitch' = agency-of-record pitch with proposal focus
1035
1128
  // 'io_entry' = direct insertion order entry from client PDF/XLSX
@@ -1180,6 +1273,7 @@ export const plans = pgTable("plans", {
1180
1273
  defaultTargeting: text("default_targeting"),
1181
1274
  defaultAudience: text("default_audience"),
1182
1275
  defaultPromoting: text("default_promoting"),
1276
+ defaultCreativeSource: varchar("default_creative_source"),
1183
1277
  defaultFlightStart: date("default_flight_start"),
1184
1278
  defaultFlightEnd: date("default_flight_end"),
1185
1279
 
@@ -1251,8 +1345,12 @@ export const proposals = pgTable(
1251
1345
  .default(sql`gen_random_uuid()`),
1252
1346
 
1253
1347
  // Optional plan reference (nullable — can be standalone)
1254
- planId: varchar("plan_id"),
1255
- engagementId: varchar("engagement_id"), // FK → engagements.id (nullable during transition)
1348
+ planId: varchar("plan_id").references(() => plans.id, {
1349
+ onDelete: "set null",
1350
+ }),
1351
+ engagementId: varchar("engagement_id").references(() => engagements.id, {
1352
+ onDelete: "set null",
1353
+ }), // nullable during transition
1256
1354
 
1257
1355
  // Configuration references (which plan_configurations to include)
1258
1356
  configurationIds: jsonb("configuration_ids").$type<string[]>(),
@@ -1323,6 +1421,41 @@ export const insertPlanSchema = createInsertSchema(plans).omit({
1323
1421
  createdAt: true,
1324
1422
  } as any);
1325
1423
 
1424
+ // ============================================================================
1425
+ // Proposal Engagement Tracking
1426
+ // ============================================================================
1427
+
1428
+ export const proposalEngagement = pgTable(
1429
+ "proposal_engagement",
1430
+ {
1431
+ id: varchar("id")
1432
+ .primaryKey()
1433
+ .default(sql`gen_random_uuid()`),
1434
+ proposalId: varchar("proposal_id"), // FK → proposals.id (nullable for legacy plan-based shares)
1435
+ planId: varchar("plan_id"), // FK → plans.id (fallback for legacy shares)
1436
+ shareToken: text("share_token").notNull(), // The share token used to access
1437
+ sessionId: text("session_id").notNull(), // Anonymous session ID (crypto.randomUUID on client)
1438
+ // Engagement data
1439
+ events: jsonb("events").notNull().default(sql`'[]'::jsonb`), // Array of engagement events
1440
+ engagementScore: integer("engagement_score").default(0), // Computed 0-100 score
1441
+ summary: text("summary"), // AI-generated follow-up brief
1442
+ // Device context
1443
+ device: text("device"), // "desktop" | "mobile" | "tablet"
1444
+ referrer: text("referrer"), // HTTP referrer
1445
+ userAgent: text("user_agent"),
1446
+ // Timestamps
1447
+ firstSeenAt: timestamp("first_seen_at").notNull().defaultNow(),
1448
+ lastSeenAt: timestamp("last_seen_at").notNull().defaultNow(),
1449
+ createdAt: timestamp("created_at").notNull().defaultNow(),
1450
+ },
1451
+ (table) => [
1452
+ index("proposal_engagement_proposal_id_idx").on(table.proposalId),
1453
+ index("proposal_engagement_plan_id_idx").on(table.planId),
1454
+ index("proposal_engagement_share_token_idx").on(table.shareToken),
1455
+ index("proposal_engagement_token_session_idx").on(table.shareToken, table.sessionId),
1456
+ ]
1457
+ );
1458
+
1326
1459
  export type InsertPlan = z.infer<typeof insertPlanSchema>;
1327
1460
  export type Plan = typeof plans.$inferSelect;
1328
1461
 
@@ -1762,6 +1895,7 @@ export const clerkUsers = pgTable("clerk_users", {
1762
1895
  createdAt: timestamp("created_at").notNull().defaultNow(),
1763
1896
  approvedAt: timestamp("approved_at"),
1764
1897
  approvedBy: text("approved_by"), // Admin who approved
1898
+ compassVoiceWarmth: numeric("compass_voice_warmth").default("0.6"),
1765
1899
  });
1766
1900
 
1767
1901
  export const insertClerkUserSchema = createInsertSchema(clerkUsers).omit({
@@ -4707,21 +4841,167 @@ export const mediaOrderStatusEnum = pgEnum("media_order_status", [
4707
4841
  "completed",
4708
4842
  ]);
4709
4843
 
4844
+ export const salesInitiatives = pgTable(
4845
+ "sales_initiatives",
4846
+ {
4847
+ id: uuid("id").primaryKey().defaultRandom(),
4848
+ code: varchar("code").notNull().unique(),
4849
+ name: varchar("name").notNull(),
4850
+ description: text("description"),
4851
+ hubspotValue: varchar("hubspot_value").notNull().unique(),
4852
+ displayOrder: integer("display_order").notNull().default(0),
4853
+ startsAt: date("starts_at"),
4854
+ endsAt: date("ends_at"),
4855
+ isActive: boolean("is_active").notNull().default(true),
4856
+ createdAt: timestamp("created_at").notNull().defaultNow(),
4857
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
4858
+ },
4859
+ (table) => [
4860
+ index("sales_initiatives_active_idx").on(table.isActive),
4861
+ index("sales_initiatives_display_order_idx").on(table.displayOrder),
4862
+ ],
4863
+ );
4864
+
4865
+ export const insertSalesInitiativeSchema = createInsertSchema(
4866
+ salesInitiatives,
4867
+ ).omit({
4868
+ id: true,
4869
+ createdAt: true,
4870
+ updatedAt: true,
4871
+ });
4872
+ export const updateSalesInitiativeSchema = insertSalesInitiativeSchema.partial();
4873
+ export type SalesInitiative = typeof salesInitiatives.$inferSelect;
4874
+ export type InsertSalesInitiative = z.infer<typeof insertSalesInitiativeSchema>;
4875
+ export type UpdateSalesInitiative = z.infer<typeof updateSalesInitiativeSchema>;
4876
+
4877
+ export const salesPackages = pgTable(
4878
+ "sales_packages",
4879
+ {
4880
+ id: uuid("id").primaryKey().defaultRandom(),
4881
+ code: varchar("code").notNull().unique(),
4882
+ name: varchar("name").notNull(),
4883
+ description: text("description"),
4884
+ initiativeCode: varchar("initiative_code"),
4885
+ sellerSummary: text("seller_summary"),
4886
+ packagePrice: numeric("package_price", { precision: 12, scale: 2 }),
4887
+ discountPercent: numeric("discount_percent", { precision: 5, scale: 2 }),
4888
+ addedValueAmount: numeric("added_value_amount", { precision: 12, scale: 2 }),
4889
+ addedValueDescription: varchar("added_value_description", { length: 255 }),
4890
+ hubspotPackageValue: varchar("hubspot_package_value").notNull().unique(),
4891
+ startsAt: date("starts_at"),
4892
+ endsAt: date("ends_at"),
4893
+ status: varchar("status").notNull().default("draft"),
4894
+ recommendationMode: varchar("recommendation_mode").notNull().default("both"),
4895
+ recommendationPriority: integer("recommendation_priority").notNull().default(0),
4896
+ eligibilityRules: jsonb("eligibility_rules").$type<{
4897
+ goals?: string[];
4898
+ industries?: string[];
4899
+ geographyTags?: string[];
4900
+ minBudget?: number | null;
4901
+ maxBudget?: number | null;
4902
+ requireActiveWindow?: boolean;
4903
+ minProductOverlap?: number | null;
4904
+ allowSubsetMatch?: boolean;
4905
+ allowNearMatch?: boolean;
4906
+ compareSignals?: string[];
4907
+ }>(),
4908
+ assistantHints: jsonb("assistant_hints").$type<{
4909
+ recommendedWhen?: string[];
4910
+ upsellMessage?: string | null;
4911
+ compareHeadline?: string | null;
4912
+ compareBody?: string | null;
4913
+ }>(),
4914
+ taskTemplateMode: varchar("task_template_mode").notNull().default("standard"),
4915
+ displayOrder: integer("display_order").notNull().default(0),
4916
+ isActive: boolean("is_active").notNull().default(true),
4917
+ createdAt: timestamp("created_at").notNull().defaultNow(),
4918
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
4919
+ },
4920
+ (table) => [
4921
+ index("sales_packages_active_idx").on(table.isActive),
4922
+ index("sales_packages_status_idx").on(table.status),
4923
+ index("sales_packages_display_order_idx").on(table.displayOrder),
4924
+ index("sales_packages_initiative_code_idx").on(table.initiativeCode),
4925
+ ],
4926
+ );
4927
+
4928
+ export const insertSalesPackageSchema = createInsertSchema(salesPackages).omit({
4929
+ id: true,
4930
+ createdAt: true,
4931
+ updatedAt: true,
4932
+ });
4933
+ export const updateSalesPackageSchema = insertSalesPackageSchema.partial();
4934
+ export type SalesPackage = typeof salesPackages.$inferSelect;
4935
+ export type InsertSalesPackage = z.infer<typeof insertSalesPackageSchema>;
4936
+ export type UpdateSalesPackage = z.infer<typeof updateSalesPackageSchema>;
4937
+
4938
+ export const salesPackageProducts = pgTable(
4939
+ "sales_package_products",
4940
+ {
4941
+ id: uuid("id").primaryKey().defaultRandom(),
4942
+ salesPackageCode: varchar("sales_package_code").notNull(),
4943
+ stribProductId: uuid("strib_product_id")
4944
+ .notNull()
4945
+ .references(() => stribProducts.id, { onDelete: "cascade" }),
4946
+ displayOrder: integer("display_order").notNull().default(0),
4947
+ approvedUnitRate: numeric("approved_unit_rate", { precision: 12, scale: 4 }),
4948
+ approvedRateType: varchar("approved_rate_type"),
4949
+ allocationMode: varchar("allocation_mode").notNull().default("percent"),
4950
+ allocationValue: numeric("allocation_value", { precision: 12, scale: 4 }),
4951
+ required: boolean("required").notNull().default(true),
4952
+ defaultFulfillmentDetails: jsonb("default_fulfillment_details"),
4953
+ createdAt: timestamp("created_at").notNull().defaultNow(),
4954
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
4955
+ },
4956
+ (table) => [
4957
+ uniqueIndex("sales_package_products_unique_idx").on(
4958
+ table.salesPackageCode,
4959
+ table.stribProductId,
4960
+ ),
4961
+ index("sales_package_products_code_idx").on(table.salesPackageCode),
4962
+ index("sales_package_products_product_idx").on(table.stribProductId),
4963
+ ],
4964
+ );
4965
+
4966
+ export type SalesPackageProduct = typeof salesPackageProducts.$inferSelect;
4967
+
4710
4968
  export const mediaOrders = pgTable(
4711
4969
  "media_orders",
4712
4970
  {
4713
4971
  id: varchar("id")
4714
4972
  .primaryKey()
4715
4973
  .default(sql`gen_random_uuid()`),
4716
- planId: varchar("plan_id"), // FK → plans.id (nullable — can exist without a plan)
4717
- engagementId: varchar("engagement_id"), // FK → engagements.id (nullable during transition)
4974
+ planId: varchar("plan_id").references(() => plans.id, {
4975
+ onDelete: "set null",
4976
+ }), // nullable — can exist without a plan
4977
+ engagementId: varchar("engagement_id").references(() => engagements.id, {
4978
+ onDelete: "set null",
4979
+ }), // nullable during transition
4718
4980
  configurationId: varchar("configuration_id"), // FK → plan_configurations.id (named product mix)
4719
4981
  optionId: varchar("option_id"), // FK → plan_options.id (canonical selected option)
4720
- contractId: uuid("contract_id"), // FK → contracts.id (nullable — links to annual contract)
4982
+ contractId: uuid("contract_id").references(() => contracts.id, {
4983
+ onDelete: "set null",
4984
+ }), // nullable — links to annual contract
4721
4985
  partnerId: varchar("partner_id"),
4722
4986
  hubspotDealId: text("hubspot_deal_id"),
4723
4987
  hubspotCompanyId: text("hubspot_company_id"),
4724
4988
  parentHubspotCompanyId: text("parent_hubspot_company_id"),
4989
+ initiativeCode: varchar("initiative_code"),
4990
+ salesPackageCode: varchar("sales_package_code"),
4991
+ salesPackageSnapshot: jsonb("sales_package_snapshot").$type<Array<{
4992
+ code: string;
4993
+ name: string;
4994
+ packagePrice: number | null;
4995
+ initiativeCode: string | null;
4996
+ componentCount: number;
4997
+ appliedAt: string;
4998
+ packageSource: "direct_package" | "upgrade_from_selection" | "manual_products";
4999
+ compareSignals?: string[];
5000
+ sellerSummary?: string | null;
5001
+ discountPercent?: number | null;
5002
+ addedValueAmount?: number | null;
5003
+ addedValueDescription?: string | null;
5004
+ }>>(),
4725
5005
 
4726
5006
  // Client header
4727
5007
  clientName: text("client_name").notNull(),
@@ -4749,6 +5029,8 @@ export const mediaOrders = pgTable(
4749
5029
  activatedAt: timestamp("activated_at"),
4750
5030
  completedAt: timestamp("completed_at"),
4751
5031
  returnReason: text("return_reason"),
5032
+ amendmentReason: amendmentReasonEnum("amendment_reason"),
5033
+ amendmentNotes: text("amendment_notes"),
4752
5034
  slaHold: boolean("sla_hold").default(false),
4753
5035
  clientTier: text("client_tier"),
4754
5036
 
@@ -4780,9 +5062,6 @@ export const mediaOrders = pgTable(
4780
5062
  // Order-level notes
4781
5063
  orderNotes: text("order_notes"),
4782
5064
 
4783
- // Sales initiative (links to sales_initiatives table)
4784
- initiativeCode: varchar("initiative_code"),
4785
-
4786
5065
  // Billing fields
4787
5066
  navigaAdvertiserId: text("naviga_advertiser_id"),
4788
5067
  purchaseOrderNumber: text("purchase_order_number"),
@@ -4839,12 +5118,14 @@ export const mediaOrders = pgTable(
4839
5118
  index("media_orders_engagement_id_idx").on(table.engagementId),
4840
5119
  index("media_orders_partner_id_idx").on(table.partnerId),
4841
5120
  index("media_orders_hubspot_company_id_idx").on(table.hubspotCompanyId),
5121
+ index("media_orders_hubspot_deal_id_idx").on(table.hubspotDealId),
4842
5122
  index("media_orders_status_idx").on(table.status),
4843
5123
  index("media_orders_vendor_id_idx").on(table.vendorId),
4844
5124
  index("media_orders_vendor_fulfillment_status_idx").on(table.vendorFulfillmentStatus),
4845
5125
  index("media_orders_parent_company_idx").on(table.parentHubspotCompanyId),
4846
5126
  index("media_orders_contract_id_idx").on(table.contractId),
4847
5127
  index("media_orders_initiative_code_idx").on(table.initiativeCode),
5128
+ index("media_orders_sales_package_code_idx").on(table.salesPackageCode),
4848
5129
  ]
4849
5130
  );
4850
5131
 
@@ -4973,6 +5254,11 @@ export const mediaOrderLineItems = pgTable(
4973
5254
  // Fulfillment details (product-specific fields for HubSpot sync)
4974
5255
  fulfillmentDetails: jsonb("fulfillment_details"),
4975
5256
  creativeSource: varchar("creative_source"),
5257
+ initiativeCode: varchar("initiative_code"),
5258
+ salesPackageCode: varchar("sales_package_code"),
5259
+ salesPackageComponentRef: varchar("sales_package_component_ref"),
5260
+ isPackageDerived: boolean("is_package_derived").notNull().default(false),
5261
+ packagePricingSource: varchar("package_pricing_source"),
4976
5262
 
4977
5263
  // Ad trafficking reference key (auto-generated naming convention)
4978
5264
  trafficKey: text("traffic_key"),
@@ -4994,9 +5280,12 @@ export const mediaOrderLineItems = pgTable(
4994
5280
  },
4995
5281
  (table) => [
4996
5282
  index("media_order_line_items_section_id_idx").on(table.sectionId),
5283
+ index("media_order_line_items_sync_status_idx").on(table.syncStatus),
4997
5284
  index("idx_line_items_parent").on(table.parentLineItemId),
4998
5285
  index("media_order_line_items_location_id_idx").on(table.locationId),
4999
5286
  index("media_order_line_items_tracking_key_idx").on(table.trackingKey),
5287
+ index("media_order_line_items_initiative_code_idx").on(table.initiativeCode),
5288
+ index("media_order_line_items_sales_package_code_idx").on(table.salesPackageCode),
5000
5289
  ]
5001
5290
  );
5002
5291
 
@@ -5217,6 +5506,30 @@ export const updateStribProductSchema = insertStribProductSchema.partial();
5217
5506
  export type StribProduct = typeof stribProducts.$inferSelect;
5218
5507
  export type InsertStribProduct = z.infer<typeof insertStribProductSchema>;
5219
5508
 
5509
+ export const salesInitiativeProducts = pgTable(
5510
+ "sales_initiative_products",
5511
+ {
5512
+ id: uuid("id").primaryKey().defaultRandom(),
5513
+ initiativeCode: varchar("initiative_code").notNull(),
5514
+ stribProductId: uuid("strib_product_id")
5515
+ .notNull()
5516
+ .references(() => stribProducts.id, { onDelete: "cascade" }),
5517
+ displayOrder: integer("display_order").notNull().default(0),
5518
+ createdAt: timestamp("created_at").notNull().defaultNow(),
5519
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
5520
+ },
5521
+ (table) => [
5522
+ uniqueIndex("sales_initiative_products_unique_idx").on(
5523
+ table.initiativeCode,
5524
+ table.stribProductId,
5525
+ ),
5526
+ index("sales_initiative_products_code_idx").on(table.initiativeCode),
5527
+ index("sales_initiative_products_product_idx").on(table.stribProductId),
5528
+ ],
5529
+ );
5530
+
5531
+ export type SalesInitiativeProduct = typeof salesInitiativeProducts.$inferSelect;
5532
+
5220
5533
  /**
5221
5534
  * strib_product_rates — tier-based pricing per product.
5222
5535
  * Different tiers depending on family: Digital uses open/advocacy/tier_1/tier_2/tier_3;
@@ -5555,6 +5868,7 @@ export const rateExceptionRequests = pgTable(
5555
5868
  escalationHistory: jsonb("escalation_history"), // Array of {level, at, by, reason}
5556
5869
  expiresAt: timestamp("expires_at"),
5557
5870
  approvalScope: varchar("approval_scope").notNull().default("request"), // "request" = affects this exception only; "catalog" = also updates global product rate
5871
+ batchId: uuid("batch_id"), // Groups bulk rate exception requests submitted together
5558
5872
  createdAt: timestamp("created_at").notNull().defaultNow(),
5559
5873
  updatedAt: timestamp("updated_at").notNull().defaultNow(),
5560
5874
  },
@@ -5563,6 +5877,8 @@ export const rateExceptionRequests = pgTable(
5563
5877
  index("rate_exception_status_idx").on(table.status),
5564
5878
  index("rate_exception_requester_idx").on(table.requestedByUserId),
5565
5879
  index("rate_exception_plan_idx").on(table.planId),
5880
+ index("rate_exception_batch_idx").on(table.batchId),
5881
+ index("rate_exception_order_status_idx").on(table.mediaOrderId, table.status),
5566
5882
  ]
5567
5883
  );
5568
5884
 
@@ -5633,6 +5949,41 @@ export const slaConfig = pgTable("sla_config", {
5633
5949
  export type SlaConfig = typeof slaConfig.$inferSelect;
5634
5950
  export type InsertSlaConfig = typeof slaConfig.$inferInsert;
5635
5951
 
5952
+ // ─── SLA Snapshots ───────────────────────────────────────────────────────────
5953
+
5954
+ /**
5955
+ * sla_snapshots — Point-in-time SLA state from fn-flux evaluations.
5956
+ * Each row captures the hold status, deadline, milestones, and profile
5957
+ * for a given media order + fulfillment ticket pair.
5958
+ */
5959
+ export const slaHoldStatusEnum = pgEnum("sla_hold_status", ["clear", "hold", "override"]);
5960
+
5961
+ export const slaSnapshots = pgTable(
5962
+ "sla_snapshots",
5963
+ {
5964
+ id: serial("id").primaryKey(),
5965
+ mediaOrderId: text("media_order_id").notNull().references(() => mediaOrders.id),
5966
+ ticketId: text("ticket_id").notNull(),
5967
+ holdStatus: slaHoldStatusEnum("hold_status").notNull().default("clear"),
5968
+ holdReason: text("hold_reason"),
5969
+ slaDays: integer("sla_days").notNull(),
5970
+ availableDays: numeric("available_days"),
5971
+ slaDeadline: timestamp("sla_deadline"),
5972
+ campaignStartDate: date("campaign_start_date"),
5973
+ milestones: jsonb("milestones"),
5974
+ profileName: text("profile_name"),
5975
+ lastUpdatedAt: timestamp("last_updated_at").defaultNow(),
5976
+ },
5977
+ (table) => [
5978
+ index("sla_snapshots_media_order_id_idx").on(table.mediaOrderId),
5979
+ index("sla_snapshots_ticket_id_idx").on(table.ticketId),
5980
+ uniqueIndex("sla_snapshots_order_ticket_uniq").on(table.mediaOrderId, table.ticketId),
5981
+ ]
5982
+ );
5983
+
5984
+ export type SlaSnapshot = typeof slaSnapshots.$inferSelect;
5985
+ export type InsertSlaSnapshot = typeof slaSnapshots.$inferInsert;
5986
+
5636
5987
  // ─── Inventory Sync Audit Trail ─────────────────────────────────────────────
5637
5988
 
5638
5989
  /**
@@ -8157,6 +8508,7 @@ export type InsertSemCampaign = typeof semCampaigns.$inferInsert;
8157
8508
 
8158
8509
  export const compassEventOutboxStatusEnum = pgEnum("compass_event_outbox_status", [
8159
8510
  "pending",
8511
+ "processing",
8160
8512
  "failed",
8161
8513
  "delivered",
8162
8514
  "dead_letter",
@@ -8205,3 +8557,133 @@ export const creativeDeliveries = pgTable("creative_deliveries", {
8205
8557
  assetCount: integer("asset_count").notNull().default(0),
8206
8558
  receivedAt: timestamp("received_at").defaultNow().notNull(),
8207
8559
  });
8560
+
8561
+ // ─── Order Activity Feed ────────────────────────────────────────────────────
8562
+
8563
+ export const orderActivityFeed = pgTable(
8564
+ "order_activity_feed",
8565
+ {
8566
+ id: uuid("id").primaryKey().defaultRandom(),
8567
+ mediaOrderId: varchar("media_order_id")
8568
+ .notNull()
8569
+ .references(() => mediaOrders.id),
8570
+ lineItemId: varchar("line_item_id").references(() => mediaOrderLineItems.id),
8571
+ eventType: activityEventTypeEnum("event_type").notNull(),
8572
+ title: text("title").notNull(),
8573
+ detail: text("detail"),
8574
+ severity: activitySeverityEnum("severity").notNull(),
8575
+ sourceSystem: activitySourceSystemEnum("source_system").notNull(),
8576
+ externalId: text("external_id").notNull().unique(), // idempotency key — prevents duplicate event ingestion
8577
+ status: text("status"), // intentionally untyped — values vary by eventType (creative vs fulfillment vs contract)
8578
+ occurredAt: timestamp("occurred_at").notNull(),
8579
+ createdAt: timestamp("created_at").notNull().defaultNow(),
8580
+ metadata: jsonb("metadata").$type<Record<string, unknown>>(),
8581
+ },
8582
+ (table) => [
8583
+ index("order_activity_feed_order_occurred_idx").on(
8584
+ table.mediaOrderId,
8585
+ table.occurredAt,
8586
+ ),
8587
+ index("order_activity_feed_line_item_idx").on(table.lineItemId),
8588
+ ],
8589
+ );
8590
+
8591
+ export type OrderActivityFeedEntry = typeof orderActivityFeed.$inferSelect;
8592
+
8593
+ // ─── Pending Actions ────────────────────────────────────────────────────────
8594
+
8595
+ export const pendingActions = pgTable(
8596
+ "pending_actions",
8597
+ {
8598
+ id: uuid("id").primaryKey().defaultRandom(),
8599
+ mediaOrderId: varchar("media_order_id")
8600
+ .notNull()
8601
+ .references(() => mediaOrders.id),
8602
+ lineItemId: varchar("line_item_id").references(() => mediaOrderLineItems.id),
8603
+ actionType: pendingActionTypeEnum("action_type").notNull(),
8604
+ message: text("message").notNull(),
8605
+ activityFeedId: uuid("activity_feed_id")
8606
+ .notNull()
8607
+ .references(() => orderActivityFeed.id),
8608
+ resolvedAt: timestamp("resolved_at"),
8609
+ createdAt: timestamp("created_at").notNull().defaultNow(),
8610
+ },
8611
+ (table) => [
8612
+ index("pending_actions_order_idx").on(table.mediaOrderId),
8613
+ index("pending_actions_line_item_idx").on(table.lineItemId),
8614
+ index("pending_actions_activity_feed_idx").on(table.activityFeedId),
8615
+ index("pending_actions_unresolved_idx").on(table.mediaOrderId).where(
8616
+ sql`resolved_at IS NULL`,
8617
+ ),
8618
+ ],
8619
+ );
8620
+
8621
+ export type PendingAction = typeof pendingActions.$inferSelect;
8622
+
8623
+ // ─── Order Version Snapshots ────────────────────────────────────────────────
8624
+
8625
+ export const orderVersionSnapshots = pgTable(
8626
+ "order_version_snapshots",
8627
+ {
8628
+ id: uuid("id").primaryKey().defaultRandom(),
8629
+ mediaOrderId: varchar("media_order_id")
8630
+ .notNull()
8631
+ .references(() => mediaOrders.id),
8632
+ version: integer("version").notNull(),
8633
+ amendmentReason: text("amendment_reason"),
8634
+ amendmentNotes: text("amendment_notes"),
8635
+ amendedBy: varchar("amended_by"),
8636
+ snapshot: jsonb("snapshot").notNull().$type<Record<string, unknown>>(),
8637
+ netInvestment: numeric("net_investment", { precision: 12, scale: 2 }),
8638
+ lineItemCount: integer("line_item_count"),
8639
+ createdAt: timestamp("created_at").notNull().defaultNow(),
8640
+ },
8641
+ (table) => [
8642
+ index("order_version_snapshots_order_version_idx").on(
8643
+ table.mediaOrderId,
8644
+ table.version,
8645
+ ),
8646
+ ],
8647
+ );
8648
+
8649
+ export type OrderVersionSnapshot = typeof orderVersionSnapshots.$inferSelect;
8650
+
8651
+ // ─── User Milestones ────────────────────────────────────────────────────────
8652
+
8653
+ export const userMilestones = pgTable(
8654
+ "user_milestones",
8655
+ {
8656
+ id: uuid("id").primaryKey().defaultRandom(),
8657
+ userId: varchar("user_id").notNull(),
8658
+ milestoneKey: text("milestone_key").notNull(),
8659
+ shownAt: timestamp("shown_at").notNull().defaultNow(),
8660
+ createdAt: timestamp("created_at").notNull().defaultNow(),
8661
+ },
8662
+ (table) => [
8663
+ index("user_milestones_user_id_idx").on(table.userId),
8664
+ unique("user_milestones_user_key_unique").on(table.userId, table.milestoneKey),
8665
+ ],
8666
+ );
8667
+
8668
+ export type UserMilestone = typeof userMilestones.$inferSelect;
8669
+
8670
+ /** Shape of the ad_activity JSONB column in business_intel_cache */
8671
+ export interface BusinessIntelAdActivity {
8672
+ google?: { active: boolean; adCount: number; lastSeen?: string };
8673
+ meta?: { active: boolean; adCount: number; lastSeen?: string };
8674
+ }
8675
+
8676
+ /** Shape of the seo_snapshot JSONB column in business_intel_cache */
8677
+ export interface BusinessIntelSeoSnapshot {
8678
+ organicTraffic?: number;
8679
+ topKeywords?: string[];
8680
+ paidTrafficCost?: number;
8681
+ }
8682
+
8683
+ /** Shape of the tech_signals JSONB column in business_intel_cache */
8684
+ export interface BusinessIntelTechSignals {
8685
+ analytics?: string[];
8686
+ advertising?: string[];
8687
+ crm?: string[];
8688
+ social?: string[];
8689
+ }