@fonderie/customers 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/brain/outcomes.md +2 -0
- package/brain/signatures.md +5 -0
- package/dist/index.cjs +54 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +54 -3
- package/dist/index.js.map +1 -1
- 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/migrations/sql/012_referral_code.sql +18 -0
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +2 -0
- package/dist/types.d.ts +2 -0
- package/package.json +6 -6
package/brain/outcomes.md
CHANGED
|
@@ -129,6 +129,8 @@ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
|
129
129
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
130
130
|
sex TEXT NOT NULL DEFAULT 'UNKNOWN'
|
|
131
131
|
blacklist_reason TEXT
|
|
132
|
+
referral_code TEXT
|
|
133
|
+
referred_by UUID REFERENCES fonderie_customers(id) ON DELETE SET NULL
|
|
132
134
|
```
|
|
133
135
|
|
|
134
136
|
Raw SQL ships in `node_modules/@fonderie/customers/dist/migrations/sql/` — read it there if you must; never download tarballs.
|
package/brain/signatures.md
CHANGED
|
@@ -51,6 +51,8 @@ interface ICustomerDTO {
|
|
|
51
51
|
avatarUrl: string;
|
|
52
52
|
locale: string;
|
|
53
53
|
referenceCode: string;
|
|
54
|
+
referralCode: string;
|
|
55
|
+
referredBy: string | null;
|
|
54
56
|
blacklisted: {
|
|
55
57
|
status: boolean;
|
|
56
58
|
reason: string | null;
|
|
@@ -119,6 +121,7 @@ new CustomerEmailModel(store: IStoreAdapter): CustomerEmailModel
|
|
|
119
121
|
.remove(emailId: string, customerId: string): Promise<void>
|
|
120
122
|
|
|
121
123
|
new CustomerModel(store: IStoreAdapter): CustomerModel
|
|
124
|
+
.resolveReferralCode(workspaceId: string, code: string): Promise<string | null>
|
|
122
125
|
.list(opts: ListCustomersOpts): Promise<ICustomer[]>
|
|
123
126
|
.findById(id: string, workspaceId: string): Promise<ICustomer | null>
|
|
124
127
|
.findDetail(id: string, workspaceId: string, depth: 2): Promise<ICustomerDetailD2 | null>
|
|
@@ -179,6 +182,8 @@ interface ICustomer {
|
|
|
179
182
|
avatarUrl: string | null;
|
|
180
183
|
locale: string;
|
|
181
184
|
referenceCode: string | null;
|
|
185
|
+
referralCode: string | null;
|
|
186
|
+
referredBy: string | null;
|
|
182
187
|
isBlacklisted: boolean;
|
|
183
188
|
blacklistReason: string | null;
|
|
184
189
|
createdBy: string | null;
|
package/dist/index.cjs
CHANGED
|
@@ -49,6 +49,8 @@ var EVENT_KEYS = {
|
|
|
49
49
|
customerUnblacklisted: "fonderie.customer.unblacklisted"
|
|
50
50
|
};
|
|
51
51
|
var DEFAULT_REFERENCE_CODE_PREFIX = "CLT";
|
|
52
|
+
var REFERRAL_CODE_ALPHABET = "23456789ABCDEFGHJKMNPQRSTUVWXYZ";
|
|
53
|
+
var REFERRAL_CODE_LENGTH = 8;
|
|
52
54
|
|
|
53
55
|
// src/dtos/customer.ts
|
|
54
56
|
var import_core = require("@fonderie/core");
|
|
@@ -64,6 +66,8 @@ function toCustomerDTO(c) {
|
|
|
64
66
|
avatarUrl: (0, import_core.stringOrEmpty)(c.avatarUrl),
|
|
65
67
|
locale: (0, import_core.stringOrEmpty)(c.locale),
|
|
66
68
|
referenceCode: (0, import_core.stringOrEmpty)(c.referenceCode),
|
|
69
|
+
referralCode: (0, import_core.stringOrEmpty)(c.referralCode),
|
|
70
|
+
referredBy: c.referredBy ?? null,
|
|
67
71
|
blacklisted: { status: (0, import_core.booleanOrFalse)(c.isBlacklisted), reason: c.blacklistReason ?? null },
|
|
68
72
|
createdBy: (0, import_core.stringOrEmpty)(c.createdBy),
|
|
69
73
|
createdAt: (0, import_core.dateOrEmpty)(c.createdAt),
|
|
@@ -185,6 +189,7 @@ function toCustomerTagDTO(t) {
|
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
// src/models/customer.model.ts
|
|
192
|
+
var import_node_crypto = require("crypto");
|
|
188
193
|
function groupByCustomer(rows) {
|
|
189
194
|
return rows.reduce((m, r) => {
|
|
190
195
|
if (!m.has(r.customerId)) m.set(r.customerId, []);
|
|
@@ -203,6 +208,8 @@ var SELECT_CUSTOMER = `
|
|
|
203
208
|
avatar_url AS "avatarUrl",
|
|
204
209
|
locale,
|
|
205
210
|
reference_code AS "referenceCode",
|
|
211
|
+
referral_code AS "referralCode",
|
|
212
|
+
referred_by AS "referredBy",
|
|
206
213
|
is_blacklisted AS "isBlacklisted",
|
|
207
214
|
blacklist_reason AS "blacklistReason",
|
|
208
215
|
created_by AS "createdBy",
|
|
@@ -225,6 +232,39 @@ var CustomerModel = class {
|
|
|
225
232
|
);
|
|
226
233
|
return `${prefix}-${String(row.nextVal).padStart(4, "0")}`;
|
|
227
234
|
}
|
|
235
|
+
/** A random referral code (crypto-random over the unambiguous alphabet). */
|
|
236
|
+
randomReferralCode() {
|
|
237
|
+
let out = "";
|
|
238
|
+
for (let i = 0; i < REFERRAL_CODE_LENGTH; i++) {
|
|
239
|
+
out += REFERRAL_CODE_ALPHABET[(0, import_node_crypto.randomInt)(REFERRAL_CODE_ALPHABET.length)];
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* A referral code unique within the workspace. Random codes collide only
|
|
245
|
+
* astronomically rarely; we still pre-check and retry a few times, and the
|
|
246
|
+
* unique index is the final guard. Throws only if the space is somehow
|
|
247
|
+
* exhausted (not reachable in practice).
|
|
248
|
+
*/
|
|
249
|
+
async allocateReferralCode(workspaceId) {
|
|
250
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
251
|
+
const code = this.randomReferralCode();
|
|
252
|
+
const [hit] = await this.store.query(
|
|
253
|
+
`SELECT 1 AS one FROM fonderie_customers WHERE workspace_id = $1 AND referral_code = $2 LIMIT 1`,
|
|
254
|
+
[workspaceId, code]
|
|
255
|
+
);
|
|
256
|
+
if (!hit) return code;
|
|
257
|
+
}
|
|
258
|
+
throw new Error("could not allocate a unique referral code");
|
|
259
|
+
}
|
|
260
|
+
/** Resolve a referral code to the referring customer's id, within a workspace. */
|
|
261
|
+
async resolveReferralCode(workspaceId, code) {
|
|
262
|
+
const [row] = await this.store.query(
|
|
263
|
+
`SELECT id FROM fonderie_customers WHERE workspace_id = $1 AND referral_code = $2 LIMIT 1`,
|
|
264
|
+
[workspaceId, code]
|
|
265
|
+
);
|
|
266
|
+
return row?.id ?? null;
|
|
267
|
+
}
|
|
228
268
|
async list(opts) {
|
|
229
269
|
const conditions = ["workspace_id = $1"];
|
|
230
270
|
const params = [opts.workspaceId];
|
|
@@ -540,10 +580,12 @@ var CustomerModel = class {
|
|
|
540
580
|
}
|
|
541
581
|
async create(opts) {
|
|
542
582
|
const referenceCode = opts.referenceCode ?? await this.allocateCode(opts.workspaceId, opts.referenceCodePrefix ?? DEFAULT_REFERENCE_CODE_PREFIX);
|
|
583
|
+
const referralCode = opts.referralCode ?? await this.allocateReferralCode(opts.workspaceId);
|
|
584
|
+
const referredBy = opts.referredByCode ? await this.resolveReferralCode(opts.workspaceId, opts.referredByCode) : null;
|
|
543
585
|
const [row] = await this.store.query(
|
|
544
586
|
`INSERT INTO fonderie_customers
|
|
545
|
-
(workspace_id, type, sex, first_name, last_name, company_name, avatar_url, locale, reference_code, created_by)
|
|
546
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
587
|
+
(workspace_id, type, sex, first_name, last_name, company_name, avatar_url, locale, reference_code, referral_code, referred_by, created_by)
|
|
588
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
547
589
|
RETURNING ${SELECT_CUSTOMER}`,
|
|
548
590
|
[
|
|
549
591
|
opts.workspaceId,
|
|
@@ -555,6 +597,8 @@ var CustomerModel = class {
|
|
|
555
597
|
opts.avatarUrl ?? null,
|
|
556
598
|
opts.locale ?? "en-US",
|
|
557
599
|
referenceCode,
|
|
600
|
+
referralCode,
|
|
601
|
+
referredBy,
|
|
558
602
|
opts.createdBy ?? null
|
|
559
603
|
]
|
|
560
604
|
);
|
|
@@ -1227,7 +1271,9 @@ var customerFields = {
|
|
|
1227
1271
|
companyName: import_zod.z.string().max(200).nullable().optional(),
|
|
1228
1272
|
avatarUrl: import_zod.z.string().trim().pipe(import_zod.z.url()).nullable().optional(),
|
|
1229
1273
|
locale: import_zod.z.string().max(35).nullable().optional(),
|
|
1230
|
-
referenceCode: import_zod.z.string().max(100).nullable().optional()
|
|
1274
|
+
referenceCode: import_zod.z.string().max(100).nullable().optional(),
|
|
1275
|
+
referralCode: import_zod.z.string().max(100).nullable().optional(),
|
|
1276
|
+
referredByCode: import_zod.z.string().max(100).nullable().optional()
|
|
1231
1277
|
};
|
|
1232
1278
|
var createCustomerSchema = import_zod.z.object(customerFields);
|
|
1233
1279
|
var updateCustomerSchema = import_zod.z.object(customerFields).refine((o) => Object.values(o).some((v) => v !== void 0), "Provide at least one field");
|
|
@@ -1349,6 +1395,8 @@ function customerController(store, config = {}, bus) {
|
|
|
1349
1395
|
const avatarUrl = body?.["avatarUrl"];
|
|
1350
1396
|
const locale = body?.["locale"];
|
|
1351
1397
|
const referenceCode = body?.["referenceCode"];
|
|
1398
|
+
const referralCode = body?.["referralCode"];
|
|
1399
|
+
const referredByCode = body?.["referredByCode"];
|
|
1352
1400
|
if (type !== void 0 && type !== "individual" && type !== "business") {
|
|
1353
1401
|
return (0, import_core2.setApiResponse)(
|
|
1354
1402
|
import_core2.HTTP.UNPROCESSABLE,
|
|
@@ -1377,6 +1425,9 @@ function customerController(store, config = {}, bus) {
|
|
|
1377
1425
|
// exactOptionalPropertyTypes: omit the key entirely when absent
|
|
1378
1426
|
...typeof referenceCode === "string" ? { referenceCode: referenceCode.toUpperCase() } : {},
|
|
1379
1427
|
referenceCodePrefix: prefix,
|
|
1428
|
+
// referral: explicit code override is rare; referredByCode is the signup input
|
|
1429
|
+
...typeof referralCode === "string" ? { referralCode: referralCode.toUpperCase() } : {},
|
|
1430
|
+
...typeof referredByCode === "string" ? { referredByCode: referredByCode.toUpperCase() } : {},
|
|
1380
1431
|
createdBy: ctx.user?.id ?? null
|
|
1381
1432
|
});
|
|
1382
1433
|
} catch (err) {
|