@voyantjs/crm 0.2.0 → 0.3.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 (37) hide show
  1. package/dist/routes/accounts.d.ts +1 -1
  2. package/dist/routes/index.d.ts +2 -2
  3. package/dist/routes/opportunities.d.ts +1 -1
  4. package/dist/schema-accounts.d.ts +1175 -0
  5. package/dist/schema-accounts.d.ts.map +1 -0
  6. package/dist/schema-accounts.js +112 -0
  7. package/dist/schema-activities.d.ts +821 -0
  8. package/dist/schema-activities.d.ts.map +1 -0
  9. package/dist/schema-activities.js +83 -0
  10. package/dist/schema-relations.d.ts +81 -0
  11. package/dist/schema-relations.d.ts.map +1 -0
  12. package/dist/schema-relations.js +123 -0
  13. package/dist/schema-sales.d.ts +1392 -0
  14. package/dist/schema-sales.d.ts.map +1 -0
  15. package/dist/schema-sales.js +142 -0
  16. package/dist/schema-shared.d.ts +13 -0
  17. package/dist/schema-shared.d.ts.map +1 -0
  18. package/dist/schema-shared.js +63 -0
  19. package/dist/schema.d.ts +5 -3477
  20. package/dist/schema.d.ts.map +1 -1
  21. package/dist/schema.js +5 -515
  22. package/dist/service/accounts-organizations.d.ts +93 -0
  23. package/dist/service/accounts-organizations.d.ts.map +1 -0
  24. package/dist/service/accounts-organizations.js +49 -0
  25. package/dist/service/accounts-people.d.ts +868 -0
  26. package/dist/service/accounts-people.d.ts.map +1 -0
  27. package/dist/service/accounts-people.js +311 -0
  28. package/dist/service/accounts-shared.d.ts +54 -0
  29. package/dist/service/accounts-shared.d.ts.map +1 -0
  30. package/dist/service/accounts-shared.js +152 -0
  31. package/dist/service/accounts.d.ts +121 -149
  32. package/dist/service/accounts.d.ts.map +1 -1
  33. package/dist/service/accounts.js +4 -507
  34. package/dist/service/index.d.ts +106 -297
  35. package/dist/service/index.d.ts.map +1 -1
  36. package/dist/service/opportunities.d.ts +1 -1
  37. package/package.json +5 -5
