@foundrynorth/compass-schema 1.0.19 → 1.0.21

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.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared types for the order activity feed — the backbone for all
3
+ * post-submission visibility (contract alerts, fulfillment status,
4
+ * creative status, amendments, rate exception resolution).
5
+ */
6
+ export declare const ACTIVITY_EVENT_TYPES: readonly ["fulfillment_stage_changed", "creative_status_changed", "creative_delivered", "signing_progress", "drawdown_alert", "renewal_reminder", "amendment_submitted", "rate_exception_resolved", "sla_hold_set", "sla_hold_cleared", "sla_milestone_at_risk", "sla_milestone_breached", "sla_evaluated"];
7
+ export type ActivityEventType = (typeof ACTIVITY_EVENT_TYPES)[number];
8
+ export declare const ACTIVITY_SEVERITIES: readonly ["info", "success", "warning", "action_needed"];
9
+ export type ActivitySeverity = (typeof ACTIVITY_SEVERITIES)[number];
10
+ export declare const ACTIVITY_SOURCE_SYSTEMS: readonly ["flux", "forge", "hubspot", "compass"];
11
+ export type ActivitySourceSystem = (typeof ACTIVITY_SOURCE_SYSTEMS)[number];
12
+ export declare const PENDING_ACTION_TYPES: readonly ["creative_revision_needed", "creative_rejected", "fulfillment_blocked", "rate_exception_pending", "sla_hold_active"];
13
+ export type PendingActionType = (typeof PENDING_ACTION_TYPES)[number];
14
+ /** The event envelope fn-flux sends to fn-legacy */
15
+ export interface CompassStatusEvent {
16
+ schemaVersion: "v1";
17
+ eventId: string;
18
+ eventType: string;
19
+ idempotencyKey: string;
20
+ emittedAt: string;
21
+ payload: {
22
+ mediaOrderId: string;
23
+ lineItemId?: string | null;
24
+ hubspotCompanyId?: string | null;
25
+ title: string;
26
+ detail?: string | null;
27
+ severity: ActivitySeverity;
28
+ status?: string | null;
29
+ metadata?: Record<string, unknown> | null;
30
+ };
31
+ }
32
+ /** Resolution mapping: which events clear which pending actions */
33
+ export declare const PENDING_ACTION_RESOLUTION_MAP: Record<PendingActionType, {
34
+ resolvedByEventType: ActivityEventType;
35
+ resolvedByStatuses: string[];
36
+ }>;
37
+ //# sourceMappingURL=activity-feed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-feed.d.ts","sourceRoot":"","sources":["../../src/types/activity-feed.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,oBAAoB,6SAcvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,eAAO,MAAM,mBAAmB,0DAKtB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE,eAAO,MAAM,uBAAuB,kDAK1B,CAAC;AAEX,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5E,eAAO,MAAM,oBAAoB,gIAMvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,oDAAoD;AACpD,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE;QACP,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACjC,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,QAAQ,EAAE,gBAAgB,CAAC;QAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KAC3C,CAAC;CACH;AAED,mEAAmE;AACnE,eAAO,MAAM,6BAA6B,EAAE,MAAM,CAChD,iBAAiB,EACjB;IAAE,mBAAmB,EAAE,iBAAiB,CAAC;IAAC,kBAAkB,EAAE,MAAM,EAAE,CAAA;CAAE,CAsBzE,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Shared types for the order activity feed — the backbone for all
3
+ * post-submission visibility (contract alerts, fulfillment status,
4
+ * creative status, amendments, rate exception resolution).
5
+ */
6
+ export const ACTIVITY_EVENT_TYPES = [
7
+ "fulfillment_stage_changed",
8
+ "creative_status_changed",
9
+ "creative_delivered",
10
+ "signing_progress",
11
+ "drawdown_alert",
12
+ "renewal_reminder",
13
+ "amendment_submitted",
14
+ "rate_exception_resolved",
15
+ "sla_hold_set",
16
+ "sla_hold_cleared",
17
+ "sla_milestone_at_risk",
18
+ "sla_milestone_breached",
19
+ "sla_evaluated",
20
+ ];
21
+ export const ACTIVITY_SEVERITIES = [
22
+ "info",
23
+ "success",
24
+ "warning",
25
+ "action_needed",
26
+ ];
27
+ export const ACTIVITY_SOURCE_SYSTEMS = [
28
+ "flux",
29
+ "forge",
30
+ "hubspot",
31
+ "compass",
32
+ ];
33
+ export const PENDING_ACTION_TYPES = [
34
+ "creative_revision_needed",
35
+ "creative_rejected",
36
+ "fulfillment_blocked",
37
+ "rate_exception_pending",
38
+ "sla_hold_active",
39
+ ];
40
+ /** Resolution mapping: which events clear which pending actions */
41
+ export const PENDING_ACTION_RESOLUTION_MAP = {
42
+ creative_revision_needed: {
43
+ resolvedByEventType: "creative_status_changed",
44
+ resolvedByStatuses: ["approved", "completed"],
45
+ },
46
+ creative_rejected: {
47
+ resolvedByEventType: "creative_status_changed",
48
+ resolvedByStatuses: ["approved"],
49
+ },
50
+ fulfillment_blocked: {
51
+ resolvedByEventType: "fulfillment_stage_changed",
52
+ resolvedByStatuses: ["pending_fulfillment", "running", "completed"],
53
+ },
54
+ rate_exception_pending: {
55
+ resolvedByEventType: "rate_exception_resolved",
56
+ resolvedByStatuses: ["approved", "auto_approved", "rejected"],
57
+ },
58
+ sla_hold_active: {
59
+ resolvedByEventType: "sla_hold_cleared",
60
+ resolvedByStatuses: ["clear"],
61
+ },
62
+ };
63
+ //# sourceMappingURL=activity-feed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-feed.js","sourceRoot":"","sources":["../../src/types/activity-feed.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,2BAA2B;IAC3B,yBAAyB;IACzB,oBAAoB;IACpB,kBAAkB;IAClB,gBAAgB;IAChB,kBAAkB;IAClB,qBAAqB;IACrB,yBAAyB;IACzB,cAAc;IACd,kBAAkB;IAClB,uBAAuB;IACvB,wBAAwB;IACxB,eAAe;CACP,CAAC;AAIX,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,MAAM;IACN,SAAS;IACT,SAAS;IACT,eAAe;CACP,CAAC;AAIX,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,MAAM;IACN,OAAO;IACP,SAAS;IACT,SAAS;CACD,CAAC;AAIX,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,0BAA0B;IAC1B,mBAAmB;IACnB,qBAAqB;IACrB,wBAAwB;IACxB,iBAAiB;CACT,CAAC;AAuBX,mEAAmE;AACnE,MAAM,CAAC,MAAM,6BAA6B,GAGtC;IACF,wBAAwB,EAAE;QACxB,mBAAmB,EAAE,yBAAyB;QAC9C,kBAAkB,EAAE,CAAC,UAAU,EAAE,WAAW,CAAC;KAC9C;IACD,iBAAiB,EAAE;QACjB,mBAAmB,EAAE,yBAAyB;QAC9C,kBAAkB,EAAE,CAAC,UAAU,CAAC;KACjC;IACD,mBAAmB,EAAE;QACnB,mBAAmB,EAAE,2BAA2B;QAChD,kBAAkB,EAAE,CAAC,qBAAqB,EAAE,SAAS,EAAE,WAAW,CAAC;KACpE;IACD,sBAAsB,EAAE;QACtB,mBAAmB,EAAE,yBAAyB;QAC9C,kBAAkB,EAAE,CAAC,UAAU,EAAE,eAAe,EAAE,UAAU,CAAC;KAC9D;IACD,eAAe,EAAE;QACf,mBAAmB,EAAE,kBAAkB;QACvC,kBAAkB,EAAE,CAAC,OAAO,CAAC;KAC9B;CACF,CAAC"}
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "@foundrynorth/compass-schema",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "Canonical Drizzle ORM schema for Foundry Compass (rough-waterfall database)",
5
- "license": "UNLICENSED",
6
5
  "type": "module",
