@voyant-travel/relationships 0.119.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +36 -0
- package/dist/action-ledger-capabilities.d.ts +20 -0
- package/dist/action-ledger-capabilities.d.ts.map +1 -0
- package/dist/action-ledger-capabilities.js +16 -0
- package/dist/events.d.ts +23 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +9 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/route-runtime.d.ts +21 -0
- package/dist/route-runtime.d.ts.map +1 -0
- package/dist/route-runtime.js +28 -0
- package/dist/routes/accounts.d.ts +1460 -0
- package/dist/routes/accounts.d.ts.map +1 -0
- package/dist/routes/accounts.js +274 -0
- package/dist/routes/activities.d.ts +299 -0
- package/dist/routes/activities.d.ts.map +1 -0
- package/dist/routes/activities.js +64 -0
- package/dist/routes/custom-fields.d.ts +256 -0
- package/dist/routes/custom-fields.d.ts.map +1 -0
- package/dist/routes/custom-fields.js +47 -0
- package/dist/routes/customer-signals.d.ts +281 -0
- package/dist/routes/customer-signals.d.ts.map +1 -0
- package/dist/routes/customer-signals.js +45 -0
- package/dist/routes/index.d.ts +2945 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +14 -0
- package/dist/routes/person-documents.d.ts +519 -0
- package/dist/routes/person-documents.d.ts.map +1 -0
- package/dist/routes/person-documents.js +240 -0
- package/dist/routes/person-relationships.d.ts +189 -0
- package/dist/routes/person-relationships.d.ts.map +1 -0
- package/dist/routes/person-relationships.js +36 -0
- package/dist/schema-accounts.d.ts +2099 -0
- package/dist/schema-accounts.d.ts.map +1 -0
- package/dist/schema-accounts.js +312 -0
- package/dist/schema-activities.d.ts +821 -0
- package/dist/schema-activities.d.ts.map +1 -0
- package/dist/schema-activities.js +92 -0
- package/dist/schema-relations.d.ts +47 -0
- package/dist/schema-relations.d.ts.map +1 -0
- package/dist/schema-relations.js +70 -0
- package/dist/schema-shared.d.ts +10 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +36 -0
- package/dist/schema-signals.d.ts +324 -0
- package/dist/schema-signals.d.ts.map +1 -0
- package/dist/schema-signals.js +80 -0
- package/dist/schema.d.ts +6 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +5 -0
- package/dist/service/accounts-merge.d.ts +63 -0
- package/dist/service/accounts-merge.d.ts.map +1 -0
- package/dist/service/accounts-merge.js +382 -0
- package/dist/service/accounts-organizations.d.ts +97 -0
- package/dist/service/accounts-organizations.d.ts.map +1 -0
- package/dist/service/accounts-organizations.js +70 -0
- package/dist/service/accounts-people.d.ts +1315 -0
- package/dist/service/accounts-people.d.ts.map +1 -0
- package/dist/service/accounts-people.js +409 -0
- package/dist/service/accounts-resolve.d.ts +76 -0
- package/dist/service/accounts-resolve.d.ts.map +1 -0
- package/dist/service/accounts-resolve.js +103 -0
- package/dist/service/accounts-shared.d.ts +68 -0
- package/dist/service/accounts-shared.d.ts.map +1 -0
- package/dist/service/accounts-shared.js +149 -0
- package/dist/service/accounts.d.ts +1465 -0
- package/dist/service/accounts.d.ts.map +1 -0
- package/dist/service/accounts.js +13 -0
- package/dist/service/activities.d.ts +486 -0
- package/dist/service/activities.d.ts.map +1 -0
- package/dist/service/activities.js +114 -0
- package/dist/service/custom-fields.d.ts +118 -0
- package/dist/service/custom-fields.d.ts.map +1 -0
- package/dist/service/custom-fields.js +88 -0
- package/dist/service/customer-signals.d.ts +733 -0
- package/dist/service/customer-signals.d.ts.map +1 -0
- package/dist/service/customer-signals.js +112 -0
- package/dist/service/helpers.d.ts +22 -0
- package/dist/service/helpers.d.ts.map +1 -0
- package/dist/service/helpers.js +39 -0
- package/dist/service/index.d.ts +4434 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +17 -0
- package/dist/service/person-documents.d.ts +1201 -0
- package/dist/service/person-documents.d.ts.map +1 -0
- package/dist/service/person-documents.js +240 -0
- package/dist/service/person-relationships.d.ts +502 -0
- package/dist/service/person-relationships.d.ts.map +1 -0
- package/dist/service/person-relationships.js +121 -0
- package/dist/validation.d.ts +3 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,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
|
+
]);
|
package/dist/schema.d.ts
ADDED
|
@@ -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,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"}
|