@voyant-travel/relationships 0.119.2

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.
Files changed (96) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +36 -0
  3. package/dist/action-ledger-capabilities.d.ts +20 -0
  4. package/dist/action-ledger-capabilities.d.ts.map +1 -0
  5. package/dist/action-ledger-capabilities.js +16 -0
  6. package/dist/events.d.ts +23 -0
  7. package/dist/events.d.ts.map +1 -0
  8. package/dist/events.js +9 -0
  9. package/dist/index.d.ts +32 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +49 -0
  12. package/dist/route-runtime.d.ts +21 -0
  13. package/dist/route-runtime.d.ts.map +1 -0
  14. package/dist/route-runtime.js +28 -0
  15. package/dist/routes/accounts.d.ts +1460 -0
  16. package/dist/routes/accounts.d.ts.map +1 -0
  17. package/dist/routes/accounts.js +274 -0
  18. package/dist/routes/activities.d.ts +299 -0
  19. package/dist/routes/activities.d.ts.map +1 -0
  20. package/dist/routes/activities.js +64 -0
  21. package/dist/routes/custom-fields.d.ts +256 -0
  22. package/dist/routes/custom-fields.d.ts.map +1 -0
  23. package/dist/routes/custom-fields.js +47 -0
  24. package/dist/routes/customer-signals.d.ts +281 -0
  25. package/dist/routes/customer-signals.d.ts.map +1 -0
  26. package/dist/routes/customer-signals.js +45 -0
  27. package/dist/routes/index.d.ts +2945 -0
  28. package/dist/routes/index.d.ts.map +1 -0
  29. package/dist/routes/index.js +14 -0
  30. package/dist/routes/person-documents.d.ts +519 -0
  31. package/dist/routes/person-documents.d.ts.map +1 -0
  32. package/dist/routes/person-documents.js +240 -0
  33. package/dist/routes/person-relationships.d.ts +189 -0
  34. package/dist/routes/person-relationships.d.ts.map +1 -0
  35. package/dist/routes/person-relationships.js +36 -0
  36. package/dist/schema-accounts.d.ts +2099 -0
  37. package/dist/schema-accounts.d.ts.map +1 -0
  38. package/dist/schema-accounts.js +312 -0
  39. package/dist/schema-activities.d.ts +821 -0
  40. package/dist/schema-activities.d.ts.map +1 -0
  41. package/dist/schema-activities.js +92 -0
  42. package/dist/schema-relations.d.ts +47 -0
  43. package/dist/schema-relations.d.ts.map +1 -0
  44. package/dist/schema-relations.js +70 -0
  45. package/dist/schema-shared.d.ts +10 -0
  46. package/dist/schema-shared.d.ts.map +1 -0
  47. package/dist/schema-shared.js +36 -0
  48. package/dist/schema-signals.d.ts +324 -0
  49. package/dist/schema-signals.d.ts.map +1 -0
  50. package/dist/schema-signals.js +80 -0
  51. package/dist/schema.d.ts +6 -0
  52. package/dist/schema.d.ts.map +1 -0
  53. package/dist/schema.js +5 -0
  54. package/dist/service/accounts-merge.d.ts +63 -0
  55. package/dist/service/accounts-merge.d.ts.map +1 -0
  56. package/dist/service/accounts-merge.js +382 -0
  57. package/dist/service/accounts-organizations.d.ts +97 -0
  58. package/dist/service/accounts-organizations.d.ts.map +1 -0
  59. package/dist/service/accounts-organizations.js +70 -0
  60. package/dist/service/accounts-people.d.ts +1315 -0
  61. package/dist/service/accounts-people.d.ts.map +1 -0
  62. package/dist/service/accounts-people.js +409 -0
  63. package/dist/service/accounts-resolve.d.ts +76 -0
  64. package/dist/service/accounts-resolve.d.ts.map +1 -0
  65. package/dist/service/accounts-resolve.js +103 -0
  66. package/dist/service/accounts-shared.d.ts +68 -0
  67. package/dist/service/accounts-shared.d.ts.map +1 -0
  68. package/dist/service/accounts-shared.js +149 -0
  69. package/dist/service/accounts.d.ts +1465 -0
  70. package/dist/service/accounts.d.ts.map +1 -0
  71. package/dist/service/accounts.js +13 -0
  72. package/dist/service/activities.d.ts +486 -0
  73. package/dist/service/activities.d.ts.map +1 -0
  74. package/dist/service/activities.js +114 -0
  75. package/dist/service/custom-fields.d.ts +118 -0
  76. package/dist/service/custom-fields.d.ts.map +1 -0
  77. package/dist/service/custom-fields.js +88 -0
  78. package/dist/service/customer-signals.d.ts +733 -0
  79. package/dist/service/customer-signals.d.ts.map +1 -0
  80. package/dist/service/customer-signals.js +112 -0
  81. package/dist/service/helpers.d.ts +22 -0
  82. package/dist/service/helpers.d.ts.map +1 -0
  83. package/dist/service/helpers.js +39 -0
  84. package/dist/service/index.d.ts +4434 -0
  85. package/dist/service/index.d.ts.map +1 -0
  86. package/dist/service/index.js +17 -0
  87. package/dist/service/person-documents.d.ts +1201 -0
  88. package/dist/service/person-documents.d.ts.map +1 -0
  89. package/dist/service/person-documents.js +240 -0
  90. package/dist/service/person-relationships.d.ts +502 -0
  91. package/dist/service/person-relationships.d.ts.map +1 -0
  92. package/dist/service/person-relationships.js +121 -0
  93. package/dist/validation.d.ts +3 -0
  94. package/dist/validation.d.ts.map +1 -0
  95. package/dist/validation.js +1 -0
  96. package/package.json +80 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-accounts.d.ts","sourceRoot":"","sources":["../src/schema-accounts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,312 @@
