@foundrynorth/flux-schema 1.17.4 → 1.18.0

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
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * CRITICAL: No stubs. All tables are real and will be created on migration.
8
8
  */
9
- import { pgTable, varchar, text, boolean, timestamp, date, jsonb, integer, bigint, numeric, uuid, real, serial, time, pgEnum, uniqueIndex, index, customType, } from "drizzle-orm/pg-core";
9
+ import { pgTable, varchar, text, boolean, timestamp, date, jsonb, integer, bigint, bigserial, numeric, uuid, real, serial, time, pgEnum, uniqueIndex, index, customType, } from "drizzle-orm/pg-core";
10
10
  import { relations, sql } from "drizzle-orm";
11
11
  /**
12
12
  * Custom pgvector type for embedding columns.
@@ -2571,6 +2571,20 @@ export const fluxFulfillmentTickets = pgTable("flux_fulfillment_tickets", {
2571
2571
  dealOrderTotal: numeric("deal_order_total", { precision: 12, scale: 2 }),
2572
2572
  /** Compass order status (e.g., "signed", "draft", "amended") */
2573
2573
  compassOrderStatus: text("compass_order_status"),
2574
+ /** Human-readable product family name derived from Compass context */
2575
+ familyName: text("family_name"),
2576
+ /** Requested creative sizes from Compass / HubSpot line item context */
2577
+ adSizes: jsonb("ad_sizes").$type(),
2578
+ /** Product / offer description for stronger creative briefs */
2579
+ productDescription: text("product_description"),
2580
+ /** Product KPIs / use cases carried from upstream when available */
2581
+ productKpis: jsonb("product_kpis").$type(),
2582
+ /** Creative refresh cadence inherited from upstream order defaults */
2583
+ creativeRefreshCadence: text("creative_refresh_cadence"),
2584
+ /** Company domain used for landing page and brand context */
2585
+ companyDomain: text("company_domain"),
2586
+ /** Company industry used for briefing and brand alignment */
2587
+ companyIndustry: text("company_industry"),
2574
2588
  // -- Native HubSpot SLA --
2575
2589
  /** HubSpot SLA status for first response (Overdue, Due Soon, Active SLA, etc.) */
2576
2590
  hubspotSlaFirstResponse: varchar("hubspot_sla_first_response"),
