@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":"accounts-people.d.ts","sourceRoot":"","sources":["../../src/service/accounts-people.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAWjE,OAAO,KAAK,EACV,8BAA8B,EAC9B,8BAA8B,EAC/B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,EAChC,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EAIvB,KAAK,eAAe,EAIpB,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAA;AA8D7B,eAAO,MAAM,qBAAqB;mBACX,kBAAkB,SAAS,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sBAkDvC,kBAAkB,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAO/B,kBAAkB,QAAQ,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAgB3C,kBAAkB,MAAM,MAAM,QAAQ,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkBvD,kBAAkB,MAAM,MAAM;;;2BAO/C,kBAAkB,cACV,cAAc,GAAG,QAAQ,YAC3B,MAAM;;;;;;;;;;;;;;4BAUZ,kBAAkB,cACV,cAAc,GAAG,QAAQ,YAC3B,MAAM,QACV,uBAAuB;;;;;;;;;;;;;;4BASD,kBAAkB,MAAM,MAAM,QAAQ,uBAAuB;;;;;;;;;;;;;;4BAI7D,kBAAkB,MAAM,MAAM;;;sBAI1C,kBAAkB,cAAc,cAAc,GAAG,QAAQ,YAAY,MAAM;;;;;;;;;;;;;;;;;;;;;sBASvF,kBAAkB,cACV,cAAc,GAAG,QAAQ,YAC3B,MAAM,QACV,kBAAkB;;;;;;;;;;;;;;;;;;;;;sBASF,kBAAkB,MAAM,MAAM,QAAQ,kBAAkB;;;;;;;;;;;;;;;;;;;;;sBAIxD,kBAAkB,MAAM,MAAM;;;wBAIlC,kBAAkB,YAAY,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBASlD,kBAAkB,YACZ,MAAM,UACR,MAAM,QACR,qBAAqB;;;;;;;8BAgBH,kBAAkB,kBAAkB,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAS9D,kBAAkB,kBACN,MAAM,UACd,MAAM,QACR,2BAA2B;;;;;;;yBAgBR,kBAAkB,MAAM,MAAM,WAAW,MAAM;;;;;;;yBAS/C,kBAAkB,MAAM,MAAM;;;;;;;iCAO5B,kBAAkB,YAAY,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAS3D,kBAAkB,YACZ,MAAM,QACV,8BAA8B;;;;;;;;;;;;kCAwBhC,kBAAkB,MAClB,MAAM,QACJ,8BAA8B;;;;;;;;;;;;kCAuBF,kBAAkB,MAAM,MAAM;;;;;;;;;;;;+BAQjC,kBAAkB,MAAM,MAAM,WAAW,MAAM;;;;;;;+BAS/C,kBAAkB,MAAM,MAAM;;;;;;;2BAMzD,kBAAkB,YACZ,MAAM,SACT,sBAAsB;;;;;;;;;;;4BAmBzB,kBAAkB,YACZ,MAAM,QACV,2BAA2B;;;;;;;;;;;qBAwBlB,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sBAIX,kBAAkB,QAAQ,kBAAkB;;;;;;;;sBAK5C,kBAAkB,aAAa,MAAM;;;wBAQnC,kBAAkB;wBA2BlB,kBAAkB,WAAW,MAAM;;;;;;;iBAuBtC,MAAM;mBAAS,MAAM;;;;CA4B7C,CAAA"}
@@ -0,0 +1,409 @@
1
+ // agent-quality: file-size exception -- owner: crm; existing service module stays co-located until a dedicated split preserves behavior and tests.
2
+ import { identityContactPoints } from "@voyant-travel/identity/schema";
3
+ import { identityService } from "@voyant-travel/identity/service";
4
+ import { toCsvRow } from "@voyant-travel/utils";
5
+ import { and, asc, desc, eq, exists, gte, ilike, lte, or, sql } from "drizzle-orm";
6
+ import { communicationLog, organizationNotes, organizations, people, personNotes, personPaymentMethods, segments, } from "../schema.js";
7
+ import { deletePersonIdentity, hydratePeople, organizationEntityType, personBaseFields, personEntityType, syncPersonIdentity, } from "./accounts-shared.js";
8
+ import { paginate } from "./helpers.js";
9
+ function unaccentedIlike(column, term) {
10
+ // agent-quality: raw-sql reviewed -- owner: crm; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
11
+ return sql `unaccent(coalesce(${column}, '')) ILIKE unaccent(${term})`;
12
+ }
13
+ function buildPersonSearchCondition(db, search) {
14
+ const trimmedSearch = search.trim();
15
+ if (!trimmedSearch)
16
+ return undefined;
17
+ const term = `%${trimmedSearch}%`;
18
+ const tokens = trimmedSearch.split(/\s+/).filter(Boolean);
19
+ const digits = trimmedSearch.replace(/\D/g, "");
20
+ const searchablePersonColumns = [
21
+ people.firstName,
22
+ people.middleName,
23
+ people.lastName,
24
+ people.jobTitle,
25
+ ];
26
+ const contactPointConditions = [
27
+ ilike(identityContactPoints.value, term),
28
+ ilike(identityContactPoints.normalizedValue, term),
29
+ ];
30
+ if (digits) {
31
+ const digitsTerm = `%${digits}%`;
32
+ contactPointConditions.push(
33
+ // agent-quality: raw-sql reviewed -- owner: crm; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
34
+ sql `regexp_replace(${identityContactPoints.value}, '[^0-9]+', '', 'g') ILIKE ${digitsTerm}`,
35
+ // agent-quality: raw-sql reviewed -- owner: crm; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
36
+ sql `regexp_replace(coalesce(${identityContactPoints.normalizedValue}, ''), '[^0-9]+', '', 'g') ILIKE ${digitsTerm}`);
37
+ }
38
+ const tokenizedPersonCondition = tokens.length
39
+ ? and(...tokens.map((token) => or(...searchablePersonColumns.map((column) => unaccentedIlike(column, `%${token}%`)))))
40
+ : undefined;
41
+ return or(tokenizedPersonCondition, exists(db
42
+ .select({ one: sql `1` })
43
+ .from(identityContactPoints)
44
+ .where(and(eq(identityContactPoints.entityType, personEntityType), eq(identityContactPoints.entityId, people.id), or(eq(identityContactPoints.kind, "email"), eq(identityContactPoints.kind, "phone")), or(...contactPointConditions)))));
45
+ }
46
+ export const peopleAccountsService = {
47
+ async listPeople(db, query) {
48
+ const conditions = [];
49
+ if (query.organizationId)
50
+ conditions.push(eq(people.organizationId, query.organizationId));
51
+ if (query.ownerId)
52
+ conditions.push(eq(people.ownerId, query.ownerId));
53
+ if (query.relation)
54
+ conditions.push(eq(people.relation, query.relation));
55
+ if (query.status)
56
+ conditions.push(eq(people.status, query.status));
57
+ if (query.search) {
58
+ const searchCondition = buildPersonSearchCondition(db, query.search);
59
+ if (searchCondition)
60
+ conditions.push(searchCondition);
61
+ }
62
+ const where = conditions.length ? and(...conditions) : undefined;
63
+ const sortColumns = (() => {
64
+ switch (query.sortBy) {
65
+ case "name":
66
+ return [people.firstName, people.lastName];
67
+ case "relation":
68
+ return [people.relation];
69
+ case "status":
70
+ return [people.status];
71
+ case "createdAt":
72
+ return [people.createdAt];
73
+ default:
74
+ return [people.updatedAt];
75
+ }
76
+ })();
77
+ const sortFn = query.sortDir === "asc" ? asc : desc;
78
+ const orderBy = [...sortColumns.map((col) => sortFn(col)), desc(people.updatedAt)];
79
+ const result = await paginate(db
80
+ .select()
81
+ .from(people)
82
+ .where(where)
83
+ .limit(query.limit)
84
+ .offset(query.offset)
85
+ .orderBy(...orderBy), db.select({ count: sql `count(*)::int` }).from(people).where(where), query.limit, query.offset);
86
+ return {
87
+ ...result,
88
+ data: await hydratePeople(db, result.data, { fallbackOnError: true }),
89
+ };
90
+ },
91
+ async getPersonById(db, id) {
92
+ const [row] = await db.select().from(people).where(eq(people.id, id)).limit(1);
93
+ if (!row)
94
+ return null;
95
+ const [hydrated] = await hydratePeople(db, [row]);
96
+ return hydrated ?? null;
97
+ },
98
+ async createPerson(db, data) {
99
+ const [row] = await db
100
+ .insert(people)
101
+ .values({
102
+ ...personBaseFields(data),
103
+ firstName: data.firstName,
104
+ lastName: data.lastName,
105
+ })
106
+ .returning();
107
+ if (!row) {
108
+ throw new Error("Failed to create person");
109
+ }
110
+ await syncPersonIdentity(db, row.id, data);
111
+ return this.getPersonById(db, row.id);
112
+ },
113
+ async updatePerson(db, id, data) {
114
+ const existing = await this.getPersonById(db, id);
115
+ if (!existing)
116
+ return null;
117
+ await db
118
+ .update(people)
119
+ .set({ ...personBaseFields(data), updatedAt: new Date() })
120
+ .where(eq(people.id, id));
121
+ await syncPersonIdentity(db, id, {
122
+ email: data.email === undefined ? existing.email : data.email,
123
+ phone: data.phone === undefined ? existing.phone : data.phone,
124
+ website: data.website === undefined ? existing.website : data.website,
125
+ });
126
+ return this.getPersonById(db, id);
127
+ },
128
+ async deletePerson(db, id) {
129
+ await deletePersonIdentity(db, id);
130
+ const [row] = await db.delete(people).where(eq(people.id, id)).returning({ id: people.id });
131
+ return row ?? null;
132
+ },
133
+ listContactMethods(db, entityType, entityId) {
134
+ return identityService.listContactPointsForEntity(db, entityType === "organization" ? organizationEntityType : personEntityType, entityId);
135
+ },
136
+ async createContactMethod(db, entityType, entityId, data) {
137
+ return identityService.createContactPoint(db, {
138
+ ...data,
139
+ entityType: entityType === "organization" ? organizationEntityType : personEntityType,
140
+ entityId,
141
+ });
142
+ },
143
+ async updateContactMethod(db, id, data) {
144
+ return identityService.updateContactPoint(db, id, data);
145
+ },
146
+ async deleteContactMethod(db, id) {
147
+ return identityService.deleteContactPoint(db, id);
148
+ },
149
+ listAddresses(db, entityType, entityId) {
150
+ return identityService.listAddressesForEntity(db, entityType === "organization" ? organizationEntityType : personEntityType, entityId);
151
+ },
152
+ async createAddress(db, entityType, entityId, data) {
153
+ return identityService.createAddress(db, {
154
+ ...data,
155
+ entityType: entityType === "organization" ? organizationEntityType : personEntityType,
156
+ entityId,
157
+ });
158
+ },
159
+ async updateAddress(db, id, data) {
160
+ return identityService.updateAddress(db, id, data);
161
+ },
162
+ async deleteAddress(db, id) {
163
+ return identityService.deleteAddress(db, id);
164
+ },
165
+ listPersonNotes(db, personId) {
166
+ return db
167
+ .select()
168
+ .from(personNotes)
169
+ .where(eq(personNotes.personId, personId))
170
+ .orderBy(personNotes.createdAt);
171
+ },
172
+ async createPersonNote(db, personId, userId, data) {
173
+ const [existing] = await db
174
+ .select({ id: people.id })
175
+ .from(people)
176
+ .where(eq(people.id, personId))
177
+ .limit(1);
178
+ if (!existing)
179
+ return null;
180
+ const [row] = await db
181
+ .insert(personNotes)
182
+ .values({ personId, authorId: userId, content: data.content })
183
+ .returning();
184
+ return row;
185
+ },
186
+ listOrganizationNotes(db, organizationId) {
187
+ return db
188
+ .select()
189
+ .from(organizationNotes)
190
+ .where(eq(organizationNotes.organizationId, organizationId))
191
+ .orderBy(organizationNotes.createdAt);
192
+ },
193
+ async createOrganizationNote(db, organizationId, userId, data) {
194
+ const [existing] = await db
195
+ .select({ id: organizations.id })
196
+ .from(organizations)
197
+ .where(eq(organizations.id, organizationId))
198
+ .limit(1);
199
+ if (!existing)
200
+ return null;
201
+ const [row] = await db
202
+ .insert(organizationNotes)
203
+ .values({ organizationId, authorId: userId, content: data.content })
204
+ .returning();
205
+ return row;
206
+ },
207
+ async updatePersonNote(db, id, content) {
208
+ const [row] = await db
209
+ .update(personNotes)
210
+ .set({ content })
211
+ .where(eq(personNotes.id, id))
212
+ .returning();
213
+ return row ?? null;
214
+ },
215
+ async deletePersonNote(db, id) {
216
+ const [row] = await db.delete(personNotes).where(eq(personNotes.id, id)).returning();
217
+ return row ?? null;
218
+ },
219
+ // ── Payment methods ────────────────────────────────────────────────────
220
+ listPersonPaymentMethods(db, personId) {
221
+ return db
222
+ .select()
223
+ .from(personPaymentMethods)
224
+ .where(eq(personPaymentMethods.personId, personId))
225
+ .orderBy(desc(personPaymentMethods.isDefault), desc(personPaymentMethods.createdAt));
226
+ },
227
+ async createPersonPaymentMethod(db, personId, data) {
228
+ const [existing] = await db
229
+ .select({ id: people.id })
230
+ .from(people)
231
+ .where(eq(people.id, personId))
232
+ .limit(1);
233
+ if (!existing)
234
+ return null;
235
+ if (data.isDefault) {
236
+ // Only one default per person — clear the others first.
237
+ await db
238
+ .update(personPaymentMethods)
239
+ .set({ isDefault: false })
240
+ .where(eq(personPaymentMethods.personId, personId));
241
+ }
242
+ const [row] = await db
243
+ .insert(personPaymentMethods)
244
+ .values({ personId, ...data })
245
+ .returning();
246
+ return row ?? null;
247
+ },
248
+ async updatePersonPaymentMethod(db, id, data) {
249
+ if (data.isDefault) {
250
+ const [target] = await db
251
+ .select({ personId: personPaymentMethods.personId })
252
+ .from(personPaymentMethods)
253
+ .where(eq(personPaymentMethods.id, id))
254
+ .limit(1);
255
+ if (target) {
256
+ await db
257
+ .update(personPaymentMethods)
258
+ .set({ isDefault: false })
259
+ .where(eq(personPaymentMethods.personId, target.personId));
260
+ }
261
+ }
262
+ const [row] = await db
263
+ .update(personPaymentMethods)
264
+ .set(data)
265
+ .where(eq(personPaymentMethods.id, id))
266
+ .returning();
267
+ return row ?? null;
268
+ },
269
+ async deletePersonPaymentMethod(db, id) {
270
+ const [row] = await db
271
+ .delete(personPaymentMethods)
272
+ .where(eq(personPaymentMethods.id, id))
273
+ .returning();
274
+ return row ?? null;
275
+ },
276
+ async updateOrganizationNote(db, id, content) {
277
+ const [row] = await db
278
+ .update(organizationNotes)
279
+ .set({ content })
280
+ .where(eq(organizationNotes.id, id))
281
+ .returning();
282
+ return row ?? null;
283
+ },
284
+ async deleteOrganizationNote(db, id) {
285
+ const [row] = await db.delete(organizationNotes).where(eq(organizationNotes.id, id)).returning();
286
+ return row ?? null;
287
+ },
288
+ async listCommunications(db, personId, query) {
289
+ const conditions = [eq(communicationLog.personId, personId)];
290
+ if (query.channel)
291
+ conditions.push(eq(communicationLog.channel, query.channel));
292
+ if (query.direction)
293
+ conditions.push(eq(communicationLog.direction, query.direction));
294
+ if (query.dateFrom)
295
+ conditions.push(gte(communicationLog.createdAt, new Date(query.dateFrom)));
296
+ if (query.dateTo)
297
+ conditions.push(lte(communicationLog.createdAt, new Date(query.dateTo)));
298
+ return db
299
+ .select()
300
+ .from(communicationLog)
301
+ .where(and(...conditions))
302
+ .limit(query.limit)
303
+ .offset(query.offset)
304
+ .orderBy(desc(communicationLog.createdAt));
305
+ },
306
+ async createCommunication(db, personId, data) {
307
+ const [existing] = await db
308
+ .select({ id: people.id })
309
+ .from(people)
310
+ .where(eq(people.id, personId))
311
+ .limit(1);
312
+ if (!existing)
313
+ return null;
314
+ const [row] = await db
315
+ .insert(communicationLog)
316
+ .values({
317
+ personId,
318
+ organizationId: data.organizationId ?? null,
319
+ channel: data.channel,
320
+ direction: data.direction,
321
+ subject: data.subject ?? null,
322
+ content: data.content ?? null,
323
+ sentAt: data.sentAt ? new Date(data.sentAt) : null,
324
+ })
325
+ .returning();
326
+ return row;
327
+ },
328
+ listSegments(db) {
329
+ return db.select().from(segments).orderBy(segments.createdAt);
330
+ },
331
+ async createSegment(db, data) {
332
+ const [row] = await db.insert(segments).values(data).returning();
333
+ return row;
334
+ },
335
+ async deleteSegment(db, segmentId) {
336
+ const [row] = await db
337
+ .delete(segments)
338
+ .where(eq(segments.id, segmentId))
339
+ .returning({ id: segments.id });
340
+ return row ?? null;
341
+ },
342
+ async exportPeopleCsv(db) {
343
+ const rows = await hydratePeople(db, await db.select().from(people).orderBy(people.createdAt));
344
+ const headers = [
345
+ "id",
346
+ "firstName",
347
+ "lastName",
348
+ "jobTitle",
349
+ "relation",
350
+ "preferredLanguage",
351
+ "preferredCurrency",
352
+ "email",
353
+ "phone",
354
+ "website",
355
+ "organizationId",
356
+ ];
357
+ const csvLines = [toCsvRow(headers)];
358
+ for (const row of rows) {
359
+ // toCsvRow quotes delimiters/quotes/newlines AND neutralizes
360
+ // spreadsheet formula-injection prefixes (= + - @ tab CR); see L4.
361
+ csvLines.push(toCsvRow(headers.map((header) => row[header])));
362
+ }
363
+ return csvLines.join("\n");
364
+ },
365
+ async importPeopleCsv(db, csvText) {
366
+ const lines = csvText.split("\n").filter((line) => line.trim());
367
+ if (lines.length < 2) {
368
+ return { error: "CSV must have a header row and at least one data row" };
369
+ }
370
+ const headers = lines[0].split(",").map((header) => header.trim());
371
+ const rows = [];
372
+ for (let i = 1; i < lines.length; i++) {
373
+ const values = lines[i].split(",").map((value) => value.trim());
374
+ const row = {};
375
+ for (let j = 0; j < headers.length; j++) {
376
+ const header = headers[j];
377
+ const value = values[j];
378
+ if (header && value) {
379
+ row[header] = value;
380
+ }
381
+ }
382
+ rows.push(row);
383
+ }
384
+ const imported = [];
385
+ const errors = [];
386
+ for (let i = 0; i < rows.length; i++) {
387
+ const row = rows[i];
388
+ const result = (await import("../validation.js")).insertPersonSchema.safeParse({
389
+ firstName: row.firstName || "",
390
+ lastName: row.lastName || "",
391
+ jobTitle: row.jobTitle || null,
392
+ relation: row.relation || null,
393
+ preferredLanguage: row.preferredLanguage || null,
394
+ preferredCurrency: row.preferredCurrency || null,
395
+ email: row.email || null,
396
+ phone: row.phone || null,
397
+ website: row.website || null,
398
+ organizationId: row.organizationId || null,
399
+ tags: [],
400
+ });
401
+ if (!result.success) {
402
+ errors.push({ row: i + 2, error: result.error.message });
403
+ continue;
404
+ }
405
+ imported.push(await this.createPerson(db, result.data));
406
+ }
407
+ return { imported: imported.length, errors };
408
+ },
409
+ };
@@ -0,0 +1,76 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import type { people } from "../schema.js";
3
+ /**
4
+ * Best-effort contact snapshot used by booking/storefront flows to
5
+ * resolve (or upsert) a CRM person from billing or traveler payloads.
6
+ * Empty-string and whitespace-only fields are normalized to `null`.
7
+ */
8
+ export interface PersonContactInput {
9
+ firstName?: string | null;
10
+ lastName?: string | null;
11
+ /**
12
+ * Single-string display name when first/last aren't provided
13
+ * separately — split on whitespace.
14
+ */
15
+ name?: string | null;
16
+ email?: string | null;
17
+ phone?: string | null;
18
+ preferredLanguage?: string | null;
19
+ }
20
+ export interface UpsertPersonFromContactOptions {
21
+ /** `source` recorded on the `people` row (`"storefront-booking"`, etc.). */
22
+ source?: string | null;
23
+ /** `source_ref` recorded on the `people` row — e.g. the session id. */
24
+ sourceRef?: string | null;
25
+ /** Tags propagated to the new person when one is created. */
26
+ tags?: string[];
27
+ /** Status override; defaults to `"active"`. */
28
+ status?: "active" | "inactive";
29
+ /**
30
+ * When true, returns `null` instead of creating a person if the
31
+ * snapshot has no email or phone dedupe key.
32
+ */
33
+ requireContactPoint?: boolean;
34
+ }
35
+ /**
36
+ * Derives `{ firstName, lastName }` for a CRM person from whatever
37
+ * contact bits the caller has. Mirrors the storefront-intake helpers
38
+ * (`personNameFromContact` / `personNameFromNewsletter`) so identity
39
+ * resolution from booking/traveler payloads stays symmetric. Falls
40
+ * back to the email local-part before resorting to literal placeholders
41
+ * — `people.first_name`/`last_name` are NOT NULL and the issue #961
42
+ * acceptance criteria call out that the literal `"Unknown"` should
43
+ * never be inserted.
44
+ */
45
+ export declare function personNameFromContact(input: PersonContactInput): {
46
+ firstName: string;
47
+ lastName: string;
48
+ };
49
+ /**
50
+ * Returns the first person whose normalized contact point matches
51
+ * `(kind, value)`. Email and website are normalized to lowercase; phone
52
+ * is trimmed. Returns `null` when no match exists, the value resolves to
53
+ * an empty string, or the matching contact point is attached to a
54
+ * non-person entity (organizations share the same table).
55
+ */
56
+ export declare function findPersonByContactPoint(db: PostgresJsDatabase, query: {
57
+ kind: "email" | "phone" | "website";
58
+ value: string | null | undefined;
59
+ }): Promise<typeof people.$inferSelect | null>;
60
+ /**
61
+ * Finds an existing CRM person by normalized email (then phone as a
62
+ * fallback) or creates a new one from the supplied contact snapshot.
63
+ * Used by booking session bootstrap + confirm flows so storefront
64
+ * bookings produce a real CRM record without each consumer reinventing
65
+ * the dedupe key. The created row carries the supplied `source` /
66
+ * `sourceRef` so the audit trail mirrors lead/newsletter signals.
67
+ *
68
+ * Lookup order: email → phone. The first hit wins; ties never happen
69
+ * because identity contact points are unique per `(kind, normalized)`
70
+ * for a given entity but consumers can re-use the same email across
71
+ * people via the unique-by-person sync logic. In that case the first
72
+ * person we see is returned — callers that need stricter ownership
73
+ * semantics should resolve themselves and pass a `personId` directly.
74
+ */
75
+ export declare function upsertPersonFromContact(db: PostgresJsDatabase, contact: PersonContactInput, options?: UpsertPersonFromContactOptions): Promise<typeof people.$inferSelect | null>;
76
+ //# sourceMappingURL=accounts-resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accounts-resolve.d.ts","sourceRoot":"","sources":["../../src/service/accounts-resolve.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAK1C;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAClC;AAED,MAAM,WAAW,8BAA8B;IAC7C,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,+CAA+C;IAC/C,MAAM,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAA;IAC9B;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B;AAaD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,kBAAkB,GAAG;IAChE,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB,CAUA;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE;IAAE,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAE,GAC/E,OAAO,CAAC,OAAO,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,CAmB5C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,kBAAkB,EACtB,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE,8BAAmC,GAC3C,OAAO,CAAC,OAAO,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,CAgC5C"}
@@ -0,0 +1,103 @@
1
+ import { identityContactPoints } from "@voyant-travel/identity/schema";
2
+ import { and, eq } from "drizzle-orm";
3
+ import { peopleAccountsService } from "./accounts-people.js";
4
+ import { personEntityType } from "./accounts-shared.js";
5
+ import { normalizeContactValue, toNullableTrimmed } from "./helpers.js";
6
+ function splitName(name) {
7
+ if (!name)
8
+ return {};
9
+ const parts = name.trim().split(/\s+/);
10
+ if (parts.length === 0)
11
+ return {};
12
+ if (parts.length === 1)
13
+ return { firstName: parts[0] };
14
+ return { firstName: parts[0], lastName: parts.slice(1).join(" ") };
15
+ }
16
+ /**
17
+ * Derives `{ firstName, lastName }` for a CRM person from whatever
18
+ * contact bits the caller has. Mirrors the storefront-intake helpers
19
+ * (`personNameFromContact` / `personNameFromNewsletter`) so identity
20
+ * resolution from booking/traveler payloads stays symmetric. Falls
21
+ * back to the email local-part before resorting to literal placeholders
22
+ * — `people.first_name`/`last_name` are NOT NULL and the issue #961
23
+ * acceptance criteria call out that the literal `"Unknown"` should
24
+ * never be inserted.
25
+ */
26
+ export function personNameFromContact(input) {
27
+ const split = splitName(input.name ?? null);
28
+ const emailLocalPart = input.email
29
+ ?.split("@")[0]
30
+ ?.replace(/[._-]+/g, " ")
31
+ .trim();
32
+ const firstName = toNullableTrimmed(input.firstName) ?? split.firstName ?? emailLocalPart ?? "Customer";
33
+ const lastName = toNullableTrimmed(input.lastName) ?? split.lastName ?? "Guest";
34
+ return { firstName, lastName };
35
+ }
36
+ /**
37
+ * Returns the first person whose normalized contact point matches
38
+ * `(kind, value)`. Email and website are normalized to lowercase; phone
39
+ * is trimmed. Returns `null` when no match exists, the value resolves to
40
+ * an empty string, or the matching contact point is attached to a
41
+ * non-person entity (organizations share the same table).
42
+ */
43
+ export async function findPersonByContactPoint(db, query) {
44
+ const value = toNullableTrimmed(query.value);
45
+ if (!value)
46
+ return null;
47
+ const normalized = normalizeContactValue(query.kind, value);
48
+ const [row] = await db
49
+ .select({ personId: identityContactPoints.entityId })
50
+ .from(identityContactPoints)
51
+ .where(and(eq(identityContactPoints.entityType, personEntityType), eq(identityContactPoints.kind, query.kind), eq(identityContactPoints.normalizedValue, normalized)))
52
+ .limit(1);
53
+ if (!row)
54
+ return null;
55
+ return peopleAccountsService.getPersonById(db, row.personId);
56
+ }
57
+ /**
58
+ * Finds an existing CRM person by normalized email (then phone as a
59
+ * fallback) or creates a new one from the supplied contact snapshot.
60
+ * Used by booking session bootstrap + confirm flows so storefront
61
+ * bookings produce a real CRM record without each consumer reinventing
62
+ * the dedupe key. The created row carries the supplied `source` /
63
+ * `sourceRef` so the audit trail mirrors lead/newsletter signals.
64
+ *
65
+ * Lookup order: email → phone. The first hit wins; ties never happen
66
+ * because identity contact points are unique per `(kind, normalized)`
67
+ * for a given entity but consumers can re-use the same email across
68
+ * people via the unique-by-person sync logic. In that case the first
69
+ * person we see is returned — callers that need stricter ownership
70
+ * semantics should resolve themselves and pass a `personId` directly.
71
+ */
72
+ export async function upsertPersonFromContact(db, contact, options = {}) {
73
+ const email = toNullableTrimmed(contact.email);
74
+ const phone = toNullableTrimmed(contact.phone);
75
+ if (email) {
76
+ const existing = await findPersonByContactPoint(db, { kind: "email", value: email });
77
+ if (existing)
78
+ return existing;
79
+ }
80
+ if (phone) {
81
+ const existing = await findPersonByContactPoint(db, { kind: "phone", value: phone });
82
+ if (existing)
83
+ return existing;
84
+ }
85
+ if (options.requireContactPoint && !email && !phone) {
86
+ return null;
87
+ }
88
+ // No match — create a new person. `personNameFromContact` ensures
89
+ // first/last name are populated even when only an email is on file.
90
+ const { firstName, lastName } = personNameFromContact(contact);
91
+ return peopleAccountsService.createPerson(db, {
92
+ firstName,
93
+ lastName,
94
+ email,
95
+ phone,
96
+ website: null,
97
+ status: options.status ?? "active",
98
+ source: options.source ?? null,
99
+ sourceRef: options.sourceRef ?? null,
100
+ preferredLanguage: toNullableTrimmed(contact.preferredLanguage),
101
+ tags: options.tags ?? [],
102
+ });
103
+ }
@@ -0,0 +1,68 @@
1
+ import type { insertAddressSchema, insertContactPointSchema, updateAddressSchema, updateContactPointSchema } from "@voyant-travel/identity/validation";
2
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
+ import type { z } from "zod";
4
+ import type { communicationListQuerySchema, insertCommunicationLogSchema, insertOrganizationNoteSchema, insertOrganizationSchema, insertPersonNoteSchema, insertPersonSchema, insertSegmentSchema, organizationListQuerySchema, personListQuerySchema, updateOrganizationSchema, updatePersonSchema } from "../validation.js";
5
+ export type OrganizationListQuery = z.infer<typeof organizationListQuerySchema>;
6
+ export type CreateOrganizationInput = z.infer<typeof insertOrganizationSchema>;
7
+ export type UpdateOrganizationInput = z.infer<typeof updateOrganizationSchema>;
8
+ export type PersonListQuery = z.infer<typeof personListQuerySchema>;
9
+ export type CreatePersonInput = z.infer<typeof insertPersonSchema>;
10
+ export type UpdatePersonInput = z.infer<typeof updatePersonSchema>;
11
+ export type CreateContactPointInput = z.infer<typeof insertContactPointSchema>;
12
+ export type UpdateContactPointInput = z.infer<typeof updateContactPointSchema>;
13
+ export type CreateAddressInput = z.infer<typeof insertAddressSchema>;
14
+ export type UpdateAddressInput = z.infer<typeof updateAddressSchema>;
15
+ export type CreatePersonNoteInput = z.infer<typeof insertPersonNoteSchema>;
16
+ export type CreateOrganizationNoteInput = z.infer<typeof insertOrganizationNoteSchema>;
17
+ export type CreateCommunicationLogInput = z.infer<typeof insertCommunicationLogSchema>;
18
+ export type CommunicationListQuery = z.infer<typeof communicationListQuerySchema>;
19
+ export type CreateSegmentInput = z.infer<typeof insertSegmentSchema>;
20
+ export declare const organizationEntityType = "organization";
21
+ export declare const personEntityType = "person";
22
+ export declare const personBaseIdentitySource = "relationships.person.base";
23
+ type PersonIdentityInput = Pick<CreatePersonInput, "email" | "phone" | "website">;
24
+ export type PersonHydratedFields = {
25
+ email: string | null;
26
+ phone: string | null;
27
+ website: string | null;
28
+ };
29
+ type HydratePeopleOptions = {
30
+ fallbackOnError?: boolean;
31
+ };
32
+ export declare function personBaseFields(data: CreatePersonInput | UpdatePersonInput): {
33
+ organizationId: string | null | undefined;
34
+ firstName: string | undefined;
35
+ middleName: string | null | undefined;
36
+ lastName: string | undefined;
37
+ gender: "M" | "F" | "X" | null | undefined;
38
+ jobTitle: string | null | undefined;
39
+ relation: "other" | "partner" | "supplier" | "client" | null | undefined;
40
+ preferredLanguage: string | null | undefined;
41
+ preferredCurrency: string | null | undefined;
42
+ ownerId: string | null | undefined;
43
+ status: "active" | "inactive" | "archived" | undefined;
44
+ source: string | null | undefined;
45
+ sourceRef: string | null | undefined;
46
+ tags: string[] | undefined;
47
+ dateOfBirth: string | null | undefined;
48
+ notes: string | null | undefined;
49
+ accessibilityEncrypted: {
50
+ enc: string;
51
+ } | null | undefined;
52
+ dietaryEncrypted: {
53
+ enc: string;
54
+ } | null | undefined;
55
+ loyaltyEncrypted: {
56
+ enc: string;
57
+ } | null | undefined;
58
+ insuranceEncrypted: {
59
+ enc: string;
60
+ } | null | undefined;
61
+ };
62
+ export declare function syncPersonIdentity(db: PostgresJsDatabase, personId: string, data: PersonIdentityInput): Promise<void>;
63
+ export declare function deletePersonIdentity(db: PostgresJsDatabase, personId: string): Promise<void>;
64
+ export declare function hydratePeople<T extends {
65
+ id: string;
66
+ }>(db: PostgresJsDatabase, rows: T[], options?: HydratePeopleOptions): Promise<Array<T & PersonHydratedFields>>;
67
+ export {};
68
+ //# sourceMappingURL=accounts-shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accounts-shared.d.ts","sourceRoot":"","sources":["../../src/service/accounts-shared.ts"],"names":[],"mappings":"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"}