1
+ import { typeId, typeIdRef } from "@voyant-travel/db/lib/typeid-column";
2
+ import { sql } from "drizzle-orm";
3
+ import { boolean, check, date, index, integer, jsonb, pgEnum, pgTable, pgView, text, timestamp, uniqueIndex, } from "drizzle-orm/pg-core";
4
+ import { communicationChannelEnum, communicationDirectionEnum, recordStatusEnum, relationTypeEnum, } from "./schema-shared.js";
5
+ /**
6
+ * Identity-document types stored on `person_documents`. Open-ended via
7
+ * "other" so we don't force a schema migration for every regional
8
+ * variant; the structured fields cover the international shape.
9
+ */
10
+ export const personDocumentTypeEnum = pgEnum("person_document_type", [
11
+ "passport",
12
+ "id_card",
13
+ "driver_license",
14
+ "visa",
15
+ "other",
16
+ ]);
17
+ /**
18
+ * Person-to-person relationship kinds. Directed: each row records a
19
+ * single edge `from → to` of a specific kind. The optional inverse
20
+ * edge (e.g. parent ↔ child) is kept as a separate row so list
21
+ * queries against either side return the same shape; the service
22
+ * layer auto-writes the inverse on insert when an `inverseKind` is
23
+ * provided.
24
+ */
25
+ export const personRelationshipKindEnum = pgEnum("person_relationship_kind", [
26
+ "spouse",
27
+ "partner",
28
+ "parent",
29
+ "child",
30
+ "sibling",
31
+ "guardian",
32
+ "ward",
33
+ "emergency_contact",
34
+ "friend",
35
+ "travel_companion",
36
+ "other",
37
+ ]);
38
+ export const organizations = pgTable("organizations", {
39
+ id: typeId("organizations"),
40
+ name: text("name").notNull(),
41
+ legalName: text("legal_name"),
42
+ website: text("website"),
43
+ /** Tax / VAT identification number — used for billing + e-invoicing. */
44
+ taxId: text("tax_id"),
45
+ industry: text("industry"),
46
+ relation: relationTypeEnum("relation"),
47
+ ownerId: text("owner_id"),
48
+ defaultCurrency: text("default_currency"),
49
+ preferredLanguage: text("preferred_language"),
50
+ paymentTerms: integer("payment_terms"),
51
+ status: recordStatusEnum("status").notNull().default("active"),
52
+ source: text("source"),
53
+ sourceRef: text("source_ref"),
54
+ tags: jsonb("tags").$type().notNull().default([]),
55
+ notes: text("notes"),
56
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
57
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
58
+ archivedAt: timestamp("archived_at", { withTimezone: true }),
59
+ }, (table) => [
60
+ index("idx_organizations_name").on(table.name),
61
+ index("idx_organizations_owner").on(table.ownerId),
62
+ index("idx_organizations_status").on(table.status),
63
+ index("idx_organizations_tax_id").on(table.taxId),
64
+ index("idx_organizations_owner_updated").on(table.ownerId, table.updatedAt),
65
+ index("idx_organizations_relation_updated").on(table.relation, table.updatedAt),
66
+ index("idx_organizations_status_updated").on(table.status, table.updatedAt),
67
+ ]);
68
+ export const people = pgTable("people", {
69
+ id: typeId("people"),
70
+ organizationId: typeIdRef("organization_id").references(() => organizations.id, {
71
+ onDelete: "set null",
72
+ }),
73
+ firstName: text("first_name").notNull(),
74
+ middleName: text("middle_name"),
75
+ lastName: text("last_name").notNull(),
76
+ /** ISO-style "M" / "F" / "X" — used by airline + travel-doc workflows. */
77
+ gender: text("gender"),
78
+ jobTitle: text("job_title"),
79
+ relation: relationTypeEnum("relation"),
80
+ preferredLanguage: text("preferred_language"),
81
+ preferredCurrency: text("preferred_currency"),
82
+ ownerId: text("owner_id"),
83
+ status: recordStatusEnum("status").notNull().default("active"),
84
+ source: text("source"),
85
+ sourceRef: text("source_ref"),
86
+ tags: jsonb("tags").$type().notNull().default([]),
87
+ dateOfBirth: date("date_of_birth"),
88
+ notes: text("notes"),
89
+ /**
90
+ * Encrypted PII slots — canonical store for person-level travel
91
+ * preferences. Documents live in their own structured table
92
+ * (`person_documents`); these four are kept as KMS envelopes
93
+ * because their internal shape is small and rarely queried.
94
+ *
95
+ * Booking-traveler rows snapshot dietary/accessibility at create
96
+ * time; the person record remains the source of truth for
97
+ * pre-fill on subsequent bookings.
98
+ */
99
+ accessibilityEncrypted: jsonb("accessibility_encrypted").$type(),
100
+ dietaryEncrypted: jsonb("dietary_encrypted").$type(),
101
+ loyaltyEncrypted: jsonb("loyalty_encrypted").$type(),
102
+ insuranceEncrypted: jsonb("insurance_encrypted").$type(),
103
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
104
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
105
+ archivedAt: timestamp("archived_at", { withTimezone: true }),
106
+ }, (table) => [
107
+ index("idx_people_org").on(table.organizationId),
108
+ index("idx_people_owner").on(table.ownerId),
109
+ index("idx_people_status").on(table.status),
110
+ index("idx_people_name").on(table.firstName, table.lastName),
111
+ index("idx_people_org_updated").on(table.organizationId, table.updatedAt),
112
+ index("idx_people_owner_updated").on(table.ownerId, table.updatedAt),
113
+ index("idx_people_relation_updated").on(table.relation, table.updatedAt),
114
+ index("idx_people_status_updated").on(table.status, table.updatedAt),
115
+ ]);
116
+ /**
117
+ * `person_directory` is a Postgres VIEW (not a table) that exposes
118
+ * each person's primary email / phone / website by `LATERAL` lookup
119
+ * against `identity_contact_points`. The view is created in the
120
+ * 0028 migration; this binding gives Drizzle a typed read surface.
121
+ *
122
+ * The previous `person_directory_projections` table was a denormalized
123
+ * cache that had to be rebuilt on every contact-point change — see
124
+ * #446 for the discussion of why we replaced it. Indexed lateral
125
+ * joins (`idx_identity_contact_points_entity_kind_primary_created`
126
+ * already exists) keep view reads sub-millisecond at realistic CRM
127
+ * volumes.
128
+ */
129
+ export const personDirectoryView = pgView("person_directory", {
130
+ personId: typeIdRef("person_id").notNull(),
131
+ email: text("email"),
132
+ phone: text("phone"),
133
+ website: text("website"),
134
+ }).existing();
135
+ export const personNotes = pgTable("person_notes", {
136
+ id: typeId("person_notes"),
137
+ personId: typeIdRef("person_id")
138
+ .notNull()
139
+ .references(() => people.id, { onDelete: "cascade" }),
140
+ authorId: text("author_id").notNull(),
141
+ content: text("content").notNull(),
142
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
143
+ }, (table) => [
144
+ index("idx_person_notes_person").on(table.personId),
145
+ index("idx_person_notes_person_created").on(table.personId, table.createdAt),
146
+ ]);
147
+ /**
148
+ * Structured identity documents owned by a person. Replaces the
149
+ * single `documentsEncrypted` blob shape with a row per document so
150
+ * we can track type / expiry / issuing authority / attachment + run
151
+ * "expiring soon" sweeps without parsing JSON.
152
+ *
153
+ * `numberEncrypted` is the only field encrypted at rest — the rest
154
+ * is non-toxic identity metadata. `attachmentId` is a free-form key
155
+ * (typically an object-storage path) until a general media table
156
+ * exists; FK is intentionally deferred.
157
+ *
158
+ * Booking-traveler rows snapshot the primary passport at create time;
159
+ * this table remains the source of truth for next-trip pre-fill.
160
+ */
161
+ export const personDocuments = pgTable("person_documents", {
162
+ id: typeId("person_documents"),
163
+ personId: typeIdRef("person_id")
164
+ .notNull()
165
+ .references(() => people.id, { onDelete: "cascade" }),
166
+ type: personDocumentTypeEnum("type").notNull(),
167
+ numberEncrypted: jsonb("number_encrypted").$type(),
168
+ issuingAuthority: text("issuing_authority"),
169
+ issuingCountry: text("issuing_country"),
170
+ issueDate: date("issue_date"),
171
+ expiryDate: date("expiry_date"),
172
+ attachmentId: text("attachment_id"),
173
+ isPrimary: boolean("is_primary").notNull().default(false),
174
+ notes: text("notes"),
175
+ metadata: jsonb("metadata").$type(),
176
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
177
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
178
+ }, (table) => [
179
+ index("idx_person_documents_person").on(table.personId),
180
+ index("idx_person_documents_person_type").on(table.personId, table.type),
181
+ index("idx_person_documents_expiry").on(table.expiryDate),
182
+ uniqueIndex("uidx_person_documents_primary_per_type")
183
+ .on(table.personId, table.type)
184
+ // agent-quality: raw-sql reviewed -- owner: crm; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
185
+ .where(sql `${table.isPrimary} = true`),
186
+ ]);
187
+ /**
188
+ * Directed person-to-person edges (kinship, emergency contacts,
189
+ * travel companions). Each row is a single edge `fromPerson → toPerson`
190
+ * of one `kind`; the reverse edge — when meaningful — is a separate
191
+ * row written by the service layer's auto-inverse helper. Self-edges
192
+ * are rejected via a CHECK constraint; the unique index on
193
+ * `(from, to, kind)` prevents the same directional edge from being
194
+ * recorded twice.
195
+ *
196
+ * `metadata` is a free-form jsonb bag for module-specific extensions
197
+ * (e.g. emergency-contact relationship to traveler). Operators that
198
+ * need richer structure should add their own typed columns.
199
+ */
200
+ export const personRelationships = pgTable("person_relationships", {
201
+ id: typeId("person_relationships"),
202
+ fromPersonId: typeIdRef("from_person_id")
203
+ .notNull()
204
+ .references(() => people.id, { onDelete: "cascade" }),
205
+ toPersonId: typeIdRef("to_person_id")
206
+ .notNull()
207
+ .references(() => people.id, { onDelete: "cascade" }),
208
+ kind: personRelationshipKindEnum("kind").notNull(),
209
+ /**
210
+ * The kind that should label the reverse edge (e.g. parent ↔
211
+ * child). When set on insert, the service layer writes the
212
+ * inverse edge in the same transaction unless `autoInverse` is
213
+ * explicitly disabled.
214
+ */
215
+ inverseKind: personRelationshipKindEnum("inverse_kind"),
216
+ startDate: date("start_date"),
217
+ endDate: date("end_date"),
218
+ isPrimary: boolean("is_primary").notNull().default(false),
219
+ notes: text("notes"),
220
+ metadata: jsonb("metadata").$type(),
221
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
222
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
223
+ }, (table) => [
224
+ index("idx_person_relationships_from").on(table.fromPersonId),
225
+ index("idx_person_relationships_to").on(table.toPersonId),
226
+ uniqueIndex("uidx_person_relationships_pair_kind").on(table.fromPersonId, table.toPersonId, table.kind),
227
+ // agent-quality: raw-sql reviewed -- owner: crm; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
228
+ check("person_relationships_no_self", sql `${table.fromPersonId} <> ${table.toPersonId}`),
229
+ ]);
230
+ /**
231
+ * Saved payment methods on file for a person. Stores processor-issued
232
+ * tokens (never raw card numbers) so the booking flow can charge the
233
+ * customer without re-entering card details. Cards have last4 + expiry +
234
+ * brand; bank-transfer "methods" carry a brand of "bank_transfer" with
235
+ * last4 / expiry omitted.
236
+ */
237
+ export const personPaymentMethods = pgTable("person_payment_methods", {
238
+ id: typeId("person_payment_methods"),
239
+ personId: typeIdRef("person_id")
240
+ .notNull()
241
+ .references(() => people.id, { onDelete: "cascade" }),
242
+ /** "visa" | "mastercard" | "amex" | "revolut" | "bank_transfer" — kept as text to stay open. */
243
+ brand: text("brand").notNull(),
244
+ /** Last four digits — null for non-card methods. */
245
+ last4: text("last4"),
246
+ holderName: text("holder_name"),
247
+ /** 1-12; null for non-card methods. */
248
+ expMonth: integer("exp_month"),
249
+ expYear: integer("exp_year"),
250
+ /** Opaque processor token — used to charge the customer. */
251
+ processorToken: text("processor_token").notNull(),
252
+ isDefault: boolean("is_default").notNull().default(false),
253
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
254
+ }, (table) => [
255
+ index("idx_person_payment_methods_person").on(table.personId),
256
+ index("idx_person_payment_methods_person_default").on(table.personId, table.isDefault),
257
+ ]);
258
+ export const organizationNotes = pgTable("organization_notes", {
259
+ id: typeId("organization_notes"),
260
+ organizationId: typeIdRef("organization_id")
261
+ .notNull()
262
+ .references(() => organizations.id, { onDelete: "cascade" }),
263
+ authorId: text("author_id").notNull(),
264
+ content: text("content").notNull(),
265
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
266
+ }, (table) => [
267
+ index("idx_organization_notes_org").on(table.organizationId),
268
+ index("idx_organization_notes_org_created").on(table.organizationId, table.createdAt),
269
+ ]);
270
+ export const communicationLog = pgTable("communication_log", {
271
+ id: typeId("communication_log"),
272
+ personId: typeIdRef("person_id")
273
+ .notNull()
274
+ .references(() => people.id, { onDelete: "cascade" }),
275
+ organizationId: typeIdRef("organization_id").references(() => organizations.id, {
276
+ onDelete: "set null",
277
+ }),
278
+ channel: communicationChannelEnum("channel").notNull(),
279
+ direction: communicationDirectionEnum("direction").notNull(),
280
+ subject: text("subject"),
281
+ content: text("content"),
282
+ sentAt: timestamp("sent_at", { withTimezone: true }),
283
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
284
+ }, (table) => [
285
+ index("idx_communication_log_person").on(table.personId),
286
+ index("idx_communication_log_person_created").on(table.personId, table.createdAt),
287
+ index("idx_communication_log_person_channel_created").on(table.personId, table.channel, table.createdAt),
288
+ index("idx_communication_log_person_direction_created").on(table.personId, table.direction, table.createdAt),
289
+ index("idx_communication_log_org").on(table.organizationId),
290
+ index("idx_communication_log_channel").on(table.channel),
291
+ ]);
292
+ export const segments = pgTable("segments", {
293
+ id: typeId("segments"),
294
+ name: text("name").notNull(),
295
+ description: text("description"),
296
+ conditions: jsonb("conditions").$type(),
297
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
298
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
299
+ }, (table) => [index("idx_segments_created").on(table.createdAt)]);
300
+ export const segmentMembers = pgTable("segment_members", {
301
+ id: typeId("segment_members"),
302
+ segmentId: typeIdRef("segment_id")
303
+ .notNull()
304
+ .references(() => segments.id, { onDelete: "cascade" }),
305
+ personId: typeIdRef("person_id")
306
+ .notNull()
307
+ .references(() => people.id, { onDelete: "cascade" }),
308
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
309
+ }, (table) => [
310
+ index("idx_segment_members_segment").on(table.segmentId),
311
+ index("idx_segment_members_person").on(table.personId),
312
+ ]);