@fonderie/customers 1.1.2 → 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 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.
@@ -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) {