@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,80 @@
1
+ /**
2
+ * Customer signals — lighter than `quotes` (no deal value or
3
+ * stages), heavier than `segments` (lifecycle + assignment). Records
4
+ * "person X expressed interest in product/departure Y from source Z,
5
+ * status pending". The most common use cases:
6
+ *
7
+ * - Notify-availability ("ping me when this departure opens up")
8
+ * - Wishlist / saved trips
9
+ * - Inquiry captured by an operator (phone call, web form)
10
+ * - Request-offer pre-pipeline that may or may not become a booking
11
+ * - Abandoned-cart recovery
12
+ *
13
+ * Cross-module IDs (`productId`, `optionUnitId`, `resolvedBookingId`)
14
+ * are intentionally plain `text()` columns rather than FK references
15
+ * — voyant's project-wide rule is that cross-module FKs go through
16
+ * link tables or stay loose. The signal is owned by CRM; products
17
+ * and bookings reference its id, not the other way around.
18
+ */
19
+ import { typeId, typeIdRef } from "@voyant-travel/db/lib/typeid-column";
20
+ import { index, jsonb, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
21
+ import { people } from "./schema-accounts.js";
22
+ export const customerSignalKindEnum = pgEnum("customer_signal_kind", [
23
+ "wishlist",
24
+ "notify",
25
+ "inquiry",
26
+ "request_offer",
27
+ "referral",
28
+ ]);
29
+ export const customerSignalSourceEnum = pgEnum("customer_signal_source", [
30
+ "form",
31
+ "phone",
32
+ "admin",
33
+ "abandoned_cart",
34
+ "website",
35
+ "booking",
36
+ ]);
37
+ export const customerSignalStatusEnum = pgEnum("customer_signal_status", [
38
+ "new",
39
+ "contacted",
40
+ "qualified",
41
+ "converted",
42
+ "lost",
43
+ "expired",
44
+ ]);
45
+ export const customerSignals = pgTable("customer_signals", {
46
+ id: typeId("customer_signals"),
47
+ personId: typeIdRef("person_id")
48
+ .notNull()
49
+ .references(() => people.id, { onDelete: "cascade" }),
50
+ /** Optional reference into `@voyant-travel/inventory`. Plain text — no FK. */
51
+ productId: text("product_id"),
52
+ /** Optional reference into a product's `option_units` row (the "departure"-equivalent). Plain text — no FK. */
53
+ optionUnitId: text("option_unit_id"),
54
+ kind: customerSignalKindEnum("kind").notNull(),
55
+ source: customerSignalSourceEnum("source").notNull(),
56
+ status: customerSignalStatusEnum("status").notNull().default("new"),
57
+ /**
58
+ * Free-form priority. The validation layer constrains input to
59
+ * `low | normal | high | urgent`; storing as text keeps room for
60
+ * deployment-specific values without a DB migration.
61
+ */
62
+ priority: text("priority").notNull().default("normal"),
63
+ notes: text("notes"),
64
+ tags: jsonb("tags").$type().notNull().default([]),
65
+ /** User id (Better Auth user) of the staff member assigned to follow up. */
66
+ assignedToUserId: text("assigned_to_user_id"),
67
+ followUpAt: timestamp("follow_up_at", { withTimezone: true }),
68
+ /** Set when the signal was converted into a real booking. Plain text — cross-module FK. */
69
+ resolvedBookingId: text("resolved_booking_id"),
70
+ /** Free-form provenance id (form-submission id, abandoned-cart key, ...). */
71
+ sourceSubmissionId: text("source_submission_id"),
72
+ metadata: jsonb("metadata").$type(),
73
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
74
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
75
+ }, (table) => [
76
+ index("idx_customer_signals_person_status_created").on(table.personId, table.status, table.createdAt),
77
+ index("idx_customer_signals_assignee_status").on(table.assignedToUserId, table.status),
78
+ index("idx_customer_signals_kind").on(table.kind),
79
+ index("idx_customer_signals_resolved_booking").on(table.resolvedBookingId),
80
+ ]);
@@ -0,0 +1,6 @@
1
+ export * from "./schema-accounts.js";
2
+ export * from "./schema-activities.js";
3
+ export * from "./schema-relations.js";
4
+ export * from "./schema-shared.js";
5
+ export * from "./schema-signals.js";
6
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA"}
package/dist/schema.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./schema-accounts.js";
2
+ export * from "./schema-activities.js";
3
+ export * from "./schema-relations.js";
4
+ export * from "./schema-shared.js";
5
+ export * from "./schema-signals.js";
@@ -0,0 +1,63 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ export declare class RelationshipsMergeError extends Error {
3
+ readonly status: 400 | 404;
4
+ constructor(message: string, status: 400 | 404);
5
+ }
6
+ export declare const accountMergeService: {
7
+ mergePerson(db: PostgresJsDatabase, keepId: string, mergeId: string): Promise<{
8
+ id: string;
9
+ organizationId: string | null;
10
+ firstName: string;
11
+ middleName: string | null;
12
+ lastName: string;
13
+ gender: string | null;
14
+ jobTitle: string | null;
15
+ relation: "other" | "partner" | "supplier" | "client" | null;
16
+ preferredLanguage: string | null;
17
+ preferredCurrency: string | null;
18
+ ownerId: string | null;
19
+ status: "active" | "inactive" | "archived";
20
+ source: string | null;
21
+ sourceRef: string | null;
22
+ tags: string[];
23
+ dateOfBirth: string | null;
24
+ notes: string | null;
25
+ accessibilityEncrypted: {
26
+ enc: string;
27
+ } | null;
28
+ dietaryEncrypted: {
29
+ enc: string;
30
+ } | null;
31
+ loyaltyEncrypted: {
32
+ enc: string;
33
+ } | null;
34
+ insuranceEncrypted: {
35
+ enc: string;
36
+ } | null;
37
+ createdAt: Date;
38
+ updatedAt: Date;
39
+ archivedAt: Date | null;
40
+ }>;
41
+ mergeOrganization(db: PostgresJsDatabase, keepId: string, mergeId: string): Promise<{
42
+ id: string;
43
+ name: string;
44
+ legalName: string | null;
45
+ website: string | null;
46
+ taxId: string | null;
47
+ industry: string | null;
48
+ relation: "other" | "partner" | "supplier" | "client" | null;
49
+ ownerId: string | null;
50
+ defaultCurrency: string | null;
51
+ preferredLanguage: string | null;
52
+ paymentTerms: number | null;
53
+ status: "active" | "inactive" | "archived";
54
+ source: string | null;
55
+ sourceRef: string | null;
56
+ tags: string[];
57
+ notes: string | null;
58
+ createdAt: Date;
59
+ updatedAt: Date;
60
+ archivedAt: Date | null;
61
+ }>;
62
+ };
63
+ //# sourceMappingURL=accounts-merge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accounts-merge.d.ts","sourceRoot":"","sources":["../../src/service/accounts-merge.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAmBjE,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAA;gBAEd,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,GAAG;CAK/C;AA+VD,eAAO,MAAM,mBAAmB;oBACR,kBAAkB,UAAU,MAAM,WAAW,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BA2H7C,kBAAkB,UAAU,MAAM,WAAW,MAAM;;;;;;;;;;;;;;;;;;;;;CAiFhF,CAAA"}
@@ -0,0 +1,382 @@
1
+ // agent-quality: file-size exception -- owner: relationships; existing service module stays co-located until a dedicated split preserves behavior and tests.
2
+ import { identityAddresses, identityContactPoints, identityNamedContacts, } from "@voyant-travel/identity/schema";
3
+ import { and, eq, inArray, ne, or, sql } from "drizzle-orm";
4
+ import { activityLinks, activityParticipants, communicationLog, customerSignals, customFieldValues, organizationNotes, organizations, people, personDocuments, personNotes, personPaymentMethods, personRelationships, segmentMembers, } from "../schema.js";
5
+ import { hydratePeople, organizationEntityType, personEntityType } from "./accounts-shared.js";
6
+ export class RelationshipsMergeError extends Error {
7
+ status;
8
+ constructor(message, status) {
9
+ super(message);
10
+ this.name = "RelationshipsMergeError";
11
+ this.status = status;
12
+ }
13
+ }
14
+ const OPTIONAL_PERSON_REFERENCES = [
15
+ { table: "bookings", column: "person_id" },
16
+ { table: "booking_travelers", column: "person_id" },
17
+ { table: "booking_staff_assignments", column: "person_id" },
18
+ { table: "offers", column: "person_id" },
19
+ { table: "offer_participants", column: "person_id" },
20
+ { table: "offer_contact_assignments", column: "person_id" },
21
+ { table: "offer_staff_assignments", column: "person_id" },
22
+ { table: "orders", column: "person_id" },
23
+ { table: "order_participants", column: "person_id" },
24
+ { table: "order_contact_assignments", column: "person_id" },
25
+ { table: "order_staff_assignments", column: "person_id" },
26
+ { table: "order_terms", column: "accepted_by" },
27
+ { table: "invoices", column: "person_id" },
28
+ { table: "vouchers", column: "issued_to_person_id" },
29
+ { table: "payment_instruments", column: "person_id" },
30
+ { table: "payment_sessions", column: "payer_person_id" },
31
+ { table: "contracts", column: "person_id" },
32
+ { table: "contract_signatures", column: "person_id" },
33
+ { table: "policy_acceptances", column: "person_id" },
34
+ { table: "policy_acceptances", column: "accepted_by" },
35
+ { table: "notification_deliveries", column: "person_id" },
36
+ { table: "notification_reminder_runs", column: "person_id" },
37
+ { table: "quotes", column: "person_id" },
38
+ { table: "quote_participants", column: "person_id" },
39
+ ];
40
+ const OPTIONAL_ORGANIZATION_REFERENCES = [
41
+ { table: "bookings", column: "organization_id" },
42
+ { table: "offers", column: "organization_id" },
43
+ { table: "orders", column: "organization_id" },
44
+ { table: "invoices", column: "organization_id" },
45
+ { table: "vouchers", column: "issued_to_organization_id" },
46
+ { table: "payment_instruments", column: "organization_id" },
47
+ { table: "payment_sessions", column: "payer_organization_id" },
48
+ { table: "contracts", column: "organization_id" },
49
+ { table: "policy_assignments", column: "organization_id" },
50
+ { table: "notification_deliveries", column: "organization_id" },
51
+ { table: "notification_reminder_runs", column: "organization_id" },
52
+ { table: "quotes", column: "organization_id" },
53
+ ];
54
+ const OPTIONAL_PERSON_ENTITY_TARGET_REFERENCES = [
55
+ { table: "notification_deliveries", entityType: "person" },
56
+ ];
57
+ const OPTIONAL_ORGANIZATION_ENTITY_TARGET_REFERENCES = [
58
+ { table: "notification_deliveries", entityType: "organization" },
59
+ ];
60
+ function quoteIdentifier(identifier) {
61
+ return `"${identifier.replaceAll('"', '""')}"`;
62
+ }
63
+ function mergeTags(keepTags, mergeTags) {
64
+ return [...new Set([...(keepTags ?? []), ...(mergeTags ?? [])])];
65
+ }
66
+ function backfill(keep, merge, keys) {
67
+ const patch = {};
68
+ for (const key of keys) {
69
+ const keepValue = keep[key];
70
+ const mergeValue = merge[key];
71
+ if ((keepValue === null || keepValue === undefined || keepValue === "") &&
72
+ mergeValue !== null &&
73
+ mergeValue !== undefined &&
74
+ mergeValue !== "") {
75
+ patch[key] = mergeValue;
76
+ }
77
+ }
78
+ return patch;
79
+ }
80
+ async function tableHasColumn(db, table, column) {
81
+ const rows = await db.execute(sql `
82
+ SELECT EXISTS (
83
+ SELECT 1
84
+ FROM information_schema.columns
85
+ WHERE table_schema = 'public'
86
+ AND table_name = ${table}
87
+ AND column_name = ${column}
88
+ ) AS "exists"
89
+ `);
90
+ return Boolean(rows[0]?.exists);
91
+ }
92
+ async function updateOptionalReference(db, reference, keepId, mergeId) {
93
+ if (!(await tableHasColumn(db, reference.table, reference.column)))
94
+ return;
95
+ const hasUpdatedAt = await tableHasColumn(db, reference.table, "updated_at");
96
+ if (hasUpdatedAt) {
97
+ await db.execute(sql `
98
+ UPDATE ${sql.raw(quoteIdentifier(reference.table))}
99
+ SET ${sql.raw(quoteIdentifier(reference.column))} = ${keepId},
100
+ updated_at = NOW()
101
+ WHERE ${sql.raw(quoteIdentifier(reference.column))} = ${mergeId}
102
+ `);
103
+ return;
104
+ }
105
+ await db.execute(sql `
106
+ UPDATE ${sql.raw(quoteIdentifier(reference.table))}
107
+ SET ${sql.raw(quoteIdentifier(reference.column))} = ${keepId}
108
+ WHERE ${sql.raw(quoteIdentifier(reference.column))} = ${mergeId}
109
+ `);
110
+ }
111
+ async function updateOptionalReferences(db, references, keepId, mergeId) {
112
+ for (const reference of references) {
113
+ await updateOptionalReference(db, reference, keepId, mergeId);
114
+ }
115
+ }
116
+ async function updateOptionalEntityTargetReference(db, reference, keepId, mergeId) {
117
+ const hasTargetType = await tableHasColumn(db, reference.table, "target_type");
118
+ const hasTargetId = await tableHasColumn(db, reference.table, "target_id");
119
+ if (!hasTargetType || !hasTargetId)
120
+ return;
121
+ await db.execute(sql `
122
+ UPDATE ${sql.raw(quoteIdentifier(reference.table))}
123
+ SET target_id = ${keepId}
124
+ WHERE target_type = ${reference.entityType}
125
+ AND target_id = ${mergeId}
126
+ `);
127
+ }
128
+ async function updateOptionalEntityTargetReferences(db, references, keepId, mergeId) {
129
+ for (const reference of references) {
130
+ await updateOptionalEntityTargetReference(db, reference, keepId, mergeId);
131
+ }
132
+ }
133
+ async function dedupeOptionalPersonJoinTable(db, reference, keepId, mergeId) {
134
+ const hasOwner = await tableHasColumn(db, reference.table, reference.ownerColumn);
135
+ const hasPerson = await tableHasColumn(db, reference.table, reference.personColumn);
136
+ if (!hasOwner || !hasPerson)
137
+ return;
138
+ const table = sql.raw(quoteIdentifier(reference.table));
139
+ const ownerColumn = sql.raw(quoteIdentifier(reference.ownerColumn));
140
+ const personColumn = sql.raw(quoteIdentifier(reference.personColumn));
141
+ await db.execute(sql `
142
+ DELETE FROM ${table} merge_row
143
+ WHERE merge_row.${personColumn} = ${mergeId}
144
+ AND EXISTS (
145
+ SELECT 1
146
+ FROM ${table} keep_row
147
+ WHERE keep_row.${ownerColumn} = merge_row.${ownerColumn}
148
+ AND keep_row.${personColumn} = ${keepId}
149
+ )
150
+ `);
151
+ }
152
+ async function mergeIdentityRows(db, entityType, keepId, mergeId) {
153
+ await db.delete(identityContactPoints).where(and(eq(identityContactPoints.entityType, entityType), eq(identityContactPoints.entityId, mergeId), sql `EXISTS (
154
+ SELECT 1
155
+ FROM identity_contact_points keep_point
156
+ WHERE keep_point.entity_type = ${entityType}
157
+ AND keep_point.entity_id = ${keepId}
158
+ AND keep_point.kind = ${identityContactPoints.kind}
159
+ AND keep_point.value = ${identityContactPoints.value}
160
+ )`));
161
+ await db
162
+ .update(identityContactPoints)
163
+ .set({ entityId: keepId, updatedAt: new Date() })
164
+ .where(and(eq(identityContactPoints.entityType, entityType), eq(identityContactPoints.entityId, mergeId)));
165
+ await db
166
+ .update(identityAddresses)
167
+ .set({ entityId: keepId, updatedAt: new Date() })
168
+ .where(and(eq(identityAddresses.entityType, entityType), eq(identityAddresses.entityId, mergeId)));
169
+ await db
170
+ .update(identityNamedContacts)
171
+ .set({ entityId: keepId, updatedAt: new Date() })
172
+ .where(and(eq(identityNamedContacts.entityType, entityType), eq(identityNamedContacts.entityId, mergeId)));
173
+ }
174
+ async function mergeEntityLinks(db, entityType, keepId, mergeId) {
175
+ await db
176
+ .update(activityLinks)
177
+ .set({ entityId: keepId })
178
+ .where(and(eq(activityLinks.entityType, entityType), eq(activityLinks.entityId, mergeId)));
179
+ await db.delete(customFieldValues).where(and(eq(customFieldValues.entityType, entityType), eq(customFieldValues.entityId, mergeId), sql `EXISTS (
180
+ SELECT 1
181
+ FROM custom_field_values keep_value
182
+ WHERE keep_value.definition_id = ${customFieldValues.definitionId}
183
+ AND keep_value.entity_type = ${entityType}
184
+ AND keep_value.entity_id = ${keepId}
185
+ )`));
186
+ await db
187
+ .update(customFieldValues)
188
+ .set({ entityId: keepId, updatedAt: new Date() })
189
+ .where(and(eq(customFieldValues.entityType, entityType), eq(customFieldValues.entityId, mergeId)));
190
+ }
191
+ async function dedupePersonJoinTables(db, keepId, mergeId) {
192
+ await dedupeOptionalPersonJoinTable(db, { table: "quote_participants", ownerColumn: "quote_id", personColumn: "person_id" }, keepId, mergeId);
193
+ await db.delete(activityParticipants).where(and(eq(activityParticipants.personId, mergeId), sql `EXISTS (
194
+ SELECT 1
195
+ FROM activity_participants keep_participant
196
+ WHERE keep_participant.activity_id = ${activityParticipants.activityId}
197
+ AND keep_participant.person_id = ${keepId}
198
+ )`));
199
+ await db
200
+ .delete(personRelationships)
201
+ .where(or(and(eq(personRelationships.fromPersonId, mergeId), eq(personRelationships.toPersonId, keepId)), and(eq(personRelationships.fromPersonId, keepId), eq(personRelationships.toPersonId, mergeId)), and(eq(personRelationships.fromPersonId, mergeId), eq(personRelationships.toPersonId, mergeId))));
202
+ await db.delete(personRelationships).where(and(eq(personRelationships.fromPersonId, mergeId), ne(personRelationships.toPersonId, mergeId), sql `EXISTS (
203
+ SELECT 1
204
+ FROM person_relationships keep_relationship
205
+ WHERE keep_relationship.from_person_id = ${keepId}
206
+ AND keep_relationship.to_person_id = ${personRelationships.toPersonId}
207
+ AND keep_relationship.kind = ${personRelationships.kind}
208
+ )`));
209
+ await db.delete(personRelationships).where(and(eq(personRelationships.toPersonId, mergeId), ne(personRelationships.fromPersonId, mergeId), sql `EXISTS (
210
+ SELECT 1
211
+ FROM person_relationships keep_relationship
212
+ WHERE keep_relationship.from_person_id = ${personRelationships.fromPersonId}
213
+ AND keep_relationship.to_person_id = ${keepId}
214
+ AND keep_relationship.kind = ${personRelationships.kind}
215
+ )`));
216
+ }
217
+ export const accountMergeService = {
218
+ async mergePerson(db, keepId, mergeId) {
219
+ if (keepId === mergeId) {
220
+ throw new RelationshipsMergeError("Cannot merge a person into itself", 400);
221
+ }
222
+ return db.transaction(async (tx) => {
223
+ const [keep, merge] = await tx
224
+ .select()
225
+ .from(people)
226
+ .where(inArray(people.id, [keepId, mergeId]))
227
+ .for("update");
228
+ const keepPerson = keep?.id === keepId ? keep : merge?.id === keepId ? merge : null;
229
+ const mergePerson = keep?.id === mergeId ? keep : merge?.id === mergeId ? merge : null;
230
+ if (!keepPerson)
231
+ throw new RelationshipsMergeError("Person to keep not found", 404);
232
+ if (!mergePerson)
233
+ throw new RelationshipsMergeError("Person to merge not found", 404);
234
+ await tx
235
+ .update(people)
236
+ .set({
237
+ ...backfill(keepPerson, mergePerson, [
238
+ "organizationId",
239
+ "middleName",
240
+ "gender",
241
+ "jobTitle",
242
+ "relation",
243
+ "preferredLanguage",
244
+ "preferredCurrency",
245
+ "ownerId",
246
+ "source",
247
+ "sourceRef",
248
+ "dateOfBirth",
249
+ "notes",
250
+ "accessibilityEncrypted",
251
+ "dietaryEncrypted",
252
+ "loyaltyEncrypted",
253
+ "insuranceEncrypted",
254
+ ]),
255
+ tags: mergeTags(keepPerson.tags, mergePerson.tags),
256
+ updatedAt: new Date(),
257
+ })
258
+ .where(eq(people.id, keepId));
259
+ await dedupePersonJoinTables(tx, keepId, mergeId);
260
+ await mergeIdentityRows(tx, personEntityType, keepId, mergeId);
261
+ await mergeEntityLinks(tx, "person", keepId, mergeId);
262
+ await tx
263
+ .update(personNotes)
264
+ .set({ personId: keepId })
265
+ .where(eq(personNotes.personId, mergeId));
266
+ await tx
267
+ .update(personDocuments)
268
+ .set({ isPrimary: false, updatedAt: new Date() })
269
+ .where(and(eq(personDocuments.personId, mergeId), eq(personDocuments.isPrimary, true), sql `EXISTS (
270
+ SELECT 1
271
+ FROM person_documents keep_document
272
+ WHERE keep_document.person_id = ${keepId}
273
+ AND keep_document.type = ${personDocuments.type}
274
+ AND keep_document.is_primary = true
275
+ )`));
276
+ await tx
277
+ .update(personDocuments)
278
+ .set({ personId: keepId, updatedAt: new Date() })
279
+ .where(eq(personDocuments.personId, mergeId));
280
+ await tx
281
+ .update(personPaymentMethods)
282
+ .set({ personId: keepId })
283
+ .where(eq(personPaymentMethods.personId, mergeId));
284
+ await tx
285
+ .update(communicationLog)
286
+ .set({ personId: keepId })
287
+ .where(eq(communicationLog.personId, mergeId));
288
+ await tx
289
+ .update(activityParticipants)
290
+ .set({ personId: keepId })
291
+ .where(eq(activityParticipants.personId, mergeId));
292
+ await tx
293
+ .update(personRelationships)
294
+ .set({ fromPersonId: keepId, updatedAt: new Date() })
295
+ .where(eq(personRelationships.fromPersonId, mergeId));
296
+ await tx
297
+ .update(personRelationships)
298
+ .set({ toPersonId: keepId, updatedAt: new Date() })
299
+ .where(eq(personRelationships.toPersonId, mergeId));
300
+ await tx
301
+ .update(segmentMembers)
302
+ .set({ personId: keepId })
303
+ .where(eq(segmentMembers.personId, mergeId));
304
+ await tx
305
+ .update(customerSignals)
306
+ .set({ personId: keepId, updatedAt: new Date() })
307
+ .where(eq(customerSignals.personId, mergeId));
308
+ await updateOptionalReferences(tx, OPTIONAL_PERSON_REFERENCES, keepId, mergeId);
309
+ await updateOptionalEntityTargetReferences(tx, OPTIONAL_PERSON_ENTITY_TARGET_REFERENCES, keepId, mergeId);
310
+ await tx.delete(people).where(eq(people.id, mergeId));
311
+ const [row] = await tx.select().from(people).where(eq(people.id, keepId)).limit(1);
312
+ if (!row)
313
+ throw new Error("Merged person disappeared");
314
+ const [hydrated] = await hydratePeople(tx, [row]);
315
+ return hydrated ?? row;
316
+ });
317
+ },
318
+ async mergeOrganization(db, keepId, mergeId) {
319
+ if (keepId === mergeId) {
320
+ throw new RelationshipsMergeError("Cannot merge an organization into itself", 400);
321
+ }
322
+ return db.transaction(async (tx) => {
323
+ const [keep, merge] = await tx
324
+ .select()
325
+ .from(organizations)
326
+ .where(inArray(organizations.id, [keepId, mergeId]))
327
+ .for("update");
328
+ const keepOrganization = keep?.id === keepId ? keep : merge?.id === keepId ? merge : null;
329
+ const mergeOrganization = keep?.id === mergeId ? keep : merge?.id === mergeId ? merge : null;
330
+ if (!keepOrganization)
331
+ throw new RelationshipsMergeError("Organization to keep not found", 404);
332
+ if (!mergeOrganization)
333
+ throw new RelationshipsMergeError("Organization to merge not found", 404);
334
+ await tx
335
+ .update(organizations)
336
+ .set({
337
+ ...backfill(keepOrganization, mergeOrganization, [
338
+ "legalName",
339
+ "website",
340
+ "taxId",
341
+ "industry",
342
+ "relation",
343
+ "ownerId",
344
+ "defaultCurrency",
345
+ "preferredLanguage",
346
+ "paymentTerms",
347
+ "source",
348
+ "sourceRef",
349
+ "notes",
350
+ ]),
351
+ tags: mergeTags(keepOrganization.tags, mergeOrganization.tags),
352
+ updatedAt: new Date(),
353
+ })
354
+ .where(eq(organizations.id, keepId));
355
+ await mergeIdentityRows(tx, organizationEntityType, keepId, mergeId);
356
+ await mergeEntityLinks(tx, "organization", keepId, mergeId);
357
+ await tx
358
+ .update(people)
359
+ .set({ organizationId: keepId, updatedAt: new Date() })
360
+ .where(eq(people.organizationId, mergeId));
361
+ await tx
362
+ .update(organizationNotes)
363
+ .set({ organizationId: keepId })
364
+ .where(eq(organizationNotes.organizationId, mergeId));
365
+ await tx
366
+ .update(communicationLog)
367
+ .set({ organizationId: keepId })
368
+ .where(eq(communicationLog.organizationId, mergeId));
369
+ await updateOptionalReferences(tx, OPTIONAL_ORGANIZATION_REFERENCES, keepId, mergeId);
370
+ await updateOptionalEntityTargetReferences(tx, OPTIONAL_ORGANIZATION_ENTITY_TARGET_REFERENCES, keepId, mergeId);
371
+ await tx.delete(organizations).where(eq(organizations.id, mergeId));
372
+ const [row] = await tx
373
+ .select()
374
+ .from(organizations)
375
+ .where(eq(organizations.id, keepId))
376
+ .limit(1);
377
+ if (!row)
378
+ throw new Error("Merged organization disappeared");
379
+ return row;
380
+ });
381
+ },
382
+ };
@@ -0,0 +1,97 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import type { CreateOrganizationInput, OrganizationListQuery, UpdateOrganizationInput } from "./accounts-shared.js";
3
+ export declare const organizationAccountsService: {
4
+ listOrganizations(db: PostgresJsDatabase, query: OrganizationListQuery): Promise<{
5
+ data: {
6
+ id: string;
7
+ name: string;
8
+ legalName: string | null;
9
+ website: string | null;
10
+ taxId: string | null;
11
+ industry: string | null;
12
+ relation: "other" | "partner" | "supplier" | "client" | null;
13
+ ownerId: string | null;
14
+ defaultCurrency: string | null;
15
+ preferredLanguage: string | null;
16
+ paymentTerms: number | null;
17
+ status: "active" | "inactive" | "archived";
18
+ source: string | null;
19
+ sourceRef: string | null;
20
+ tags: string[];
21
+ notes: string | null;
22
+ createdAt: Date;
23
+ updatedAt: Date;
24
+ archivedAt: Date | null;
25
+ }[];
26
+ total: number;
27
+ limit: number;
28
+ offset: number;
29
+ }>;
30
+ getOrganizationById(db: PostgresJsDatabase, id: string): Promise<{
31
+ id: string;
32
+ name: string;
33
+ legalName: string | null;
34
+ website: string | null;
35
+ taxId: string | null;
36
+ industry: string | null;
37
+ relation: "other" | "partner" | "supplier" | "client" | null;
38
+ ownerId: string | null;
39
+ defaultCurrency: string | null;
40
+ preferredLanguage: string | null;
41
+ paymentTerms: number | null;
42
+ status: "active" | "inactive" | "archived";
43
+ source: string | null;
44
+ sourceRef: string | null;
45
+ tags: string[];
46
+ notes: string | null;
47
+ createdAt: Date;
48
+ updatedAt: Date;
49
+ archivedAt: Date | null;
50
+ } | null>;
51
+ createOrganization(db: PostgresJsDatabase, data: CreateOrganizationInput): Promise<{
52
+ name: string;
53
+ id: string;
54
+ status: "active" | "inactive" | "archived";
55
+ createdAt: Date;
56
+ source: string | null;
57
+ tags: string[];
58
+ notes: string | null;
59
+ updatedAt: Date;
60
+ website: string | null;
61
+ legalName: string | null;
62
+ taxId: string | null;
63
+ industry: string | null;
64
+ relation: "other" | "partner" | "supplier" | "client" | null;
65
+ ownerId: string | null;
66
+ defaultCurrency: string | null;
67
+ preferredLanguage: string | null;
68
+ paymentTerms: number | null;
69
+ sourceRef: string | null;
70
+ archivedAt: Date | null;
71
+ } | undefined>;
72
+ updateOrganization(db: PostgresJsDatabase, id: string, data: UpdateOrganizationInput): Promise<{
73
+ id: string;
74
+ name: string;
75
+ legalName: string | null;
76
+ website: string | null;
77
+ taxId: string | null;
78
+ industry: string | null;
79
+ relation: "other" | "partner" | "supplier" | "client" | null;
80
+ ownerId: string | null;
81
+ defaultCurrency: string | null;
82
+ preferredLanguage: string | null;
83
+ paymentTerms: number | null;
84
+ status: "active" | "inactive" | "archived";
85
+ source: string | null;
86
+ sourceRef: string | null;
87
+ tags: string[];
88
+ notes: string | null;
89
+ createdAt: Date;
90
+ updatedAt: Date;
91
+ archivedAt: Date | null;
92
+ } | null>;
93
+ deleteOrganization(db: PostgresJsDatabase, id: string): Promise<{
94
+ id: string;
95
+ } | null>;
96
+ };
97
+ //# sourceMappingURL=accounts-organizations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accounts-organizations.d.ts","sourceRoot":"","sources":["../../src/service/accounts-organizations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,KAAK,EACV,uBAAuB,EACvB,qBAAqB,EACrB,uBAAuB,EACxB,MAAM,sBAAsB,CAAA;AAG7B,eAAO,MAAM,2BAA2B;0BACV,kBAAkB,SAAS,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;4BAsD9C,kBAAkB,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;2BAK/B,kBAAkB,QAAQ,uBAAuB;;;;;;;;;;;;;;;;;;;;;2BAKjD,kBAAkB,MAAM,MAAM,QAAQ,uBAAuB;;;;;;;;;;;;;;;;;;;;;2BAS7D,kBAAkB,MAAM,MAAM;;;CAO5D,CAAA"}