@foundrynorth/compass-schema 1.0.22 → 1.0.24

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.
@@ -3,13 +3,13 @@
3
3
  * post-submission visibility (contract alerts, fulfillment status,
4
4
  * creative status, amendments, rate exception resolution).
5
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"];
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", "mid_campaign_change_applied", "mid_campaign_task_state_changed", "mid_campaign_change_verified"];
7
7
  export type ActivityEventType = (typeof ACTIVITY_EVENT_TYPES)[number];
8
8
  export declare const ACTIVITY_SEVERITIES: readonly ["info", "success", "warning", "action_needed"];
9
9
  export type ActivitySeverity = (typeof ACTIVITY_SEVERITIES)[number];
10
10
  export declare const ACTIVITY_SOURCE_SYSTEMS: readonly ["flux", "forge", "hubspot", "compass"];
11
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"];
12
+ export declare const PENDING_ACTION_TYPES: readonly ["creative_revision_needed", "creative_rejected", "fulfillment_blocked", "rate_exception_pending", "sla_hold_active", "mid_campaign_tasks_pending"];
13
13
  export type PendingActionType = (typeof PENDING_ACTION_TYPES)[number];
14
14
  /** The event envelope fn-flux sends to fn-legacy */
15
15
  export interface CompassStatusEvent {
@@ -1 +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"}
1
+ {"version":3,"file":"activity-feed.d.ts","sourceRoot":"","sources":["../../src/types/activity-feed.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,oBAAoB,+YAiBvB,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,8JAOvB,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,CA0BzE,CAAC"}
@@ -17,6 +17,9 @@ export const ACTIVITY_EVENT_TYPES = [
17
17
  "sla_milestone_at_risk",
18
18
  "sla_milestone_breached",
19
19
  "sla_evaluated",
20
+ "mid_campaign_change_applied",
21
+ "mid_campaign_task_state_changed",
22
+ "mid_campaign_change_verified",
20
23
  ];
21
24
  export const ACTIVITY_SEVERITIES = [
22
25
  "info",
@@ -36,6 +39,7 @@ export const PENDING_ACTION_TYPES = [
36
39
  "fulfillment_blocked",
37
40
  "rate_exception_pending",
38
41
  "sla_hold_active",
42
+ "mid_campaign_tasks_pending",
39
43
  ];
40
44
  /** Resolution mapping: which events clear which pending actions */
41
45
  export const PENDING_ACTION_RESOLUTION_MAP = {
@@ -59,5 +63,9 @@ export const PENDING_ACTION_RESOLUTION_MAP = {
59
63
  resolvedByEventType: "sla_hold_cleared",
60
64
  resolvedByStatuses: ["clear"],
61
65
  },
66
+ mid_campaign_tasks_pending: {
67
+ resolvedByEventType: "mid_campaign_change_verified",
68
+ resolvedByStatuses: ["verified"],
69
+ },
62
70
  };
63
71
  //# sourceMappingURL=activity-feed.js.map
@@ -1 +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"}
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;IACf,6BAA6B;IAC7B,iCAAiC;IACjC,8BAA8B;CACtB,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;IACjB,4BAA4B;CACpB,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;IACD,0BAA0B,EAAE;QAC1B,mBAAmB,EAAE,8BAA8B;QACnD,kBAAkB,EAAE,CAAC,UAAU,CAAC;KACjC;CACF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foundrynorth/compass-schema",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "description": "Canonical Drizzle ORM schema for Foundry Compass (rough-waterfall database)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/schema.ts CHANGED
@@ -8503,7 +8503,15 @@ export type SemCampaignRecord = typeof semCampaigns.$inferSelect;
8503
8503
  export type InsertSemCampaign = typeof semCampaigns.$inferInsert;
8504
8504
 
8505
8505
  // =============================================================================
8506
- // COMPASS EVENT OUTBOX (dead-letter queue for fn-flux webhook events)
8506
+ // COMPASS EVENT OUTBOX (durable outbox for cross-system writes)
8507
+ //
8508
+ // Generic claim/retry/DLQ pattern used by multiple producers. Events are
8509
+ // namespaced by `eventType` prefix so multiple workers can drain the same
8510
+ // table without contention:
8511
+ // • "flux.*" — fn-flux webhook delivery to external consumers
8512
+ // • "hubspot.*" — HubSpot ticket / Campaign Task writes from fn-legacy
8513
+ // (drained by fn-v2 hubspot-outbox-worker)
8514
+ // Workers filter on `event_type LIKE '{namespace}.%'` in their claim query.
8507
8515
  // =============================================================================
8508
8516
 
8509
8517
  export const compassEventOutboxStatusEnum = pgEnum("compass_event_outbox_status", [
@@ -8667,6 +8675,236 @@ export const userMilestones = pgTable(
8667
8675
 
8668
8676
  export type UserMilestone = typeof userMilestones.$inferSelect;
8669
8677
 
8678
+ // ─── Mid-Campaign Change Management ────────────────────────────────────────
8679
+ //
8680
+ // Finance-grade in-place edits to live media orders. Distinct from
8681
+ // AmendOrderWizard (which takes orders active→draft→sent). A mid-campaign
8682
+ // change keeps the order `active`, touches a surgical set of line items,
8683
+ // and routes through the existing rate-exception-engine if it would alter
8684
+ // rate rules or margin. HubSpot push is the canonical output — HubSpot's
8685
+ // own workflow pushes deltas to Naviga.
8686
+ //
8687
+ // State machine:
8688
+ // draft → (rate violation?) ─┬─ pending_rate_approval ─┬─ applied → hubspot_syncing → hubspot_synced
8689
+ // │ └─ rejected (terminal, line items rolled back)
8690
+ // └─ applied → hubspot_syncing ─┬─ hubspot_synced
8691
+ // └─ hubspot_failed (retryable)
8692
+
8693
+ export const midCampaignChangeRequests = pgTable(
8694
+ "mid_campaign_change_requests",
8695
+ {
8696
+ id: uuid("id").primaryKey().defaultRandom(),
8697
+ mediaOrderId: varchar("media_order_id")
8698
+ .notNull()
8699
+ .references(() => mediaOrders.id),
8700
+ reason: amendmentReasonEnum("reason").notNull(),
8701
+ notes: text("notes").notNull(),
8702
+ requestedByUserId: varchar("requested_by_user_id").notNull(),
8703
+ requestedByEmail: varchar("requested_by_email"),
8704
+ requestedByName: varchar("requested_by_name"),
8705
+ requestedAt: timestamp("requested_at").notNull().defaultNow(),
8706
+ appliedAt: timestamp("applied_at"),
8707
+ status: varchar("status").notNull().default("draft"),
8708
+ // draft | pending_rate_approval | applied | rejected | hubspot_syncing | hubspot_synced | hubspot_failed | verified
8709
+ // `verified` is terminal — set when taskStateJson.allComplete && hubspotTicketStage is terminal.
8710
+
8711
+ // Denormalized summary (so finance reports never join into ops)
8712
+ totalInvestmentDelta: numeric("total_investment_delta", { precision: 12, scale: 2 }).notNull().default("0"),
8713
+ affectedLineItemCount: integer("affected_line_item_count").notNull().default(0),
8714
+ marginDeltaPercent: numeric("margin_delta_percent", { precision: 7, scale: 4 }),
8715
+ marginDeltaDollars: numeric("margin_delta_dollars", { precision: 12, scale: 2 }),
8716
+
8717
+ // Approval state (nullable — only set when rate engine returned pending_approval for any op)
8718
+ rateApprovalCompletedAt: timestamp("rate_approval_completed_at"),
8719
+ rejectionReason: text("rejection_reason"),
8720
+
8721
+ // HubSpot ticket (fulfillment pipeline, CHANGE_REQUIRED stage)
8722
+ hubspotTicketId: text("hubspot_ticket_id"),
8723
+ hubspotCampaignTaskIds: jsonb("hubspot_campaign_task_ids").$type<string[]>().default(sql`'[]'::jsonb`),
8724
+
8725
+ // HubSpot line-item sync (via fn-v2 hubspot-order-sync)
8726
+ hubspotSyncRunId: text("hubspot_sync_run_id"),
8727
+ hubspotSyncStartedAt: timestamp("hubspot_sync_started_at"),
8728
+ hubspotSyncCompletedAt: timestamp("hubspot_sync_completed_at"),
8729
+ hubspotSyncError: text("hubspot_sync_error"),
8730
+
8731
+ // Closed-loop projection from HubSpot Campaign Task webhooks
8732
+ // (updated by handleCampaignTaskChange in hubspot-mid-campaign-writeback.ts)
8733
+ hubspotTicketStage: varchar("hubspot_ticket_stage", { length: 40 }),
8734
+ // Current ticket stage as seen in the most recent webhook delivery.
8735
+ // Values: CHANGE_REQUIRED | BILLING_CHANGE_REQUIRED | CANCELLED | COMPLETED | etc.
8736
+ taskStateJson: jsonb("task_state_json").$type<{
8737
+ tasks: Array<{
8738
+ hubspotTaskId: string;
8739
+ templateId: string;
8740
+ queue: string;
8741
+ status: "not_started" | "in_progress" | "completed" | "waiting" | "blocked";
8742
+ completedAt: string | null;
8743
+ ownerId: string | null;
8744
+ ownerName: string | null;
8745
+ }>;
8746
+ completedCount: number;
8747
+ totalCount: number;
8748
+ allComplete: boolean;
8749
+ }>(),
8750
+ taskStateUpdatedAt: timestamp("task_state_updated_at"),
8751
+ verifiedAt: timestamp("verified_at"),
8752
+
8753
+ // Slack alert (thread follow-up replies on approval/sync completion)
8754
+ slackMessageTs: text("slack_message_ts"),
8755
+ slackChannelId: text("slack_channel_id"),
8756
+
8757
+ // Snapshot for rollback on rate-exception rejection
8758
+ rollbackSnapshotVersion: integer("rollback_snapshot_version"),
8759
+
8760
+ createdAt: timestamp("created_at").notNull().defaultNow(),
8761
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
8762
+ },
8763
+ (table) => [
8764
+ index("mcc_requests_order_idx").on(table.mediaOrderId),
8765
+ index("mcc_requests_status_idx").on(table.status),
8766
+ index("mcc_requests_requested_at_idx").on(table.requestedAt),
8767
+ index("mcc_requests_order_status_idx").on(table.mediaOrderId, table.status),
8768
+ ],
8769
+ );
8770
+
8771
+ export type MidCampaignChangeRequest = typeof midCampaignChangeRequests.$inferSelect;
8772
+ export type InsertMidCampaignChangeRequest = typeof midCampaignChangeRequests.$inferInsert;
8773
+
8774
+ /**
8775
+ * One row per line-item operation inside a change request. This is the
8776
+ * finance report source — one row per field changed per line item.
8777
+ */
8778
+ export const midCampaignChangeOps = pgTable(
8779
+ "mid_campaign_change_ops",
8780
+ {
8781
+ id: uuid("id").primaryKey().defaultRandom(),
8782
+ changeRequestId: uuid("change_request_id")
8783
+ .notNull()
8784
+ .references(() => midCampaignChangeRequests.id, { onDelete: "cascade" }),
8785
+ opType: varchar("op_type").notNull(), // update | cancel | create
8786
+ lineItemId: varchar("line_item_id")
8787
+ .notNull()
8788
+ .references(() => mediaOrderLineItems.id),
8789
+ // For cancel+create pairs (date-shift across month boundary, product swap)
8790
+ supersedesLineItemId: varchar("supersedes_line_item_id").references(() => mediaOrderLineItems.id),
8791
+
8792
+ // Denormalized for reporting (so the report query is one table scan)
8793
+ periodLabel: text("period_label"), // "Aug 2026"
8794
+ productCode: text("product_code"),
8795
+ productName: text("product_name"),
8796
+ placement: text("placement"),
8797
+
8798
+ // The per-field diff — this is the atom finance sees
8799
+ fieldChanges: jsonb("field_changes")
8800
+ .notNull()
8801
+ .$type<Array<{ field: string; from: string | number | null; to: string | number | null }>>(),
8802
+
8803
+ // Per-op investment delta (sums to totalInvestmentDelta on the request)
8804
+ investmentDelta: numeric("investment_delta", { precision: 12, scale: 2 }).notNull().default("0"),
8805
+
8806
+ // Tracks which HubSpot action this op triggered when synced
8807
+ hubspotOp: varchar("hubspot_op"), // update | cancel | create | skipped
8808
+ hubspotLineItemIdBefore: text("hubspot_line_item_id_before"),
8809
+ hubspotLineItemIdAfter: text("hubspot_line_item_id_after"),
8810
+
8811
+ createdAt: timestamp("created_at").notNull().defaultNow(),
8812
+ },
8813
+ (table) => [
8814
+ index("mcc_ops_change_request_idx").on(table.changeRequestId),
8815
+ index("mcc_ops_line_item_idx").on(table.lineItemId),
8816
+ index("mcc_ops_period_idx").on(table.periodLabel),
8817
+ uniqueIndex("mcc_ops_change_request_line_item_unique").on(
8818
+ table.changeRequestId,
8819
+ table.lineItemId,
8820
+ ),
8821
+ ],
8822
+ );
8823
+
8824
+ export type MidCampaignChangeOp = typeof midCampaignChangeOps.$inferSelect;
8825
+ export type InsertMidCampaignChangeOp = typeof midCampaignChangeOps.$inferInsert;
8826
+
8827
+ /**
8828
+ * Join table: change requests ↔ rate exception requests. A single change
8829
+ * request may trigger N rate exceptions (one per line item that violates
8830
+ * rate rules). The change request blocks in `pending_rate_approval` until
8831
+ * every linked exception is decided; if any reject, the change rolls back.
8832
+ */
8833
+ export const midCampaignChangeRateExceptions = pgTable(
8834
+ "mid_campaign_change_rate_exceptions",
8835
+ {
8836
+ id: uuid("id").primaryKey().defaultRandom(),
8837
+ changeRequestId: uuid("change_request_id")
8838
+ .notNull()
8839
+ .references(() => midCampaignChangeRequests.id, { onDelete: "cascade" }),
8840
+ rateExceptionRequestId: uuid("rate_exception_request_id")
8841
+ .notNull()
8842
+ .references(() => rateExceptionRequests.id, { onDelete: "restrict" }),
8843
+ // Which op on the change request this exception corresponds to
8844
+ changeOpId: uuid("change_op_id").references(() => midCampaignChangeOps.id, { onDelete: "cascade" }),
8845
+ createdAt: timestamp("created_at").notNull().defaultNow(),
8846
+ },
8847
+ (table) => [
8848
+ index("mcc_rate_exc_change_request_idx").on(table.changeRequestId),
8849
+ index("mcc_rate_exc_exception_idx").on(table.rateExceptionRequestId),
8850
+ uniqueIndex("mcc_rate_exc_change_request_exception_unique").on(
8851
+ table.changeRequestId,
8852
+ table.rateExceptionRequestId,
8853
+ ),
8854
+ ],
8855
+ );
8856
+
8857
+ export type MidCampaignChangeRateException = typeof midCampaignChangeRateExceptions.$inferSelect;
8858
+ export type InsertMidCampaignChangeRateException = typeof midCampaignChangeRateExceptions.$inferInsert;
8859
+
8860
+ /**
8861
+ * Campaign Task templates for mid-campaign changes. Hybrid storage — config
8862
+ * in DB (queue, offset, priority, is_active, display_order) so admins can
8863
+ * hot-edit without deploys; predicate logic stays in a code registry
8864
+ * (mid-campaign-template-predicates.ts) keyed by `predicateKey`.
8865
+ *
8866
+ * CI validator asserts every row's `queue` value is in the live HubSpot
8867
+ * Campaign Task `queue` enum. Prevents the "wrong queue ships to HubSpot and
8868
+ * silently fails enum validation" bug.
8869
+ */
8870
+ export const midCampaignTaskTemplates = pgTable(
8871
+ "mid_campaign_task_templates",
8872
+ {
8873
+ // Stable key (e.g. "review_change_request", "verify_billing_impact").
8874
+ // Used as the templateId in the task plan and for deduplication.
8875
+ id: varchar("id", { length: 64 }).primaryKey(),
8876
+ // Human-readable name — sent as campaign_task_name to HubSpot.
8877
+ name: text("name").notNull(),
8878
+ // Portal Campaign Task queue enum value. CI validator enforces.
8879
+ queue: varchar("queue", { length: 40 }).notNull(),
8880
+ // Days from applied-at when the task is due. Negative = before apply.
8881
+ offsetDays: integer("offset_days").notNull().default(0),
8882
+ // Written to HubSpot priority: high | medium | low.
8883
+ priority: varchar("priority", { length: 16 }).notNull().default("medium"),
8884
+ // Key into the PREDICATES registry in code. Must resolve at runtime.
8885
+ // Valid: always, has_financial_change, has_digital_update_or_cancel,
8886
+ // has_digital_create, has_print_change, touches_dates
8887
+ predicateKey: varchar("predicate_key", { length: 64 }).notNull(),
8888
+ // Inactive templates are skipped by buildMidCampaignChangeTaskPlan.
8889
+ isActive: boolean("is_active").notNull().default(true),
8890
+ // Display/evaluation order. Lower = higher up in the Compass task list.
8891
+ displayOrder: integer("display_order").notNull().default(0),
8892
+ // Optional human description for the admin UI.
8893
+ description: text("description"),
8894
+ // Audit — who last edited and when.
8895
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
8896
+ updatedBy: varchar("updated_by"),
8897
+ createdAt: timestamp("created_at").notNull().defaultNow(),
8898
+ },
8899
+ (table) => [
8900
+ index("mid_campaign_task_templates_active_idx").on(table.isActive),
8901
+ index("mid_campaign_task_templates_order_idx").on(table.displayOrder),
8902
+ ],
8903
+ );
8904
+
8905
+ export type MidCampaignTaskTemplate = typeof midCampaignTaskTemplates.$inferSelect;
8906
+ export type InsertMidCampaignTaskTemplate = typeof midCampaignTaskTemplates.$inferInsert;
8907
+
8670
8908
  /** Shape of the ad_activity JSONB column in business_intel_cache */
8671
8909
  export interface BusinessIntelAdActivity {
8672
8910
  google?: { active: boolean; adCount: number; lastSeen?: string };
@@ -18,6 +18,9 @@ export const ACTIVITY_EVENT_TYPES = [
18
18
  "sla_milestone_at_risk",
19
19
  "sla_milestone_breached",
20
20
  "sla_evaluated",
21
+ "mid_campaign_change_applied",
22
+ "mid_campaign_task_state_changed",
23
+ "mid_campaign_change_verified",
21
24
  ] as const;
22
25
 
23
26
  export type ActivityEventType = (typeof ACTIVITY_EVENT_TYPES)[number];
@@ -46,6 +49,7 @@ export const PENDING_ACTION_TYPES = [
46
49
  "fulfillment_blocked",
47
50
  "rate_exception_pending",
48
51
  "sla_hold_active",
52
+ "mid_campaign_tasks_pending",
49
53
  ] as const;
50
54
 
51
55
  export type PendingActionType = (typeof PENDING_ACTION_TYPES)[number];
@@ -94,4 +98,8 @@ export const PENDING_ACTION_RESOLUTION_MAP: Record<
94
98
  resolvedByEventType: "sla_hold_cleared",
95
99
  resolvedByStatuses: ["clear"],
96
100
  },
101
+ mid_campaign_tasks_pending: {
102
+ resolvedByEventType: "mid_campaign_change_verified",
103
+ resolvedByStatuses: ["verified"],
104
+ },
97
105
  };