@@ -4047,26 +4061,46 @@ export const fluxSlaAuditLog = pgTable("flux_sla_audit_log", {
4047
4061
  // Agreement Tracking
4048
4062
  // ============================================================================
4049
4063
  /**
4050
- * Agreement tier: contract (annual commitment), io (per-order), approval (lightweight)
4064
+ * Agreement tier discriminator. Contracts are the umbrella commitment,
4065
+ * IOs are tactical executions (usually under a parent contract), NDAs
4066
+ * are non-disclosure documents with no commitment.
4051
4067
  */
4052
- export const fluxAgreementTierEnum = pgEnum("flux_agreement_tier", [
4068
+ export const fluxAgreementTier = pgEnum("flux_agreement_tier", [
4053
4069
  "contract",
4054
4070
  "io",
4055
- "approval",
4071
+ "nda",
4056
4072
  ]);
4057
4073
  /**
4058
- * Agreement lifecycle status
4074
+ * Signing workflow state machine (pre-execution).
4059
4075
  */
4060
- export const fluxAgreementStatusEnum = pgEnum("flux_agreement_status", [
4076
+ export const fluxSigningStatus = pgEnum("flux_signing_status", [
4061
4077
  "draft",
4062
4078
  "pending_internal",
4063
4079
  "pending_client",
4064
4080
  "pending_countersign",
4065
- "negotiation",
4081
+ "signed",
4082
+ "voided",
4083
+ ]);
4084
+ /**
4085
+ * Post-signing lifecycle state machine. Separate from signing_status so
4086
+ * the two concerns don't interleave in a single enum.
4087
+ */
4088
+ export const fluxLifecycleStatus = pgEnum("flux_lifecycle_status", [
4089
+ "pre_active",
4066
4090
  "active",
4067
- "amended",
4091
+ "expiring_soon",
4068
4092
  "expired",
4069
- "voided",
4093
+ "renewed",
4094
+ "terminated_early",
4095
+ ]);
4096
+ /**
4097
+ * Relationship between a version chain entry and its predecessor.
4098
+ * NULL = original (not a revision). 'amendment' = revision of the same
4099
+ * commitment period. 'renewal' = sequential new commitment period.
4100
+ */
4101
+ export const fluxRelationType = pgEnum("flux_relation_type", [
4102
+ "amendment",
4103
+ "renewal",
4070
4104
  ]);
4071
4105
  /**
4072
4106
  * Signer role in the signing workflow
@@ -4086,29 +4120,69 @@ export const fluxSignerStatusEnum = pgEnum("flux_signer_status", [
4086
4120
  "delegated",
4087
4121
  ]);
4088
4122
  /**
4089
- * Agreements — tracks contracts, IOs, and approvals across their lifecycle.
4090
- * Links to projects, deals, fulfillment tickets, and Compass entities.
4123
+ * Agreements — unified table for contracts, insertion orders, and NDAs.
4124
+ * Discriminated by `tier`. fn-flux is the system of record for this
4125
+ * entity; Compass keeps shadow rows in its own `contracts` table via
4126
+ * the `fluxAgreementId` back-reference.
4127
+ *
4128
+ * See docs/superpowers/specs/2026-04-10-contract-agreement-p0-design.md
4129
+ * for the full design rationale.
4091
4130
  */
4092
4131
  export const fluxAgreements = pgTable("flux_agreements", {
4132
+ // ─── Identity + discriminator ─────────────────────────────────────
4093
4133
  id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
4094
- tier: fluxAgreementTierEnum("tier").notNull(),
4095
- status: fluxAgreementStatusEnum("status").notNull().default("draft"),
4134
+ tier: fluxAgreementTier("tier").notNull(),
4135
+ version: integer("version").notNull().default(1),
4136
+ previousVersionId: text("previous_version_id").references(() => fluxAgreements.id),
4137
+ relationType: fluxRelationType("relation_type"),
4138
+ // ─── Two status state machines ─────────────────────────────────────
4139
+ signingStatus: fluxSigningStatus("signing_status")
4140
+ .notNull()
4141
+ .default("draft"),
4142
+ lifecycleStatus: fluxLifecycleStatus("lifecycle_status"),
4143
+ // ─── Counterparty (canonical: hubspot_company_id) ─────────────────
4144
+ hubspotCompanyId: text("hubspot_company_id"),
4145
+ counterpartyName: text("counterparty_name"),
4096
4146
  projectId: text("project_id").references(() => fluxProjects.id),
4097
- dealId: text("deal_id").references(() => fluxDealPipeline.id),
4098
4147
  hubspotDealId: text("hubspot_deal_id"),
4099
- fulfillmentTicketId: text("fulfillment_ticket_id").references(() => fluxFulfillmentTickets.id),
4148
+ // ─── Hierarchy ─────────────────────────────────────────────────────
4149
+ parentAgreementId: text("parent_agreement_id").references(() => fluxAgreements.id),
4150
+ pendingCompassContractIdHint: text("pending_compass_contract_id_hint"),
4151
+ standaloneConfirmedAt: timestamp("standalone_confirmed_at", {
4152
+ withTimezone: true,
4153
+ }),
4154
+ standaloneConfirmedByUserId: text("standalone_confirmed_by_user_id").references(() => fluxUsers.id),
4155
+ // ─── Cross-system identity ─────────────────────────────────────────
4100
4156
  compassContractId: text("compass_contract_id"),
4101
4157
  compassOrderId: text("compass_order_id"),
4158
+ fulfillmentTicketId: text("fulfillment_ticket_id").references(() => fluxFulfillmentTickets.id),
4159
+ // ─── Terms ─────────────────────────────────────────────────────────
4102
4160
  title: text("title").notNull(),
4103
4161
  description: text("description"),
4104
- commitmentAmount: numeric("commitment_amount", { precision: 14, scale: 2 }),
4105
- tierCode: text("tier_code"),
4106
- currency: text("currency").default("USD"),
4107
4162
  effectiveDate: date("effective_date"),
4108
4163
  expirationDate: date("expiration_date"),
4109
- version: integer("version").notNull().default(1),
4110
- previousVersionId: text("previous_version_id").references(() => fluxAgreements.id),
4111
- amendmentReason: text("amendment_reason"),
4164
+ currency: text("currency").notNull().default("USD"),
4165
+ entitlementTierCode: text("entitlement_tier_code"),
4166
+ // ─── Commitment + 3-stage drawdown (contract-tier only) ───────────
4167
+ commitmentAmount: numeric("commitment_amount", { precision: 14, scale: 2 }),
4168
+ committedAmount: numeric("committed_amount", { precision: 14, scale: 2 }),
4169
+ allocatedAmount: numeric("allocated_amount", { precision: 14, scale: 2 }),
4170
+ billedAmount: numeric("billed_amount", { precision: 14, scale: 2 }),
4171
+ remainingAmount: numeric("remaining_amount", { precision: 14, scale: 2 }),
4172
+ // ─── Margin (contract-tier only) ──────────────────────────────────
4173
+ projectedMarginPct: numeric("projected_margin_pct", { precision: 5, scale: 2 }),
4174
+ projectedMarginDollars: numeric("projected_margin_dollars", { precision: 14, scale: 2 }),
4175
+ actualMarginPct: numeric("actual_margin_pct", { precision: 5, scale: 2 }),
4176
+ actualMarginDollars: numeric("actual_margin_dollars", { precision: 14, scale: 2 }),
4177
+ // ─── Value-add, rate card, product mix (contract-tier only) ───────
4178
+ valueAddTotal: numeric("value_add_total", { precision: 14, scale: 2 }),
4179
+ rateCard: jsonb("rate_card"),
4180
+ productAllocations: jsonb("product_allocations"),
4181
+ // ─── Renewal terms (contract-tier only) ───────────────────────────
4182
+ autorenew: boolean("autorenew").notNull().default(false),
4183
+ renewalTermMonths: integer("renewal_term_months"),
4184
+ noticeDeadline: date("notice_deadline"),
4185
+ // ─── E-sign + document (all tiers) ────────────────────────────────
4112
4186
  documentUrl: text("document_url"),
4113
4187
  documentFingerprint: text("document_fingerprint"),
4114
4188
  documentFileName: text("document_file_name"),
@@ -4116,31 +4190,19 @@ export const fluxAgreements = pgTable("flux_agreements", {
4116
4190
  esignEnvelopeId: text("esign_envelope_id"),
4117
4191
  esignCallbackUrl: text("esign_callback_url"),
4118
4192
  esignStatus: text("esign_status"),
4119
- hubspotPropertySyncedAt: timestamp("hubspot_property_synced_at", {
4120
- withTimezone: true,
4121
- }),
4122
- slackChannelId: text("slack_channel_id"),
4123
- slackMessageTs: text("slack_message_ts"),
4193
+ // ─── Health / risk / compliance (feeds P4) ────────────────────────
4124
4194
  healthScore: integer("health_score"),
4125
- complianceStatus: text("compliance_status"),
4126
- riskIndicators: jsonb("risk_indicators").default([]),
4127
- parentContractId: text("parent_contract_id").references(() => fluxMasterContracts.id),
4128
- metadata: jsonb("metadata").default({}),
4129
- // --- Workspace columns (graduated from metadata JSONB) ---
4130
- priority: text("priority").default("medium"),
4131
- workspaceStatus: text("workspace_status"),
4132
- counterpartyName: text("counterparty_name"),
4133
- currentModule: text("current_module"),
4134
- receivedAt: timestamp("received_at", { withTimezone: true }),
4135
- requestedBy: text("requested_by"),
4136
- ownerName: text("owner_name"),
4137
- needsBy: text("needs_by"),
4138
- autorenew: boolean("autorenew"),
4139
- renewalTermMonths: integer("renewal_term_months"),
4140
- noticeDeadline: text("notice_deadline"),
4141
- packageType: jsonb("package_type").default([]),
4142
- tags: jsonb("tags").default([]),
4143
4195
  riskScore: integer("risk_score"),
4196
+ complianceStatus: text("compliance_status"),
4197
+ riskIndicators: jsonb("risk_indicators").notNull().default([]),
4198
+ // ─── Operational ──────────────────────────────────────────────────
4199
+ priority: text("priority").notNull().default("medium"),
4200
+ slackChannelId: text("slack_channel_id"),
4201
+ slackMessageTs: text("slack_message_ts"),
4202
+ tags: jsonb("tags").notNull().default([]),
4203
+ amendmentReason: text("amendment_reason"),
4204
+ metadata: jsonb("metadata").notNull().default({}),
4205
+ // ─── Audit ────────────────────────────────────────────────────────
4144
4206
  createdById: text("created_by_id").references(() => fluxUsers.id),
4145
4207
  createdAt: timestamp("created_at", { withTimezone: true })
4146
4208
  .notNull()
@@ -4148,25 +4210,52 @@ export const fluxAgreements = pgTable("flux_agreements", {
4148
4210
  updatedAt: timestamp("updated_at", { withTimezone: true })
4149
4211
  .notNull()
4150
4212
  .defaultNow(),
4213
+ hubspotPropertySyncedAt: timestamp("hubspot_property_synced_at", {
4214
+ withTimezone: true,
4215
+ }),
4151
4216
  }, (table) => ({
4152
- projectIdx: index("flux_agreements_project_idx").on(table.projectId),
4153
- dealIdx: index("flux_agreements_deal_idx").on(table.dealId),
4154
- fulfillmentTicketIdx: index("flux_agreements_fulfillment_ticket_idx").on(table.fulfillmentTicketId),
4155
- workspaceStatusIdx: index("flux_agreements_workspace_status_idx").on(table.workspaceStatus),
4156
- priorityIdx: index("flux_agreements_priority_idx").on(table.priority),
4157
- needsByIdx: index("flux_agreements_needs_by_idx").on(table.needsBy),
4158
- counterpartyNameIdx: index("flux_agreements_counterparty_name_idx").on(table.counterpartyName),
4159
- ownerNameIdx: index("flux_agreements_owner_name_idx").on(table.ownerName),
4217
+ idxTierLifecycle: index("idx_flux_agreements_tier_lifecycle")
4218
+ .on(table.tier, table.lifecycleStatus)
4219
+ .where(sql `${table.lifecycleStatus} IS NOT NULL`),
4220
+ idxHubspotCompany: index("idx_flux_agreements_hubspot_company")
4221
+ .on(table.hubspotCompanyId)
4222
+ .where(sql `${table.hubspotCompanyId} IS NOT NULL`),
4223
+ idxParent: index("idx_flux_agreements_parent")
4224
+ .on(table.parentAgreementId)
4225
+ .where(sql `${table.parentAgreementId} IS NOT NULL`),
4226
+ idxCompassContract: index("idx_flux_agreements_compass_contract")
4227
+ .on(table.compassContractId)
4228
+ .where(sql `${table.compassContractId} IS NOT NULL`),
4229
+ idxCompassOrder: index("idx_flux_agreements_compass_order")
4230
+ .on(table.compassOrderId)
4231
+ .where(sql `${table.compassOrderId} IS NOT NULL`),
4232
+ idxSigningPending: index("idx_flux_agreements_signing_pending")
4233
+ .on(table.signingStatus)
4234
+ .where(sql `${table.signingStatus} IN ('pending_internal', 'pending_client', 'pending_countersign')`),
4235
+ idxExpiring: index("idx_flux_agreements_expiring")
4236
+ .on(table.expirationDate)
4237
+ .where(sql `${table.lifecycleStatus} IN ('active', 'expiring_soon')`),
4238
+ idxVersionChain: index("idx_flux_agreements_version_chain")
4239
+ .on(table.previousVersionId)
4240
+ .where(sql `${table.previousVersionId} IS NOT NULL`),
4241
+ idxUnlinkedIos: index("idx_flux_agreements_unlinked_ios")
4242
+ .on(table.tier, table.signingStatus, table.createdAt)
4243
+ .where(sql `${table.tier} = 'io' AND ${table.parentAgreementId} IS NULL AND ${table.standaloneConfirmedAt} IS NULL`),
4244
+ idxPendingHint: index("idx_flux_agreements_pending_hint")
4245
+ .on(table.pendingCompassContractIdHint)
4246
+ .where(sql `${table.pendingCompassContractIdHint} IS NOT NULL`),
4247
+ uqCompassContract: uniqueIndex("uq_flux_agreements_compass_contract")
4248
+ .on(table.compassContractId)
4249
+ .where(sql `${table.compassContractId} IS NOT NULL`),
4250
+ uqCompassOrder: uniqueIndex("uq_flux_agreements_compass_order")
4251
+ .on(table.compassOrderId)
4252
+ .where(sql `${table.compassOrderId} IS NOT NULL`),
4160
4253
  }));
4161
4254
  export const fluxAgreementsRelations = relations(fluxAgreements, ({ one, many }) => ({
4162
4255
  project: one(fluxProjects, {
4163
4256
  fields: [fluxAgreements.projectId],
4164
4257
  references: [fluxProjects.id],
4165
4258
  }),
4166
- deal: one(fluxDealPipeline, {
4167
- fields: [fluxAgreements.dealId],
4168
- references: [fluxDealPipeline.id],
4169
- }),
4170
4259
  fulfillmentTicket: one(fluxFulfillmentTickets, {
4171
4260
  fields: [fluxAgreements.fulfillmentTicketId],
4172
4261
  references: [fluxFulfillmentTickets.id],
@@ -4174,15 +4263,24 @@ export const fluxAgreementsRelations = relations(fluxAgreements, ({ one, many })
4174
4263
  previousVersion: one(fluxAgreements, {
4175
4264
  fields: [fluxAgreements.previousVersionId],
4176
4265
  references: [fluxAgreements.id],
4266
+ relationName: "versionChain",
4267
+ }),
4268
+ parentAgreement: one(fluxAgreements, {
4269
+ fields: [fluxAgreements.parentAgreementId],
4270
+ references: [fluxAgreements.id],
4271
+ relationName: "parentChild",
4177
4272
  }),
4178
- parentContract: one(fluxMasterContracts, {
4179
- fields: [fluxAgreements.parentContractId],
4180
- references: [fluxMasterContracts.id],
4273
+ childAgreements: many(fluxAgreements, {
4274
+ relationName: "parentChild",
4181
4275
  }),
4182
4276
  createdBy: one(fluxUsers, {
4183
4277
  fields: [fluxAgreements.createdById],
4184
4278
  references: [fluxUsers.id],
4185
4279
  }),
4280
+ standaloneConfirmedBy: one(fluxUsers, {
4281
+ fields: [fluxAgreements.standaloneConfirmedByUserId],
4282
+ references: [fluxUsers.id],
4283
+ }),
4186
4284
  signers: many(fluxAgreementSigners),
4187
4285
  activity: many(fluxAgreementActivity),
4188
4286
  comments: many(fluxAgreementComments),
@@ -4191,6 +4289,7 @@ export const fluxAgreementsRelations = relations(fluxAgreements, ({ one, many })
4191
4289
  approvalRuns: many(fluxAgreementApprovalRuns),
4192
4290
  esignRuns: many(fluxAgreementEsignRuns),
4193
4291
  lifecycleEvents: many(fluxAgreementLifecycleEvents),
4292
+ agreementEvents: many(fluxAgreementEvents),
4194
4293
  }));
4195
4294
  /**
4196
4295
  * Agreement signers — tracks each signer in the signing workflow.
@@ -4276,7 +4375,7 @@ export const fluxAgreementActivityRelations = relations(fluxAgreementActivity, (
4276
4375
  */
4277
4376
  export const fluxEsignConfig = pgTable("flux_esign_config", {
4278
4377
  id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
4279
- tier: fluxAgreementTierEnum("tier").notNull().unique(),
4378
+ tier: fluxAgreementTier("tier").notNull().unique(),
4280
4379
  firmaTemplateId: text("firma_template_id"),
4281
4380
  defaultVpSignerIds: text("default_vp_signer_ids")
4282
4381
  .array()
@@ -4772,118 +4871,114 @@ export const fluxSimplifiPacing = pgTable("flux_simplifi_pacing", {
4772
4871
  endDateIdx: index("flux_simplifi_pacing_end_date_idx").on(table.endDate),
4773
4872
  }));
4774
4873
  // ============================================================================
4775
- // CONTRACT VAULTMaster Contracts, Versioning, Search, Pacing
4874
+ // AGREEMENT SUPPORTING TABLES Events, Inbound Dedupe, Versions, Chunks
4776
4875
  // ============================================================================
4777
4876
  /**
4778
- * Master contract type classification
4779
- */
4780
- export const fluxMasterContractTypeEnum = pgEnum("flux_master_contract_type", [
4781
- "msa",
4782
- "nda",
4783
- "sow",
4784
- "vendor",
4785
- "partnership",
4786
- "other",
4787
- ]);
4788
- /**
4789
- * Master contract lifecycle status — simpler than the 8-status agreement machine
4877
+ * flux_agreement_events Internal audit log of every state change on a
4878
+ * flux_agreements row. Written within the same transaction as the
4879
+ * mutation via the agreement repository. Consumed by P4 risk scoring
4880
+ * and P5 alerting.
4790
4881
  */
4791
- export const fluxMasterContractStatusEnum = pgEnum("flux_master_contract_status", ["draft", "active", "expiring", "expired", "renewed", "terminated"]);
4882
+ export const fluxAgreementEvents = pgTable("flux_agreement_events", {
4883
+ id: bigserial("id", { mode: "number" }).primaryKey(),
4884
+ agreementId: text("agreement_id")
4885
+ .notNull()
4886
+ .references(() => fluxAgreements.id),
4887
+ eventType: text("event_type").notNull(),
4888
+ previousValue: text("previous_value"),
4889
+ newValue: text("new_value"),
4890
+ actorUserId: text("actor_user_id").references(() => fluxUsers.id),
4891
+ reason: text("reason"),
4892
+ emittedAt: timestamp("emitted_at", { withTimezone: true })
4893
+ .notNull()
4894
+ .defaultNow(),
4895
+ }, (table) => ({
4896
+ byAgreementIdx: index("idx_flux_agreement_events_by_agreement").on(table.agreementId, table.emittedAt),
4897
+ byTypeIdx: index("idx_flux_agreement_events_by_type").on(table.eventType, table.emittedAt),
4898
+ }));
4899
+ export const fluxAgreementEventsRelations = relations(fluxAgreementEvents, ({ one }) => ({
4900
+ agreement: one(fluxAgreements, {
4901
+ fields: [fluxAgreementEvents.agreementId],
4902
+ references: [fluxAgreements.id],
4903
+ }),
4904
+ actor: one(fluxUsers, {
4905
+ fields: [fluxAgreementEvents.actorUserId],
4906
+ references: [fluxUsers.id],
4907
+ }),
4908
+ }));
4792
4909
  /**
4793
- * Contract version source type polymorphic link to master contract or agreement
4910
+ * flux_inbound_eventsDedupe log for events received from external
4911
+ * sources (Compass). Used by the legacy-event-ingest handlers to
4912
+ * gracefully skip retried events without double-processing. 30-day
4913
+ * retention via a prune cron (not in this schema; lives in fn-flux).
4794
4914
  */
4795
- export const fluxContractSourceTypeEnum = pgEnum("flux_contract_source_type", [
4796
- "master",
4797
- "agreement",
4798
- ]);
4915
+ export const fluxInboundEvents = pgTable("flux_inbound_events", {
4916
+ eventId: text("event_id").primaryKey(),
4917
+ source: text("source").notNull(),
4918
+ eventType: text("event_type").notNull(),
4919
+ receivedAt: timestamp("received_at", { withTimezone: true })
4920
+ .notNull()
4921
+ .defaultNow(),
4922
+ processingResult: text("processing_result").notNull().default("pending"),
4923
+ processingError: text("processing_error"),
4924
+ });
4799
4925
  /**
4800
- * flux_master_contractsGoverning umbrella contracts.
4801
- * Child agreements (IOs, approvals) link via flux_agreements.parent_contract_id.
4926
+ * flux_outbound_eventsOutbox for events Flux sends to external systems
4927
+ * (currently only Compass at POST /flux-contract-events). The agreement
4928
+ * repository enqueues rows in the same transaction as the state mutation,
4929
+ * and a cron worker delivers them with exponential backoff + HMAC signing.
4930
+ *
4931
+ * Design goals:
4932
+ * - At-least-once delivery (cron retries until success or max attempts)
4933
+ * - Transactional consistency with the underlying mutation (if the tx rolls
4934
+ * back, the outbox row rolls back too — no phantom events)
4935
+ * - Idempotent on the receiver side via the stable event_id
4936
+ * - Observable: last_error + attempts for operator debugging
4937
+ *
4938
+ * Retention: delivered rows older than 7 days are pruned by a cron;
4939
+ * failed rows are kept indefinitely for forensic investigation.
4802
4940
  */
4803
- export const fluxMasterContracts = pgTable("flux_master_contracts", {
4941
+ export const fluxOutboundEvents = pgTable("flux_outbound_events", {
4804
4942
  id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
4805
- title: text("title").notNull(),
4806
- counterparty: text("counterparty").notNull(),
4807
- contractType: fluxMasterContractTypeEnum("contract_type"),
4808
- contractValue: numeric("contract_value", { precision: 14, scale: 2 }),
4809
- currency: text("currency").default("USD"),
4810
- effectiveDate: date("effective_date"),
4811
- expirationDate: date("expiration_date"),
4812
- terminationDate: date("termination_date"),
4813
- autoRenewal: boolean("auto_renewal").default(false),
4814
- renewalTermMonths: integer("renewal_term_months"),
4815
- renewalNoticeDays: integer("renewal_notice_days"),
4816
- status: fluxMasterContractStatusEnum("status").notNull().default("draft"),
4817
- tags: text("tags")
4818
- .array()
4819
- .default(sql `'{}'::text[]`),
4820
- customFields: jsonb("custom_fields").default({}),
4821
- notes: text("notes"),
4822
- documentUrl: text("document_url"),
4823
- documentFileName: text("document_file_name"),
4824
- documentStorageKey: text("document_storage_key"),
4825
- riskScore: integer("risk_score"),
4826
- riskIndicators: jsonb("risk_indicators").default([]),
4827
- // Financial fields — Phase: Contract Financial Alignment
4828
- tierCode: text("tier_code"), // tier_1 | tier_2 | tier_3 | open
4829
- rateCard: jsonb("rate_card"), // [{ stribProductId?, familyCode?, categoryCode?, rate, rateNotes? }]
4830
- productAllocations: jsonb("product_allocations"), // [{ familyCode, familyName, planned, notes? }]
4831
- billingCadence: text("billing_cadence"), // monthly | quarterly | annual
4832
- termsUrl: text("terms_url"),
4833
- termsAddendum: text("terms_addendum"),
4834
- allocatedAmount: numeric("allocated_amount", { precision: 14, scale: 2 }),
4835
- remainingAmount: numeric("remaining_amount", { precision: 14, scale: 2 }),
4836
- projectedMarginPct: real("projected_margin_pct"),
4837
- projectedMarginDollars: numeric("projected_margin_dollars", { precision: 14, scale: 2 }),
4838
- actualMarginPct: real("actual_margin_pct"),
4839
- actualMarginDollars: numeric("actual_margin_dollars", { precision: 14, scale: 2 }),
4840
- valueAddTotal: numeric("value_add_total", { precision: 14, scale: 2 }),
4841
- fluxVelocityProfileId: text("flux_velocity_profile_id"),
4842
- customSlaDays: integer("custom_sla_days"),
4843
- customWarnDays: integer("custom_warn_days"),
4844
- customBlockDays: integer("custom_block_days"),
4845
- orderCount: integer("order_count"),
4846
- createdById: text("created_by_id").references(() => fluxUsers.id),
4847
- projectId: text("project_id").references(() => fluxProjects.id),
4848
- hubspotCompanyId: text("hubspot_company_id"),
4943
+ /** Stable event identifier used by the receiver for dedupe */
4944
+ eventId: text("event_id").notNull().unique(),
4945
+ /** 'compass' | 'hubspot' | future destinations */
4946
+ destination: text("destination").notNull(),
4947
+ /** Event type (e.g. 'contract.created', 'contract.updated') */
4948
+ eventType: text("event_type").notNull(),
4949
+ /** Optional FK-ish link to the agreement that triggered the event */
4950
+ fluxAgreementId: text("flux_agreement_id"),
4951
+ /** Full JSON payload posted to the receiver */
4952
+ payload: jsonb("payload").notNull(),
4953
+ /** When the row was created (same tx as the state mutation) */
4849
4954
  createdAt: timestamp("created_at", { withTimezone: true })
4850
4955
  .notNull()
4851
4956
  .defaultNow(),
4852
- updatedAt: timestamp("updated_at", { withTimezone: true })
4853
- .notNull()
4854
- .defaultNow(),
4957
+ /** When the event was successfully delivered (NULL until confirmed 2xx) */
4958
+ deliveredAt: timestamp("delivered_at", { withTimezone: true }),
4959
+ /** Delivery attempt count — used for exponential backoff */
4960
+ attempts: integer("attempts").notNull().default(0),
4961
+ /** Last error string from a failed delivery attempt */
4962
+ lastError: text("last_error"),
4963
+ /** When the next retry should fire (NULL = ready now, past = retry overdue) */
4964
+ nextRetryAt: timestamp("next_retry_at", { withTimezone: true }),
4855
4965
  }, (table) => ({
4856
- statusIdx: index("flux_master_contracts_status_idx").on(table.status),
4857
- counterpartyIdx: index("flux_master_contracts_counterparty_idx").on(table.counterparty),
4858
- expirationIdx: index("flux_master_contracts_expiration_idx").on(table.expirationDate),
4859
- projectIdx: index("flux_master_contracts_project_idx").on(table.projectId),
4860
- hubspotCompanyIdx: index("flux_master_contracts_hubspot_company_idx").on(table.hubspotCompanyId),
4861
- }));
4862
- export const fluxMasterContractsRelations = relations(fluxMasterContracts, ({ one, many }) => ({
4863
- createdBy: one(fluxUsers, {
4864
- fields: [fluxMasterContracts.createdById],
4865
- references: [fluxUsers.id],
4866
- }),
4867
- project: one(fluxProjects, {
4868
- fields: [fluxMasterContracts.projectId],
4869
- references: [fluxProjects.id],
4870
- }),
4871
- childAgreements: many(fluxAgreements),
4872
- versions: many(fluxContractVersions),
4873
- pacing: many(fluxContractPacing),
4874
- comments: many(fluxAgreementComments),
4966
+ pendingIdx: index("idx_flux_outbound_events_pending")
4967
+ .on(table.destination, table.nextRetryAt)
4968
+ .where(sql `${table.deliveredAt} IS NULL`),
4969
+ agreementIdx: index("idx_flux_outbound_events_agreement")
4970
+ .on(table.fluxAgreementId)
4971
+ .where(sql `${table.fluxAgreementId} IS NOT NULL`),
4875
4972
  }));
4876
4973
  /**
4877
- * flux_contract_versions — Document version history for master contracts and agreements.
4974
+ * flux_contract_versions — Document version history for agreements.
4878
4975
  * Each revision stored as a separate R2 key with version number.
4879
4976
  */
4880
4977
  export const fluxContractVersions = pgTable("flux_contract_versions", {
4881
4978
  id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
4882
- // Polymorphic: references flux_master_contracts.id or flux_agreements.id
4883
- // depending on contractType. No FK constraint due to polymorphism —
4884
- // application-level enforcement in master-contract-lifecycle.ts.
4885
- contractId: text("contract_id").notNull(),
4886
- contractType: fluxContractSourceTypeEnum("contract_type").notNull(),
4979
+ agreementId: text("agreement_id")
4980
+ .notNull()
4981
+ .references(() => fluxAgreements.id, { onDelete: "cascade" }),
4887
4982
  version: integer("version").notNull(),
4888
4983
  storageKey: text("storage_key").notNull(),
4889
4984
  fileName: text("file_name").notNull(),
@@ -4893,25 +4988,28 @@ export const fluxContractVersions = pgTable("flux_contract_versions", {
4893
4988
  .notNull()
4894
4989
  .defaultNow(),
4895
4990
  }, (table) => ({
4896
- contractIdx: index("flux_contract_versions_contract_idx").on(table.contractId, table.contractType),
4897
- versionIdx: uniqueIndex("flux_contract_versions_unique_idx").on(table.contractId, table.contractType, table.version),
4991
+ contractIdx: index("flux_contract_versions_contract_idx").on(table.agreementId),
4992
+ versionIdx: uniqueIndex("flux_contract_versions_unique_idx").on(table.agreementId, table.version),
4898
4993
  }));
4899
4994
  export const fluxContractVersionsRelations = relations(fluxContractVersions, ({ one }) => ({
4995
+ agreement: one(fluxAgreements, {
4996
+ fields: [fluxContractVersions.agreementId],
4997
+ references: [fluxAgreements.id],
4998
+ }),
4900
4999
  uploadedBy: one(fluxUsers, {
4901
5000
  fields: [fluxContractVersions.uploadedById],
4902
5001
  references: [fluxUsers.id],
4903
5002
  }),
4904
5003
  }));
4905
5004
  /**
4906
- * flux_agreement_comments — Threaded comments on agreements or master contracts.
5005
+ * flux_agreement_comments — Threaded comments on agreements.
4907
5006
  * Supports clause references (page, paragraph, quote) for negotiation tracking.
4908
5007
  */
4909
5008
  export const fluxAgreementComments = pgTable("flux_agreement_comments", {
4910
5009
  id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
4911
- agreementId: text("agreement_id").references(() => fluxAgreements.id, {
4912
- onDelete: "cascade",
4913
- }),
4914
- contractId: text("contract_id").references(() => fluxMasterContracts.id, {
5010
+ agreementId: text("agreement_id")
5011
+ .notNull()
5012
+ .references(() => fluxAgreements.id, {
4915
5013
  onDelete: "cascade",
4916
5014
  }),
4917
5015
  authorId: text("author_id").references(() => fluxUsers.id),
@@ -4924,7 +5022,6 @@ export const fluxAgreementComments = pgTable("flux_agreement_comments", {
4924
5022
  .defaultNow(),
4925
5023
  }, (table) => ({
4926
5024
  agreementIdx: index("flux_agreement_comments_agreement_idx").on(table.agreementId),
4927
- contractIdx: index("flux_agreement_comments_contract_idx").on(table.contractId),
4928
5025
  parentIdx: index("flux_agreement_comments_parent_idx").on(table.parentCommentId),
4929
5026
  }));
4930
5027
  export const fluxAgreementCommentsRelations = relations(fluxAgreementComments, ({ one, many }) => ({
@@ -4932,10 +5029,6 @@ export const fluxAgreementCommentsRelations = relations(fluxAgreementComments, (
4932
5029
  fields: [fluxAgreementComments.agreementId],
4933
5030
  references: [fluxAgreements.id],
4934
5031
  }),
4935
- contract: one(fluxMasterContracts, {
4936
- fields: [fluxAgreementComments.contractId],
4937
- references: [fluxMasterContracts.id],
4938
- }),
4939
5032
  author: one(fluxUsers, {
4940
5033
  fields: [fluxAgreementComments.authorId],
4941
5034
  references: [fluxUsers.id],
@@ -4950,14 +5043,15 @@ export const fluxAgreementCommentsRelations = relations(fluxAgreementComments, (
4950
5043
  }),
4951
5044
  }));
4952
5045
  /**
4953
- * flux_contract_chunks — pgvector-indexed contract content for AI search.
5046
+ * flux_contract_chunks — pgvector-indexed agreement content for AI search.
4954
5047
  * Embeddings via OpenAI text-embedding-3-large (1536 dimensions).
4955
5048
  * Hybrid search: vector cosine + FTS via websearch_to_tsquery.
4956
5049
  */
4957
5050
  export const fluxContractChunks = pgTable("flux_contract_chunks", {
4958
5051
  id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
4959
- contractId: text("contract_id").notNull(),
4960
- contractType: fluxContractSourceTypeEnum("contract_type").notNull(),
5052
+ agreementId: text("agreement_id")
5053
+ .notNull()
5054
+ .references(() => fluxAgreements.id, { onDelete: "cascade" }),
4961
5055
  chunkIndex: integer("chunk_index").notNull(),
4962
5056
  chunkText: text("chunk_text").notNull(),
4963
5057
  embedding: vector("embedding", { dimensions: 1536 }),
@@ -4966,7 +5060,7 @@ export const fluxContractChunks = pgTable("flux_contract_chunks", {
4966
5060
  .notNull()
4967
5061
  .defaultNow(),
4968
5062
  }, (table) => ({
4969
- contractIdx: index("flux_contract_chunks_contract_idx").on(table.contractId, table.contractType),
5063
+ contractIdx: index("flux_contract_chunks_contract_idx").on(table.agreementId),
4970
5064
  metadataIdx: index("flux_contract_chunks_metadata_idx")
4971
5065
  .using("gin", table.metadata),
4972
5066
  }));
@@ -4988,68 +5082,6 @@ export const fluxContractPolicyChunks = pgTable("flux_contract_policy_chunks", {
4988
5082
  }, (table) => ({
4989
5083
  policyIdx: index("flux_contract_policy_chunks_policy_idx").on(table.policyName),
4990
5084
  }));
4991
- /**
4992
- * flux_contract_pacing — Computed drawdown pacing per active master contract.
4993
- * Mirrors flux_tapclicks_pacing pattern: velocity, trend, anomaly, status.
4994
- */
4995
- export const fluxContractPacing = pgTable("flux_contract_pacing", {
4996
- id: text("id").primaryKey().default(sql `gen_random_uuid()::text`),
4997
- contractId: text("contract_id")
4998
- .notNull()
4999
- .references(() => fluxMasterContracts.id),
5000
- pacingStatus: text("pacing_status").notNull(),
5001
- pacingPercent: numeric("pacing_percent", { precision: 8, scale: 2 }),
5002
- dailyVelocity: numeric("daily_velocity", { precision: 14, scale: 2 }),
5003
- velocityTrend: text("velocity_trend"),
5004
- totalDrawnDown: numeric("total_drawn_down", { precision: 14, scale: 2 }),
5005
- remainingCapacity: numeric("remaining_capacity", {
5006
- precision: 14,
5007
- scale: 2,
5008
- }),
5009
- projectedEndSpend: numeric("projected_end_spend", {
5010
- precision: 14,
5011
- scale: 2,
5012
- }),
5013
- runwayDays: integer("runway_days"),
5014
- anomalies: jsonb("anomalies").default([]),
5015
- // ── Contract remainder forecast columns (populated by pacing cron) ──
5016
- /** Extrapolated margin for unordered remainder (actualMargin - decay) */
5017
- extrapolatedMarginPct: real("extrapolated_margin_pct"),
5018
- /** Contract-level confidence score for remainder */
5019
- forecastConfidence: real("forecast_confidence"),
5020
- /** Confidence tier for remainder: commit/likely/upside/longshot */
5021
- forecastConfidenceTier: text("forecast_confidence_tier"),
5022
- /** Remainder revenue forecasts by window */
5023
- remainderForecast30d: numeric("remainder_forecast_30d", { precision: 14, scale: 2 }),
5024
- remainderForecast60d: numeric("remainder_forecast_60d", { precision: 14, scale: 2 }),
5025
- remainderForecast90d: numeric("remainder_forecast_90d", { precision: 14, scale: 2 }),
5026
- /** Remainder margin forecasts by window */
5027
- remainderMarginForecast30d: numeric("remainder_margin_forecast_30d", { precision: 14, scale: 2 }),
5028
- remainderMarginForecast60d: numeric("remainder_margin_forecast_60d", { precision: 14, scale: 2 }),
5029
- remainderMarginForecast90d: numeric("remainder_margin_forecast_90d", { precision: 14, scale: 2 }),
5030
- /** Periods until next expected IO */
5031
- periodDistance: integer("period_distance"),
5032
- /** Periods remaining on contract */
5033
- periodsRemaining: integer("periods_remaining"),
5034
- computedAt: timestamp("computed_at", { withTimezone: true })
5035
- .notNull()
5036
- .defaultNow(),
5037
- createdAt: timestamp("created_at", { withTimezone: true })
5038
- .notNull()
5039
- .defaultNow(),
5040
- updatedAt: timestamp("updated_at", { withTimezone: true })
5041
- .notNull()
5042
- .defaultNow(),
5043
- }, (table) => ({
5044
- contractIdx: uniqueIndex("flux_contract_pacing_contract_idx").on(table.contractId),
5045
- statusIdx: index("flux_contract_pacing_status_idx").on(table.pacingStatus),
5046
- }));
5047
- export const fluxContractPacingRelations = relations(fluxContractPacing, ({ one }) => ({
5048
- contract: one(fluxMasterContracts, {
5049
- fields: [fluxContractPacing.contractId],
5050
- references: [fluxMasterContracts.id],
5051
- }),
5052
- }));
5053
5085
  // ---------------------------------------------------------------------------
5054
5086
  // Agreement Workspace Tables — graduated from metadata JSONB
5055
5087
  // ---------------------------------------------------------------------------