@voyantjs/crm 0.25.0 → 0.26.1

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 (40) hide show
  1. package/dist/index.d.ts +18 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +25 -6
  4. package/dist/route-runtime.d.ts +21 -0
  5. package/dist/route-runtime.d.ts.map +1 -0
  6. package/dist/route-runtime.js +28 -0
  7. package/dist/routes/accounts.d.ts +50 -2
  8. package/dist/routes/accounts.d.ts.map +1 -1
  9. package/dist/routes/index.d.ts +496 -2
  10. package/dist/routes/index.d.ts.map +1 -1
  11. package/dist/routes/index.js +2 -0
  12. package/dist/routes/person-documents.d.ts +458 -0
  13. package/dist/routes/person-documents.d.ts.map +1 -0
  14. package/dist/routes/person-documents.js +160 -0
  15. package/dist/schema-accounts.d.ts +367 -0
  16. package/dist/schema-accounts.d.ts.map +1 -1
  17. package/dist/schema-accounts.js +68 -2
  18. package/dist/schema-activities.js +2 -2
  19. package/dist/schema-relations.js +3 -3
  20. package/dist/schema-sales.js +2 -2
  21. package/dist/schema.d.ts +6 -6
  22. package/dist/schema.d.ts.map +1 -1
  23. package/dist/schema.js +6 -6
  24. package/dist/service/accounts-people.d.ts +49 -1
  25. package/dist/service/accounts-people.d.ts.map +1 -1
  26. package/dist/service/accounts-shared.d.ts +12 -0
  27. package/dist/service/accounts-shared.d.ts.map +1 -1
  28. package/dist/service/accounts-shared.js +4 -0
  29. package/dist/service/accounts.d.ts +49 -1
  30. package/dist/service/accounts.d.ts.map +1 -1
  31. package/dist/service/index.d.ts +1172 -1
  32. package/dist/service/index.d.ts.map +1 -1
  33. package/dist/service/index.js +3 -0
  34. package/dist/service/person-documents.d.ts +1188 -0
  35. package/dist/service/person-documents.d.ts.map +1 -0
  36. package/dist/service/person-documents.js +216 -0
  37. package/dist/validation.d.ts +168 -0
  38. package/dist/validation.d.ts.map +1 -1
  39. package/dist/validation.js +68 -0
  40. package/package.json +6 -5
