@fonderie/customers 1.0.0
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 +21 -0
- package/README.md +41 -0
- package/dist/index.cjs +2220 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +237 -0
- package/dist/index.d.ts +237 -0
- package/dist/index.js +2178 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/index.js +7 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/sql/001_customers.sql +111 -0
- package/dist/migrations/sql/002_customers_sex.sql +2 -0
- package/dist/migrations/sql/003_customer_sequences.sql +6 -0
- package/dist/migrations/sql/004_customer_sequences_per_workspace.sql +1 -0
- package/dist/migrations/sql/005_drop_job_title.sql +1 -0
- package/dist/migrations/sql/006_unique_email_phone.sql +23 -0
- package/dist/migrations/sql/007_rename_archived_to_blacklisted.sql +13 -0
- package/dist/migrations/sql/008_blacklist_reason.sql +2 -0
- package/dist/migrations/sql/009_customer_relationships.sql +25 -0
- package/dist/migrations/sql/010_customer_labels.sql +75 -0
- package/dist/migrations/sql/011_label_fk_set_null.sql +17 -0
- package/dist/types.cjs +19 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +124 -0
- package/dist/types.d.ts +124 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +85 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CustomerAddressModel: () => CustomerAddressModel,
|
|
24
|
+
CustomerEmailModel: () => CustomerEmailModel,
|
|
25
|
+
CustomerModel: () => CustomerModel,
|
|
26
|
+
CustomerNoteModel: () => CustomerNoteModel,
|
|
27
|
+
CustomerPhoneModel: () => CustomerPhoneModel,
|
|
28
|
+
CustomerTagModel: () => CustomerTagModel,
|
|
29
|
+
CustomersModule: () => CustomersModule,
|
|
30
|
+
EVENT_KEYS: () => EVENT_KEYS,
|
|
31
|
+
toAddressDTO: () => toAddressDTO,
|
|
32
|
+
toCustomerAddressDTO: () => toCustomerAddressDTO,
|
|
33
|
+
toCustomerDTO: () => toCustomerDTO,
|
|
34
|
+
toCustomerDetailDTO: () => toCustomerDetailDTO,
|
|
35
|
+
toCustomerEmailDTO: () => toCustomerEmailDTO,
|
|
36
|
+
toCustomerNoteDTO: () => toCustomerNoteDTO,
|
|
37
|
+
toCustomerPhoneDTO: () => toCustomerPhoneDTO,
|
|
38
|
+
toCustomerTagDTO: () => toCustomerTagDTO
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
|
|
42
|
+
// src/config.ts
|
|
43
|
+
var EVENT_KEYS = {
|
|
44
|
+
customerCreated: "fonderie.customer.created",
|
|
45
|
+
customerUpdated: "fonderie.customer.updated",
|
|
46
|
+
customerDeleted: "fonderie.customer.deleted",
|
|
47
|
+
customerBlacklisted: "fonderie.customer.blacklisted",
|
|
48
|
+
customerUnblacklisted: "fonderie.customer.unblacklisted"
|
|
49
|
+
};
|
|
50
|
+
var DEFAULT_REFERENCE_CODE_PREFIX = "CLT";
|
|
51
|
+
|
|
52
|
+
// src/dtos/customer.ts
|
|
53
|
+
var import_core = require("@fonderie/core");
|
|
54
|
+
var VALID_SEX = ["UNKNOWN", "MALE", "FEMALE"];
|
|
55
|
+
function toCustomerDTO(c) {
|
|
56
|
+
return {
|
|
57
|
+
id: (0, import_core.stringOrEmpty)(c.id),
|
|
58
|
+
type: (0, import_core.stringOrEmpty)(c.type),
|
|
59
|
+
sex: VALID_SEX.includes(c.sex) ? c.sex : "UNKNOWN",
|
|
60
|
+
firstName: (0, import_core.stringOrEmpty)(c.firstName),
|
|
61
|
+
lastName: (0, import_core.stringOrEmpty)(c.lastName),
|
|
62
|
+
companyName: (0, import_core.stringOrEmpty)(c.companyName),
|
|
63
|
+
avatarUrl: (0, import_core.stringOrEmpty)(c.avatarUrl),
|
|
64
|
+
locale: (0, import_core.stringOrEmpty)(c.locale),
|
|
65
|
+
referenceCode: (0, import_core.stringOrEmpty)(c.referenceCode),
|
|
66
|
+
blacklisted: { status: (0, import_core.booleanOrFalse)(c.isBlacklisted), reason: c.blacklistReason ?? null },
|
|
67
|
+
createdBy: (0, import_core.stringOrEmpty)(c.createdBy),
|
|
68
|
+
createdAt: (0, import_core.dateOrEmpty)(c.createdAt),
|
|
69
|
+
updatedAt: (0, import_core.dateOrEmpty)(c.updatedAt)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function toCustomerRelationshipDTO(r) {
|
|
73
|
+
return {
|
|
74
|
+
id: (0, import_core.stringOrEmpty)(r.id),
|
|
75
|
+
relatedId: (0, import_core.stringOrEmpty)(r.relatedId),
|
|
76
|
+
relationship: (0, import_core.stringOrEmpty)(r.relationship),
|
|
77
|
+
isPrimary: (0, import_core.booleanOrFalse)(r.isPrimary),
|
|
78
|
+
createdAt: (0, import_core.dateOrEmpty)(r.createdAt)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function toCustomerShallowDTO(c) {
|
|
82
|
+
return {
|
|
83
|
+
...toCustomerDTO(c),
|
|
84
|
+
emails: (0, import_core.arrayOrEmpty)(c.emails).map(toCustomerEmailDTO),
|
|
85
|
+
phones: (0, import_core.arrayOrEmpty)(c.phones).map(toCustomerPhoneDTO),
|
|
86
|
+
addresses: (0, import_core.arrayOrEmpty)(c.addresses).map(toCustomerAddressDTO),
|
|
87
|
+
notes: (0, import_core.arrayOrEmpty)(c.notes).map(toCustomerNoteDTO),
|
|
88
|
+
tags: (0, import_core.arrayOrEmpty)(c.tags)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function toCustomerRelationshipExpandedDTO(r) {
|
|
92
|
+
const { id: customerId, ...customerFields } = toCustomerShallowDTO(r.customer);
|
|
93
|
+
return {
|
|
94
|
+
id: (0, import_core.stringOrEmpty)(r.id),
|
|
95
|
+
customerId,
|
|
96
|
+
relationship: (0, import_core.stringOrEmpty)(r.relationship),
|
|
97
|
+
isPrimary: (0, import_core.booleanOrFalse)(r.isPrimary),
|
|
98
|
+
...customerFields
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function toCustomerDetailDTO(c) {
|
|
102
|
+
return {
|
|
103
|
+
...toCustomerDTO(c),
|
|
104
|
+
emails: (0, import_core.arrayOrEmpty)(c.emails).map(toCustomerEmailDTO),
|
|
105
|
+
phones: (0, import_core.arrayOrEmpty)(c.phones).map(toCustomerPhoneDTO),
|
|
106
|
+
addresses: (0, import_core.arrayOrEmpty)(c.addresses).map(toCustomerAddressDTO),
|
|
107
|
+
notes: (0, import_core.arrayOrEmpty)(c.notes).map(toCustomerNoteDTO),
|
|
108
|
+
relationships: (0, import_core.arrayOrEmpty)(c.relationships).map(toCustomerRelationshipExpandedDTO),
|
|
109
|
+
tags: (0, import_core.arrayOrEmpty)(c.tags)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function toCustomerRelationshipExpandedD2DTO(r) {
|
|
113
|
+
const { id: customerId, ...customerFields } = toCustomerShallowDTO(r.customer);
|
|
114
|
+
return {
|
|
115
|
+
id: (0, import_core.stringOrEmpty)(r.id),
|
|
116
|
+
customerId,
|
|
117
|
+
relationship: (0, import_core.stringOrEmpty)(r.relationship),
|
|
118
|
+
isPrimary: (0, import_core.booleanOrFalse)(r.isPrimary),
|
|
119
|
+
...customerFields,
|
|
120
|
+
relationships: (0, import_core.arrayOrEmpty)(r.customer.relationships).map(toCustomerRelationshipExpandedDTO)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function toCustomerDetailD2DTO(c) {
|
|
124
|
+
return {
|
|
125
|
+
...toCustomerDTO(c),
|
|
126
|
+
emails: (0, import_core.arrayOrEmpty)(c.emails).map(toCustomerEmailDTO),
|
|
127
|
+
phones: (0, import_core.arrayOrEmpty)(c.phones).map(toCustomerPhoneDTO),
|
|
128
|
+
addresses: (0, import_core.arrayOrEmpty)(c.addresses).map(toCustomerAddressDTO),
|
|
129
|
+
notes: (0, import_core.arrayOrEmpty)(c.notes).map(toCustomerNoteDTO),
|
|
130
|
+
relationships: (0, import_core.arrayOrEmpty)(c.relationships).map(toCustomerRelationshipExpandedD2DTO),
|
|
131
|
+
tags: (0, import_core.arrayOrEmpty)(c.tags)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function toAddressDTO(a) {
|
|
135
|
+
return {
|
|
136
|
+
countryIso: (0, import_core.stringOrEmpty)(a.countryIso),
|
|
137
|
+
subdivision1Iso: (0, import_core.stringOrEmpty)(a.subdivision1Iso),
|
|
138
|
+
subdivision2Iso: (0, import_core.stringOrEmpty)(a.subdivision2Iso),
|
|
139
|
+
zipPostalCode: (0, import_core.stringOrEmpty)(a.zipPostalCode),
|
|
140
|
+
unit: (0, import_core.stringOrEmpty)(a.unit),
|
|
141
|
+
line1: (0, import_core.stringOrEmpty)(a.line1),
|
|
142
|
+
line2: (0, import_core.stringOrEmpty)(a.line2)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function toCustomerEmailDTO(e) {
|
|
146
|
+
return {
|
|
147
|
+
id: (0, import_core.stringOrEmpty)(e.id),
|
|
148
|
+
email: (0, import_core.stringOrEmpty)(e.email),
|
|
149
|
+
label: (0, import_core.stringOrEmpty)(e.label),
|
|
150
|
+
isPrimary: (0, import_core.booleanOrFalse)(e.isPrimary),
|
|
151
|
+
createdAt: (0, import_core.dateOrEmpty)(e.createdAt)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function toCustomerPhoneDTO(p) {
|
|
155
|
+
return {
|
|
156
|
+
id: (0, import_core.stringOrEmpty)(p.id),
|
|
157
|
+
phone: (0, import_core.stringOrEmpty)(p.phone),
|
|
158
|
+
label: (0, import_core.stringOrEmpty)(p.label),
|
|
159
|
+
isPrimary: (0, import_core.booleanOrFalse)(p.isPrimary),
|
|
160
|
+
createdAt: (0, import_core.dateOrEmpty)(p.createdAt)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function toCustomerAddressDTO(ca) {
|
|
164
|
+
return {
|
|
165
|
+
id: (0, import_core.stringOrEmpty)(ca.addrId),
|
|
166
|
+
label: (0, import_core.stringOrEmpty)(ca.label),
|
|
167
|
+
isPrimary: (0, import_core.booleanOrFalse)(ca.isPrimary),
|
|
168
|
+
address: toAddressDTO(ca.address)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function toCustomerNoteDTO(n) {
|
|
172
|
+
return {
|
|
173
|
+
id: (0, import_core.stringOrEmpty)(n.id),
|
|
174
|
+
authorId: (0, import_core.stringOrEmpty)(n.authorId),
|
|
175
|
+
body: (0, import_core.stringOrEmpty)(n.body),
|
|
176
|
+
createdAt: (0, import_core.dateOrEmpty)(n.createdAt),
|
|
177
|
+
updatedAt: (0, import_core.dateOrEmpty)(n.updatedAt)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function toCustomerTagDTO(t) {
|
|
181
|
+
return {
|
|
182
|
+
tag: (0, import_core.stringOrEmpty)(t.tag)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/models/customer.model.ts
|
|
187
|
+
function groupByCustomer(rows) {
|
|
188
|
+
return rows.reduce((m, r) => {
|
|
189
|
+
if (!m.has(r.customerId)) m.set(r.customerId, []);
|
|
190
|
+
m.get(r.customerId).push(r);
|
|
191
|
+
return m;
|
|
192
|
+
}, /* @__PURE__ */ new Map());
|
|
193
|
+
}
|
|
194
|
+
var SELECT_CUSTOMER = `
|
|
195
|
+
id,
|
|
196
|
+
workspace_id AS "workspaceId",
|
|
197
|
+
type,
|
|
198
|
+
sex,
|
|
199
|
+
first_name AS "firstName",
|
|
200
|
+
last_name AS "lastName",
|
|
201
|
+
company_name AS "companyName",
|
|
202
|
+
avatar_url AS "avatarUrl",
|
|
203
|
+
locale,
|
|
204
|
+
reference_code AS "referenceCode",
|
|
205
|
+
is_blacklisted AS "isBlacklisted",
|
|
206
|
+
blacklist_reason AS "blacklistReason",
|
|
207
|
+
created_by AS "createdBy",
|
|
208
|
+
created_at AS "createdAt",
|
|
209
|
+
updated_at AS "updatedAt"
|
|
210
|
+
`;
|
|
211
|
+
var CustomerModel = class {
|
|
212
|
+
constructor(store) {
|
|
213
|
+
this.store = store;
|
|
214
|
+
}
|
|
215
|
+
store;
|
|
216
|
+
async allocateCode(workspaceId, prefix) {
|
|
217
|
+
const [row] = await this.store.query(
|
|
218
|
+
`INSERT INTO fonderie_customer_sequences (workspace_id, prefix, next_val)
|
|
219
|
+
VALUES ($1, $2, 1)
|
|
220
|
+
ON CONFLICT (workspace_id, prefix) DO UPDATE
|
|
221
|
+
SET next_val = fonderie_customer_sequences.next_val + 1
|
|
222
|
+
RETURNING next_val AS "nextVal"`,
|
|
223
|
+
[workspaceId, prefix]
|
|
224
|
+
);
|
|
225
|
+
return `${prefix}-${String(row.nextVal).padStart(4, "0")}`;
|
|
226
|
+
}
|
|
227
|
+
async list(opts) {
|
|
228
|
+
const conditions = ["workspace_id = $1"];
|
|
229
|
+
const params = [opts.workspaceId];
|
|
230
|
+
if (opts.blacklisted !== void 0) {
|
|
231
|
+
params.push(opts.blacklisted);
|
|
232
|
+
conditions.push(`is_blacklisted = $${params.length}`);
|
|
233
|
+
}
|
|
234
|
+
if (opts.search) {
|
|
235
|
+
params.push(`%${opts.search}%`);
|
|
236
|
+
const idx = params.length;
|
|
237
|
+
conditions.push(
|
|
238
|
+
`(first_name ILIKE $${idx} OR last_name ILIKE $${idx} OR company_name ILIKE $${idx} OR reference_code ILIKE $${idx})`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const limit = Math.min(opts.limit ?? 50, 200);
|
|
242
|
+
const offset = opts.offset ?? 0;
|
|
243
|
+
params.push(limit, offset);
|
|
244
|
+
const limitIdx = params.length - 1;
|
|
245
|
+
const offsetIdx = params.length;
|
|
246
|
+
return this.store.query(
|
|
247
|
+
`SELECT ${SELECT_CUSTOMER}
|
|
248
|
+
FROM fonderie_customers
|
|
249
|
+
WHERE ${conditions.join(" AND ")}
|
|
250
|
+
ORDER BY created_at DESC
|
|
251
|
+
LIMIT $${limitIdx} OFFSET $${offsetIdx}`,
|
|
252
|
+
params
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
async findById(id, workspaceId) {
|
|
256
|
+
const [row] = await this.store.query(
|
|
257
|
+
`SELECT ${SELECT_CUSTOMER}
|
|
258
|
+
FROM fonderie_customers
|
|
259
|
+
WHERE id = $1 AND workspace_id = $2`,
|
|
260
|
+
[id, workspaceId]
|
|
261
|
+
);
|
|
262
|
+
return row ?? null;
|
|
263
|
+
}
|
|
264
|
+
async findDetail(id, workspaceId, depth = 1) {
|
|
265
|
+
const [row] = await this.store.query(
|
|
266
|
+
`SELECT ${SELECT_CUSTOMER}
|
|
267
|
+
FROM fonderie_customers
|
|
268
|
+
WHERE id = $1 AND workspace_id = $2`,
|
|
269
|
+
[id, workspaceId]
|
|
270
|
+
);
|
|
271
|
+
if (!row) return null;
|
|
272
|
+
const [emailRows, phoneRows, addressRows, noteRows, relationshipRows, tagRows] = await Promise.all([
|
|
273
|
+
this.store.query(
|
|
274
|
+
`SELECT id,
|
|
275
|
+
customer_id AS "customerId",
|
|
276
|
+
email,
|
|
277
|
+
label_id AS "labelId",
|
|
278
|
+
(SELECT value FROM fonderie_customer_labels WHERE id = label_id) AS label,
|
|
279
|
+
is_primary AS "isPrimary",
|
|
280
|
+
created_at AS "createdAt"
|
|
281
|
+
FROM fonderie_customer_emails
|
|
282
|
+
WHERE customer_id = $1
|
|
283
|
+
ORDER BY is_primary DESC, created_at ASC`,
|
|
284
|
+
[id]
|
|
285
|
+
),
|
|
286
|
+
this.store.query(
|
|
287
|
+
`SELECT id,
|
|
288
|
+
customer_id AS "customerId",
|
|
289
|
+
phone,
|
|
290
|
+
label_id AS "labelId",
|
|
291
|
+
(SELECT value FROM fonderie_customer_labels WHERE id = label_id) AS label,
|
|
292
|
+
is_primary AS "isPrimary",
|
|
293
|
+
created_at AS "createdAt"
|
|
294
|
+
FROM fonderie_customer_phones
|
|
295
|
+
WHERE customer_id = $1
|
|
296
|
+
ORDER BY is_primary DESC, created_at ASC`,
|
|
297
|
+
[id]
|
|
298
|
+
),
|
|
299
|
+
this.store.query(
|
|
300
|
+
`SELECT ca.addr_id AS "addrId",
|
|
301
|
+
ca.customer_id AS "customerId",
|
|
302
|
+
ca.label_id AS "labelId",
|
|
303
|
+
(SELECT value FROM fonderie_customer_labels WHERE id = ca.label_id) AS label,
|
|
304
|
+
ca.is_primary AS "isPrimary",
|
|
305
|
+
jsonb_build_object(
|
|
306
|
+
'id', a.id,
|
|
307
|
+
'countryIso', a.country_iso,
|
|
308
|
+
'subdivision1Iso', a.subdivision1_iso,
|
|
309
|
+
'subdivision2Iso', a.subdivision2_iso,
|
|
310
|
+
'zipPostalCode', a.zip_postal_code,
|
|
311
|
+
'unit', a.unit,
|
|
312
|
+
'line1', a.line1,
|
|
313
|
+
'line2', a.line2
|
|
314
|
+
) AS address
|
|
315
|
+
FROM fonderie_customer_addresses ca
|
|
316
|
+
JOIN fonderie_addresses a ON a.id = ca.addr_id
|
|
317
|
+
WHERE ca.customer_id = $1
|
|
318
|
+
ORDER BY ca.is_primary DESC`,
|
|
319
|
+
[id]
|
|
320
|
+
),
|
|
321
|
+
this.store.query(
|
|
322
|
+
`SELECT id,
|
|
323
|
+
customer_id AS "customerId",
|
|
324
|
+
author_id AS "authorId",
|
|
325
|
+
body,
|
|
326
|
+
created_at AS "createdAt",
|
|
327
|
+
updated_at AS "updatedAt"
|
|
328
|
+
FROM fonderie_customer_notes
|
|
329
|
+
WHERE customer_id = $1
|
|
330
|
+
ORDER BY created_at DESC`,
|
|
331
|
+
[id]
|
|
332
|
+
),
|
|
333
|
+
this.store.query(
|
|
334
|
+
`SELECT id,
|
|
335
|
+
workspace_id AS "workspaceId",
|
|
336
|
+
customer_id AS "customerId",
|
|
337
|
+
related_id AS "relatedId",
|
|
338
|
+
relationship,
|
|
339
|
+
is_primary AS "isPrimary",
|
|
340
|
+
created_at AS "createdAt"
|
|
341
|
+
FROM fonderie_customer_relationships
|
|
342
|
+
WHERE customer_id = $1
|
|
343
|
+
ORDER BY is_primary DESC, created_at ASC`,
|
|
344
|
+
[id]
|
|
345
|
+
),
|
|
346
|
+
this.store.query(
|
|
347
|
+
`SELECT tag FROM fonderie_customer_tags WHERE customer_id = $1 ORDER BY tag ASC`,
|
|
348
|
+
[id]
|
|
349
|
+
)
|
|
350
|
+
]);
|
|
351
|
+
const relatedIds = relationshipRows.map((r) => r.relatedId);
|
|
352
|
+
let expandedRelationships = [];
|
|
353
|
+
if (relatedIds.length > 0) {
|
|
354
|
+
const [relCustomers, relEmails, relPhones, relAddresses, relNotes, relTags] = await Promise.all([
|
|
355
|
+
this.store.query(
|
|
356
|
+
`SELECT ${SELECT_CUSTOMER} FROM fonderie_customers WHERE id = ANY($1::uuid[]) AND workspace_id = $2`,
|
|
357
|
+
[relatedIds, workspaceId]
|
|
358
|
+
),
|
|
359
|
+
this.store.query(
|
|
360
|
+
`SELECT id, customer_id AS "customerId", email, label_id AS "labelId",
|
|
361
|
+
(SELECT value FROM fonderie_customer_labels WHERE id = label_id) AS label,
|
|
362
|
+
is_primary AS "isPrimary", created_at AS "createdAt"
|
|
363
|
+
FROM fonderie_customer_emails WHERE customer_id = ANY($1::uuid[]) ORDER BY is_primary DESC, created_at ASC`,
|
|
364
|
+
[relatedIds]
|
|
365
|
+
),
|
|
366
|
+
this.store.query(
|
|
367
|
+
`SELECT id, customer_id AS "customerId", phone, label_id AS "labelId",
|
|
368
|
+
(SELECT value FROM fonderie_customer_labels WHERE id = label_id) AS label,
|
|
369
|
+
is_primary AS "isPrimary", created_at AS "createdAt"
|
|
370
|
+
FROM fonderie_customer_phones WHERE customer_id = ANY($1::uuid[]) ORDER BY is_primary DESC, created_at ASC`,
|
|
371
|
+
[relatedIds]
|
|
372
|
+
),
|
|
373
|
+
this.store.query(
|
|
374
|
+
`SELECT ca.addr_id AS "addrId",
|
|
375
|
+
ca.customer_id AS "customerId",
|
|
376
|
+
ca.label_id AS "labelId",
|
|
377
|
+
(SELECT value FROM fonderie_customer_labels WHERE id = ca.label_id) AS label,
|
|
378
|
+
ca.is_primary AS "isPrimary",
|
|
379
|
+
jsonb_build_object(
|
|
380
|
+
'id', a.id,
|
|
381
|
+
'countryIso', a.country_iso,
|
|
382
|
+
'subdivision1Iso', a.subdivision1_iso,
|
|
383
|
+
'subdivision2Iso', a.subdivision2_iso,
|
|
384
|
+
'zipPostalCode', a.zip_postal_code,
|
|
385
|
+
'unit', a.unit,
|
|
386
|
+
'line1', a.line1,
|
|
387
|
+
'line2', a.line2
|
|
388
|
+
) AS address
|
|
389
|
+
FROM fonderie_customer_addresses ca
|
|
390
|
+
JOIN fonderie_addresses a ON a.id = ca.addr_id
|
|
391
|
+
WHERE ca.customer_id = ANY($1::uuid[])
|
|
392
|
+
ORDER BY ca.is_primary DESC`,
|
|
393
|
+
[relatedIds]
|
|
394
|
+
),
|
|
395
|
+
this.store.query(
|
|
396
|
+
`SELECT id, customer_id AS "customerId", author_id AS "authorId", body, created_at AS "createdAt", updated_at AS "updatedAt"
|
|
397
|
+
FROM fonderie_customer_notes WHERE customer_id = ANY($1::uuid[]) ORDER BY created_at DESC`,
|
|
398
|
+
[relatedIds]
|
|
399
|
+
),
|
|
400
|
+
this.store.query(
|
|
401
|
+
`SELECT customer_id AS "customerId", tag FROM fonderie_customer_tags WHERE customer_id = ANY($1::uuid[]) ORDER BY tag ASC`,
|
|
402
|
+
[relatedIds]
|
|
403
|
+
)
|
|
404
|
+
]);
|
|
405
|
+
const customerMap = new Map(relCustomers.map((c) => [c.id, c]));
|
|
406
|
+
const emailMap = groupByCustomer(relEmails);
|
|
407
|
+
const phoneMap = groupByCustomer(relPhones);
|
|
408
|
+
const addrMap = groupByCustomer(relAddresses);
|
|
409
|
+
const noteMap = groupByCustomer(relNotes);
|
|
410
|
+
const tagMap = relTags.reduce((m, t) => {
|
|
411
|
+
if (!m.has(t.customerId)) m.set(t.customerId, []);
|
|
412
|
+
m.get(t.customerId).push(t.tag);
|
|
413
|
+
return m;
|
|
414
|
+
}, /* @__PURE__ */ new Map());
|
|
415
|
+
expandedRelationships = relationshipRows.map((rel) => ({
|
|
416
|
+
id: rel.id,
|
|
417
|
+
workspaceId: rel.workspaceId,
|
|
418
|
+
customerId: rel.customerId,
|
|
419
|
+
relationship: rel.relationship,
|
|
420
|
+
isPrimary: rel.isPrimary,
|
|
421
|
+
createdAt: rel.createdAt,
|
|
422
|
+
customer: {
|
|
423
|
+
...customerMap.get(rel.relatedId) ?? { id: rel.relatedId },
|
|
424
|
+
emails: emailMap.get(rel.relatedId) ?? [],
|
|
425
|
+
phones: phoneMap.get(rel.relatedId) ?? [],
|
|
426
|
+
addresses: addrMap.get(rel.relatedId) ?? [],
|
|
427
|
+
notes: noteMap.get(rel.relatedId) ?? [],
|
|
428
|
+
tags: tagMap.get(rel.relatedId) ?? []
|
|
429
|
+
}
|
|
430
|
+
}));
|
|
431
|
+
}
|
|
432
|
+
if (depth >= 2 && expandedRelationships.length > 0) {
|
|
433
|
+
const visited = /* @__PURE__ */ new Set([id]);
|
|
434
|
+
const d1Ids = expandedRelationships.map((r) => r.customer.id);
|
|
435
|
+
const d2RelRows = await this.store.query(
|
|
436
|
+
`SELECT id,
|
|
437
|
+
workspace_id AS "workspaceId",
|
|
438
|
+
customer_id AS "customerId",
|
|
439
|
+
related_id AS "relatedId",
|
|
440
|
+
relationship,
|
|
441
|
+
is_primary AS "isPrimary",
|
|
442
|
+
created_at AS "createdAt"
|
|
443
|
+
FROM fonderie_customer_relationships
|
|
444
|
+
WHERE customer_id = ANY($1::uuid[])
|
|
445
|
+
ORDER BY is_primary DESC, created_at ASC`,
|
|
446
|
+
[d1Ids]
|
|
447
|
+
);
|
|
448
|
+
const validD2Rows = d2RelRows.filter((r) => !visited.has(r.relatedId));
|
|
449
|
+
const d2Ids = [...new Set(validD2Rows.map((r) => r.relatedId))];
|
|
450
|
+
let d2CustomerMap = /* @__PURE__ */ new Map();
|
|
451
|
+
let d2EmailMap = /* @__PURE__ */ new Map();
|
|
452
|
+
let d2PhoneMap = /* @__PURE__ */ new Map();
|
|
453
|
+
let d2NoteMap = /* @__PURE__ */ new Map();
|
|
454
|
+
let d2TagMap = /* @__PURE__ */ new Map();
|
|
455
|
+
if (d2Ids.length > 0) {
|
|
456
|
+
const [d2Customers, d2Emails, d2Phones, d2Notes, d2Tags] = await Promise.all([
|
|
457
|
+
this.store.query(
|
|
458
|
+
`SELECT ${SELECT_CUSTOMER} FROM fonderie_customers WHERE id = ANY($1::uuid[]) AND workspace_id = $2`,
|
|
459
|
+
[d2Ids, workspaceId]
|
|
460
|
+
),
|
|
461
|
+
this.store.query(
|
|
462
|
+
`SELECT id, customer_id AS "customerId", email, label_id AS "labelId",
|
|
463
|
+
(SELECT value FROM fonderie_customer_labels WHERE id = label_id) AS label,
|
|
464
|
+
is_primary AS "isPrimary", created_at AS "createdAt"
|
|
465
|
+
FROM fonderie_customer_emails WHERE customer_id = ANY($1::uuid[]) ORDER BY is_primary DESC, created_at ASC`,
|
|
466
|
+
[d2Ids]
|
|
467
|
+
),
|
|
468
|
+
this.store.query(
|
|
469
|
+
`SELECT id, customer_id AS "customerId", phone, label_id AS "labelId",
|
|
470
|
+
(SELECT value FROM fonderie_customer_labels WHERE id = label_id) AS label,
|
|
471
|
+
is_primary AS "isPrimary", created_at AS "createdAt"
|
|
472
|
+
FROM fonderie_customer_phones WHERE customer_id = ANY($1::uuid[]) ORDER BY is_primary DESC, created_at ASC`,
|
|
473
|
+
[d2Ids]
|
|
474
|
+
),
|
|
475
|
+
this.store.query(
|
|
476
|
+
`SELECT id, customer_id AS "customerId", author_id AS "authorId", body, created_at AS "createdAt", updated_at AS "updatedAt"
|
|
477
|
+
FROM fonderie_customer_notes WHERE customer_id = ANY($1::uuid[]) ORDER BY created_at DESC`,
|
|
478
|
+
[d2Ids]
|
|
479
|
+
),
|
|
480
|
+
this.store.query(
|
|
481
|
+
`SELECT customer_id AS "customerId", tag FROM fonderie_customer_tags WHERE customer_id = ANY($1::uuid[]) ORDER BY tag ASC`,
|
|
482
|
+
[d2Ids]
|
|
483
|
+
)
|
|
484
|
+
]);
|
|
485
|
+
d2CustomerMap = new Map(d2Customers.map((c) => [c.id, c]));
|
|
486
|
+
d2EmailMap = groupByCustomer(d2Emails);
|
|
487
|
+
d2PhoneMap = groupByCustomer(d2Phones);
|
|
488
|
+
d2NoteMap = groupByCustomer(d2Notes);
|
|
489
|
+
d2TagMap = d2Tags.reduce((m, t) => {
|
|
490
|
+
if (!m.has(t.customerId)) m.set(t.customerId, []);
|
|
491
|
+
m.get(t.customerId).push(t.tag);
|
|
492
|
+
return m;
|
|
493
|
+
}, /* @__PURE__ */ new Map());
|
|
494
|
+
}
|
|
495
|
+
const d2Relationships = validD2Rows.map((d2rel) => ({
|
|
496
|
+
id: d2rel.id,
|
|
497
|
+
workspaceId: d2rel.workspaceId,
|
|
498
|
+
customerId: d2rel.customerId,
|
|
499
|
+
relationship: d2rel.relationship,
|
|
500
|
+
isPrimary: d2rel.isPrimary,
|
|
501
|
+
createdAt: d2rel.createdAt,
|
|
502
|
+
customer: {
|
|
503
|
+
...d2CustomerMap.get(d2rel.relatedId) ?? { id: d2rel.relatedId },
|
|
504
|
+
emails: d2EmailMap.get(d2rel.relatedId) ?? [],
|
|
505
|
+
phones: d2PhoneMap.get(d2rel.relatedId) ?? [],
|
|
506
|
+
addresses: [],
|
|
507
|
+
notes: d2NoteMap.get(d2rel.relatedId) ?? [],
|
|
508
|
+
tags: d2TagMap.get(d2rel.relatedId) ?? []
|
|
509
|
+
}
|
|
510
|
+
}));
|
|
511
|
+
const d2RelsByD1Id = groupByCustomer(d2Relationships);
|
|
512
|
+
const d2ExpandedRelationships = expandedRelationships.map((rel) => ({
|
|
513
|
+
...rel,
|
|
514
|
+
customer: {
|
|
515
|
+
...rel.customer,
|
|
516
|
+
relationships: d2RelsByD1Id.get(rel.customer.id) ?? []
|
|
517
|
+
}
|
|
518
|
+
}));
|
|
519
|
+
return {
|
|
520
|
+
...row,
|
|
521
|
+
emails: emailRows,
|
|
522
|
+
phones: phoneRows,
|
|
523
|
+
addresses: addressRows,
|
|
524
|
+
notes: noteRows,
|
|
525
|
+
relationships: d2ExpandedRelationships,
|
|
526
|
+
tags: tagRows.map((t) => t.tag)
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
...row,
|
|
531
|
+
// DB returns TEXT for label; cast to the narrow union that callers expect.
|
|
532
|
+
emails: emailRows,
|
|
533
|
+
phones: phoneRows,
|
|
534
|
+
addresses: addressRows,
|
|
535
|
+
notes: noteRows,
|
|
536
|
+
relationships: expandedRelationships,
|
|
537
|
+
tags: tagRows.map((t) => t.tag)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
async create(opts) {
|
|
541
|
+
const referenceCode = opts.referenceCode ?? await this.allocateCode(opts.workspaceId, opts.referenceCodePrefix ?? DEFAULT_REFERENCE_CODE_PREFIX);
|
|
542
|
+
const [row] = await this.store.query(
|
|
543
|
+
`INSERT INTO fonderie_customers
|
|
544
|
+
(workspace_id, type, sex, first_name, last_name, company_name, avatar_url, locale, reference_code, created_by)
|
|
545
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
546
|
+
RETURNING ${SELECT_CUSTOMER}`,
|
|
547
|
+
[
|
|
548
|
+
opts.workspaceId,
|
|
549
|
+
opts.type ?? "individual",
|
|
550
|
+
opts.sex ?? "UNKNOWN",
|
|
551
|
+
opts.firstName ?? null,
|
|
552
|
+
opts.lastName ?? null,
|
|
553
|
+
opts.companyName ?? null,
|
|
554
|
+
opts.avatarUrl ?? null,
|
|
555
|
+
opts.locale ?? "en-US",
|
|
556
|
+
referenceCode,
|
|
557
|
+
opts.createdBy ?? null
|
|
558
|
+
]
|
|
559
|
+
);
|
|
560
|
+
if (!row) throw new Error("Failed to create customer");
|
|
561
|
+
return row;
|
|
562
|
+
}
|
|
563
|
+
async update(id, workspaceId, opts, referenceCodePrefix = DEFAULT_REFERENCE_CODE_PREFIX) {
|
|
564
|
+
let autoCode;
|
|
565
|
+
if (!opts.referenceCode) {
|
|
566
|
+
const current = await this.findById(id, workspaceId);
|
|
567
|
+
if (!current) return null;
|
|
568
|
+
if (!current.referenceCode) {
|
|
569
|
+
autoCode = await this.allocateCode(workspaceId, referenceCodePrefix);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const sets = ["updated_at = now()"];
|
|
573
|
+
const params = [id, workspaceId];
|
|
574
|
+
if (opts.type !== void 0) {
|
|
575
|
+
params.push(opts.type);
|
|
576
|
+
sets.push(`type = $${params.length}`);
|
|
577
|
+
}
|
|
578
|
+
if (opts.sex !== void 0) {
|
|
579
|
+
params.push(opts.sex);
|
|
580
|
+
sets.push(`sex = $${params.length}`);
|
|
581
|
+
}
|
|
582
|
+
if (opts.firstName !== void 0) {
|
|
583
|
+
params.push(opts.firstName);
|
|
584
|
+
sets.push(`first_name = $${params.length}`);
|
|
585
|
+
}
|
|
586
|
+
if (opts.lastName !== void 0) {
|
|
587
|
+
params.push(opts.lastName);
|
|
588
|
+
sets.push(`last_name = $${params.length}`);
|
|
589
|
+
}
|
|
590
|
+
if (opts.companyName !== void 0) {
|
|
591
|
+
params.push(opts.companyName);
|
|
592
|
+
sets.push(`company_name = $${params.length}`);
|
|
593
|
+
}
|
|
594
|
+
if (opts.avatarUrl !== void 0) {
|
|
595
|
+
params.push(opts.avatarUrl);
|
|
596
|
+
sets.push(`avatar_url = $${params.length}`);
|
|
597
|
+
}
|
|
598
|
+
if (opts.locale !== void 0) {
|
|
599
|
+
params.push(opts.locale);
|
|
600
|
+
sets.push(`locale = $${params.length}`);
|
|
601
|
+
}
|
|
602
|
+
const resolvedCode = opts.referenceCode ?? autoCode;
|
|
603
|
+
if (resolvedCode !== void 0) {
|
|
604
|
+
params.push(resolvedCode);
|
|
605
|
+
sets.push(`reference_code = $${params.length}`);
|
|
606
|
+
}
|
|
607
|
+
const [row] = await this.store.query(
|
|
608
|
+
`UPDATE fonderie_customers
|
|
609
|
+
SET ${sets.join(", ")}
|
|
610
|
+
WHERE id = $1 AND workspace_id = $2
|
|
611
|
+
RETURNING ${SELECT_CUSTOMER}`,
|
|
612
|
+
params
|
|
613
|
+
);
|
|
614
|
+
return row ?? null;
|
|
615
|
+
}
|
|
616
|
+
async delete(id, workspaceId) {
|
|
617
|
+
await Promise.all([
|
|
618
|
+
this.store.query(`DELETE FROM fonderie_customer_emails WHERE customer_id = $1`, [id]),
|
|
619
|
+
this.store.query(`DELETE FROM fonderie_customer_phones WHERE customer_id = $1`, [id]),
|
|
620
|
+
this.store.query(
|
|
621
|
+
`DELETE FROM fonderie_addresses WHERE id IN (
|
|
622
|
+
SELECT addr_id FROM fonderie_customer_addresses WHERE customer_id = $1
|
|
623
|
+
)`,
|
|
624
|
+
[id]
|
|
625
|
+
),
|
|
626
|
+
this.store.query(`DELETE FROM fonderie_customer_notes WHERE customer_id = $1`, [id]),
|
|
627
|
+
this.store.query(`DELETE FROM fonderie_customer_tags WHERE customer_id = $1`, [id]),
|
|
628
|
+
this.store.query(
|
|
629
|
+
`DELETE FROM fonderie_customer_relationships WHERE customer_id = $1 OR related_id = $1`,
|
|
630
|
+
[id]
|
|
631
|
+
)
|
|
632
|
+
]);
|
|
633
|
+
await this.store.query(`DELETE FROM fonderie_customers WHERE id = $1 AND workspace_id = $2`, [
|
|
634
|
+
id,
|
|
635
|
+
workspaceId
|
|
636
|
+
]);
|
|
637
|
+
}
|
|
638
|
+
async blacklist(id, workspaceId, reason) {
|
|
639
|
+
await this.store.query(
|
|
640
|
+
`UPDATE fonderie_customers
|
|
641
|
+
SET is_blacklisted = true, blacklist_reason = $3, updated_at = now()
|
|
642
|
+
WHERE id = $1 AND workspace_id = $2`,
|
|
643
|
+
[id, workspaceId, reason ?? null]
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
async unblacklist(id, workspaceId) {
|
|
647
|
+
await this.store.query(
|
|
648
|
+
`UPDATE fonderie_customers
|
|
649
|
+
SET is_blacklisted = false, blacklist_reason = null, updated_at = now()
|
|
650
|
+
WHERE id = $1 AND workspace_id = $2`,
|
|
651
|
+
[id, workspaceId]
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// src/models/customer-relationship.model.ts
|
|
657
|
+
var CustomerRelationshipModel = class {
|
|
658
|
+
constructor(store) {
|
|
659
|
+
this.store = store;
|
|
660
|
+
}
|
|
661
|
+
store;
|
|
662
|
+
async list(customerId) {
|
|
663
|
+
return this.store.query(
|
|
664
|
+
`SELECT id,
|
|
665
|
+
workspace_id AS "workspaceId",
|
|
666
|
+
customer_id AS "customerId",
|
|
667
|
+
related_id AS "relatedId",
|
|
668
|
+
relationship,
|
|
669
|
+
is_primary AS "isPrimary",
|
|
670
|
+
created_at AS "createdAt"
|
|
671
|
+
FROM fonderie_customer_relationships
|
|
672
|
+
WHERE customer_id = $1
|
|
673
|
+
ORDER BY is_primary DESC, created_at ASC`,
|
|
674
|
+
[customerId]
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
async add(opts) {
|
|
678
|
+
return this.store.transaction(async (tx) => {
|
|
679
|
+
if (opts.isPrimary) {
|
|
680
|
+
await tx.query(
|
|
681
|
+
`UPDATE fonderie_customer_relationships
|
|
682
|
+
SET is_primary = false
|
|
683
|
+
WHERE customer_id = $1`,
|
|
684
|
+
[opts.customerId]
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
const [row] = await tx.query(
|
|
688
|
+
`INSERT INTO fonderie_customer_relationships
|
|
689
|
+
(workspace_id, customer_id, related_id, relationship, is_primary)
|
|
690
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
691
|
+
ON CONFLICT (customer_id, related_id) DO UPDATE
|
|
692
|
+
SET relationship = EXCLUDED.relationship,
|
|
693
|
+
is_primary = EXCLUDED.is_primary
|
|
694
|
+
RETURNING
|
|
695
|
+
id,
|
|
696
|
+
workspace_id AS "workspaceId",
|
|
697
|
+
customer_id AS "customerId",
|
|
698
|
+
related_id AS "relatedId",
|
|
699
|
+
relationship,
|
|
700
|
+
is_primary AS "isPrimary",
|
|
701
|
+
created_at AS "createdAt"`,
|
|
702
|
+
[opts.workspaceId, opts.customerId, opts.relatedId, opts.relationship, opts.isPrimary ?? false]
|
|
703
|
+
);
|
|
704
|
+
if (!row) throw new Error("Failed to create relationship");
|
|
705
|
+
return row;
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
async setPrimary(customerId, relatedId) {
|
|
709
|
+
await this.store.transaction(async (tx) => {
|
|
710
|
+
await tx.query(
|
|
711
|
+
`UPDATE fonderie_customer_relationships SET is_primary = false WHERE customer_id = $1`,
|
|
712
|
+
[customerId]
|
|
713
|
+
);
|
|
714
|
+
await tx.query(
|
|
715
|
+
`UPDATE fonderie_customer_relationships
|
|
716
|
+
SET is_primary = true
|
|
717
|
+
WHERE customer_id = $1 AND related_id = $2`,
|
|
718
|
+
[customerId, relatedId]
|
|
719
|
+
);
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
async remove(customerId, relatedId) {
|
|
723
|
+
await this.store.query(
|
|
724
|
+
`DELETE FROM fonderie_customer_relationships
|
|
725
|
+
WHERE customer_id = $1 AND related_id = $2`,
|
|
726
|
+
[customerId, relatedId]
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// src/models/customer-address.model.ts
|
|
732
|
+
var SELECT_CUSTOMER_ADDRESS = `
|
|
733
|
+
ca.addr_id AS "addrId",
|
|
734
|
+
ca.customer_id AS "customerId",
|
|
735
|
+
ca.label_id AS "labelId",
|
|
736
|
+
(SELECT value FROM fonderie_customer_labels WHERE id = ca.label_id) AS label,
|
|
737
|
+
ca.is_primary AS "isPrimary",
|
|
738
|
+
jsonb_build_object(
|
|
739
|
+
'id', a.id,
|
|
740
|
+
'countryIso', a.country_iso,
|
|
741
|
+
'subdivision1Iso', a.subdivision1_iso,
|
|
742
|
+
'subdivision2Iso', a.subdivision2_iso,
|
|
743
|
+
'zipPostalCode', a.zip_postal_code,
|
|
744
|
+
'unit', a.unit,
|
|
745
|
+
'line1', a.line1,
|
|
746
|
+
'line2', a.line2
|
|
747
|
+
) AS address
|
|
748
|
+
`;
|
|
749
|
+
var CustomerAddressModel = class {
|
|
750
|
+
constructor(store) {
|
|
751
|
+
this.store = store;
|
|
752
|
+
}
|
|
753
|
+
store;
|
|
754
|
+
list(customerId) {
|
|
755
|
+
return this.store.query(
|
|
756
|
+
`SELECT ${SELECT_CUSTOMER_ADDRESS}
|
|
757
|
+
FROM fonderie_customer_addresses ca
|
|
758
|
+
JOIN fonderie_addresses a ON a.id = ca.addr_id
|
|
759
|
+
WHERE ca.customer_id = $1
|
|
760
|
+
ORDER BY ca.is_primary DESC`,
|
|
761
|
+
[customerId]
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
async add(opts) {
|
|
765
|
+
const [existing] = await this.store.query(
|
|
766
|
+
`SELECT ca.addr_id AS "addrId"
|
|
767
|
+
FROM fonderie_customer_addresses ca
|
|
768
|
+
JOIN fonderie_addresses a ON a.id = ca.addr_id
|
|
769
|
+
WHERE ca.customer_id = $1
|
|
770
|
+
AND a.country_iso = $2
|
|
771
|
+
AND a.zip_postal_code = $3
|
|
772
|
+
AND a.subdivision1_iso IS NOT DISTINCT FROM $4
|
|
773
|
+
AND a.subdivision2_iso IS NOT DISTINCT FROM $5
|
|
774
|
+
AND a.unit IS NOT DISTINCT FROM $6
|
|
775
|
+
AND a.line1 IS NOT DISTINCT FROM $7
|
|
776
|
+
AND a.line2 IS NOT DISTINCT FROM $8
|
|
777
|
+
LIMIT 1`,
|
|
778
|
+
[
|
|
779
|
+
opts.customerId,
|
|
780
|
+
opts.countryIso,
|
|
781
|
+
opts.zipPostalCode,
|
|
782
|
+
opts.subdivision1Iso ?? null,
|
|
783
|
+
opts.subdivision2Iso ?? null,
|
|
784
|
+
opts.unit ?? null,
|
|
785
|
+
opts.line1 ?? null,
|
|
786
|
+
opts.line2 ?? null
|
|
787
|
+
]
|
|
788
|
+
);
|
|
789
|
+
if (existing) {
|
|
790
|
+
throw Object.assign(new Error("Duplicate address for this customer"), { code: "DUPLICATE_ADDRESS" });
|
|
791
|
+
}
|
|
792
|
+
return this.store.transaction(async (tx) => {
|
|
793
|
+
if (opts.isPrimary) {
|
|
794
|
+
await tx.query(
|
|
795
|
+
`UPDATE fonderie_customer_addresses
|
|
796
|
+
SET is_primary = false
|
|
797
|
+
WHERE customer_id = $1`,
|
|
798
|
+
[opts.customerId]
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
const [addr] = await tx.query(
|
|
802
|
+
`INSERT INTO fonderie_addresses
|
|
803
|
+
(id, country_iso, subdivision1_iso, subdivision2_iso, zip_postal_code, unit, line1, line2)
|
|
804
|
+
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7)
|
|
805
|
+
RETURNING id`,
|
|
806
|
+
[
|
|
807
|
+
opts.countryIso,
|
|
808
|
+
opts.subdivision1Iso ?? null,
|
|
809
|
+
opts.subdivision2Iso ?? null,
|
|
810
|
+
opts.zipPostalCode,
|
|
811
|
+
opts.unit ?? null,
|
|
812
|
+
opts.line1 ?? null,
|
|
813
|
+
opts.line2 ?? null
|
|
814
|
+
]
|
|
815
|
+
);
|
|
816
|
+
if (!addr) throw new Error("Failed to create address");
|
|
817
|
+
const [row] = await tx.query(
|
|
818
|
+
`INSERT INTO fonderie_customer_addresses (addr_id, customer_id, label_id, is_primary)
|
|
819
|
+
VALUES ($1, $2, $3, $4)
|
|
820
|
+
RETURNING
|
|
821
|
+
addr_id AS "addrId",
|
|
822
|
+
customer_id AS "customerId",
|
|
823
|
+
label_id AS "labelId",
|
|
824
|
+
NULL::text AS label,
|
|
825
|
+
is_primary AS "isPrimary",
|
|
826
|
+
NULL::jsonb AS address`,
|
|
827
|
+
[addr.id, opts.customerId, opts.labelId, opts.isPrimary ?? false]
|
|
828
|
+
);
|
|
829
|
+
if (!row) throw new Error("Failed to link address");
|
|
830
|
+
const [full] = await tx.query(
|
|
831
|
+
`SELECT ${SELECT_CUSTOMER_ADDRESS}
|
|
832
|
+
FROM fonderie_customer_addresses ca
|
|
833
|
+
JOIN fonderie_addresses a ON a.id = ca.addr_id
|
|
834
|
+
WHERE ca.addr_id = $1 AND ca.customer_id = $2`,
|
|
835
|
+
[addr.id, opts.customerId]
|
|
836
|
+
);
|
|
837
|
+
if (!full) throw new Error("Failed to fetch created address");
|
|
838
|
+
return full;
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
async updateLabel(addrId, customerId, labelId) {
|
|
842
|
+
const [row] = await this.store.query(
|
|
843
|
+
`UPDATE fonderie_customer_addresses
|
|
844
|
+
SET label_id = $3
|
|
845
|
+
WHERE addr_id = $1 AND customer_id = $2
|
|
846
|
+
RETURNING addr_id AS "addrId", customer_id AS "customerId", label_id AS "labelId", NULL::text AS label, is_primary AS "isPrimary", NULL::jsonb AS address`,
|
|
847
|
+
[addrId, customerId, labelId]
|
|
848
|
+
);
|
|
849
|
+
if (!row) throw Object.assign(new Error("Address not found"), { code: "NOT_FOUND" });
|
|
850
|
+
const [full] = await this.store.query(
|
|
851
|
+
`SELECT ${SELECT_CUSTOMER_ADDRESS}
|
|
852
|
+
FROM fonderie_customer_addresses ca
|
|
853
|
+
JOIN fonderie_addresses a ON a.id = ca.addr_id
|
|
854
|
+
WHERE ca.addr_id = $1 AND ca.customer_id = $2`,
|
|
855
|
+
[addrId, customerId]
|
|
856
|
+
);
|
|
857
|
+
if (!full) throw new Error("Failed to fetch updated address");
|
|
858
|
+
return full;
|
|
859
|
+
}
|
|
860
|
+
async setPrimary(addrId, customerId) {
|
|
861
|
+
await this.store.transaction(async (tx) => {
|
|
862
|
+
await tx.query(
|
|
863
|
+
`UPDATE fonderie_customer_addresses SET is_primary = false WHERE customer_id = $1`,
|
|
864
|
+
[customerId]
|
|
865
|
+
);
|
|
866
|
+
await tx.query(
|
|
867
|
+
`UPDATE fonderie_customer_addresses SET is_primary = true WHERE addr_id = $1 AND customer_id = $2`,
|
|
868
|
+
[addrId, customerId]
|
|
869
|
+
);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
async remove(addrId, customerId) {
|
|
873
|
+
await this.store.transaction(async (tx) => {
|
|
874
|
+
const [deleted] = await tx.query(
|
|
875
|
+
`DELETE FROM fonderie_customer_addresses
|
|
876
|
+
WHERE addr_id = $1 AND customer_id = $2
|
|
877
|
+
RETURNING is_primary AS "isPrimary"`,
|
|
878
|
+
[addrId, customerId]
|
|
879
|
+
);
|
|
880
|
+
await tx.query(`DELETE FROM fonderie_addresses WHERE id = $1`, [addrId]);
|
|
881
|
+
if (deleted?.isPrimary) {
|
|
882
|
+
await tx.query(
|
|
883
|
+
`UPDATE fonderie_customer_addresses
|
|
884
|
+
SET is_primary = true
|
|
885
|
+
WHERE addr_id = (
|
|
886
|
+
SELECT addr_id FROM fonderie_customer_addresses
|
|
887
|
+
WHERE customer_id = $1
|
|
888
|
+
ORDER BY addr_id ASC
|
|
889
|
+
LIMIT 1
|
|
890
|
+
)`,
|
|
891
|
+
[customerId]
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
// src/models/customer-email.model.ts
|
|
899
|
+
var SELECT_EMAIL = `
|
|
900
|
+
id,
|
|
901
|
+
customer_id AS "customerId",
|
|
902
|
+
email,
|
|
903
|
+
label_id AS "labelId",
|
|
904
|
+
(SELECT value FROM fonderie_customer_labels WHERE id = label_id) AS label,
|
|
905
|
+
is_primary AS "isPrimary",
|
|
906
|
+
created_at AS "createdAt"
|
|
907
|
+
`;
|
|
908
|
+
var CustomerEmailModel = class {
|
|
909
|
+
constructor(store) {
|
|
910
|
+
this.store = store;
|
|
911
|
+
}
|
|
912
|
+
store;
|
|
913
|
+
list(customerId) {
|
|
914
|
+
return this.store.query(
|
|
915
|
+
`SELECT ${SELECT_EMAIL}
|
|
916
|
+
FROM fonderie_customer_emails
|
|
917
|
+
WHERE customer_id = $1
|
|
918
|
+
ORDER BY is_primary DESC, created_at ASC`,
|
|
919
|
+
[customerId]
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
async add(opts) {
|
|
923
|
+
return this.store.transaction(async (tx) => {
|
|
924
|
+
if (opts.isPrimary) {
|
|
925
|
+
await tx.query(
|
|
926
|
+
`UPDATE fonderie_customer_emails SET is_primary = false WHERE customer_id = $1`,
|
|
927
|
+
[opts.customerId]
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
const [row] = await tx.query(
|
|
931
|
+
`INSERT INTO fonderie_customer_emails (id, customer_id, email, label_id, is_primary)
|
|
932
|
+
VALUES (gen_random_uuid(), $1, $2, $3, $4)
|
|
933
|
+
RETURNING ${SELECT_EMAIL}`,
|
|
934
|
+
[opts.customerId, opts.email, opts.labelId, opts.isPrimary ?? false]
|
|
935
|
+
);
|
|
936
|
+
if (!row) throw new Error("Failed to add customer email");
|
|
937
|
+
return row;
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
async updateLabel(emailId, customerId, labelId) {
|
|
941
|
+
const [row] = await this.store.query(
|
|
942
|
+
`UPDATE fonderie_customer_emails
|
|
943
|
+
SET label_id = $3
|
|
944
|
+
WHERE id = $1 AND customer_id = $2
|
|
945
|
+
RETURNING ${SELECT_EMAIL}`,
|
|
946
|
+
[emailId, customerId, labelId]
|
|
947
|
+
);
|
|
948
|
+
if (!row) throw Object.assign(new Error("Email not found"), { code: "NOT_FOUND" });
|
|
949
|
+
return row;
|
|
950
|
+
}
|
|
951
|
+
async setPrimary(emailId, customerId) {
|
|
952
|
+
await this.store.transaction(async (tx) => {
|
|
953
|
+
await tx.query(
|
|
954
|
+
`UPDATE fonderie_customer_emails SET is_primary = false WHERE customer_id = $1`,
|
|
955
|
+
[customerId]
|
|
956
|
+
);
|
|
957
|
+
await tx.query(
|
|
958
|
+
`UPDATE fonderie_customer_emails SET is_primary = true WHERE id = $1 AND customer_id = $2`,
|
|
959
|
+
[emailId, customerId]
|
|
960
|
+
);
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
async remove(emailId, customerId) {
|
|
964
|
+
await this.store.transaction(async (tx) => {
|
|
965
|
+
const [deleted] = await tx.query(
|
|
966
|
+
`DELETE FROM fonderie_customer_emails
|
|
967
|
+
WHERE id = $1 AND customer_id = $2
|
|
968
|
+
RETURNING is_primary AS "isPrimary"`,
|
|
969
|
+
[emailId, customerId]
|
|
970
|
+
);
|
|
971
|
+
if (deleted?.isPrimary) {
|
|
972
|
+
await tx.query(
|
|
973
|
+
`UPDATE fonderie_customer_emails
|
|
974
|
+
SET is_primary = true
|
|
975
|
+
WHERE id = (
|
|
976
|
+
SELECT id FROM fonderie_customer_emails
|
|
977
|
+
WHERE customer_id = $1
|
|
978
|
+
ORDER BY created_at ASC
|
|
979
|
+
LIMIT 1
|
|
980
|
+
)`,
|
|
981
|
+
[customerId]
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
// src/models/customer-label.model.ts
|
|
989
|
+
var CustomerLabelModel = class {
|
|
990
|
+
constructor(store) {
|
|
991
|
+
this.store = store;
|
|
992
|
+
}
|
|
993
|
+
store;
|
|
994
|
+
list(type) {
|
|
995
|
+
return this.store.query(
|
|
996
|
+
`SELECT id, type, value, created_at AS "createdAt"
|
|
997
|
+
FROM fonderie_customer_labels
|
|
998
|
+
WHERE type = $1
|
|
999
|
+
ORDER BY value ASC`,
|
|
1000
|
+
[type]
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
remove(id) {
|
|
1004
|
+
return this.store.query(
|
|
1005
|
+
`DELETE FROM fonderie_customer_labels WHERE id = $1`,
|
|
1006
|
+
[id]
|
|
1007
|
+
).then(() => void 0);
|
|
1008
|
+
}
|
|
1009
|
+
async findOrCreate(type, raw) {
|
|
1010
|
+
const value = raw.trim().toLowerCase();
|
|
1011
|
+
const [row] = await this.store.query(
|
|
1012
|
+
`INSERT INTO fonderie_customer_labels (type, value)
|
|
1013
|
+
VALUES ($1, $2)
|
|
1014
|
+
ON CONFLICT (type, value) DO UPDATE SET value = EXCLUDED.value
|
|
1015
|
+
RETURNING id, type, value, created_at AS "createdAt"`,
|
|
1016
|
+
[type, value]
|
|
1017
|
+
);
|
|
1018
|
+
if (!row) throw new Error("Failed to find or create label");
|
|
1019
|
+
return row;
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
// src/models/customer-note.model.ts
|
|
1024
|
+
var SELECT_NOTE = `
|
|
1025
|
+
id,
|
|
1026
|
+
customer_id AS "customerId",
|
|
1027
|
+
author_id AS "authorId",
|
|
1028
|
+
body,
|
|
1029
|
+
created_at AS "createdAt",
|
|
1030
|
+
updated_at AS "updatedAt"
|
|
1031
|
+
`;
|
|
1032
|
+
var CustomerNoteModel = class {
|
|
1033
|
+
constructor(store) {
|
|
1034
|
+
this.store = store;
|
|
1035
|
+
}
|
|
1036
|
+
store;
|
|
1037
|
+
list(customerId) {
|
|
1038
|
+
return this.store.query(
|
|
1039
|
+
`SELECT ${SELECT_NOTE}
|
|
1040
|
+
FROM fonderie_customer_notes
|
|
1041
|
+
WHERE customer_id = $1
|
|
1042
|
+
ORDER BY created_at DESC`,
|
|
1043
|
+
[customerId]
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
async create(opts) {
|
|
1047
|
+
const [row] = await this.store.query(
|
|
1048
|
+
`INSERT INTO fonderie_customer_notes (id, customer_id, author_id, body)
|
|
1049
|
+
VALUES (gen_random_uuid(), $1, $2, $3)
|
|
1050
|
+
RETURNING ${SELECT_NOTE}`,
|
|
1051
|
+
[opts.customerId, opts.authorId ?? null, opts.body]
|
|
1052
|
+
);
|
|
1053
|
+
if (!row) throw new Error("Failed to create note");
|
|
1054
|
+
return row;
|
|
1055
|
+
}
|
|
1056
|
+
async update(noteId, customerId, body) {
|
|
1057
|
+
const [row] = await this.store.query(
|
|
1058
|
+
`UPDATE fonderie_customer_notes
|
|
1059
|
+
SET body = $1, updated_at = now()
|
|
1060
|
+
WHERE id = $2 AND customer_id = $3
|
|
1061
|
+
RETURNING ${SELECT_NOTE}`,
|
|
1062
|
+
[body, noteId, customerId]
|
|
1063
|
+
);
|
|
1064
|
+
return row ?? null;
|
|
1065
|
+
}
|
|
1066
|
+
async delete(noteId, customerId) {
|
|
1067
|
+
await this.store.query(
|
|
1068
|
+
`DELETE FROM fonderie_customer_notes
|
|
1069
|
+
WHERE id = $1 AND customer_id = $2`,
|
|
1070
|
+
[noteId, customerId]
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
// src/models/customer-phone.model.ts
|
|
1076
|
+
var SELECT_PHONE = `
|
|
1077
|
+
id,
|
|
1078
|
+
customer_id AS "customerId",
|
|
1079
|
+
phone,
|
|
1080
|
+
label_id AS "labelId",
|
|
1081
|
+
(SELECT value FROM fonderie_customer_labels WHERE id = label_id) AS label,
|
|
1082
|
+
is_primary AS "isPrimary",
|
|
1083
|
+
created_at AS "createdAt"
|
|
1084
|
+
`;
|
|
1085
|
+
var CustomerPhoneModel = class {
|
|
1086
|
+
constructor(store) {
|
|
1087
|
+
this.store = store;
|
|
1088
|
+
}
|
|
1089
|
+
store;
|
|
1090
|
+
list(customerId) {
|
|
1091
|
+
return this.store.query(
|
|
1092
|
+
`SELECT ${SELECT_PHONE}
|
|
1093
|
+
FROM fonderie_customer_phones
|
|
1094
|
+
WHERE customer_id = $1
|
|
1095
|
+
ORDER BY is_primary DESC, created_at ASC`,
|
|
1096
|
+
[customerId]
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
async add(opts) {
|
|
1100
|
+
return this.store.transaction(async (tx) => {
|
|
1101
|
+
if (opts.isPrimary) {
|
|
1102
|
+
await tx.query(
|
|
1103
|
+
`UPDATE fonderie_customer_phones SET is_primary = false WHERE customer_id = $1`,
|
|
1104
|
+
[opts.customerId]
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
const [row] = await tx.query(
|
|
1108
|
+
`INSERT INTO fonderie_customer_phones (id, customer_id, phone, label_id, is_primary)
|
|
1109
|
+
VALUES (gen_random_uuid(), $1, $2, $3, $4)
|
|
1110
|
+
RETURNING ${SELECT_PHONE}`,
|
|
1111
|
+
[opts.customerId, opts.phone, opts.labelId, opts.isPrimary ?? false]
|
|
1112
|
+
);
|
|
1113
|
+
if (!row) throw new Error("Failed to add customer phone");
|
|
1114
|
+
return row;
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
async updateLabel(phoneId, customerId, labelId) {
|
|
1118
|
+
const [row] = await this.store.query(
|
|
1119
|
+
`UPDATE fonderie_customer_phones
|
|
1120
|
+
SET label_id = $3
|
|
1121
|
+
WHERE id = $1 AND customer_id = $2
|
|
1122
|
+
RETURNING ${SELECT_PHONE}`,
|
|
1123
|
+
[phoneId, customerId, labelId]
|
|
1124
|
+
);
|
|
1125
|
+
if (!row) throw Object.assign(new Error("Phone not found"), { code: "NOT_FOUND" });
|
|
1126
|
+
return row;
|
|
1127
|
+
}
|
|
1128
|
+
async setPrimary(phoneId, customerId) {
|
|
1129
|
+
await this.store.transaction(async (tx) => {
|
|
1130
|
+
await tx.query(
|
|
1131
|
+
`UPDATE fonderie_customer_phones SET is_primary = false WHERE customer_id = $1`,
|
|
1132
|
+
[customerId]
|
|
1133
|
+
);
|
|
1134
|
+
await tx.query(
|
|
1135
|
+
`UPDATE fonderie_customer_phones SET is_primary = true WHERE id = $1 AND customer_id = $2`,
|
|
1136
|
+
[phoneId, customerId]
|
|
1137
|
+
);
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
async remove(phoneId, customerId) {
|
|
1141
|
+
await this.store.transaction(async (tx) => {
|
|
1142
|
+
const [deleted] = await tx.query(
|
|
1143
|
+
`DELETE FROM fonderie_customer_phones
|
|
1144
|
+
WHERE id = $1 AND customer_id = $2
|
|
1145
|
+
RETURNING is_primary AS "isPrimary"`,
|
|
1146
|
+
[phoneId, customerId]
|
|
1147
|
+
);
|
|
1148
|
+
if (deleted?.isPrimary) {
|
|
1149
|
+
await tx.query(
|
|
1150
|
+
`UPDATE fonderie_customer_phones
|
|
1151
|
+
SET is_primary = true
|
|
1152
|
+
WHERE id = (
|
|
1153
|
+
SELECT id FROM fonderie_customer_phones
|
|
1154
|
+
WHERE customer_id = $1
|
|
1155
|
+
ORDER BY created_at ASC
|
|
1156
|
+
LIMIT 1
|
|
1157
|
+
)`,
|
|
1158
|
+
[customerId]
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
// src/models/customer-tag.model.ts
|
|
1166
|
+
var CustomerTagModel = class {
|
|
1167
|
+
constructor(store) {
|
|
1168
|
+
this.store = store;
|
|
1169
|
+
}
|
|
1170
|
+
store;
|
|
1171
|
+
async list(customerId) {
|
|
1172
|
+
const rows = await this.store.query(
|
|
1173
|
+
`SELECT tag FROM fonderie_customer_tags
|
|
1174
|
+
WHERE customer_id = $1
|
|
1175
|
+
ORDER BY tag ASC`,
|
|
1176
|
+
[customerId]
|
|
1177
|
+
);
|
|
1178
|
+
return rows.map((r) => r.tag);
|
|
1179
|
+
}
|
|
1180
|
+
async add(customerId, tag) {
|
|
1181
|
+
await this.store.query(
|
|
1182
|
+
`INSERT INTO fonderie_customer_tags (customer_id, tag)
|
|
1183
|
+
VALUES ($1, $2)
|
|
1184
|
+
ON CONFLICT (customer_id, tag) DO NOTHING`,
|
|
1185
|
+
[customerId, tag]
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
async remove(customerId, tag) {
|
|
1189
|
+
await this.store.query(
|
|
1190
|
+
`DELETE FROM fonderie_customer_tags
|
|
1191
|
+
WHERE customer_id = $1 AND tag = $2`,
|
|
1192
|
+
[customerId, tag]
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
// src/routes.ts
|
|
1198
|
+
var import_middlewares = require("@fonderie/core/middlewares");
|
|
1199
|
+
var import_workspaces = require("@fonderie/workspaces");
|
|
1200
|
+
|
|
1201
|
+
// src/controllers/customer.controller.ts
|
|
1202
|
+
var import_core2 = require("@fonderie/core");
|
|
1203
|
+
|
|
1204
|
+
// src/utils.ts
|
|
1205
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1206
|
+
function isUuid(value) {
|
|
1207
|
+
return typeof value === "string" && UUID_RE.test(value);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// src/controllers/customer.controller.ts
|
|
1211
|
+
function customerController(store, config = {}, bus) {
|
|
1212
|
+
const customers = new CustomerModel(store);
|
|
1213
|
+
const prefix = (config.referenceCodePrefix ?? DEFAULT_REFERENCE_CODE_PREFIX).toUpperCase();
|
|
1214
|
+
return {
|
|
1215
|
+
async list(ctx) {
|
|
1216
|
+
const workspaceId = ctx.workspace?.id;
|
|
1217
|
+
if (!workspaceId) {
|
|
1218
|
+
return (0, import_core2.setApiResponse)(
|
|
1219
|
+
import_core2.HTTP.BAD_REQUEST,
|
|
1220
|
+
"MISSING_WORKSPACE",
|
|
1221
|
+
"Workspace context is required"
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
const query = ctx.request.url ? new URL(ctx.request.url).searchParams : null;
|
|
1225
|
+
const search = query?.get("search") ?? void 0;
|
|
1226
|
+
const archived = query?.get("blacklisted");
|
|
1227
|
+
const limit = Number(query?.get("limit") ?? 50);
|
|
1228
|
+
const offset = Number(query?.get("offset") ?? 0);
|
|
1229
|
+
const listOpts = {
|
|
1230
|
+
workspaceId,
|
|
1231
|
+
limit: Number.isFinite(limit) ? limit : 50,
|
|
1232
|
+
offset: Number.isFinite(offset) ? offset : 0
|
|
1233
|
+
};
|
|
1234
|
+
if (search !== void 0) {
|
|
1235
|
+
listOpts.search = search;
|
|
1236
|
+
}
|
|
1237
|
+
if (archived !== null && archived !== void 0) {
|
|
1238
|
+
listOpts.blacklisted = archived === "true" || archived === "1";
|
|
1239
|
+
}
|
|
1240
|
+
const list = await customers.list(listOpts);
|
|
1241
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "CUSTOMERS_FETCHED", "Customers retrieved successfully.", {
|
|
1242
|
+
customers: list.map(toCustomerDTO)
|
|
1243
|
+
});
|
|
1244
|
+
},
|
|
1245
|
+
async get(ctx) {
|
|
1246
|
+
const workspaceId = ctx.workspace?.id;
|
|
1247
|
+
if (!workspaceId) {
|
|
1248
|
+
return (0, import_core2.setApiResponse)(
|
|
1249
|
+
import_core2.HTTP.BAD_REQUEST,
|
|
1250
|
+
"MISSING_WORKSPACE",
|
|
1251
|
+
"Workspace context is required"
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
const params = ctx.meta["params"];
|
|
1255
|
+
const id = params?.["customerId"];
|
|
1256
|
+
if (!isUuid(id)) {
|
|
1257
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "customerId must be a valid UUID");
|
|
1258
|
+
}
|
|
1259
|
+
const query = ctx.request.url ? new URL(ctx.request.url).searchParams : null;
|
|
1260
|
+
const depth = query?.get("depth") === "1" ? 1 : 2;
|
|
1261
|
+
if (depth === 2) {
|
|
1262
|
+
const customer2 = await customers.findDetail(id, workspaceId, 2);
|
|
1263
|
+
if (!customer2) return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Customer not found");
|
|
1264
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "CUSTOMER_FETCHED", "Customer retrieved successfully.", toCustomerDetailD2DTO(customer2));
|
|
1265
|
+
}
|
|
1266
|
+
const customer = await customers.findDetail(id, workspaceId);
|
|
1267
|
+
if (!customer) {
|
|
1268
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Customer not found");
|
|
1269
|
+
}
|
|
1270
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "CUSTOMER_FETCHED", "Customer retrieved successfully.", toCustomerDetailDTO(customer));
|
|
1271
|
+
},
|
|
1272
|
+
async create(ctx) {
|
|
1273
|
+
const workspaceId = ctx.workspace?.id;
|
|
1274
|
+
if (!workspaceId) {
|
|
1275
|
+
return (0, import_core2.setApiResponse)(
|
|
1276
|
+
import_core2.HTTP.BAD_REQUEST,
|
|
1277
|
+
"MISSING_WORKSPACE",
|
|
1278
|
+
"Workspace context is required"
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
const body = ctx.meta["body"];
|
|
1282
|
+
const type = body?.["type"];
|
|
1283
|
+
const sex = body?.["sex"];
|
|
1284
|
+
const firstName = body?.["firstName"];
|
|
1285
|
+
const lastName = body?.["lastName"];
|
|
1286
|
+
const companyName = body?.["companyName"];
|
|
1287
|
+
const avatarUrl = body?.["avatarUrl"];
|
|
1288
|
+
const locale = body?.["locale"];
|
|
1289
|
+
const referenceCode = body?.["referenceCode"];
|
|
1290
|
+
if (type !== void 0 && type !== "individual" && type !== "business") {
|
|
1291
|
+
return (0, import_core2.setApiResponse)(
|
|
1292
|
+
import_core2.HTTP.UNPROCESSABLE,
|
|
1293
|
+
"INVALID_PARAMETER",
|
|
1294
|
+
"type must be individual or business"
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
if (sex !== void 0 && sex !== "UNKNOWN" && sex !== "MALE" && sex !== "FEMALE") {
|
|
1298
|
+
return (0, import_core2.setApiResponse)(
|
|
1299
|
+
import_core2.HTTP.UNPROCESSABLE,
|
|
1300
|
+
"INVALID_PARAMETER",
|
|
1301
|
+
"sex must be UNKNOWN, MALE, or FEMALE"
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
let customer;
|
|
1305
|
+
try {
|
|
1306
|
+
customer = await customers.create({
|
|
1307
|
+
workspaceId,
|
|
1308
|
+
type: typeof type === "string" ? type : "individual",
|
|
1309
|
+
sex: typeof sex === "string" ? sex : "UNKNOWN",
|
|
1310
|
+
firstName: typeof firstName === "string" ? firstName : null,
|
|
1311
|
+
lastName: typeof lastName === "string" ? lastName : null,
|
|
1312
|
+
companyName: typeof companyName === "string" ? companyName : null,
|
|
1313
|
+
avatarUrl: typeof avatarUrl === "string" ? avatarUrl : null,
|
|
1314
|
+
locale: typeof locale === "string" ? locale : "en-US",
|
|
1315
|
+
referenceCode: typeof referenceCode === "string" ? referenceCode.toUpperCase() : void 0,
|
|
1316
|
+
referenceCodePrefix: prefix,
|
|
1317
|
+
createdBy: ctx.user?.id ?? null
|
|
1318
|
+
});
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
if (err instanceof Error && err.message.includes("idx_fc_reference_code")) {
|
|
1321
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.CONFLICT, "DUPLICATE_REFERENCE_CODE", "A customer with this reference code already exists");
|
|
1322
|
+
}
|
|
1323
|
+
throw err;
|
|
1324
|
+
}
|
|
1325
|
+
bus?.emit(EVENT_KEYS.customerCreated, {
|
|
1326
|
+
customerId: customer.id,
|
|
1327
|
+
workspaceId: customer.workspaceId
|
|
1328
|
+
}).catch(() => {
|
|
1329
|
+
});
|
|
1330
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.CREATED, "CUSTOMER_CREATED", "Customer created successfully.", {
|
|
1331
|
+
customer: toCustomerDTO(customer)
|
|
1332
|
+
});
|
|
1333
|
+
},
|
|
1334
|
+
async update(ctx) {
|
|
1335
|
+
const workspaceId = ctx.workspace?.id;
|
|
1336
|
+
if (!workspaceId) {
|
|
1337
|
+
return (0, import_core2.setApiResponse)(
|
|
1338
|
+
import_core2.HTTP.BAD_REQUEST,
|
|
1339
|
+
"MISSING_WORKSPACE",
|
|
1340
|
+
"Workspace context is required"
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
const params = ctx.meta["params"];
|
|
1344
|
+
const id = params?.["customerId"];
|
|
1345
|
+
if (!isUuid(id)) {
|
|
1346
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "customerId must be a valid UUID");
|
|
1347
|
+
}
|
|
1348
|
+
const body = ctx.meta["body"];
|
|
1349
|
+
const opts = {};
|
|
1350
|
+
if (body?.["type"] !== void 0) {
|
|
1351
|
+
const t = body["type"];
|
|
1352
|
+
if (t !== "individual" && t !== "business") {
|
|
1353
|
+
return (0, import_core2.setApiResponse)(
|
|
1354
|
+
import_core2.HTTP.UNPROCESSABLE,
|
|
1355
|
+
"INVALID_PARAMETER",
|
|
1356
|
+
"type must be individual or business"
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
opts.type = t;
|
|
1360
|
+
}
|
|
1361
|
+
if (body?.["sex"] !== void 0) {
|
|
1362
|
+
const s = body["sex"];
|
|
1363
|
+
if (s !== "UNKNOWN" && s !== "MALE" && s !== "FEMALE") {
|
|
1364
|
+
return (0, import_core2.setApiResponse)(
|
|
1365
|
+
import_core2.HTTP.UNPROCESSABLE,
|
|
1366
|
+
"INVALID_PARAMETER",
|
|
1367
|
+
"sex must be UNKNOWN, MALE, or FEMALE"
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
opts.sex = s;
|
|
1371
|
+
}
|
|
1372
|
+
if (body?.["firstName"] !== void 0) {
|
|
1373
|
+
opts.firstName = typeof body["firstName"] === "string" ? body["firstName"] : null;
|
|
1374
|
+
}
|
|
1375
|
+
if (body?.["lastName"] !== void 0) {
|
|
1376
|
+
opts.lastName = typeof body["lastName"] === "string" ? body["lastName"] : null;
|
|
1377
|
+
}
|
|
1378
|
+
if (body?.["companyName"] !== void 0) {
|
|
1379
|
+
opts.companyName = typeof body["companyName"] === "string" ? body["companyName"] : null;
|
|
1380
|
+
}
|
|
1381
|
+
if (body?.["avatarUrl"] !== void 0) {
|
|
1382
|
+
opts.avatarUrl = typeof body["avatarUrl"] === "string" ? body["avatarUrl"] : null;
|
|
1383
|
+
}
|
|
1384
|
+
if (body?.["locale"] !== void 0 && typeof body["locale"] === "string") {
|
|
1385
|
+
opts.locale = body["locale"];
|
|
1386
|
+
}
|
|
1387
|
+
if (body?.["referenceCode"] !== void 0 && typeof body["referenceCode"] === "string") {
|
|
1388
|
+
opts.referenceCode = body["referenceCode"].toUpperCase();
|
|
1389
|
+
}
|
|
1390
|
+
const customer = await customers.update(id, workspaceId, opts, prefix);
|
|
1391
|
+
if (!customer) {
|
|
1392
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Customer not found");
|
|
1393
|
+
}
|
|
1394
|
+
bus?.emit(EVENT_KEYS.customerUpdated, {
|
|
1395
|
+
customerId: customer.id,
|
|
1396
|
+
workspaceId: customer.workspaceId
|
|
1397
|
+
}).catch(() => {
|
|
1398
|
+
});
|
|
1399
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "CUSTOMER_UPDATED", "Customer updated successfully.", {
|
|
1400
|
+
customer: toCustomerDTO(customer)
|
|
1401
|
+
});
|
|
1402
|
+
},
|
|
1403
|
+
async delete(ctx) {
|
|
1404
|
+
const workspaceId = ctx.workspace?.id;
|
|
1405
|
+
if (!workspaceId) {
|
|
1406
|
+
return (0, import_core2.setApiResponse)(
|
|
1407
|
+
import_core2.HTTP.BAD_REQUEST,
|
|
1408
|
+
"MISSING_WORKSPACE",
|
|
1409
|
+
"Workspace context is required"
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
const params = ctx.meta["params"];
|
|
1413
|
+
const id = params?.["customerId"];
|
|
1414
|
+
if (!isUuid(id)) {
|
|
1415
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "customerId must be a valid UUID");
|
|
1416
|
+
}
|
|
1417
|
+
const existing = await customers.findById(id, workspaceId);
|
|
1418
|
+
if (!existing) {
|
|
1419
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Customer not found");
|
|
1420
|
+
}
|
|
1421
|
+
await customers.delete(id, workspaceId);
|
|
1422
|
+
bus?.emit(EVENT_KEYS.customerDeleted, {
|
|
1423
|
+
customerId: id,
|
|
1424
|
+
workspaceId
|
|
1425
|
+
}).catch(() => {
|
|
1426
|
+
});
|
|
1427
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "CUSTOMER_DELETED", "Customer deleted successfully.");
|
|
1428
|
+
},
|
|
1429
|
+
async blacklist(ctx) {
|
|
1430
|
+
const workspaceId = ctx.workspace?.id;
|
|
1431
|
+
if (!workspaceId) {
|
|
1432
|
+
return (0, import_core2.setApiResponse)(
|
|
1433
|
+
import_core2.HTTP.BAD_REQUEST,
|
|
1434
|
+
"MISSING_WORKSPACE",
|
|
1435
|
+
"Workspace context is required"
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
const params = ctx.meta["params"];
|
|
1439
|
+
const id = params?.["customerId"];
|
|
1440
|
+
if (!isUuid(id)) {
|
|
1441
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "customerId must be a valid UUID");
|
|
1442
|
+
}
|
|
1443
|
+
const existing = await customers.findById(id, workspaceId);
|
|
1444
|
+
if (!existing) {
|
|
1445
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Customer not found");
|
|
1446
|
+
}
|
|
1447
|
+
const body = ctx.meta["body"];
|
|
1448
|
+
const reason = typeof body?.["reason"] === "string" ? body["reason"].trim() || null : null;
|
|
1449
|
+
await customers.blacklist(id, workspaceId, reason);
|
|
1450
|
+
bus?.emit(EVENT_KEYS.customerBlacklisted, {
|
|
1451
|
+
customerId: id,
|
|
1452
|
+
workspaceId
|
|
1453
|
+
}).catch(() => {
|
|
1454
|
+
});
|
|
1455
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "CUSTOMER_BLACKLISTED", "Customer blacklisted successfully.");
|
|
1456
|
+
},
|
|
1457
|
+
async unblacklist(ctx) {
|
|
1458
|
+
const workspaceId = ctx.workspace?.id;
|
|
1459
|
+
if (!workspaceId) {
|
|
1460
|
+
return (0, import_core2.setApiResponse)(
|
|
1461
|
+
import_core2.HTTP.BAD_REQUEST,
|
|
1462
|
+
"MISSING_WORKSPACE",
|
|
1463
|
+
"Workspace context is required"
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
const params = ctx.meta["params"];
|
|
1467
|
+
const id = params?.["customerId"];
|
|
1468
|
+
if (!isUuid(id)) {
|
|
1469
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "customerId must be a valid UUID");
|
|
1470
|
+
}
|
|
1471
|
+
const existing = await customers.findById(id, workspaceId);
|
|
1472
|
+
if (!existing) {
|
|
1473
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.NOT_FOUND, "NOT_FOUND", "Customer not found");
|
|
1474
|
+
}
|
|
1475
|
+
await customers.unblacklist(id, workspaceId);
|
|
1476
|
+
bus?.emit(EVENT_KEYS.customerUnblacklisted, {
|
|
1477
|
+
customerId: id,
|
|
1478
|
+
workspaceId
|
|
1479
|
+
}).catch(() => {
|
|
1480
|
+
});
|
|
1481
|
+
return (0, import_core2.setApiResponse)(import_core2.HTTP.OK, "CUSTOMER_UNBLACKLISTED", "Customer removed from blacklist successfully.");
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// src/controllers/customer-address.controller.ts
|
|
1487
|
+
var import_core3 = require("@fonderie/core");
|
|
1488
|
+
function customerAddressController(store) {
|
|
1489
|
+
const customers = new CustomerModel(store);
|
|
1490
|
+
const addresses = new CustomerAddressModel(store);
|
|
1491
|
+
const labels = new CustomerLabelModel(store);
|
|
1492
|
+
async function resolveCustomer(ctx) {
|
|
1493
|
+
const workspaceId = ctx.workspace?.id;
|
|
1494
|
+
if (!workspaceId) {
|
|
1495
|
+
return {
|
|
1496
|
+
error: (0, import_core3.setApiResponse)(
|
|
1497
|
+
import_core3.HTTP.BAD_REQUEST,
|
|
1498
|
+
"MISSING_WORKSPACE",
|
|
1499
|
+
"Workspace context is required"
|
|
1500
|
+
)
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
const params = ctx.meta["params"];
|
|
1504
|
+
const id = params?.["customerId"];
|
|
1505
|
+
if (!isUuid(id)) {
|
|
1506
|
+
return { error: (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "customerId must be a valid UUID") };
|
|
1507
|
+
}
|
|
1508
|
+
const customer = await customers.findById(id, workspaceId);
|
|
1509
|
+
if (!customer) {
|
|
1510
|
+
return { error: (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Customer not found") };
|
|
1511
|
+
}
|
|
1512
|
+
return { customer, workspaceId };
|
|
1513
|
+
}
|
|
1514
|
+
return {
|
|
1515
|
+
async list(ctx) {
|
|
1516
|
+
const r = await resolveCustomer(ctx);
|
|
1517
|
+
if ("error" in r) {
|
|
1518
|
+
return r.error;
|
|
1519
|
+
}
|
|
1520
|
+
const list = await addresses.list(r.customer.id);
|
|
1521
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ADDRESSES_FETCHED", "Addresses retrieved successfully.", {
|
|
1522
|
+
addresses: list.map(toCustomerAddressDTO)
|
|
1523
|
+
});
|
|
1524
|
+
},
|
|
1525
|
+
async add(ctx) {
|
|
1526
|
+
const r = await resolveCustomer(ctx);
|
|
1527
|
+
if ("error" in r) {
|
|
1528
|
+
return r.error;
|
|
1529
|
+
}
|
|
1530
|
+
const body = ctx.meta["body"];
|
|
1531
|
+
const countryIso = body?.["countryIso"];
|
|
1532
|
+
const zipPostalCode = body?.["zipPostalCode"];
|
|
1533
|
+
if (typeof countryIso !== "string" || countryIso.trim().length === 0) {
|
|
1534
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "countryIso is required");
|
|
1535
|
+
}
|
|
1536
|
+
if (typeof zipPostalCode !== "string" || zipPostalCode.trim().length === 0) {
|
|
1537
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "zipPostalCode is required");
|
|
1538
|
+
}
|
|
1539
|
+
const rawLabel = typeof body?.["label"] === "string" ? body["label"] : "service";
|
|
1540
|
+
const { id: labelId } = await labels.findOrCreate("address", rawLabel);
|
|
1541
|
+
let created;
|
|
1542
|
+
try {
|
|
1543
|
+
created = await addresses.add({
|
|
1544
|
+
customerId: r.customer.id,
|
|
1545
|
+
countryIso: countryIso.trim(),
|
|
1546
|
+
zipPostalCode: zipPostalCode.trim(),
|
|
1547
|
+
subdivision1Iso: typeof body?.["subdivision1Iso"] === "string" ? body["subdivision1Iso"] : null,
|
|
1548
|
+
subdivision2Iso: typeof body?.["subdivision2Iso"] === "string" ? body["subdivision2Iso"] : null,
|
|
1549
|
+
unit: typeof body?.["unit"] === "string" ? body["unit"] : null,
|
|
1550
|
+
line1: typeof body?.["line1"] === "string" ? body["line1"] : null,
|
|
1551
|
+
line2: typeof body?.["line2"] === "string" ? body["line2"] : null,
|
|
1552
|
+
labelId,
|
|
1553
|
+
isPrimary: body?.["isPrimary"] === true
|
|
1554
|
+
});
|
|
1555
|
+
} catch (err) {
|
|
1556
|
+
if (err instanceof Error && err.code === "DUPLICATE_ADDRESS") {
|
|
1557
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.CONFLICT, "DUPLICATE_ADDRESS", "This address is already linked to this customer");
|
|
1558
|
+
}
|
|
1559
|
+
throw err;
|
|
1560
|
+
}
|
|
1561
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.CREATED, "ADDRESS_ADDED", "Address added successfully.", {
|
|
1562
|
+
address: toCustomerAddressDTO(created)
|
|
1563
|
+
});
|
|
1564
|
+
},
|
|
1565
|
+
async update(ctx) {
|
|
1566
|
+
const r = await resolveCustomer(ctx);
|
|
1567
|
+
if ("error" in r) return r.error;
|
|
1568
|
+
const params = ctx.meta["params"];
|
|
1569
|
+
const addrId = params?.["addrId"];
|
|
1570
|
+
if (!isUuid(addrId)) {
|
|
1571
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "addrId must be a valid UUID");
|
|
1572
|
+
}
|
|
1573
|
+
const body = ctx.meta["body"];
|
|
1574
|
+
if (typeof body?.["label"] !== "string" || body["label"].trim().length === 0) {
|
|
1575
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "label is required");
|
|
1576
|
+
}
|
|
1577
|
+
try {
|
|
1578
|
+
const { id: labelId } = await labels.findOrCreate("address", body["label"].trim());
|
|
1579
|
+
const updated = await addresses.updateLabel(addrId, r.customer.id, labelId);
|
|
1580
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ADDRESS_UPDATED", "Address updated successfully.", {
|
|
1581
|
+
address: toCustomerAddressDTO(updated)
|
|
1582
|
+
});
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
if (err instanceof Error && err.code === "NOT_FOUND") {
|
|
1585
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.NOT_FOUND, "NOT_FOUND", "Address not found");
|
|
1586
|
+
}
|
|
1587
|
+
throw err;
|
|
1588
|
+
}
|
|
1589
|
+
},
|
|
1590
|
+
async setPrimary(ctx) {
|
|
1591
|
+
const r = await resolveCustomer(ctx);
|
|
1592
|
+
if ("error" in r) {
|
|
1593
|
+
return r.error;
|
|
1594
|
+
}
|
|
1595
|
+
const params = ctx.meta["params"];
|
|
1596
|
+
const addrId = params?.["addrId"];
|
|
1597
|
+
if (!isUuid(addrId)) {
|
|
1598
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "addrId must be a valid UUID");
|
|
1599
|
+
}
|
|
1600
|
+
await addresses.setPrimary(addrId, r.customer.id);
|
|
1601
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ADDRESS_PRIMARY_SET", "Primary address updated successfully.");
|
|
1602
|
+
},
|
|
1603
|
+
async remove(ctx) {
|
|
1604
|
+
const r = await resolveCustomer(ctx);
|
|
1605
|
+
if ("error" in r) {
|
|
1606
|
+
return r.error;
|
|
1607
|
+
}
|
|
1608
|
+
const params = ctx.meta["params"];
|
|
1609
|
+
const addrId = params?.["addrId"];
|
|
1610
|
+
if (!isUuid(addrId)) {
|
|
1611
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "addrId must be a valid UUID");
|
|
1612
|
+
}
|
|
1613
|
+
await addresses.remove(addrId, r.customer.id);
|
|
1614
|
+
return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ADDRESS_REMOVED", "Address removed successfully.");
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// src/controllers/customer-email.controller.ts
|
|
1620
|
+
var import_core4 = require("@fonderie/core");
|
|
1621
|
+
function customerEmailController(store) {
|
|
1622
|
+
const customers = new CustomerModel(store);
|
|
1623
|
+
const emails = new CustomerEmailModel(store);
|
|
1624
|
+
const labels = new CustomerLabelModel(store);
|
|
1625
|
+
async function resolveCustomer(ctx) {
|
|
1626
|
+
const workspaceId = ctx.workspace?.id;
|
|
1627
|
+
if (!workspaceId) {
|
|
1628
|
+
return {
|
|
1629
|
+
error: (0, import_core4.setApiResponse)(
|
|
1630
|
+
import_core4.HTTP.BAD_REQUEST,
|
|
1631
|
+
"MISSING_WORKSPACE",
|
|
1632
|
+
"Workspace context is required"
|
|
1633
|
+
)
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
const params = ctx.meta["params"];
|
|
1637
|
+
const id = params?.["customerId"];
|
|
1638
|
+
if (!isUuid(id)) {
|
|
1639
|
+
return { error: (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "customerId must be a valid UUID") };
|
|
1640
|
+
}
|
|
1641
|
+
const customer = await customers.findById(id, workspaceId);
|
|
1642
|
+
if (!customer) {
|
|
1643
|
+
return { error: (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Customer not found") };
|
|
1644
|
+
}
|
|
1645
|
+
return { customer, workspaceId };
|
|
1646
|
+
}
|
|
1647
|
+
return {
|
|
1648
|
+
async list(ctx) {
|
|
1649
|
+
const r = await resolveCustomer(ctx);
|
|
1650
|
+
if ("error" in r) {
|
|
1651
|
+
return r.error;
|
|
1652
|
+
}
|
|
1653
|
+
const list = await emails.list(r.customer.id);
|
|
1654
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "EMAILS_FETCHED", "Emails retrieved successfully.", {
|
|
1655
|
+
emails: list.map(toCustomerEmailDTO)
|
|
1656
|
+
});
|
|
1657
|
+
},
|
|
1658
|
+
async add(ctx) {
|
|
1659
|
+
const r = await resolveCustomer(ctx);
|
|
1660
|
+
if ("error" in r) {
|
|
1661
|
+
return r.error;
|
|
1662
|
+
}
|
|
1663
|
+
const body = ctx.meta["body"];
|
|
1664
|
+
const email = body?.["email"];
|
|
1665
|
+
if (typeof email !== "string" || email.trim().length === 0) {
|
|
1666
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
|
|
1667
|
+
}
|
|
1668
|
+
const rawLabel = typeof body?.["label"] === "string" ? body["label"] : "work";
|
|
1669
|
+
const isPrimary = body?.["isPrimary"] === true;
|
|
1670
|
+
const { id: labelId } = await labels.findOrCreate("email", rawLabel);
|
|
1671
|
+
let created;
|
|
1672
|
+
try {
|
|
1673
|
+
created = await emails.add({
|
|
1674
|
+
customerId: r.customer.id,
|
|
1675
|
+
email: email.trim(),
|
|
1676
|
+
labelId,
|
|
1677
|
+
isPrimary
|
|
1678
|
+
});
|
|
1679
|
+
} catch (err) {
|
|
1680
|
+
if (err instanceof Error && err.message.includes("idx_fce_unique_email")) {
|
|
1681
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.CONFLICT, "DUPLICATE_EMAIL", "This email is already linked to this customer");
|
|
1682
|
+
}
|
|
1683
|
+
throw err;
|
|
1684
|
+
}
|
|
1685
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.CREATED, "EMAIL_ADDED", "Email added successfully.", {
|
|
1686
|
+
email: toCustomerEmailDTO(created)
|
|
1687
|
+
});
|
|
1688
|
+
},
|
|
1689
|
+
async update(ctx) {
|
|
1690
|
+
const r = await resolveCustomer(ctx);
|
|
1691
|
+
if ("error" in r) return r.error;
|
|
1692
|
+
const params = ctx.meta["params"];
|
|
1693
|
+
const emailId = params?.["emailId"];
|
|
1694
|
+
if (!isUuid(emailId)) {
|
|
1695
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "emailId must be a valid UUID");
|
|
1696
|
+
}
|
|
1697
|
+
const body = ctx.meta["body"];
|
|
1698
|
+
if (typeof body?.["label"] !== "string" || body["label"].trim().length === 0) {
|
|
1699
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "label is required");
|
|
1700
|
+
}
|
|
1701
|
+
try {
|
|
1702
|
+
const { id: labelId } = await labels.findOrCreate("email", body["label"].trim());
|
|
1703
|
+
const updated = await emails.updateLabel(emailId, r.customer.id, labelId);
|
|
1704
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "EMAIL_UPDATED", "Email updated successfully.", {
|
|
1705
|
+
email: toCustomerEmailDTO(updated)
|
|
1706
|
+
});
|
|
1707
|
+
} catch (err) {
|
|
1708
|
+
if (err instanceof Error && err.code === "NOT_FOUND") {
|
|
1709
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "Email not found");
|
|
1710
|
+
}
|
|
1711
|
+
throw err;
|
|
1712
|
+
}
|
|
1713
|
+
},
|
|
1714
|
+
async setPrimary(ctx) {
|
|
1715
|
+
const r = await resolveCustomer(ctx);
|
|
1716
|
+
if ("error" in r) {
|
|
1717
|
+
return r.error;
|
|
1718
|
+
}
|
|
1719
|
+
const params = ctx.meta["params"];
|
|
1720
|
+
const emailId = params?.["emailId"];
|
|
1721
|
+
if (!isUuid(emailId)) {
|
|
1722
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "emailId must be a valid UUID");
|
|
1723
|
+
}
|
|
1724
|
+
await emails.setPrimary(emailId, r.customer.id);
|
|
1725
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "EMAIL_PRIMARY_SET", "Primary email updated successfully.");
|
|
1726
|
+
},
|
|
1727
|
+
async remove(ctx) {
|
|
1728
|
+
const r = await resolveCustomer(ctx);
|
|
1729
|
+
if ("error" in r) {
|
|
1730
|
+
return r.error;
|
|
1731
|
+
}
|
|
1732
|
+
const params = ctx.meta["params"];
|
|
1733
|
+
const emailId = params?.["emailId"];
|
|
1734
|
+
if (!isUuid(emailId)) {
|
|
1735
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "emailId must be a valid UUID");
|
|
1736
|
+
}
|
|
1737
|
+
await emails.remove(emailId, r.customer.id);
|
|
1738
|
+
return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "EMAIL_REMOVED", "Email removed successfully.");
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// src/controllers/customer-note.controller.ts
|
|
1744
|
+
var import_core5 = require("@fonderie/core");
|
|
1745
|
+
function customerNoteController(store) {
|
|
1746
|
+
const customers = new CustomerModel(store);
|
|
1747
|
+
const notes = new CustomerNoteModel(store);
|
|
1748
|
+
async function resolveCustomer(ctx) {
|
|
1749
|
+
const workspaceId = ctx.workspace?.id;
|
|
1750
|
+
if (!workspaceId)
|
|
1751
|
+
return {
|
|
1752
|
+
error: (0, import_core5.setApiResponse)(
|
|
1753
|
+
import_core5.HTTP.BAD_REQUEST,
|
|
1754
|
+
"MISSING_WORKSPACE",
|
|
1755
|
+
"Workspace context is required"
|
|
1756
|
+
)
|
|
1757
|
+
};
|
|
1758
|
+
const params = ctx.meta["params"];
|
|
1759
|
+
const id = params?.["customerId"];
|
|
1760
|
+
if (!isUuid(id))
|
|
1761
|
+
return { error: (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "customerId must be a valid UUID") };
|
|
1762
|
+
const customer = await customers.findById(id, workspaceId);
|
|
1763
|
+
if (!customer)
|
|
1764
|
+
return { error: (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Customer not found") };
|
|
1765
|
+
return { customer, workspaceId };
|
|
1766
|
+
}
|
|
1767
|
+
return {
|
|
1768
|
+
async list(ctx) {
|
|
1769
|
+
const r = await resolveCustomer(ctx);
|
|
1770
|
+
if ("error" in r) return r.error;
|
|
1771
|
+
const list = await notes.list(r.customer.id);
|
|
1772
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "NOTES_FETCHED", "Notes retrieved successfully.", {
|
|
1773
|
+
notes: list.map(toCustomerNoteDTO)
|
|
1774
|
+
});
|
|
1775
|
+
},
|
|
1776
|
+
async create(ctx) {
|
|
1777
|
+
const r = await resolveCustomer(ctx);
|
|
1778
|
+
if ("error" in r) return r.error;
|
|
1779
|
+
const body = ctx.meta["body"];
|
|
1780
|
+
const bodyText = body?.["body"];
|
|
1781
|
+
if (typeof bodyText !== "string" || bodyText.trim().length === 0) {
|
|
1782
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "body is required");
|
|
1783
|
+
}
|
|
1784
|
+
const note = await notes.create({
|
|
1785
|
+
customerId: r.customer.id,
|
|
1786
|
+
authorId: ctx.user?.id ?? null,
|
|
1787
|
+
body: bodyText.trim()
|
|
1788
|
+
});
|
|
1789
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.CREATED, "NOTE_CREATED", "Note created successfully.", {
|
|
1790
|
+
note: toCustomerNoteDTO(note)
|
|
1791
|
+
});
|
|
1792
|
+
},
|
|
1793
|
+
async update(ctx) {
|
|
1794
|
+
const r = await resolveCustomer(ctx);
|
|
1795
|
+
if ("error" in r) return r.error;
|
|
1796
|
+
const params = ctx.meta["params"];
|
|
1797
|
+
const noteId = params?.["noteId"];
|
|
1798
|
+
if (!isUuid(noteId)) {
|
|
1799
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "noteId must be a valid UUID");
|
|
1800
|
+
}
|
|
1801
|
+
const body = ctx.meta["body"];
|
|
1802
|
+
const bodyText = body?.["body"];
|
|
1803
|
+
if (typeof bodyText !== "string" || bodyText.trim().length === 0) {
|
|
1804
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "body is required");
|
|
1805
|
+
}
|
|
1806
|
+
const note = await notes.update(noteId, r.customer.id, bodyText.trim());
|
|
1807
|
+
if (!note) {
|
|
1808
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "Note not found");
|
|
1809
|
+
}
|
|
1810
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "NOTE_UPDATED", "Note updated successfully.", {
|
|
1811
|
+
note: toCustomerNoteDTO(note)
|
|
1812
|
+
});
|
|
1813
|
+
},
|
|
1814
|
+
async delete(ctx) {
|
|
1815
|
+
const r = await resolveCustomer(ctx);
|
|
1816
|
+
if ("error" in r) return r.error;
|
|
1817
|
+
const params = ctx.meta["params"];
|
|
1818
|
+
const noteId = params?.["noteId"];
|
|
1819
|
+
if (!isUuid(noteId)) {
|
|
1820
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "noteId must be a valid UUID");
|
|
1821
|
+
}
|
|
1822
|
+
await notes.delete(noteId, r.customer.id);
|
|
1823
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "NOTE_DELETED", "Note deleted successfully.");
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// src/controllers/customer-phone.controller.ts
|
|
1829
|
+
var import_core6 = require("@fonderie/core");
|
|
1830
|
+
function customerPhoneController(store) {
|
|
1831
|
+
const customers = new CustomerModel(store);
|
|
1832
|
+
const phones = new CustomerPhoneModel(store);
|
|
1833
|
+
const labels = new CustomerLabelModel(store);
|
|
1834
|
+
async function resolveCustomer(ctx) {
|
|
1835
|
+
const workspaceId = ctx.workspace?.id;
|
|
1836
|
+
if (!workspaceId) {
|
|
1837
|
+
return {
|
|
1838
|
+
error: (0, import_core6.setApiResponse)(
|
|
1839
|
+
import_core6.HTTP.BAD_REQUEST,
|
|
1840
|
+
"MISSING_WORKSPACE",
|
|
1841
|
+
"Workspace context is required"
|
|
1842
|
+
)
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
const params = ctx.meta["params"];
|
|
1846
|
+
const id = params?.["customerId"];
|
|
1847
|
+
if (!isUuid(id)) {
|
|
1848
|
+
return { error: (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "customerId must be a valid UUID") };
|
|
1849
|
+
}
|
|
1850
|
+
const customer = await customers.findById(id, workspaceId);
|
|
1851
|
+
if (!customer) {
|
|
1852
|
+
return { error: (0, import_core6.setApiResponse)(import_core6.HTTP.NOT_FOUND, "NOT_FOUND", "Customer not found") };
|
|
1853
|
+
}
|
|
1854
|
+
return { customer, workspaceId };
|
|
1855
|
+
}
|
|
1856
|
+
return {
|
|
1857
|
+
async list(ctx) {
|
|
1858
|
+
const r = await resolveCustomer(ctx);
|
|
1859
|
+
if ("error" in r) {
|
|
1860
|
+
return r.error;
|
|
1861
|
+
}
|
|
1862
|
+
const list = await phones.list(r.customer.id);
|
|
1863
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "PHONES_FETCHED", "Phones retrieved successfully.", {
|
|
1864
|
+
phones: list.map(toCustomerPhoneDTO)
|
|
1865
|
+
});
|
|
1866
|
+
},
|
|
1867
|
+
async add(ctx) {
|
|
1868
|
+
const r = await resolveCustomer(ctx);
|
|
1869
|
+
if ("error" in r) {
|
|
1870
|
+
return r.error;
|
|
1871
|
+
}
|
|
1872
|
+
const body = ctx.meta["body"];
|
|
1873
|
+
const phone = body?.["phone"];
|
|
1874
|
+
if (typeof phone !== "string" || phone.trim().length === 0) {
|
|
1875
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "phone is required");
|
|
1876
|
+
}
|
|
1877
|
+
const rawLabel = typeof body?.["label"] === "string" ? body["label"] : "mobile";
|
|
1878
|
+
const isPrimary = body?.["isPrimary"] === true;
|
|
1879
|
+
const { id: labelId } = await labels.findOrCreate("phone", rawLabel);
|
|
1880
|
+
let created;
|
|
1881
|
+
try {
|
|
1882
|
+
created = await phones.add({
|
|
1883
|
+
customerId: r.customer.id,
|
|
1884
|
+
phone: phone.trim(),
|
|
1885
|
+
labelId,
|
|
1886
|
+
isPrimary
|
|
1887
|
+
});
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
if (err instanceof Error && err.message.includes("idx_fcp_unique_phone")) {
|
|
1890
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.CONFLICT, "DUPLICATE_PHONE", "This phone is already linked to this customer");
|
|
1891
|
+
}
|
|
1892
|
+
throw err;
|
|
1893
|
+
}
|
|
1894
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.CREATED, "PHONE_ADDED", "Phone added successfully.", {
|
|
1895
|
+
phone: toCustomerPhoneDTO(created)
|
|
1896
|
+
});
|
|
1897
|
+
},
|
|
1898
|
+
async update(ctx) {
|
|
1899
|
+
const r = await resolveCustomer(ctx);
|
|
1900
|
+
if ("error" in r) return r.error;
|
|
1901
|
+
const params = ctx.meta["params"];
|
|
1902
|
+
const phoneId = params?.["phoneId"];
|
|
1903
|
+
if (!isUuid(phoneId)) {
|
|
1904
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "phoneId must be a valid UUID");
|
|
1905
|
+
}
|
|
1906
|
+
const body = ctx.meta["body"];
|
|
1907
|
+
if (typeof body?.["label"] !== "string" || body["label"].trim().length === 0) {
|
|
1908
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "label is required");
|
|
1909
|
+
}
|
|
1910
|
+
try {
|
|
1911
|
+
const { id: labelId } = await labels.findOrCreate("phone", body["label"].trim());
|
|
1912
|
+
const updated = await phones.updateLabel(phoneId, r.customer.id, labelId);
|
|
1913
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "PHONE_UPDATED", "Phone updated successfully.", {
|
|
1914
|
+
phone: toCustomerPhoneDTO(updated)
|
|
1915
|
+
});
|
|
1916
|
+
} catch (err) {
|
|
1917
|
+
if (err instanceof Error && err.code === "NOT_FOUND") {
|
|
1918
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.NOT_FOUND, "NOT_FOUND", "Phone not found");
|
|
1919
|
+
}
|
|
1920
|
+
throw err;
|
|
1921
|
+
}
|
|
1922
|
+
},
|
|
1923
|
+
async setPrimary(ctx) {
|
|
1924
|
+
const r = await resolveCustomer(ctx);
|
|
1925
|
+
if ("error" in r) {
|
|
1926
|
+
return r.error;
|
|
1927
|
+
}
|
|
1928
|
+
const params = ctx.meta["params"];
|
|
1929
|
+
const phoneId = params?.["phoneId"];
|
|
1930
|
+
if (!isUuid(phoneId)) {
|
|
1931
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "phoneId must be a valid UUID");
|
|
1932
|
+
}
|
|
1933
|
+
await phones.setPrimary(phoneId, r.customer.id);
|
|
1934
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "PHONE_PRIMARY_SET", "Primary phone updated successfully.");
|
|
1935
|
+
},
|
|
1936
|
+
async remove(ctx) {
|
|
1937
|
+
const r = await resolveCustomer(ctx);
|
|
1938
|
+
if ("error" in r) {
|
|
1939
|
+
return r.error;
|
|
1940
|
+
}
|
|
1941
|
+
const params = ctx.meta["params"];
|
|
1942
|
+
const phoneId = params?.["phoneId"];
|
|
1943
|
+
if (!isUuid(phoneId)) {
|
|
1944
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "phoneId must be a valid UUID");
|
|
1945
|
+
}
|
|
1946
|
+
await phones.remove(phoneId, r.customer.id);
|
|
1947
|
+
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "PHONE_REMOVED", "Phone removed successfully.");
|
|
1948
|
+
}
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// src/controllers/customer-tag.controller.ts
|
|
1953
|
+
var import_core7 = require("@fonderie/core");
|
|
1954
|
+
function customerTagController(store) {
|
|
1955
|
+
const customers = new CustomerModel(store);
|
|
1956
|
+
const tags = new CustomerTagModel(store);
|
|
1957
|
+
async function resolveCustomer(ctx) {
|
|
1958
|
+
const workspaceId = ctx.workspace?.id;
|
|
1959
|
+
if (!workspaceId)
|
|
1960
|
+
return {
|
|
1961
|
+
error: (0, import_core7.setApiResponse)(
|
|
1962
|
+
import_core7.HTTP.BAD_REQUEST,
|
|
1963
|
+
"MISSING_WORKSPACE",
|
|
1964
|
+
"Workspace context is required"
|
|
1965
|
+
)
|
|
1966
|
+
};
|
|
1967
|
+
const params = ctx.meta["params"];
|
|
1968
|
+
const id = params?.["customerId"];
|
|
1969
|
+
if (!isUuid(id))
|
|
1970
|
+
return { error: (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "customerId must be a valid UUID") };
|
|
1971
|
+
const customer = await customers.findById(id, workspaceId);
|
|
1972
|
+
if (!customer)
|
|
1973
|
+
return { error: (0, import_core7.setApiResponse)(import_core7.HTTP.NOT_FOUND, "NOT_FOUND", "Customer not found") };
|
|
1974
|
+
return { customer, workspaceId };
|
|
1975
|
+
}
|
|
1976
|
+
return {
|
|
1977
|
+
async list(ctx) {
|
|
1978
|
+
const r = await resolveCustomer(ctx);
|
|
1979
|
+
if ("error" in r) return r.error;
|
|
1980
|
+
const list = await tags.list(r.customer.id);
|
|
1981
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "TAGS_FETCHED", "Tags retrieved successfully.", {
|
|
1982
|
+
tags: list
|
|
1983
|
+
});
|
|
1984
|
+
},
|
|
1985
|
+
async add(ctx) {
|
|
1986
|
+
const r = await resolveCustomer(ctx);
|
|
1987
|
+
if ("error" in r) return r.error;
|
|
1988
|
+
const body = ctx.meta["body"];
|
|
1989
|
+
const tag = body?.["tag"];
|
|
1990
|
+
if (typeof tag !== "string" || tag.trim().length === 0) {
|
|
1991
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "tag is required");
|
|
1992
|
+
}
|
|
1993
|
+
await tags.add(r.customer.id, tag.trim());
|
|
1994
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.CREATED, "TAG_ADDED", "Tag added successfully.");
|
|
1995
|
+
},
|
|
1996
|
+
async remove(ctx) {
|
|
1997
|
+
const r = await resolveCustomer(ctx);
|
|
1998
|
+
if ("error" in r) return r.error;
|
|
1999
|
+
const params = ctx.meta["params"];
|
|
2000
|
+
const tag = params?.["tag"];
|
|
2001
|
+
if (!tag) {
|
|
2002
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "tag is required");
|
|
2003
|
+
}
|
|
2004
|
+
await tags.remove(r.customer.id, tag);
|
|
2005
|
+
return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "TAG_REMOVED", "Tag removed successfully.");
|
|
2006
|
+
}
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// src/controllers/customer-label.controller.ts
|
|
2011
|
+
var import_core8 = require("@fonderie/core");
|
|
2012
|
+
var VALID_TYPES = ["phone", "email", "address"];
|
|
2013
|
+
function customerLabelController(store) {
|
|
2014
|
+
const labels = new CustomerLabelModel(store);
|
|
2015
|
+
return {
|
|
2016
|
+
async list(ctx) {
|
|
2017
|
+
const query = ctx.request.url ? new URL(ctx.request.url).searchParams : null;
|
|
2018
|
+
const type = query?.get("type");
|
|
2019
|
+
if (!type || !VALID_TYPES.includes(type)) {
|
|
2020
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "type must be phone, email, or address");
|
|
2021
|
+
}
|
|
2022
|
+
const rows = await labels.list(type);
|
|
2023
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "LABELS_FETCHED", "Labels retrieved successfully.", { labels: rows });
|
|
2024
|
+
},
|
|
2025
|
+
async remove(ctx) {
|
|
2026
|
+
const params = ctx.meta["params"];
|
|
2027
|
+
const labelId = params?.["labelId"];
|
|
2028
|
+
if (!isUuid(labelId)) {
|
|
2029
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "labelId must be a valid UUID");
|
|
2030
|
+
}
|
|
2031
|
+
await labels.remove(labelId);
|
|
2032
|
+
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "LABEL_DELETED", "Label deleted successfully.");
|
|
2033
|
+
}
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// src/controllers/customer-relationship.controller.ts
|
|
2038
|
+
var import_core9 = require("@fonderie/core");
|
|
2039
|
+
function customerRelationshipController(store) {
|
|
2040
|
+
const customers = new CustomerModel(store);
|
|
2041
|
+
const relationships = new CustomerRelationshipModel(store);
|
|
2042
|
+
async function resolveCustomer(ctx) {
|
|
2043
|
+
const workspaceId = ctx.workspace?.id;
|
|
2044
|
+
if (!workspaceId) {
|
|
2045
|
+
return {
|
|
2046
|
+
error: (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "MISSING_WORKSPACE", "Workspace context is required")
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
const params = ctx.meta["params"];
|
|
2050
|
+
const id = params?.["customerId"];
|
|
2051
|
+
if (!isUuid(id)) {
|
|
2052
|
+
return { error: (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "customerId must be a valid UUID") };
|
|
2053
|
+
}
|
|
2054
|
+
const customer = await customers.findById(id, workspaceId);
|
|
2055
|
+
if (!customer) {
|
|
2056
|
+
return { error: (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "Customer not found") };
|
|
2057
|
+
}
|
|
2058
|
+
return { customer, workspaceId };
|
|
2059
|
+
}
|
|
2060
|
+
return {
|
|
2061
|
+
async list(ctx) {
|
|
2062
|
+
const r = await resolveCustomer(ctx);
|
|
2063
|
+
if ("error" in r) return r.error;
|
|
2064
|
+
const list = await relationships.list(r.customer.id);
|
|
2065
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "RELATIONSHIPS_FETCHED", "Relationships retrieved successfully.", {
|
|
2066
|
+
relationships: list.map(toCustomerRelationshipDTO)
|
|
2067
|
+
});
|
|
2068
|
+
},
|
|
2069
|
+
async add(ctx) {
|
|
2070
|
+
const r = await resolveCustomer(ctx);
|
|
2071
|
+
if ("error" in r) return r.error;
|
|
2072
|
+
const body = ctx.meta["body"];
|
|
2073
|
+
const relatedId = body?.["relatedId"];
|
|
2074
|
+
const relationship = body?.["relationship"];
|
|
2075
|
+
if (!isUuid(relatedId)) {
|
|
2076
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "relatedId must be a valid UUID");
|
|
2077
|
+
}
|
|
2078
|
+
if (typeof relationship !== "string" || relationship.trim().length === 0) {
|
|
2079
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "relationship is required");
|
|
2080
|
+
}
|
|
2081
|
+
if (relatedId === r.customer.id) {
|
|
2082
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "A customer cannot be related to itself");
|
|
2083
|
+
}
|
|
2084
|
+
const related = await customers.findById(relatedId, r.workspaceId);
|
|
2085
|
+
if (!related) {
|
|
2086
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "Related customer not found");
|
|
2087
|
+
}
|
|
2088
|
+
const created = await relationships.add({
|
|
2089
|
+
workspaceId: r.workspaceId,
|
|
2090
|
+
customerId: r.customer.id,
|
|
2091
|
+
relatedId,
|
|
2092
|
+
relationship: relationship.trim(),
|
|
2093
|
+
isPrimary: body?.["isPrimary"] === true
|
|
2094
|
+
});
|
|
2095
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.CREATED, "RELATIONSHIP_ADDED", "Relationship added successfully.", {
|
|
2096
|
+
relationship: toCustomerRelationshipDTO(created)
|
|
2097
|
+
});
|
|
2098
|
+
},
|
|
2099
|
+
async setPrimary(ctx) {
|
|
2100
|
+
const r = await resolveCustomer(ctx);
|
|
2101
|
+
if ("error" in r) return r.error;
|
|
2102
|
+
const params = ctx.meta["params"];
|
|
2103
|
+
const relatedId = params?.["relatedId"];
|
|
2104
|
+
if (!isUuid(relatedId)) {
|
|
2105
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "relatedId must be a valid UUID");
|
|
2106
|
+
}
|
|
2107
|
+
await relationships.setPrimary(r.customer.id, relatedId);
|
|
2108
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "RELATIONSHIP_PRIMARY_SET", "Primary relationship updated successfully.");
|
|
2109
|
+
},
|
|
2110
|
+
async remove(ctx) {
|
|
2111
|
+
const r = await resolveCustomer(ctx);
|
|
2112
|
+
if ("error" in r) return r.error;
|
|
2113
|
+
const params = ctx.meta["params"];
|
|
2114
|
+
const relatedId = params?.["relatedId"];
|
|
2115
|
+
if (!isUuid(relatedId)) {
|
|
2116
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "relatedId must be a valid UUID");
|
|
2117
|
+
}
|
|
2118
|
+
await relationships.remove(r.customer.id, relatedId);
|
|
2119
|
+
return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "RELATIONSHIP_REMOVED", "Relationship removed successfully.");
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/routes.ts
|
|
2125
|
+
function buildCustomerRoutes(store, config, bus) {
|
|
2126
|
+
const wsCtx = (0, import_workspaces.withWorkspace)(store);
|
|
2127
|
+
const customer = customerController(store, config, bus);
|
|
2128
|
+
const email = customerEmailController(store);
|
|
2129
|
+
const phone = customerPhoneController(store);
|
|
2130
|
+
const address = customerAddressController(store);
|
|
2131
|
+
const note = customerNoteController(store);
|
|
2132
|
+
const tag = customerTagController(store);
|
|
2133
|
+
const relationship = customerRelationshipController(store);
|
|
2134
|
+
const label = customerLabelController(store);
|
|
2135
|
+
return [
|
|
2136
|
+
// ── Labels ───────────────────────────────────────────────────────
|
|
2137
|
+
["GET", "/customers/labels", import_middlewares.requireAuth, wsCtx, label.list],
|
|
2138
|
+
["DELETE", "/customers/labels/:labelId", import_middlewares.requireAuth, wsCtx, label.remove],
|
|
2139
|
+
// ── Core customer CRUD ───────────────────────────────────────────
|
|
2140
|
+
["GET", "/customers", import_middlewares.requireAuth, wsCtx, customer.list],
|
|
2141
|
+
["POST", "/customers", import_middlewares.requireAuth, wsCtx, customer.create],
|
|
2142
|
+
["GET", "/customers/:customerId", import_middlewares.requireAuth, wsCtx, customer.get],
|
|
2143
|
+
["PUT", "/customers/:customerId", import_middlewares.requireAuth, wsCtx, customer.update],
|
|
2144
|
+
["DELETE", "/customers/:customerId", import_middlewares.requireAuth, wsCtx, customer.delete],
|
|
2145
|
+
["POST", "/customers/:customerId/blacklist", import_middlewares.requireAuth, wsCtx, customer.blacklist],
|
|
2146
|
+
["POST", "/customers/:customerId/unblacklist", import_middlewares.requireAuth, wsCtx, customer.unblacklist],
|
|
2147
|
+
// ── Emails ───────────────────────────────────────────────────────
|
|
2148
|
+
["GET", "/customers/:customerId/emails", import_middlewares.requireAuth, wsCtx, email.list],
|
|
2149
|
+
["POST", "/customers/:customerId/emails", import_middlewares.requireAuth, wsCtx, email.add],
|
|
2150
|
+
["PATCH", "/customers/:customerId/emails/:emailId", import_middlewares.requireAuth, wsCtx, email.update],
|
|
2151
|
+
["PUT", "/customers/:customerId/emails/:emailId/primary", import_middlewares.requireAuth, wsCtx, email.setPrimary],
|
|
2152
|
+
["DELETE", "/customers/:customerId/emails/:emailId", import_middlewares.requireAuth, wsCtx, email.remove],
|
|
2153
|
+
// ── Phones ───────────────────────────────────────────────────────
|
|
2154
|
+
["GET", "/customers/:customerId/phones", import_middlewares.requireAuth, wsCtx, phone.list],
|
|
2155
|
+
["POST", "/customers/:customerId/phones", import_middlewares.requireAuth, wsCtx, phone.add],
|
|
2156
|
+
["PATCH", "/customers/:customerId/phones/:phoneId", import_middlewares.requireAuth, wsCtx, phone.update],
|
|
2157
|
+
["PUT", "/customers/:customerId/phones/:phoneId/primary", import_middlewares.requireAuth, wsCtx, phone.setPrimary],
|
|
2158
|
+
["DELETE", "/customers/:customerId/phones/:phoneId", import_middlewares.requireAuth, wsCtx, phone.remove],
|
|
2159
|
+
// ── Addresses ────────────────────────────────────────────────────
|
|
2160
|
+
["GET", "/customers/:customerId/addresses", import_middlewares.requireAuth, wsCtx, address.list],
|
|
2161
|
+
["POST", "/customers/:customerId/addresses", import_middlewares.requireAuth, wsCtx, address.add],
|
|
2162
|
+
["PATCH", "/customers/:customerId/addresses/:addrId", import_middlewares.requireAuth, wsCtx, address.update],
|
|
2163
|
+
["PUT", "/customers/:customerId/addresses/:addrId/primary", import_middlewares.requireAuth, wsCtx, address.setPrimary],
|
|
2164
|
+
["DELETE", "/customers/:customerId/addresses/:addrId", import_middlewares.requireAuth, wsCtx, address.remove],
|
|
2165
|
+
// ── Notes ────────────────────────────────────────────────────────
|
|
2166
|
+
["GET", "/customers/:customerId/notes", import_middlewares.requireAuth, wsCtx, note.list],
|
|
2167
|
+
["POST", "/customers/:customerId/notes", import_middlewares.requireAuth, wsCtx, note.create],
|
|
2168
|
+
["PUT", "/customers/:customerId/notes/:noteId", import_middlewares.requireAuth, wsCtx, note.update],
|
|
2169
|
+
["DELETE", "/customers/:customerId/notes/:noteId", import_middlewares.requireAuth, wsCtx, note.delete],
|
|
2170
|
+
// ── Tags ─────────────────────────────────────────────────────────
|
|
2171
|
+
["GET", "/customers/:customerId/tags", import_middlewares.requireAuth, wsCtx, tag.list],
|
|
2172
|
+
["POST", "/customers/:customerId/tags", import_middlewares.requireAuth, wsCtx, tag.add],
|
|
2173
|
+
["DELETE", "/customers/:customerId/tags/:tag", import_middlewares.requireAuth, wsCtx, tag.remove],
|
|
2174
|
+
// ── Relationships ────────────────────────────────────────────────────
|
|
2175
|
+
["GET", "/customers/:customerId/relationships", import_middlewares.requireAuth, wsCtx, relationship.list],
|
|
2176
|
+
["POST", "/customers/:customerId/relationships", import_middlewares.requireAuth, wsCtx, relationship.add],
|
|
2177
|
+
["PUT", "/customers/:customerId/relationships/:relatedId/primary", import_middlewares.requireAuth, wsCtx, relationship.setPrimary],
|
|
2178
|
+
["DELETE", "/customers/:customerId/relationships/:relatedId", import_middlewares.requireAuth, wsCtx, relationship.remove]
|
|
2179
|
+
];
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// src/module.ts
|
|
2183
|
+
var CustomersModule = class {
|
|
2184
|
+
constructor(store, config = {}, bus) {
|
|
2185
|
+
this.store = store;
|
|
2186
|
+
this.config = config;
|
|
2187
|
+
this.bus = bus;
|
|
2188
|
+
}
|
|
2189
|
+
store;
|
|
2190
|
+
config;
|
|
2191
|
+
bus;
|
|
2192
|
+
name = "@fonderie/customers";
|
|
2193
|
+
deps = ["@fonderie/workspaces"];
|
|
2194
|
+
install(app) {
|
|
2195
|
+
const routes = buildCustomerRoutes(this.store, this.config, this.bus);
|
|
2196
|
+
for (const [method, path, ...handlers] of routes) {
|
|
2197
|
+
app.addRoute(method, path, ...handlers);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2202
|
+
0 && (module.exports = {
|
|
2203
|
+
CustomerAddressModel,
|
|
2204
|
+
CustomerEmailModel,
|
|
2205
|
+
CustomerModel,
|
|
2206
|
+
CustomerNoteModel,
|
|
2207
|
+
CustomerPhoneModel,
|
|
2208
|
+
CustomerTagModel,
|
|
2209
|
+
CustomersModule,
|
|
2210
|
+
EVENT_KEYS,
|
|
2211
|
+
toAddressDTO,
|
|
2212
|
+
toCustomerAddressDTO,
|
|
2213
|
+
toCustomerDTO,
|
|
2214
|
+
toCustomerDetailDTO,
|
|
2215
|
+
toCustomerEmailDTO,
|
|
2216
|
+
toCustomerNoteDTO,
|
|
2217
|
+
toCustomerPhoneDTO,
|
|
2218
|
+
toCustomerTagDTO
|
|
2219
|
+
});
|
|
2220
|
+
//# sourceMappingURL=index.cjs.map
|