@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":"person-documents.d.ts","sourceRoot":"","sources":["../../src/service/person-documents.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,240 @@
1
+ import { decryptOptionalJsonEnvelope } from "@voyant-travel/utils";
2
+ import { and, asc, desc, eq, gte, isNotNull, lte, sql } from "drizzle-orm";
3
+ import { z } from "zod";
4
+ import { people, personDocuments } from "../schema.js";
5
+ /**
6
+ * Canonical plaintext shape for free-text PII blobs encrypted as
7
+ * `accessibilityEncrypted` / `dietaryEncrypted` / `loyaltyEncrypted` /
8
+ * `insuranceEncrypted` on `relationships.people`. Wrapped so future fields can
9
+ * be added without breaking existing ciphertexts.
10
+ *
11
+ * Writers (customer-portal) and readers (crm + bookings snapshot)
12
+ * must use this shape — drift between sides means decryption fails
13
+ * silently and pre-fill stops working.
14
+ */
15
+ export const personPiiBlobPlaintextSchema = z.object({ text: z.string() });
16
+ /**
17
+ * Canonical plaintext shape for `numberEncrypted` on
18
+ * `relationships.person_documents`. Same compatibility contract as
19
+ * `personPiiBlobPlaintextSchema`.
20
+ */
21
+ export const personDocumentNumberPlaintextSchema = z.object({ number: z.string() });
22
+ async function personExists(db, personId) {
23
+ const [row] = await db
24
+ .select({ id: people.id })
25
+ .from(people)
26
+ .where(eq(people.id, personId))
27
+ .limit(1);
28
+ return Boolean(row);
29
+ }
30
+ async function clearPrimaryForType(db, personId, type, exceptDocumentId) {
31
+ const conditions = [
32
+ eq(personDocuments.personId, personId),
33
+ eq(personDocuments.type, type),
34
+ eq(personDocuments.isPrimary, true),
35
+ ];
36
+ if (exceptDocumentId) {
37
+ // agent-quality: raw-sql reviewed -- owner: crm; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
38
+ conditions.push(sql `${personDocuments.id} <> ${exceptDocumentId}`);
39
+ }
40
+ await db
41
+ .update(personDocuments)
42
+ .set({ isPrimary: false, updatedAt: new Date() })
43
+ .where(and(...conditions));
44
+ }
45
+ export const personDocumentsService = {
46
+ listPersonDocuments(db, personId, query) {
47
+ const conditions = [eq(personDocuments.personId, personId)];
48
+ if (query?.type)
49
+ conditions.push(eq(personDocuments.type, query.type));
50
+ if (query?.expiringBefore) {
51
+ conditions.push(isNotNull(personDocuments.expiryDate));
52
+ conditions.push(lte(personDocuments.expiryDate, query.expiringBefore));
53
+ }
54
+ const limit = query?.limit ?? 50;
55
+ const offset = query?.offset ?? 0;
56
+ return db
57
+ .select()
58
+ .from(personDocuments)
59
+ .where(and(...conditions))
60
+ .orderBy(asc(personDocuments.type), asc(personDocuments.createdAt))
61
+ .limit(limit)
62
+ .offset(offset);
63
+ },
64
+ async getPersonDocument(db, documentId) {
65
+ const [row] = await db
66
+ .select()
67
+ .from(personDocuments)
68
+ .where(eq(personDocuments.id, documentId))
69
+ .limit(1);
70
+ return row ?? null;
71
+ },
72
+ /**
73
+ * Decrypts the `numberEncrypted` slot for a single document and
74
+ * returns the plaintext. Returns `null` when the document has no
75
+ * encrypted number on file. Caller is responsible for authorization
76
+ * and audit-logging; this is just the KMS unwrap.
77
+ */
78
+ async revealPersonDocumentNumber(db, documentId, options) {
79
+ const row = await personDocumentsService.getPersonDocument(db, documentId);
80
+ if (!row)
81
+ return null;
82
+ if (!row.numberEncrypted)
83
+ return { documentId, number: null };
84
+ const keyRef = options.keyRef ?? { keyType: "people" };
85
+ const decrypted = await decryptOptionalJsonEnvelope(options.kms, keyRef, row.numberEncrypted, personDocumentNumberPlaintextSchema);
86
+ return { documentId, number: decrypted?.number ?? null };
87
+ },
88
+ /**
89
+ * Returns the primary document of a given type for a person, or
90
+ * `null` if no primary is set.
91
+ */
92
+ async getPrimaryPersonDocument(db, personId, type) {
93
+ const [row] = await db
94
+ .select()
95
+ .from(personDocuments)
96
+ .where(and(eq(personDocuments.personId, personId), eq(personDocuments.type, type), eq(personDocuments.isPrimary, true)))
97
+ .limit(1);
98
+ return row ?? null;
99
+ },
100
+ async createPersonDocument(db, personId, data) {
101
+ if (!(await personExists(db, personId)))
102
+ return null;
103
+ return db.transaction(async (tx) => {
104
+ if (data.isPrimary) {
105
+ await clearPrimaryForType(tx, personId, data.type);
106
+ }
107
+ const [row] = await tx
108
+ .insert(personDocuments)
109
+ .values({ ...data, personId })
110
+ .returning();
111
+ return row ?? null;
112
+ });
113
+ },
114
+ async updatePersonDocument(db, documentId, data) {
115
+ return db.transaction(async (tx) => {
116
+ const [existing] = await tx
117
+ .select()
118
+ .from(personDocuments)
119
+ .where(eq(personDocuments.id, documentId))
120
+ .limit(1);
121
+ if (!existing)
122
+ return null;
123
+ // Clear prior primary of the *target* type whenever the row
124
+ // will end up primary after this update — including the case
125
+ // where `isPrimary` is unchanged but `type` is being switched
126
+ // and the existing row is already primary. Without this, the
127
+ // partial unique index `(person_id, type) WHERE is_primary`
128
+ // rejects type-only edits on a primary doc.
129
+ const effectiveIsPrimary = data.isPrimary ?? existing.isPrimary;
130
+ if (effectiveIsPrimary) {
131
+ const targetType = data.type ?? existing.type;
132
+ await clearPrimaryForType(tx, existing.personId, targetType, documentId);
133
+ }
134
+ const [row] = await tx
135
+ .update(personDocuments)
136
+ .set({ ...data, updatedAt: new Date() })
137
+ .where(eq(personDocuments.id, documentId))
138
+ .returning();
139
+ return row ?? null;
140
+ });
141
+ },
142
+ async deletePersonDocument(db, documentId) {
143
+ const [row] = await db
144
+ .delete(personDocuments)
145
+ .where(eq(personDocuments.id, documentId))
146
+ .returning({ id: personDocuments.id });
147
+ return row ?? null;
148
+ },
149
+ /**
150
+ * Atomically promotes a document to primary, demoting any prior
151
+ * primary of the same type for the same person.
152
+ */
153
+ async setPrimaryPersonDocument(db, documentId) {
154
+ return db.transaction(async (tx) => {
155
+ const [existing] = await tx
156
+ .select()
157
+ .from(personDocuments)
158
+ .where(eq(personDocuments.id, documentId))
159
+ .limit(1);
160
+ if (!existing)
161
+ return null;
162
+ await clearPrimaryForType(tx, existing.personId, existing.type, documentId);
163
+ const [row] = await tx
164
+ .update(personDocuments)
165
+ .set({ isPrimary: true, updatedAt: new Date() })
166
+ .where(eq(personDocuments.id, documentId))
167
+ .returning();
168
+ return row ?? null;
169
+ });
170
+ },
171
+ /**
172
+ * Plaintext snapshot of the fields a booking-traveler creation
173
+ * pulls from a person record: dietary, accessibility, primary identity
174
+ * document (type + number + expiry + country + authority + provenance id),
175
+ * and date-of-birth from `people.dateOfBirth`.
176
+ *
177
+ * Caller passes a KMS provider so decryption happens in-process.
178
+ * Missing person → returns null. Missing document or blob → that
179
+ * field returns null in the snapshot.
180
+ */
181
+ async loadPersonTravelSnapshot(db, personId, options) {
182
+ const [personRow] = await db
183
+ .select({
184
+ dateOfBirth: people.dateOfBirth,
185
+ accessibilityEncrypted: people.accessibilityEncrypted,
186
+ dietaryEncrypted: people.dietaryEncrypted,
187
+ })
188
+ .from(people)
189
+ .where(eq(people.id, personId))
190
+ .limit(1);
191
+ if (!personRow)
192
+ return null;
193
+ const keyRef = options.keyRef ?? { keyType: "people" };
194
+ const primaryDocumentPromise = db
195
+ .select()
196
+ .from(personDocuments)
197
+ .where(and(eq(personDocuments.personId, personId), eq(personDocuments.isPrimary, true)))
198
+ .orderBy(desc(personDocuments.updatedAt))
199
+ .limit(1);
200
+ const [accessibilityBlob, dietaryBlob, primaryDocuments] = await Promise.all([
201
+ decryptOptionalJsonEnvelope(options.kms, keyRef, personRow.accessibilityEncrypted, personPiiBlobPlaintextSchema),
202
+ decryptOptionalJsonEnvelope(options.kms, keyRef, personRow.dietaryEncrypted, personPiiBlobPlaintextSchema),
203
+ primaryDocumentPromise,
204
+ ]);
205
+ const primaryDocument = primaryDocuments[0] ?? null;
206
+ let documentNumber = null;
207
+ if (primaryDocument?.numberEncrypted) {
208
+ const decrypted = await decryptOptionalJsonEnvelope(options.kms, keyRef, primaryDocument.numberEncrypted, personDocumentNumberPlaintextSchema);
209
+ documentNumber = decrypted?.number ?? null;
210
+ }
211
+ return {
212
+ dateOfBirth: personRow.dateOfBirth ?? null,
213
+ dietaryRequirements: dietaryBlob?.text ?? null,
214
+ accessibilityNeeds: accessibilityBlob?.text ?? null,
215
+ documentType: primaryDocument?.type ?? null,
216
+ documentNumber,
217
+ documentExpiry: primaryDocument?.expiryDate ?? null,
218
+ documentIssuingCountry: primaryDocument?.issuingCountry ?? null,
219
+ documentIssuingAuthority: primaryDocument?.issuingAuthority ?? null,
220
+ documentPersonDocumentId: primaryDocument?.id ?? null,
221
+ };
222
+ },
223
+ /**
224
+ * Documents whose `expiryDate` falls within the next `daysAhead`
225
+ * days. Used by the future `crm.detect-expiring-documents` cron;
226
+ * shipped now since the helper is free.
227
+ */
228
+ listExpiringPersonDocuments(db, daysAhead = 90) {
229
+ const today = new Date();
230
+ const horizon = new Date(today);
231
+ horizon.setUTCDate(horizon.getUTCDate() + daysAhead);
232
+ const todayIso = today.toISOString().slice(0, 10);
233
+ const horizonIso = horizon.toISOString().slice(0, 10);
234
+ return db
235
+ .select()
236
+ .from(personDocuments)
237
+ .where(and(isNotNull(personDocuments.expiryDate), gte(personDocuments.expiryDate, todayIso), lte(personDocuments.expiryDate, horizonIso)))
238
+ .orderBy(asc(personDocuments.expiryDate));
239
+ },
240
+ };