@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.
package/dist/schema.js CHANGED
@@ -6168,7 +6168,15 @@ export const semCampaigns = pgTable("sem_campaigns", {
6168
6168
  index("sem_campaigns_status_idx").on(table.status),
6169
6169
  ]);
6170
6170
  // =============================================================================
6171
- // COMPASS EVENT OUTBOX (dead-letter queue for fn-flux webhook events)
6171
+ // COMPASS EVENT OUTBOX (durable outbox for cross-system writes)
6172
+ //
6173
+ // Generic claim/retry/DLQ pattern used by multiple producers. Events are
6174
+ // namespaced by `eventType` prefix so multiple workers can drain the same
6175
+ // table without contention:
6176
+ // • "flux.*" — fn-flux webhook delivery to external consumers
6177
+ // • "hubspot.*" — HubSpot ticket / Campaign Task writes from fn-legacy
6178
+ // (drained by fn-v2 hubspot-outbox-worker)
6179
+ // Workers filter on `event_type LIKE '{namespace}.%'` in their claim query.
6172
6180
  // =============================================================================
6173
6181
  export const compassEventOutboxStatusEnum = pgEnum("compass_event_outbox_status", [
6174
6182
  "pending",
@@ -6277,4 +6285,169 @@ export const userMilestones = pgTable("user_milestones", {
6277
6285
  index("user_milestones_user_id_idx").on(table.userId),
6278
6286
  unique("user_milestones_user_key_unique").on(table.userId, table.milestoneKey),
6279
6287
  ]);
6288
+ // ─── Mid-Campaign Change Management ────────────────────────────────────────
6289
+ //
6290
+ // Finance-grade in-place edits to live media orders. Distinct from
6291
+ // AmendOrderWizard (which takes orders active→draft→sent). A mid-campaign
6292
+ // change keeps the order `active`, touches a surgical set of line items,
6293
+ // and routes through the existing rate-exception-engine if it would alter
6294
+ // rate rules or margin. HubSpot push is the canonical output — HubSpot's
6295
+ // own workflow pushes deltas to Naviga.
6296
+ //
6297
+ // State machine:
6298
+ // draft → (rate violation?) ─┬─ pending_rate_approval ─┬─ applied → hubspot_syncing → hubspot_synced
6299
+ // │ └─ rejected (terminal, line items rolled back)
6300
+ // └─ applied → hubspot_syncing ─┬─ hubspot_synced
6301
+ // └─ hubspot_failed (retryable)
6302
+ export const midCampaignChangeRequests = pgTable("mid_campaign_change_requests", {
6303
+ id: uuid("id").primaryKey().defaultRandom(),
6304
+ mediaOrderId: varchar("media_order_id")
6305
+ .notNull()
6306
+ .references(() => mediaOrders.id),
6307
+ reason: amendmentReasonEnum("reason").notNull(),
6308
+ notes: text("notes").notNull(),
6309
+ requestedByUserId: varchar("requested_by_user_id").notNull(),
6310
+ requestedByEmail: varchar("requested_by_email"),
6311
+ requestedByName: varchar("requested_by_name"),
6312
+ requestedAt: timestamp("requested_at").notNull().defaultNow(),
6313
+ appliedAt: timestamp("applied_at"),
6314
+ status: varchar("status").notNull().default("draft"),
6315
+ // draft | pending_rate_approval | applied | rejected | hubspot_syncing | hubspot_synced | hubspot_failed | verified
6316
+ // `verified` is terminal — set when taskStateJson.allComplete && hubspotTicketStage is terminal.
6317
+ // Denormalized summary (so finance reports never join into ops)
6318
+ totalInvestmentDelta: numeric("total_investment_delta", { precision: 12, scale: 2 }).notNull().default("0"),
6319
+ affectedLineItemCount: integer("affected_line_item_count").notNull().default(0),
6320
+ marginDeltaPercent: numeric("margin_delta_percent", { precision: 7, scale: 4 }),
6321
+ marginDeltaDollars: numeric("margin_delta_dollars", { precision: 12, scale: 2 }),
6322
+ // Approval state (nullable — only set when rate engine returned pending_approval for any op)
6323
+ rateApprovalCompletedAt: timestamp("rate_approval_completed_at"),
6324
+ rejectionReason: text("rejection_reason"),
6325
+ // HubSpot ticket (fulfillment pipeline, CHANGE_REQUIRED stage)
6326
+ hubspotTicketId: text("hubspot_ticket_id"),
6327
+ hubspotCampaignTaskIds: jsonb("hubspot_campaign_task_ids").$type().default(sql `'[]'::jsonb`),
6328
+ // HubSpot line-item sync (via fn-v2 hubspot-order-sync)
6329
+ hubspotSyncRunId: text("hubspot_sync_run_id"),
6330
+ hubspotSyncStartedAt: timestamp("hubspot_sync_started_at"),
6331
+ hubspotSyncCompletedAt: timestamp("hubspot_sync_completed_at"),
6332
+ hubspotSyncError: text("hubspot_sync_error"),
6333
+ // Closed-loop projection from HubSpot Campaign Task webhooks
6334
+ // (updated by handleCampaignTaskChange in hubspot-mid-campaign-writeback.ts)
6335
+ hubspotTicketStage: varchar("hubspot_ticket_stage", { length: 40 }),
6336
+ // Current ticket stage as seen in the most recent webhook delivery.
6337
+ // Values: CHANGE_REQUIRED | BILLING_CHANGE_REQUIRED | CANCELLED | COMPLETED | etc.
6338
+ taskStateJson: jsonb("task_state_json").$type(),
6339
+ taskStateUpdatedAt: timestamp("task_state_updated_at"),
6340
+ verifiedAt: timestamp("verified_at"),
6341
+ // Slack alert (thread follow-up replies on approval/sync completion)
6342
+ slackMessageTs: text("slack_message_ts"),
6343
+ slackChannelId: text("slack_channel_id"),
6344
+ // Snapshot for rollback on rate-exception rejection
6345
+ rollbackSnapshotVersion: integer("rollback_snapshot_version"),
6346
+ createdAt: timestamp("created_at").notNull().defaultNow(),
6347
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
6348
+ }, (table) => [
6349
+ index("mcc_requests_order_idx").on(table.mediaOrderId),
6350
+ index("mcc_requests_status_idx").on(table.status),
6351
+ index("mcc_requests_requested_at_idx").on(table.requestedAt),
6352
+ index("mcc_requests_order_status_idx").on(table.mediaOrderId, table.status),
6353
+ ]);
6354
+ /**
6355
+ * One row per line-item operation inside a change request. This is the
6356
+ * finance report source — one row per field changed per line item.
6357
+ */
6358
+ export const midCampaignChangeOps = pgTable("mid_campaign_change_ops", {
6359
+ id: uuid("id").primaryKey().defaultRandom(),
6360
+ changeRequestId: uuid("change_request_id")
6361
+ .notNull()
6362
+ .references(() => midCampaignChangeRequests.id, { onDelete: "cascade" }),
6363
+ opType: varchar("op_type").notNull(), // update | cancel | create
6364
+ lineItemId: varchar("line_item_id")
6365
+ .notNull()
6366
+ .references(() => mediaOrderLineItems.id),
6367
+ // For cancel+create pairs (date-shift across month boundary, product swap)
6368
+ supersedesLineItemId: varchar("supersedes_line_item_id").references(() => mediaOrderLineItems.id),
6369
+ // Denormalized for reporting (so the report query is one table scan)
6370
+ periodLabel: text("period_label"), // "Aug 2026"
6371
+ productCode: text("product_code"),
6372
+ productName: text("product_name"),
6373
+ placement: text("placement"),
6374
+ // The per-field diff — this is the atom finance sees
6375
+ fieldChanges: jsonb("field_changes")
6376
+ .notNull()
6377
+ .$type(),
6378
+ // Per-op investment delta (sums to totalInvestmentDelta on the request)
6379
+ investmentDelta: numeric("investment_delta", { precision: 12, scale: 2 }).notNull().default("0"),
6380
+ // Tracks which HubSpot action this op triggered when synced
6381
+ hubspotOp: varchar("hubspot_op"), // update | cancel | create | skipped
6382
+ hubspotLineItemIdBefore: text("hubspot_line_item_id_before"),
6383
+ hubspotLineItemIdAfter: text("hubspot_line_item_id_after"),
6384
+ createdAt: timestamp("created_at").notNull().defaultNow(),
6385
+ }, (table) => [
6386
+ index("mcc_ops_change_request_idx").on(table.changeRequestId),
6387
+ index("mcc_ops_line_item_idx").on(table.lineItemId),
6388
+ index("mcc_ops_period_idx").on(table.periodLabel),
6389
+ uniqueIndex("mcc_ops_change_request_line_item_unique").on(table.changeRequestId, table.lineItemId),
6390
+ ]);
6391
+ /**
6392
+ * Join table: change requests ↔ rate exception requests. A single change
6393
+ * request may trigger N rate exceptions (one per line item that violates
6394
+ * rate rules). The change request blocks in `pending_rate_approval` until
6395
+ * every linked exception is decided; if any reject, the change rolls back.
6396
+ */
6397
+ export const midCampaignChangeRateExceptions = pgTable("mid_campaign_change_rate_exceptions", {
6398
+ id: uuid("id").primaryKey().defaultRandom(),
6399
+ changeRequestId: uuid("change_request_id")
6400
+ .notNull()
6401
+ .references(() => midCampaignChangeRequests.id, { onDelete: "cascade" }),
6402
+ rateExceptionRequestId: uuid("rate_exception_request_id")
6403
+ .notNull()
6404
+ .references(() => rateExceptionRequests.id, { onDelete: "restrict" }),
6405
+ // Which op on the change request this exception corresponds to
6406
+ changeOpId: uuid("change_op_id").references(() => midCampaignChangeOps.id, { onDelete: "cascade" }),
6407
+ createdAt: timestamp("created_at").notNull().defaultNow(),
6408
+ }, (table) => [
6409
+ index("mcc_rate_exc_change_request_idx").on(table.changeRequestId),
6410
+ index("mcc_rate_exc_exception_idx").on(table.rateExceptionRequestId),
6411
+ uniqueIndex("mcc_rate_exc_change_request_exception_unique").on(table.changeRequestId, table.rateExceptionRequestId),
6412
+ ]);
6413
+ /**
6414
+ * Campaign Task templates for mid-campaign changes. Hybrid storage — config
6415
+ * in DB (queue, offset, priority, is_active, display_order) so admins can
6416
+ * hot-edit without deploys; predicate logic stays in a code registry
6417
+ * (mid-campaign-template-predicates.ts) keyed by `predicateKey`.
6418
+ *
6419
+ * CI validator asserts every row's `queue` value is in the live HubSpot
6420
+ * Campaign Task `queue` enum. Prevents the "wrong queue ships to HubSpot and
6421
+ * silently fails enum validation" bug.
6422
+ */
6423
+ export const midCampaignTaskTemplates = pgTable("mid_campaign_task_templates", {
6424
+ // Stable key (e.g. "review_change_request", "verify_billing_impact").
6425
+ // Used as the templateId in the task plan and for deduplication.
6426
+ id: varchar("id", { length: 64 }).primaryKey(),
6427
+ // Human-readable name — sent as campaign_task_name to HubSpot.
6428
+ name: text("name").notNull(),
6429
+ // Portal Campaign Task queue enum value. CI validator enforces.
6430
+ queue: varchar("queue", { length: 40 }).notNull(),
6431
+ // Days from applied-at when the task is due. Negative = before apply.
6432
+ offsetDays: integer("offset_days").notNull().default(0),
6433
+ // Written to HubSpot priority: high | medium | low.
6434
+ priority: varchar("priority", { length: 16 }).notNull().default("medium"),
6435
+ // Key into the PREDICATES registry in code. Must resolve at runtime.
6436
+ // Valid: always, has_financial_change, has_digital_update_or_cancel,
6437
+ // has_digital_create, has_print_change, touches_dates
6438
+ predicateKey: varchar("predicate_key", { length: 64 }).notNull(),
6439
+ // Inactive templates are skipped by buildMidCampaignChangeTaskPlan.
6440
+ isActive: boolean("is_active").notNull().default(true),
6441
+ // Display/evaluation order. Lower = higher up in the Compass task list.
6442
+ displayOrder: integer("display_order").notNull().default(0),
6443
+ // Optional human description for the admin UI.
6444
+ description: text("description"),
6445
+ // Audit — who last edited and when.
6446
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
6447
+ updatedBy: varchar("updated_by"),
6448
+ createdAt: timestamp("created_at").notNull().defaultNow(),
6449
+ }, (table) => [
6450
+ index("mid_campaign_task_templates_active_idx").on(table.isActive),
6451
+ index("mid_campaign_task_templates_order_idx").on(table.displayOrder),
6452
+ ]);
6280
6453
  //# sourceMappingURL=schema.js.map