@@ -1,509 +1,6 @@
1
- import { identityAddresses, identityContactPoints } from "@voyantjs/identity/schema";
2
- import { identityService } from "@voyantjs/identity/service";
3
- import { and, desc, eq, gte, ilike, inArray, lte, or, sql } from "drizzle-orm";
4
- import { communicationLog, organizationNotes, organizations, people, personNotes, segments, } from "../schema.js";
5
- import { formatAddress, isManagedBySource, normalizeContactValue, paginate, toNullableTrimmed, } from "./helpers.js";
6
- const organizationEntityType = "organization";
7
- const personEntityType = "person";
8
- const personBaseIdentitySource = "crm.person.base";
9
- function personBaseFields(data) {
10
- return {
11
- organizationId: data.organizationId,
12
- firstName: data.firstName,
13
- lastName: data.lastName,
14
- jobTitle: data.jobTitle,
15
- relation: data.relation,
16
- preferredLanguage: data.preferredLanguage,
17
- preferredCurrency: data.preferredCurrency,
18
- ownerId: data.ownerId,
19
- status: data.status,
20
- source: data.source,
21
- sourceRef: data.sourceRef,
22
- tags: data.tags,
23
- birthday: data.birthday,
24
- notes: data.notes,
25
- };
26
- }
27
- async function syncPersonIdentity(db, personId, data) {
28
- const existingContactPoints = await identityService.listContactPointsForEntity(db, personEntityType, personId);
29
- const existingAddresses = await identityService.listAddressesForEntity(db, personEntityType, personId);
30
- const managedContactPoints = existingContactPoints.filter((point) => isManagedBySource(point.metadata, personBaseIdentitySource));
31
- const managedAddress = existingAddresses.find((address) => isManagedBySource(address.metadata, personBaseIdentitySource));
32
- for (const [kind, rawValue] of Object.entries({
33
- email: data.email,
34
- phone: data.phone,
35
- website: data.website,
36
- })) {
37
- const value = toNullableTrimmed(rawValue);
38
- const existing = managedContactPoints.find((point) => point.kind === kind) ??
39
- existingContactPoints.find((point) => point.kind === kind && point.isPrimary);
40
- if (!value) {
41
- if (existing) {
42
- await identityService.deleteContactPoint(db, existing.id);
43
- }
44
- continue;
45
- }
46
- const payload = {
47
- entityType: personEntityType,
48
- entityId: personId,
49
- kind,
50
- label: kind === "website" ? "website" : "primary",
51
- value,
52
- normalizedValue: normalizeContactValue(kind, value),
53
- isPrimary: true,
54
- metadata: { managedBy: personBaseIdentitySource },
55
- };
56
- if (existing) {
57
- await identityService.updateContactPoint(db, existing.id, payload);
58
- }
59
- else {
60
- await identityService.createContactPoint(db, payload);
61
- }
62
- }
63
- const addressLine = toNullableTrimmed(data.address);
64
- const city = toNullableTrimmed(data.city);
65
- const country = toNullableTrimmed(data.country);
66
- const hasAddress = Boolean(addressLine || city || country);
67
- if (!hasAddress) {
68
- if (managedAddress) {
69
- await identityService.deleteAddress(db, managedAddress.id);
70
- }
71
- return;
72
- }
73
- const addressPayload = {
74
- entityType: personEntityType,
75
- entityId: personId,
76
- label: "primary",
77
- fullText: addressLine,
78
- line1: addressLine,
79
- city,
80
- country,
81
- isPrimary: true,
82
- metadata: { managedBy: personBaseIdentitySource },
83
- };
84
- if (managedAddress) {
85
- await identityService.updateAddress(db, managedAddress.id, addressPayload);
86
- }
87
- else {
88
- await identityService.createAddress(db, addressPayload);
89
- }
90
- }
91
- async function deletePersonIdentity(db, personId) {
92
- const [contactPoints, addresses] = await Promise.all([
93
- identityService.listContactPointsForEntity(db, personEntityType, personId),
94
- identityService.listAddressesForEntity(db, personEntityType, personId),
95
- ]);
96
- await Promise.all([
97
- ...contactPoints.map((point) => identityService.deleteContactPoint(db, point.id)),
98
- ...addresses.map((address) => identityService.deleteAddress(db, address.id)),
99
- ]);
100
- }
101
- async function hydratePeople(db, rows) {
102
- if (rows.length === 0) {
103
- return rows.map((row) => ({
104
- ...row,
105
- email: null,
106
- phone: null,
107
- website: null,
108
- address: null,
109
- city: null,
110
- country: null,
111
- }));
112
- }
113
- const ids = rows.map((row) => row.id);
114
- const [contactPoints, addresses] = await Promise.all([
115
- db
116
- .select()
117
- .from(identityContactPoints)
118
- .where(and(eq(identityContactPoints.entityType, personEntityType), inArray(identityContactPoints.entityId, ids))),
119
- db
120
- .select()
121
- .from(identityAddresses)
122
- .where(and(eq(identityAddresses.entityType, personEntityType), inArray(identityAddresses.entityId, ids))),
123
- ]);
124
- const contactPointMap = new Map();
125
- const addressMap = new Map();
126
- for (const point of contactPoints) {
127
- const bucket = contactPointMap.get(point.entityId) ?? [];
128
- bucket.push(point);
129
- contactPointMap.set(point.entityId, bucket);
130
- }
131
- for (const address of addresses) {
132
- const bucket = addressMap.get(address.entityId) ?? [];
133
- bucket.push(address);
134
- addressMap.set(address.entityId, bucket);
135
- }
136
- return rows.map((row) => {
137
- const entityContactPoints = contactPointMap.get(row.id) ?? [];
138
- const entityAddresses = addressMap.get(row.id) ?? [];
139
- const findPrimaryContactPoint = (kind) => entityContactPoints.find((point) => point.kind === kind && point.isPrimary) ??
140
- entityContactPoints.find((point) => point.kind === kind) ??
141
- null;
142
- const primaryAddress = entityAddresses.find((address) => address.isPrimary) ?? entityAddresses[0] ?? null;
143
- return {
144
- ...row,
145
- email: findPrimaryContactPoint("email")?.value ?? null,
146
- phone: findPrimaryContactPoint("phone")?.value ?? null,
147
- website: findPrimaryContactPoint("website")?.value ?? null,
148
- address: primaryAddress ? formatAddress(primaryAddress) : null,
149
- city: primaryAddress?.city ?? null,
150
- country: primaryAddress?.country ?? null,
151
- };
152
- });
153
- }
1
+ import { organizationAccountsService } from "./accounts-organizations.js";
2
+ import { peopleAccountsService } from "./accounts-people.js";
154
3
  export const accountsService = {
155
- async listOrganizations(db, query) {
156
- const conditions = [];
157
- if (query.ownerId)
158
- conditions.push(eq(organizations.ownerId, query.ownerId));
159
- if (query.relation)
160
- conditions.push(eq(organizations.relation, query.relation));
161
- if (query.status)
162
- conditions.push(eq(organizations.status, query.status));
163
- if (query.search) {
164
- const term = `%${query.search}%`;
165
- conditions.push(or(ilike(organizations.name, term), ilike(organizations.legalName, term), ilike(organizations.website, term)));
166
- }
167
- const where = conditions.length ? and(...conditions) : undefined;
168
- return paginate(db
169
- .select()
170
- .from(organizations)
171
- .where(where)
172
- .limit(query.limit)
173
- .offset(query.offset)
174
- .orderBy(desc(organizations.updatedAt)), db.select({ count: sql `count(*)::int` }).from(organizations).where(where), query.limit, query.offset);
175
- },
176
- async getOrganizationById(db, id) {
177
- const [row] = await db.select().from(organizations).where(eq(organizations.id, id)).limit(1);
178
- return row ?? null;
179
- },
180
- async createOrganization(db, data) {
181
- const [row] = await db.insert(organizations).values(data).returning();
182
- return row;
183
- },
184
- async updateOrganization(db, id, data) {
185
- const [row] = await db
186
- .update(organizations)
187
- .set({ ...data, updatedAt: new Date() })
188
- .where(eq(organizations.id, id))
189
- .returning();
190
- return row ?? null;
191
- },
192
- async deleteOrganization(db, id) {
193
- const [row] = await db
194
- .delete(organizations)
195
- .where(eq(organizations.id, id))
196
- .returning({ id: organizations.id });
197
- return row ?? null;
198
- },
199
- async listPeople(db, query) {
200
- const conditions = [];
201
- if (query.organizationId)
202
- conditions.push(eq(people.organizationId, query.organizationId));
203
- if (query.ownerId)
204
- conditions.push(eq(people.ownerId, query.ownerId));
205
- if (query.relation)
206
- conditions.push(eq(people.relation, query.relation));
207
- if (query.status)
208
- conditions.push(eq(people.status, query.status));
209
- if (query.search) {
210
- const term = `%${query.search}%`;
211
- conditions.push(or(ilike(people.firstName, term), ilike(people.lastName, term), ilike(people.jobTitle, term)));
212
- }
213
- const where = conditions.length ? and(...conditions) : undefined;
214
- const result = await paginate(db
215
- .select()
216
- .from(people)
217
- .where(where)
218
- .limit(query.limit)
219
- .offset(query.offset)
220
- .orderBy(desc(people.updatedAt)), db.select({ count: sql `count(*)::int` }).from(people).where(where), query.limit, query.offset);
221
- return {
222
- ...result,
223
- data: await hydratePeople(db, result.data),
224
- };
225
- },
226
- async getPersonById(db, id) {
227
- const [row] = await db.select().from(people).where(eq(people.id, id)).limit(1);
228
- if (!row)
229
- return null;
230
- const [hydrated] = await hydratePeople(db, [row]);
231
- return hydrated ?? null;
232
- },
233
- async createPerson(db, data) {
234
- const [row] = await db
235
- .insert(people)
236
- .values({
237
- ...personBaseFields(data),
238
- firstName: data.firstName,
239
- lastName: data.lastName,
240
- })
241
- .returning();
242
- if (!row) {
243
- throw new Error("Failed to create person");
244
- }
245
- await syncPersonIdentity(db, row.id, data);
246
- return this.getPersonById(db, row.id);
247
- },
248
- async updatePerson(db, id, data) {
249
- const existing = await this.getPersonById(db, id);
250
- if (!existing)
251
- return null;
252
- await db
253
- .update(people)
254
- .set({ ...personBaseFields(data), updatedAt: new Date() })
255
- .where(eq(people.id, id));
256
- await syncPersonIdentity(db, id, {
257
- email: data.email === undefined ? existing.email : data.email,
258
- phone: data.phone === undefined ? existing.phone : data.phone,
259
- website: data.website === undefined ? existing.website : data.website,
260
- address: data.address === undefined ? existing.address : data.address,
261
- city: data.city === undefined ? existing.city : data.city,
262
- country: data.country === undefined ? existing.country : data.country,
263
- });
264
- return this.getPersonById(db, id);
265
- },
266
- async deletePerson(db, id) {
267
- await deletePersonIdentity(db, id);
268
- const [row] = await db.delete(people).where(eq(people.id, id)).returning({ id: people.id });
269
- return row ?? null;
270
- },
271
- // --- Contact methods & addresses (explicit CRUD via identity) ---
272
- listContactMethods(db, entityType, entityId) {
273
- return identityService.listContactPointsForEntity(db, entityType === "organization" ? organizationEntityType : personEntityType, entityId);
274
- },
275
- async createContactMethod(db, entityType, entityId, data) {
276
- return identityService.createContactPoint(db, {
277
- ...data,
278
- entityType: entityType === "organization" ? organizationEntityType : personEntityType,
279
- entityId,
280
- });
281
- },
282
- async updateContactMethod(db, id, data) {
283
- return identityService.updateContactPoint(db, id, data);
284
- },
285
- async deleteContactMethod(db, id) {
286
- return identityService.deleteContactPoint(db, id);
287
- },
288
- listAddresses(db, entityType, entityId) {
289
- return identityService.listAddressesForEntity(db, entityType === "organization" ? organizationEntityType : personEntityType, entityId);
290
- },
291
- async createAddress(db, entityType, entityId, data) {
292
- return identityService.createAddress(db, {
293
- ...data,
294
- entityType: entityType === "organization" ? organizationEntityType : personEntityType,
295
- entityId,
296
- });
297
- },
298
- async updateAddress(db, id, data) {
299
- return identityService.updateAddress(db, id, data);
300
- },
301
- async deleteAddress(db, id) {
302
- return identityService.deleteAddress(db, id);
303
- },
304
- // --- Person notes ---
305
- listPersonNotes(db, personId) {
306
- return db
307
- .select()
308
- .from(personNotes)
309
- .where(eq(personNotes.personId, personId))
310
- .orderBy(personNotes.createdAt);
311
- },
312
- async createPersonNote(db, personId, userId, data) {
313
- const [existing] = await db
314
- .select({ id: people.id })
315
- .from(people)
316
- .where(eq(people.id, personId))
317
- .limit(1);
318
- if (!existing)
319
- return null;
320
- const [row] = await db
321
- .insert(personNotes)
322
- .values({ personId, authorId: userId, content: data.content })
323
- .returning();
324
- return row;
325
- },
326
- // --- Organization notes ---
327
- listOrganizationNotes(db, organizationId) {
328
- return db
329
- .select()
330
- .from(organizationNotes)
331
- .where(eq(organizationNotes.organizationId, organizationId))
332
- .orderBy(organizationNotes.createdAt);
333
- },
334
- async createOrganizationNote(db, organizationId, userId, data) {
335
- const [existing] = await db
336
- .select({ id: organizations.id })
337
- .from(organizations)
338
- .where(eq(organizations.id, organizationId))
339
- .limit(1);
340
- if (!existing)
341
- return null;
342
- const [row] = await db
343
- .insert(organizationNotes)
344
- .values({ organizationId, authorId: userId, content: data.content })
345
- .returning();
346
- return row;
347
- },
348
- async updatePersonNote(db, id, content) {
349
- const [row] = await db
350
- .update(personNotes)
351
- .set({ content })
352
- .where(eq(personNotes.id, id))
353
- .returning();
354
- return row ?? null;
355
- },
356
- async deletePersonNote(db, id) {
357
- const [row] = await db.delete(personNotes).where(eq(personNotes.id, id)).returning();
358
- return row ?? null;
359
- },
360
- async updateOrganizationNote(db, id, content) {
361
- const [row] = await db
362
- .update(organizationNotes)
363
- .set({ content })
364
- .where(eq(organizationNotes.id, id))
365
- .returning();
366
- return row ?? null;
367
- },
368
- async deleteOrganizationNote(db, id) {
369
- const [row] = await db.delete(organizationNotes).where(eq(organizationNotes.id, id)).returning();
370
- return row ?? null;
371
- },
372
- // --- Communication log ---
373
- async listCommunications(db, personId, query) {
374
- const conditions = [eq(communicationLog.personId, personId)];
375
- if (query.channel)
376
- conditions.push(eq(communicationLog.channel, query.channel));
377
- if (query.direction)
378
- conditions.push(eq(communicationLog.direction, query.direction));
379
- if (query.dateFrom)
380
- conditions.push(gte(communicationLog.createdAt, new Date(query.dateFrom)));
381
- if (query.dateTo)
382
- conditions.push(lte(communicationLog.createdAt, new Date(query.dateTo)));
383
- return db
384
- .select()
385
- .from(communicationLog)
386
- .where(and(...conditions))
387
- .limit(query.limit)
388
- .offset(query.offset)
389
- .orderBy(desc(communicationLog.createdAt));
390
- },
391
- async createCommunication(db, personId, data) {
392
- const [existing] = await db
393
- .select({ id: people.id })
394
- .from(people)
395
- .where(eq(people.id, personId))
396
- .limit(1);
397
- if (!existing)
398
- return null;
399
- const [row] = await db
400
- .insert(communicationLog)
401
- .values({
402
- personId,
403
- organizationId: data.organizationId ?? null,
404
- channel: data.channel,
405
- direction: data.direction,
406
- subject: data.subject ?? null,
407
- content: data.content ?? null,
408
- sentAt: data.sentAt ? new Date(data.sentAt) : null,
409
- })
410
- .returning();
411
- return row;
412
- },
413
- // --- Segments ---
414
- listSegments(db) {
415
- return db.select().from(segments).orderBy(segments.createdAt);
416
- },
417
- async createSegment(db, data) {
418
- const [row] = await db.insert(segments).values(data).returning();
419
- return row;
420
- },
421
- async deleteSegment(db, segmentId) {
422
- const [row] = await db
423
- .delete(segments)
424
- .where(eq(segments.id, segmentId))
425
- .returning({ id: segments.id });
426
- return row ?? null;
427
- },
428
- // --- CSV export/import ---
429
- async exportPeopleCsv(db) {
430
- const rows = await hydratePeople(db, await db.select().from(people).orderBy(people.createdAt));
431
- const headers = [
432
- "id",
433
- "firstName",
434
- "lastName",
435
- "jobTitle",
436
- "relation",
437
- "preferredLanguage",
438
- "preferredCurrency",
439
- "email",
440
- "phone",
441
- "website",
442
- "address",
443
- "city",
444
- "country",
445
- "organizationId",
446
- ];
447
- const csvLines = [headers.join(",")];
448
- for (const row of rows) {
449
- const values = headers.map((header) => {
450
- const value = row[header];
451
- if (value === null || value === undefined)
452
- return "";
453
- const stringValue = String(value);
454
- return stringValue.includes(",") || stringValue.includes('"') || stringValue.includes("\n")
455
- ? `"${stringValue.replace(/"/g, '""')}"`
456
- : stringValue;
457
- });
458
- csvLines.push(values.join(","));
459
- }
460
- return csvLines.join("\n");
461
- },
462
- async importPeopleCsv(db, csvText) {
463
- const lines = csvText.split("\n").filter((line) => line.trim());
464
- if (lines.length < 2) {
465
- return { error: "CSV must have a header row and at least one data row" };
466
- }
467
- const headers = lines[0].split(",").map((header) => header.trim());
468
- const rows = [];
469
- for (let i = 1; i < lines.length; i++) {
470
- const values = lines[i].split(",").map((value) => value.trim());
471
- const row = {};
472
- for (let j = 0; j < headers.length; j++) {
473
- const header = headers[j];
474
- const value = values[j];
475
- if (header && value) {
476
- row[header] = value;
477
- }
478
- }
479
- rows.push(row);
480
- }
481
- const imported = [];
482
- const errors = [];
483
- for (let i = 0; i < rows.length; i++) {
484
- const row = rows[i];
485
- const result = (await import("../validation.js")).insertPersonSchema.safeParse({
486
- firstName: row.firstName || "",
487
- lastName: row.lastName || "",
488
- jobTitle: row.jobTitle || null,
489
- relation: row.relation || null,
490
- preferredLanguage: row.preferredLanguage || null,
491
- preferredCurrency: row.preferredCurrency || null,
492
- email: row.email || null,
493
- phone: row.phone || null,
494
- website: row.website || null,
495
- address: row.address || null,
496
- city: row.city || null,
497
- country: row.country || null,
498
- organizationId: row.organizationId || null,
499
- tags: [],
500
- });
501
- if (!result.success) {
502
- errors.push({ row: i + 2, error: result.error.message });
503
- continue;
504
- }
505
- imported.push(await this.createPerson(db, result.data));
506
- }
507
- return { imported: imported.length, errors };
508
- },
4
+ ...organizationAccountsService,
5
+ ...peopleAccountsService,
509
6
  };