@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.
- package/LICENSE +201 -0
- package/README.md +36 -0
- package/dist/action-ledger-capabilities.d.ts +20 -0
- package/dist/action-ledger-capabilities.d.ts.map +1 -0
- package/dist/action-ledger-capabilities.js +16 -0
- package/dist/events.d.ts +23 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +9 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/route-runtime.d.ts +21 -0
- package/dist/route-runtime.d.ts.map +1 -0
- package/dist/route-runtime.js +28 -0
- package/dist/routes/accounts.d.ts +1460 -0
- package/dist/routes/accounts.d.ts.map +1 -0
- package/dist/routes/accounts.js +274 -0
- package/dist/routes/activities.d.ts +299 -0
- package/dist/routes/activities.d.ts.map +1 -0
- package/dist/routes/activities.js +64 -0
- package/dist/routes/custom-fields.d.ts +256 -0
- package/dist/routes/custom-fields.d.ts.map +1 -0
- package/dist/routes/custom-fields.js +47 -0
- package/dist/routes/customer-signals.d.ts +281 -0
- package/dist/routes/customer-signals.d.ts.map +1 -0
- package/dist/routes/customer-signals.js +45 -0
- package/dist/routes/index.d.ts +2945 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +14 -0
- package/dist/routes/person-documents.d.ts +519 -0
- package/dist/routes/person-documents.d.ts.map +1 -0
- package/dist/routes/person-documents.js +240 -0
- package/dist/routes/person-relationships.d.ts +189 -0
- package/dist/routes/person-relationships.d.ts.map +1 -0
- package/dist/routes/person-relationships.js +36 -0
- package/dist/schema-accounts.d.ts +2099 -0
- package/dist/schema-accounts.d.ts.map +1 -0
- package/dist/schema-accounts.js +312 -0
- package/dist/schema-activities.d.ts +821 -0
- package/dist/schema-activities.d.ts.map +1 -0
- package/dist/schema-activities.js +92 -0
- package/dist/schema-relations.d.ts +47 -0
- package/dist/schema-relations.d.ts.map +1 -0
- package/dist/schema-relations.js +70 -0
- package/dist/schema-shared.d.ts +10 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +36 -0
- package/dist/schema-signals.d.ts +324 -0
- package/dist/schema-signals.d.ts.map +1 -0
- package/dist/schema-signals.js +80 -0
- package/dist/schema.d.ts +6 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +5 -0
- package/dist/service/accounts-merge.d.ts +63 -0
- package/dist/service/accounts-merge.d.ts.map +1 -0
- package/dist/service/accounts-merge.js +382 -0
- package/dist/service/accounts-organizations.d.ts +97 -0
- package/dist/service/accounts-organizations.d.ts.map +1 -0
- package/dist/service/accounts-organizations.js +70 -0
- package/dist/service/accounts-people.d.ts +1315 -0
- package/dist/service/accounts-people.d.ts.map +1 -0
- package/dist/service/accounts-people.js +409 -0
- package/dist/service/accounts-resolve.d.ts +76 -0
- package/dist/service/accounts-resolve.d.ts.map +1 -0
- package/dist/service/accounts-resolve.js +103 -0
- package/dist/service/accounts-shared.d.ts +68 -0
- package/dist/service/accounts-shared.d.ts.map +1 -0
- package/dist/service/accounts-shared.js +149 -0
- package/dist/service/accounts.d.ts +1465 -0
- package/dist/service/accounts.d.ts.map +1 -0
- package/dist/service/accounts.js +13 -0
- package/dist/service/activities.d.ts +486 -0
- package/dist/service/activities.d.ts.map +1 -0
- package/dist/service/activities.js +114 -0
- package/dist/service/custom-fields.d.ts +118 -0
- package/dist/service/custom-fields.d.ts.map +1 -0
- package/dist/service/custom-fields.js +88 -0
- package/dist/service/customer-signals.d.ts +733 -0
- package/dist/service/customer-signals.d.ts.map +1 -0
- package/dist/service/customer-signals.js +112 -0
- package/dist/service/helpers.d.ts +22 -0
- package/dist/service/helpers.d.ts.map +1 -0
- package/dist/service/helpers.js +39 -0
- package/dist/service/index.d.ts +4434 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +17 -0
- package/dist/service/person-documents.d.ts +1201 -0
- package/dist/service/person-documents.d.ts.map +1 -0
- package/dist/service/person-documents.js +240 -0
- package/dist/service/person-relationships.d.ts +502 -0
- package/dist/service/person-relationships.d.ts.map +1 -0
- package/dist/service/person-relationships.js +121 -0
- package/dist/validation.d.ts +3 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +1 -0
- 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
|
+
};
|