7
6
  "main": "dist/index.js",
8
7
  "types": "dist/index.d.ts",
@@ -21,6 +20,7 @@
21
20
  "src"
22
21
  ],
23
22
  "scripts": {
23
+ "check": "tsc --noEmit",
24
24
  "build": "tsc",
25
25
  "prepublishOnly": "npm run build"
26
26
  },
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(),
@@ -1029,7 +1093,7 @@ export const plans = pgTable("plans", {
1029
1093
  contractId: uuid("contract_id"), // FK → contracts.id (nullable — links to annual contract)
1030
1094
 
1031
1095
  // Workflow mode: determines which UI sections and features are available
1032
- // 'research_backed' = full AnalyzeAndPlan flow (default, all existing plans)
1096
+ // 'research_backed' = discovery mode creates plan, runs analysis in background
1033
1097
  // 'direct_config' = configure products directly without research
1034
1098
  // 'aor_pitch' = agency-of-record pitch with proposal focus
1035
1099
  // 'io_entry' = direct insertion order entry from client PDF/XLSX
@@ -1762,6 +1826,7 @@ export const clerkUsers = pgTable("clerk_users", {
1762
1826
  createdAt: timestamp("created_at").notNull().defaultNow(),
1763
1827
  approvedAt: timestamp("approved_at"),
1764
1828
  approvedBy: text("approved_by"), // Admin who approved
1829
+ compassVoiceWarmth: numeric("compass_voice_warmth").default("0.6"),
1765
1830
  });
1766
1831
 
1767
1832
  export const insertClerkUserSchema = createInsertSchema(clerkUsers).omit({
@@ -4707,6 +4772,39 @@ export const mediaOrderStatusEnum = pgEnum("media_order_status", [
4707
4772
  "completed",
4708
4773
  ]);
4709
4774
 
4775
+ export const salesInitiatives = pgTable(
4776
+ "sales_initiatives",
4777
+ {
4778
+ id: uuid("id").primaryKey().defaultRandom(),
4779
+ code: varchar("code").notNull().unique(),
4780
+ name: varchar("name").notNull(),
4781
+ description: text("description"),
4782
+ hubspotValue: varchar("hubspot_value").notNull().unique(),
4783
+ displayOrder: integer("display_order").notNull().default(0),
4784
+ startsAt: date("starts_at"),
4785
+ endsAt: date("ends_at"),
4786
+ isActive: boolean("is_active").notNull().default(true),
4787
+ createdAt: timestamp("created_at").notNull().defaultNow(),
4788
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
4789
+ },
4790
+ (table) => [
4791
+ index("sales_initiatives_active_idx").on(table.isActive),
4792
+ index("sales_initiatives_display_order_idx").on(table.displayOrder),
4793
+ ],
4794
+ );
4795
+
4796
+ export const insertSalesInitiativeSchema = createInsertSchema(
4797
+ salesInitiatives,
4798
+ ).omit({
4799
+ id: true,
4800
+ createdAt: true,
4801
+ updatedAt: true,
4802
+ });
4803
+ export const updateSalesInitiativeSchema = insertSalesInitiativeSchema.partial();
4804
+ export type SalesInitiative = typeof salesInitiatives.$inferSelect;
4805
+ export type InsertSalesInitiative = z.infer<typeof insertSalesInitiativeSchema>;
4806
+ export type UpdateSalesInitiative = z.infer<typeof updateSalesInitiativeSchema>;
4807
+
4710
4808
  export const mediaOrders = pgTable(
4711
4809
  "media_orders",
4712
4810
  {
@@ -4722,6 +4820,7 @@ export const mediaOrders = pgTable(
4722
4820
  hubspotDealId: text("hubspot_deal_id"),
4723
4821
  hubspotCompanyId: text("hubspot_company_id"),
4724
4822
  parentHubspotCompanyId: text("parent_hubspot_company_id"),
4823
+ initiativeCode: varchar("initiative_code"),
4725
4824
 
4726
4825
  // Client header
4727
4826
  clientName: text("client_name").notNull(),
@@ -4749,6 +4848,8 @@ export const mediaOrders = pgTable(
4749
4848
  activatedAt: timestamp("activated_at"),
4750
4849
  completedAt: timestamp("completed_at"),
4751
4850
  returnReason: text("return_reason"),
4851
+ amendmentReason: amendmentReasonEnum("amendment_reason"),
4852
+ amendmentNotes: text("amendment_notes"),
4752
4853
  slaHold: boolean("sla_hold").default(false),
4753
4854
  clientTier: text("client_tier"),
4754
4855
 
@@ -4780,9 +4881,6 @@ export const mediaOrders = pgTable(
4780
4881
  // Order-level notes
4781
4882
  orderNotes: text("order_notes"),
4782
4883
 
4783
- // Sales initiative (links to sales_initiatives table)
4784
- initiativeCode: varchar("initiative_code"),
4785
-
4786
4884
  // Billing fields
4787
4885
  navigaAdvertiserId: text("naviga_advertiser_id"),
4788
4886
  purchaseOrderNumber: text("purchase_order_number"),
@@ -4839,6 +4937,7 @@ export const mediaOrders = pgTable(
4839
4937
  index("media_orders_engagement_id_idx").on(table.engagementId),
4840
4938
  index("media_orders_partner_id_idx").on(table.partnerId),
4841
4939
  index("media_orders_hubspot_company_id_idx").on(table.hubspotCompanyId),
4940
+ index("media_orders_hubspot_deal_id_idx").on(table.hubspotDealId),
4842
4941
  index("media_orders_status_idx").on(table.status),
4843
4942
  index("media_orders_vendor_id_idx").on(table.vendorId),
4844
4943
  index("media_orders_vendor_fulfillment_status_idx").on(table.vendorFulfillmentStatus),
@@ -4973,6 +5072,7 @@ export const mediaOrderLineItems = pgTable(
4973
5072
  // Fulfillment details (product-specific fields for HubSpot sync)
4974
5073
  fulfillmentDetails: jsonb("fulfillment_details"),
4975
5074
  creativeSource: varchar("creative_source"),
5075
+ initiativeCode: varchar("initiative_code"),
4976
5076
 
4977
5077
  // Ad trafficking reference key (auto-generated naming convention)
4978
5078
  trafficKey: text("traffic_key"),
@@ -4994,9 +5094,11 @@ export const mediaOrderLineItems = pgTable(
4994
5094
  },
4995
5095
  (table) => [
4996
5096
  index("media_order_line_items_section_id_idx").on(table.sectionId),
5097
+ index("media_order_line_items_sync_status_idx").on(table.syncStatus),
4997
5098
  index("idx_line_items_parent").on(table.parentLineItemId),
4998
5099
  index("media_order_line_items_location_id_idx").on(table.locationId),
4999
5100
  index("media_order_line_items_tracking_key_idx").on(table.trackingKey),
5101
+ index("media_order_line_items_initiative_code_idx").on(table.initiativeCode),
5000
5102
  ]
5001
5103
  );
5002
5104
 
@@ -5182,6 +5284,7 @@ export const stribProducts = pgTable(
5182
5284
  strengths: text("strengths").array(), // Product strengths (High CTR, Cost Effective, etc.)
5183
5285
  kpis: text("kpis").array(), // Relevant KPIs (CPM, CTR, CVR, etc.)
5184
5286
  descriptionLong: text("description_long"), // Long-form product description
5287
+ talkingPoints: text("talking_points").array(), // Top 3 sales talking points per product
5185
5288
  disclaimer: text("disclaimer"), // Product disclaimers or fine print
5186
5289
  minimumBudget: numeric("minimum_budget", { precision: 10, scale: 2 }), // Minimum budget required
5187
5290
  minimumCommitmentMonths: integer("minimum_commitment_months"), // Minimum commitment period
@@ -5216,6 +5319,30 @@ export const updateStribProductSchema = insertStribProductSchema.partial();
5216
5319
  export type StribProduct = typeof stribProducts.$inferSelect;
5217
5320
  export type InsertStribProduct = z.infer<typeof insertStribProductSchema>;
5218
5321
 
5322
+ export const salesInitiativeProducts = pgTable(
5323
+ "sales_initiative_products",
5324
+ {
5325
+ id: uuid("id").primaryKey().defaultRandom(),
5326
+ initiativeCode: varchar("initiative_code").notNull(),
5327
+ stribProductId: uuid("strib_product_id")
5328
+ .notNull()
5329
+ .references(() => stribProducts.id, { onDelete: "cascade" }),
5330
+ displayOrder: integer("display_order").notNull().default(0),
5331
+ createdAt: timestamp("created_at").notNull().defaultNow(),
5332
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
5333
+ },
5334
+ (table) => [
5335
+ uniqueIndex("sales_initiative_products_unique_idx").on(
5336
+ table.initiativeCode,
5337
+ table.stribProductId,
5338
+ ),
5339
+ index("sales_initiative_products_code_idx").on(table.initiativeCode),
5340
+ index("sales_initiative_products_product_idx").on(table.stribProductId),
5341
+ ],
5342
+ );
5343
+
5344
+ export type SalesInitiativeProduct = typeof salesInitiativeProducts.$inferSelect;
5345
+
5219
5346
  /**
5220
5347
  * strib_product_rates — tier-based pricing per product.
5221
5348
  * Different tiers depending on family: Digital uses open/advocacy/tier_1/tier_2/tier_3;
@@ -5554,6 +5681,7 @@ export const rateExceptionRequests = pgTable(
5554
5681
  escalationHistory: jsonb("escalation_history"), // Array of {level, at, by, reason}
5555
5682
  expiresAt: timestamp("expires_at"),
5556
5683
  approvalScope: varchar("approval_scope").notNull().default("request"), // "request" = affects this exception only; "catalog" = also updates global product rate
5684
+ batchId: uuid("batch_id"), // Groups bulk rate exception requests submitted together
5557
5685
  createdAt: timestamp("created_at").notNull().defaultNow(),
5558
5686
  updatedAt: timestamp("updated_at").notNull().defaultNow(),
5559
5687
  },
@@ -5562,6 +5690,8 @@ export const rateExceptionRequests = pgTable(
5562
5690
  index("rate_exception_status_idx").on(table.status),
5563
5691
  index("rate_exception_requester_idx").on(table.requestedByUserId),
5564
5692
  index("rate_exception_plan_idx").on(table.planId),
5693
+ index("rate_exception_batch_idx").on(table.batchId),
5694
+ index("rate_exception_order_status_idx").on(table.mediaOrderId, table.status),
5565
5695
  ]
5566
5696
  );
5567
5697
 
@@ -5632,6 +5762,41 @@ export const slaConfig = pgTable("sla_config", {
5632
5762
  export type SlaConfig = typeof slaConfig.$inferSelect;
5633
5763
  export type InsertSlaConfig = typeof slaConfig.$inferInsert;
5634
5764
 
5765
+ // ─── SLA Snapshots ───────────────────────────────────────────────────────────
5766
+
5767
+ /**
5768
+ * sla_snapshots — Point-in-time SLA state from fn-flux evaluations.
5769
+ * Each row captures the hold status, deadline, milestones, and profile
5770
+ * for a given media order + fulfillment ticket pair.
5771
+ */
5772
+ export const slaHoldStatusEnum = pgEnum("sla_hold_status", ["clear", "hold", "override"]);
5773
+
5774
+ export const slaSnapshots = pgTable(
5775
+ "sla_snapshots",
5776
+ {
5777
+ id: serial("id").primaryKey(),
5778
+ mediaOrderId: text("media_order_id").notNull().references(() => mediaOrders.id),
5779
+ ticketId: text("ticket_id").notNull(),
5780
+ holdStatus: slaHoldStatusEnum("hold_status").notNull().default("clear"),
5781
+ holdReason: text("hold_reason"),
5782
+ slaDays: integer("sla_days").notNull(),
5783
+ availableDays: numeric("available_days"),
5784
+ slaDeadline: timestamp("sla_deadline"),
5785
+ campaignStartDate: date("campaign_start_date"),
5786
+ milestones: jsonb("milestones"),
5787
+ profileName: text("profile_name"),
5788
+ lastUpdatedAt: timestamp("last_updated_at").defaultNow(),
5789
+ },
5790
+ (table) => [
5791
+ index("sla_snapshots_media_order_id_idx").on(table.mediaOrderId),
5792
+ index("sla_snapshots_ticket_id_idx").on(table.ticketId),
5793
+ uniqueIndex("sla_snapshots_order_ticket_uniq").on(table.mediaOrderId, table.ticketId),
5794
+ ]
5795
+ );
5796
+
5797
+ export type SlaSnapshot = typeof slaSnapshots.$inferSelect;
5798
+ export type InsertSlaSnapshot = typeof slaSnapshots.$inferInsert;
5799
+
5635
5800
  // ─── Inventory Sync Audit Trail ─────────────────────────────────────────────
5636
5801
 
5637
5802
  /**
@@ -8204,3 +8369,131 @@ export const creativeDeliveries = pgTable("creative_deliveries", {
8204
8369
  assetCount: integer("asset_count").notNull().default(0),
8205
8370
  receivedAt: timestamp("received_at").defaultNow().notNull(),
8206
8371
  });
8372
+
8373
+ // ─── Order Activity Feed ────────────────────────────────────────────────────
8374
+
8375
+ export const orderActivityFeed = pgTable(
8376
+ "order_activity_feed",
8377
+ {
8378
+ id: uuid("id").primaryKey().defaultRandom(),
8379
+ mediaOrderId: varchar("media_order_id")
8380
+ .notNull()
8381
+ .references(() => mediaOrders.id),
8382
+ lineItemId: varchar("line_item_id").references(() => mediaOrderLineItems.id),
8383
+ eventType: activityEventTypeEnum("event_type").notNull(),
8384
+ title: text("title").notNull(),
8385
+ detail: text("detail"),
8386
+ severity: activitySeverityEnum("severity").notNull(),
8387
+ sourceSystem: activitySourceSystemEnum("source_system").notNull(),
8388
+ externalId: text("external_id").notNull().unique(), // idempotency key — prevents duplicate event ingestion
8389
+ status: text("status"), // intentionally untyped — values vary by eventType (creative vs fulfillment vs contract)
8390
+ occurredAt: timestamp("occurred_at").notNull(),
8391
+ createdAt: timestamp("created_at").notNull().defaultNow(),
8392
+ metadata: jsonb("metadata").$type<Record<string, unknown>>(),
8393
+ },
8394
+ (table) => [
8395
+ index("order_activity_feed_order_occurred_idx").on(
8396
+ table.mediaOrderId,
8397
+ table.occurredAt,
8398
+ ),
8399
+ index("order_activity_feed_line_item_idx").on(table.lineItemId),
8400
+ ],
8401
+ );
8402
+
8403
+ export type OrderActivityFeedEntry = typeof orderActivityFeed.$inferSelect;
8404
+
8405
+ // ─── Pending Actions ────────────────────────────────────────────────────────
8406
+
8407
+ export const pendingActions = pgTable(
8408
+ "pending_actions",
8409
+ {
8410
+ id: uuid("id").primaryKey().defaultRandom(),
8411
+ mediaOrderId: varchar("media_order_id")
8412
+ .notNull()
8413
+ .references(() => mediaOrders.id),
8414
+ lineItemId: varchar("line_item_id").references(() => mediaOrderLineItems.id),
8415
+ actionType: pendingActionTypeEnum("action_type").notNull(),
8416
+ message: text("message").notNull(),
8417
+ activityFeedId: uuid("activity_feed_id")
8418
+ .notNull()
8419
+ .references(() => orderActivityFeed.id),
8420
+ resolvedAt: timestamp("resolved_at"),
8421
+ createdAt: timestamp("created_at").notNull().defaultNow(),
8422
+ },
8423
+ (table) => [
8424
+ index("pending_actions_order_idx").on(table.mediaOrderId),
8425
+ index("pending_actions_unresolved_idx").on(table.mediaOrderId).where(
8426
+ sql`resolved_at IS NULL`,
8427
+ ),
8428
+ ],
8429
+ );
8430
+
8431
+ export type PendingAction = typeof pendingActions.$inferSelect;
8432
+
8433
+ // ─── Order Version Snapshots ────────────────────────────────────────────────
8434
+
8435
+ export const orderVersionSnapshots = pgTable(
8436
+ "order_version_snapshots",
8437
+ {
8438
+ id: uuid("id").primaryKey().defaultRandom(),
8439
+ mediaOrderId: varchar("media_order_id")
8440
+ .notNull()
8441
+ .references(() => mediaOrders.id),
8442
+ version: integer("version").notNull(),
8443
+ amendmentReason: text("amendment_reason"),
8444
+ amendmentNotes: text("amendment_notes"),
8445
+ amendedBy: varchar("amended_by"),
8446
+ snapshot: jsonb("snapshot").notNull().$type<Record<string, unknown>>(),
8447
+ netInvestment: numeric("net_investment", { precision: 12, scale: 2 }),
8448
+ lineItemCount: integer("line_item_count"),
8449
+ createdAt: timestamp("created_at").notNull().defaultNow(),
8450
+ },
8451
+ (table) => [
8452
+ index("order_version_snapshots_order_version_idx").on(
8453
+ table.mediaOrderId,
8454
+ table.version,
8455
+ ),
8456
+ ],
8457
+ );
8458
+
8459
+ export type OrderVersionSnapshot = typeof orderVersionSnapshots.$inferSelect;
8460
+
8461
+ // ─── User Milestones ────────────────────────────────────────────────────────
8462
+
8463
+ export const userMilestones = pgTable(
8464
+ "user_milestones",
8465
+ {
8466
+ id: uuid("id").primaryKey().defaultRandom(),
8467
+ userId: varchar("user_id").notNull(),
8468
+ milestoneKey: text("milestone_key").notNull(),
8469
+ shownAt: timestamp("shown_at").notNull().defaultNow(),
8470
+ createdAt: timestamp("created_at").notNull().defaultNow(),
8471
+ },
8472
+ (table) => [
8473
+ index("user_milestones_user_id_idx").on(table.userId),
8474
+ unique("user_milestones_user_key_unique").on(table.userId, table.milestoneKey),
8475
+ ],
8476
+ );
8477
+
8478
+ export type UserMilestone = typeof userMilestones.$inferSelect;
8479
+
8480
+ /** Shape of the ad_activity JSONB column in business_intel_cache */
8481
+ export interface BusinessIntelAdActivity {
8482
+ google?: { active: boolean; adCount: number; lastSeen?: string };
8483
+ meta?: { active: boolean; adCount: number; lastSeen?: string };
8484
+ }
8485
+
8486
+ /** Shape of the seo_snapshot JSONB column in business_intel_cache */
8487
+ export interface BusinessIntelSeoSnapshot {
8488
+ organicTraffic?: number;
8489
+ topKeywords?: string[];
8490
+ paidTrafficCost?: number;
8491
+ }
8492
+
8493
+ /** Shape of the tech_signals JSONB column in business_intel_cache */
8494
+ export interface BusinessIntelTechSignals {
8495
+ analytics?: string[];
8496
+ advertising?: string[];
8497
+ crm?: string[];
8498
+ social?: string[];
8499
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Shared types for the order activity feed — the backbone for all
3
+ * post-submission visibility (contract alerts, fulfillment status,
4
+ * creative status, amendments, rate exception resolution).
5
+ */
6
+
7
+ export const ACTIVITY_EVENT_TYPES = [
8
+ "fulfillment_stage_changed",
9
+ "creative_status_changed",
10
+ "creative_delivered",
11
+ "signing_progress",
12
+ "drawdown_alert",
13
+ "renewal_reminder",
14
+ "amendment_submitted",
15
+ "rate_exception_resolved",
16
+ "sla_hold_set",
17
+ "sla_hold_cleared",
18
+ "sla_milestone_at_risk",
19
+ "sla_milestone_breached",
20
+ "sla_evaluated",
21
+ ] as const;
22
+
23
+ export type ActivityEventType = (typeof ACTIVITY_EVENT_TYPES)[number];
24
+
25
+ export const ACTIVITY_SEVERITIES = [
26
+ "info",
27
+ "success",
28
+ "warning",
29
+ "action_needed",
30
+ ] as const;
31
+
32
+ export type ActivitySeverity = (typeof ACTIVITY_SEVERITIES)[number];
33
+
34
+ export const ACTIVITY_SOURCE_SYSTEMS = [
35
+ "flux",
36
+ "forge",
37
+ "hubspot",
38
+ "compass",
39
+ ] as const;
40
+
41
+ export type ActivitySourceSystem = (typeof ACTIVITY_SOURCE_SYSTEMS)[number];
42
+
43
+ export const PENDING_ACTION_TYPES = [
44
+ "creative_revision_needed",
45
+ "creative_rejected",
46
+ "fulfillment_blocked",
47
+ "rate_exception_pending",
48
+ "sla_hold_active",
49
+ ] as const;
50
+
51
+ export type PendingActionType = (typeof PENDING_ACTION_TYPES)[number];
52
+
53
+ /** The event envelope fn-flux sends to fn-legacy */
54
+ export interface CompassStatusEvent {
55
+ schemaVersion: "v1";
56
+ eventId: string;
57
+ eventType: string;
58
+ idempotencyKey: string;
59
+ emittedAt: string;
60
+ payload: {
61
+ mediaOrderId: string;
62
+ lineItemId?: string | null;
63
+ hubspotCompanyId?: string | null;
64
+ title: string;
65
+ detail?: string | null;
66
+ severity: ActivitySeverity;
67
+ status?: string | null;
68
+ metadata?: Record<string, unknown> | null;
69
+ };
70
+ }
71
+
72
+ /** Resolution mapping: which events clear which pending actions */
73
+ export const PENDING_ACTION_RESOLUTION_MAP: Record<
74
+ PendingActionType,
75
+ { resolvedByEventType: ActivityEventType; resolvedByStatuses: string[] }
76
+ > = {
77
+ creative_revision_needed: {
78
+ resolvedByEventType: "creative_status_changed",
79
+ resolvedByStatuses: ["approved", "completed"],
80
+ },
81
+ creative_rejected: {
82
+ resolvedByEventType: "creative_status_changed",
83
+ resolvedByStatuses: ["approved"],
84
+ },
85
+ fulfillment_blocked: {
86
+ resolvedByEventType: "fulfillment_stage_changed",
87
+ resolvedByStatuses: ["pending_fulfillment", "running", "completed"],
88
+ },
89
+ rate_exception_pending: {
90
+ resolvedByEventType: "rate_exception_resolved",
91
+ resolvedByStatuses: ["approved", "auto_approved", "rejected"],
92
+ },
93
+ sla_hold_active: {
94
+ resolvedByEventType: "sla_hold_cleared",
95
+ resolvedByStatuses: ["clear"],
96
+ },
97
+ };