@@ -0,0 +1,458 @@
1
+ import type { ModuleContainer } from "@voyantjs/core";
2
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
+ type Env = {
4
+ Variables: {
5
+ container?: ModuleContainer;
6
+ db: PostgresJsDatabase;
7
+ userId?: string;
8
+ };
9
+ };
10
+ export declare const personDocumentRoutes: import("hono/hono-base").HonoBase<Env, {
11
+ "/people/:id/documents": {
12
+ $get: {
13
+ input: {
14
+ param: {
15
+ id: string;
16
+ };
17
+ };
18
+ output: {
19
+ data: {
20
+ id: string;
21
+ personId: string;
22
+ type: "passport" | "visa" | "other" | "id_card" | "driver_license";
23
+ numberEncrypted: {
24
+ enc: string;
25
+ } | null;
26
+ issuingAuthority: string | null;
27
+ issuingCountry: string | null;
28
+ issueDate: string | null;
29
+ expiryDate: string | null;
30
+ attachmentId: string | null;
31
+ isPrimary: boolean;
32
+ notes: string | null;
33
+ metadata: {
34
+ [x: string]: import("hono/utils/types").JSONValue;
35
+ } | null;
36
+ createdAt: string;
37
+ updatedAt: string;
38
+ }[];
39
+ };
40
+ outputFormat: "json";
41
+ status: import("hono/utils/http-status").ContentfulStatusCode;
42
+ };
43
+ };
44
+ } & {
45
+ "/people/:id/documents": {
46
+ $post: {
47
+ input: {
48
+ param: {
49
+ id: string;
50
+ };
51
+ };
52
+ output: {
53
+ error: string;
54
+ };
55
+ outputFormat: "json";
56
+ status: 404;
57
+ } | {
58
+ input: {
59
+ param: {
60
+ id: string;
61
+ };
62
+ };
63
+ output: {
64
+ data: {
65
+ metadata: {
66
+ [x: string]: import("hono/utils/types").JSONValue;
67
+ } | null;
68
+ id: string;
69
+ createdAt: string;
70
+ updatedAt: string;
71
+ type: "passport" | "visa" | "other" | "id_card" | "driver_license";
72
+ issuingAuthority: string | null;
73
+ issuingCountry: string | null;
74
+ expiryDate: string | null;
75
+ issueDate: string | null;
76
+ notes: string | null;
77
+ isPrimary: boolean;
78
+ personId: string;
79
+ numberEncrypted: {
80
+ enc: string;
81
+ } | null;
82
+ attachmentId: string | null;
83
+ };
84
+ };
85
+ outputFormat: "json";
86
+ status: 201;
87
+ };
88
+ };
89
+ } & {
90
+ "/person-documents/:id": {
91
+ $get: {
92
+ input: {
93
+ param: {
94
+ id: string;
95
+ };
96
+ };
97
+ output: {
98
+ error: string;
99
+ };
100
+ outputFormat: "json";
101
+ status: 404;
102
+ } | {
103
+ input: {
104
+ param: {
105
+ id: string;
106
+ };
107
+ };
108
+ output: {
109
+ data: {
110
+ id: string;
111
+ personId: string;
112
+ type: "passport" | "visa" | "other" | "id_card" | "driver_license";
113
+ numberEncrypted: {
114
+ enc: string;
115
+ } | null;
116
+ issuingAuthority: string | null;
117
+ issuingCountry: string | null;
118
+ issueDate: string | null;
119
+ expiryDate: string | null;
120
+ attachmentId: string | null;
121
+ isPrimary: boolean;
122
+ notes: string | null;
123
+ metadata: {
124
+ [x: string]: import("hono/utils/types").JSONValue;
125
+ } | null;
126
+ createdAt: string;
127
+ updatedAt: string;
128
+ };
129
+ };
130
+ outputFormat: "json";
131
+ status: import("hono/utils/http-status").ContentfulStatusCode;
132
+ };
133
+ };
134
+ } & {
135
+ "/person-documents/:id": {
136
+ $patch: {
137
+ input: {
138
+ param: {
139
+ id: string;
140
+ };
141
+ };
142
+ output: {
143
+ error: string;
144
+ };
145
+ outputFormat: "json";
146
+ status: 404;
147
+ } | {
148
+ input: {
149
+ param: {
150
+ id: string;
151
+ };
152
+ };
153
+ output: {
154
+ data: {
155
+ id: string;
156
+ personId: string;
157
+ type: "passport" | "visa" | "other" | "id_card" | "driver_license";
158
+ numberEncrypted: {
159
+ enc: string;
160
+ } | null;
161
+ issuingAuthority: string | null;
162
+ issuingCountry: string | null;
163
+ issueDate: string | null;
164
+ expiryDate: string | null;
165
+ attachmentId: string | null;
166
+ isPrimary: boolean;
167
+ notes: string | null;
168
+ metadata: {
169
+ [x: string]: import("hono/utils/types").JSONValue;
170
+ } | null;
171
+ createdAt: string;
172
+ updatedAt: string;
173
+ };
174
+ };
175
+ outputFormat: "json";
176
+ status: import("hono/utils/http-status").ContentfulStatusCode;
177
+ };
178
+ };
179
+ } & {
180
+ "/person-documents/:id": {
181
+ $delete: {
182
+ input: {
183
+ param: {
184
+ id: string;
185
+ };
186
+ };
187
+ output: {
188
+ error: string;
189
+ };
190
+ outputFormat: "json";
191
+ status: 404;
192
+ } | {
193
+ input: {
194
+ param: {
195
+ id: string;
196
+ };
197
+ };
198
+ output: {
199
+ success: true;
200
+ };
201
+ outputFormat: "json";
202
+ status: import("hono/utils/http-status").ContentfulStatusCode;
203
+ };
204
+ };
205
+ } & {
206
+ "/person-documents/:id/set-primary": {
207
+ $post: {
208
+ input: {
209
+ param: {
210
+ id: string;
211
+ };
212
+ };
213
+ output: {
214
+ error: string;
215
+ };
216
+ outputFormat: "json";
217
+ status: 404;
218
+ } | {
219
+ input: {
220
+ param: {
221
+ id: string;
222
+ };
223
+ };
224
+ output: {
225
+ data: {
226
+ id: string;
227
+ personId: string;
228
+ type: "passport" | "visa" | "other" | "id_card" | "driver_license";
229
+ numberEncrypted: {
230
+ enc: string;
231
+ } | null;
232
+ issuingAuthority: string | null;
233
+ issuingCountry: string | null;
234
+ issueDate: string | null;
235
+ expiryDate: string | null;
236
+ attachmentId: string | null;
237
+ isPrimary: boolean;
238
+ notes: string | null;
239
+ metadata: {
240
+ [x: string]: import("hono/utils/types").JSONValue;
241
+ } | null;
242
+ createdAt: string;
243
+ updatedAt: string;
244
+ };
245
+ };
246
+ outputFormat: "json";
247
+ status: import("hono/utils/http-status").ContentfulStatusCode;
248
+ };
249
+ };
250
+ } & {
251
+ "/people/:id/travel-snapshot": {
252
+ $get: {
253
+ input: {
254
+ param: {
255
+ id: string;
256
+ };
257
+ };
258
+ output: {
259
+ error: string;
260
+ };
261
+ outputFormat: "json";
262
+ status: 503;
263
+ } | {
264
+ input: {
265
+ param: {
266
+ id: string;
267
+ };
268
+ };
269
+ output: {
270
+ error: string;
271
+ };
272
+ outputFormat: "json";
273
+ status: 404;
274
+ } | {
275
+ input: {
276
+ param: {
277
+ id: string;
278
+ };
279
+ };
280
+ output: {
281
+ data: {
282
+ dateOfBirth: string | null;
283
+ dietaryRequirements: string | null;
284
+ accessibilityNeeds: string | null;
285
+ passportNumber: string | null;
286
+ passportExpiry: string | null;
287
+ passportIssuingCountry: string | null;
288
+ passportIssuingAuthority: string | null;
289
+ passportPersonDocumentId: string | null;
290
+ };
291
+ };
292
+ outputFormat: "json";
293
+ status: import("hono/utils/http-status").ContentfulStatusCode;
294
+ };
295
+ };
296
+ } & {
297
+ "/people/:id/profile-pii": {
298
+ $patch: {
299
+ input: {
300
+ param: {
301
+ id: string;
302
+ };
303
+ };
304
+ output: {
305
+ error: string;
306
+ };
307
+ outputFormat: "json";
308
+ status: 503;
309
+ } | {
310
+ input: {
311
+ param: {
312
+ id: string;
313
+ };
314
+ };
315
+ output: {
316
+ error: string;
317
+ };
318
+ outputFormat: "json";
319
+ status: 400;
320
+ } | {
321
+ input: {
322
+ param: {
323
+ id: string;
324
+ };
325
+ };
326
+ output: {
327
+ error: string;
328
+ };
329
+ outputFormat: "json";
330
+ status: 404;
331
+ } | {
332
+ input: {
333
+ param: {
334
+ id: string;
335
+ };
336
+ };
337
+ output: {
338
+ success: true;
339
+ };
340
+ outputFormat: "json";
341
+ status: import("hono/utils/http-status").ContentfulStatusCode;
342
+ };
343
+ };
344
+ } & {
345
+ "/people/:id/documents/from-plaintext": {
346
+ $post: {
347
+ input: {
348
+ param: {
349
+ id: string;
350
+ };
351
+ };
352
+ output: {
353
+ error: string;
354
+ };
355
+ outputFormat: "json";
356
+ status: 503;
357
+ } | {
358
+ input: {
359
+ param: {
360
+ id: string;
361
+ };
362
+ };
363
+ output: {
364
+ error: string;
365
+ };
366
+ outputFormat: "json";
367
+ status: 404;
368
+ } | {
369
+ input: {
370
+ param: {
371
+ id: string;
372
+ };
373
+ };
374
+ output: {
375
+ data: {
376
+ metadata: {
377
+ [x: string]: import("hono/utils/types").JSONValue;
378
+ } | null;
379
+ id: string;
380
+ createdAt: string;
381
+ updatedAt: string;
382
+ type: "passport" | "visa" | "other" | "id_card" | "driver_license";
383
+ issuingAuthority: string | null;
384
+ issuingCountry: string | null;
385
+ expiryDate: string | null;
386
+ issueDate: string | null;
387
+ notes: string | null;
388
+ isPrimary: boolean;
389
+ personId: string;
390
+ numberEncrypted: {
391
+ enc: string;
392
+ } | null;
393
+ attachmentId: string | null;
394
+ };
395
+ };
396
+ outputFormat: "json";
397
+ status: 201;
398
+ };
399
+ };
400
+ } & {
401
+ "/person-documents/:id/from-plaintext": {
402
+ $patch: {
403
+ input: {
404
+ param: {
405
+ id: string;
406
+ };
407
+ };
408
+ output: {
409
+ error: string;
410
+ };
411
+ outputFormat: "json";
412
+ status: 503;
413
+ } | {
414
+ input: {
415
+ param: {
416
+ id: string;
417
+ };
418
+ };
419
+ output: {
420
+ error: string;
421
+ };
422
+ outputFormat: "json";
423
+ status: 404;
424
+ } | {
425
+ input: {
426
+ param: {
427
+ id: string;
428
+ };
429
+ };
430
+ output: {
431
+ data: {
432
+ id: string;
433
+ personId: string;
434
+ type: "passport" | "visa" | "other" | "id_card" | "driver_license";
435
+ numberEncrypted: {
436
+ enc: string;
437
+ } | null;
438
+ issuingAuthority: string | null;
439
+ issuingCountry: string | null;
440
+ issueDate: string | null;
441
+ expiryDate: string | null;
442
+ attachmentId: string | null;
443
+ isPrimary: boolean;
444
+ notes: string | null;
445
+ metadata: {
446
+ [x: string]: import("hono/utils/types").JSONValue;
447
+ } | null;
448
+ createdAt: string;
449
+ updatedAt: string;
450
+ };
451
+ };
452
+ outputFormat: "json";
453
+ status: import("hono/utils/http-status").ContentfulStatusCode;
454
+ };
455
+ };
456
+ }, "/", "/person-documents/:id/from-plaintext">;
457
+ export {};
458
+ //# sourceMappingURL=person-documents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"person-documents.d.ts","sourceRoot":"","sources":["../../src/routes/person-documents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAIrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAejE,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,SAAS,CAAC,EAAE,eAAe,CAAA;QAC3B,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AAiBD,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+CA8I7B,CAAA"}
@@ -0,0 +1,160 @@
1
+ import { parseJsonBody, parseQuery } from "@voyantjs/hono";
2
+ import { encryptOptionalJsonEnvelope } from "@voyantjs/utils";
3
+ import { eq } from "drizzle-orm";
4
+ import { Hono } from "hono";
5
+ import { CRM_ROUTE_RUNTIME_CONTAINER_KEY } from "../route-runtime.js";
6
+ import { people } from "../schema.js";
7
+ import { crmService } from "../service/index.js";
8
+ import { insertPersonDocumentFromPlaintextSchema, insertPersonDocumentSchema, personDocumentListQuerySchema, updatePersonDocumentFromPlaintextSchema, updatePersonDocumentSchema, updatePersonProfilePiiSchema, } from "../validation.js";
9
+ const peopleKeyRef = { keyType: "people" };
10
+ async function getCrmKms(c) {
11
+ const runtime = c.var.container?.resolve(CRM_ROUTE_RUNTIME_CONTAINER_KEY);
12
+ if (!runtime)
13
+ return null;
14
+ return runtime.getKmsProvider();
15
+ }
16
+ function kmsRequired(c) {
17
+ return c.json({ error: "KMS provider not configured — admin PII routes require a wired KMS key" }, 503);
18
+ }
19
+ export const personDocumentRoutes = new Hono()
20
+ .get("/people/:id/documents", async (c) => {
21
+ const query = parseQuery(c, personDocumentListQuerySchema);
22
+ return c.json({
23
+ data: await crmService.listPersonDocuments(c.get("db"), c.req.param("id"), query),
24
+ });
25
+ })
26
+ .post("/people/:id/documents", async (c) => {
27
+ const row = await crmService.createPersonDocument(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertPersonDocumentSchema));
28
+ if (!row)
29
+ return c.json({ error: "Person not found" }, 404);
30
+ return c.json({ data: row }, 201);
31
+ })
32
+ .get("/person-documents/:id", async (c) => {
33
+ const row = await crmService.getPersonDocument(c.get("db"), c.req.param("id"));
34
+ if (!row)
35
+ return c.json({ error: "Document not found" }, 404);
36
+ return c.json({ data: row });
37
+ })
38
+ .patch("/person-documents/:id", async (c) => {
39
+ const row = await crmService.updatePersonDocument(c.get("db"), c.req.param("id"), await parseJsonBody(c, updatePersonDocumentSchema));
40
+ if (!row)
41
+ return c.json({ error: "Document not found" }, 404);
42
+ return c.json({ data: row });
43
+ })
44
+ .delete("/person-documents/:id", async (c) => {
45
+ const row = await crmService.deletePersonDocument(c.get("db"), c.req.param("id"));
46
+ if (!row)
47
+ return c.json({ error: "Document not found" }, 404);
48
+ return c.json({ success: true });
49
+ })
50
+ .post("/person-documents/:id/set-primary", async (c) => {
51
+ const row = await crmService.setPrimaryPersonDocument(c.get("db"), c.req.param("id"));
52
+ if (!row)
53
+ return c.json({ error: "Document not found" }, 404);
54
+ return c.json({ data: row });
55
+ })
56
+ // ── Admin PII conveniences (server-side encrypt/decrypt) ──────────────
57
+ // Operator UIs (booking-traveler dialog) need to read a person's
58
+ // primary passport + free-text PII in plaintext to pre-fill forms,
59
+ // and write changes back without round-tripping ciphertext through
60
+ // the browser. Endpoints below use the request-scoped CRM runtime
61
+ // to access the people KMS key.
62
+ /**
63
+ * Decrypted snapshot of a person's primary passport + dietary +
64
+ * accessibility values. Used by the booking-traveler dialog to
65
+ * pre-fill snapshot fields when an operator picks an existing
66
+ * person. Returns 404 when person missing, 503 when KMS unwired.
67
+ */
68
+ .get("/people/:id/travel-snapshot", async (c) => {
69
+ const kms = await getCrmKms(c);
70
+ if (!kms)
71
+ return kmsRequired(c);
72
+ const snapshot = await crmService.loadPersonTravelSnapshot(c.get("db"), c.req.param("id"), {
73
+ kms,
74
+ });
75
+ if (!snapshot)
76
+ return c.json({ error: "Person not found" }, 404);
77
+ return c.json({ data: snapshot });
78
+ })
79
+ /**
80
+ * Plaintext PATCH for the four free-text PII slots on
81
+ * `crm.people`. The route encrypts each provided value server-side
82
+ * with the people KMS key. `null` clears a slot.
83
+ */
84
+ .patch("/people/:id/profile-pii", async (c) => {
85
+ const kms = await getCrmKms(c);
86
+ if (!kms)
87
+ return kmsRequired(c);
88
+ const body = await parseJsonBody(c, updatePersonProfilePiiSchema);
89
+ const updates = {};
90
+ for (const [key, column] of [
91
+ ["accessibility", "accessibilityEncrypted"],
92
+ ["dietary", "dietaryEncrypted"],
93
+ ["loyalty", "loyaltyEncrypted"],
94
+ ["insurance", "insuranceEncrypted"],
95
+ ]) {
96
+ const value = body[key];
97
+ if (value === undefined)
98
+ continue;
99
+ if (value === null) {
100
+ updates[column] = null;
101
+ }
102
+ else {
103
+ updates[column] = await encryptOptionalJsonEnvelope(kms, peopleKeyRef, { text: value });
104
+ }
105
+ }
106
+ if (Object.keys(updates).length === 0) {
107
+ return c.json({ error: "Nothing to update" }, 400);
108
+ }
109
+ const [row] = await c
110
+ .get("db")
111
+ .update(people)
112
+ .set({ ...updates, updatedAt: new Date() })
113
+ .where(eq(people.id, c.req.param("id")))
114
+ .returning({ id: people.id });
115
+ if (!row)
116
+ return c.json({ error: "Person not found" }, 404);
117
+ return c.json({ success: true });
118
+ })
119
+ /**
120
+ * Plaintext document create — accepts `number` as cleartext, the
121
+ * route encrypts via the people KMS key. Mirrors the existing
122
+ * `POST /people/:id/documents` shape but spares clients from
123
+ * holding KMS material.
124
+ */
125
+ .post("/people/:id/documents/from-plaintext", async (c) => {
126
+ const kms = await getCrmKms(c);
127
+ if (!kms)
128
+ return kmsRequired(c);
129
+ const body = await parseJsonBody(c, insertPersonDocumentFromPlaintextSchema);
130
+ const { number, ...rest } = body;
131
+ const numberEncrypted = number == null ? null : await encryptOptionalJsonEnvelope(kms, peopleKeyRef, { number });
132
+ const row = await crmService.createPersonDocument(c.get("db"), c.req.param("id"), {
133
+ ...rest,
134
+ ...(numberEncrypted !== undefined ? { numberEncrypted } : {}),
135
+ });
136
+ if (!row)
137
+ return c.json({ error: "Person not found" }, 404);
138
+ return c.json({ data: row }, 201);
139
+ })
140
+ /**
141
+ * Plaintext document update — same encryption convention as the
142
+ * create variant. Only fields explicitly provided are written;
143
+ * `number: null` clears the encrypted slot.
144
+ */
145
+ .patch("/person-documents/:id/from-plaintext", async (c) => {
146
+ const kms = await getCrmKms(c);
147
+ if (!kms)
148
+ return kmsRequired(c);
149
+ const body = await parseJsonBody(c, updatePersonDocumentFromPlaintextSchema);
150
+ const { number, ...rest } = body;
151
+ const updateInput = { ...rest };
152
+ if (number !== undefined) {
153
+ updateInput.numberEncrypted =
154
+ number === null ? null : await encryptOptionalJsonEnvelope(kms, peopleKeyRef, { number });
155
+ }
156
+ const row = await crmService.updatePersonDocument(c.get("db"), c.req.param("id"), updateInput);
157
+ if (!row)
158
+ return c.json({ error: "Document not found" }, 404);
159
+ return c.json({ data: row });
160
+ });