@fonderie/customers 2.0.0 → 3.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/dist/index.d.cts CHANGED
@@ -28,6 +28,8 @@ interface ICustomerDTO {
28
28
  avatarUrl: string;
29
29
  locale: string;
30
30
  referenceCode: string;
31
+ referralCode: string;
32
+ referredBy: string | null;
31
33
  blacklisted: {
32
34
  status: boolean;
33
35
  reason: string | null;
@@ -125,6 +127,10 @@ interface CreateCustomerOpts {
125
127
  referenceCode?: string;
126
128
  /** Prefix used when auto-generating. Defaults to 'CLT'. */
127
129
  referenceCodePrefix?: string;
130
+ /** Explicit referral code. Omit to auto-generate a random, workspace-unique one. */
131
+ referralCode?: string;
132
+ /** The referrer's referral code (from signup). Resolved to `referredBy` within the same workspace; ignored if it doesn't match a customer. */
133
+ referredByCode?: string;
128
134
  createdBy?: string | null;
129
135
  }
130
136
  interface UpdateCustomerOpts {
@@ -142,6 +148,17 @@ declare class CustomerModel {
142
148
  private readonly store;
143
149
  constructor(store: IStoreAdapter);
144
150
  private allocateCode;
151
+ /** A random referral code (crypto-random over the unambiguous alphabet). */
152
+ private randomReferralCode;
153
+ /**
154
+ * A referral code unique within the workspace. Random codes collide only
155
+ * astronomically rarely; we still pre-check and retry a few times, and the
156
+ * unique index is the final guard. Throws only if the space is somehow
157
+ * exhausted (not reachable in practice).
158
+ */
159
+ private allocateReferralCode;
160
+ /** Resolve a referral code to the referring customer's id, within a workspace. */
161
+ resolveReferralCode(workspaceId: string, code: string): Promise<string | null>;
145
162
  list(opts: ListCustomersOpts): Promise<ICustomer[]>;
146
163
  findById(id: string, workspaceId: string): Promise<ICustomer | null>;
147
164
  findDetail(id: string, workspaceId: string, depth: 2): Promise<ICustomerDetailD2 | null>;
@@ -251,6 +268,8 @@ declare const createCustomerSchema: z.ZodObject<{
251
268
  avatarUrl: z.ZodOptional<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodURL>>>;
252
269
  locale: z.ZodOptional<z.ZodNullable<z.ZodString>>;
253
270
  referenceCode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
271
+ referralCode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
272
+ referredByCode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
254
273
  }, z.core.$strip>;
255
274
  declare const updateCustomerSchema: z.ZodObject<{
256
275
  type: z.ZodOptional<z.ZodEnum<{
@@ -268,6 +287,8 @@ declare const updateCustomerSchema: z.ZodObject<{
268
287
  avatarUrl: z.ZodOptional<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodURL>>>;
269
288
  locale: z.ZodOptional<z.ZodNullable<z.ZodString>>;
270
289
  referenceCode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
290
+ referralCode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
291
+ referredByCode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
271
292
  }, z.core.$strip>;
272
293
  declare const blacklistSchema: z.ZodObject<{
273
294
  reason: z.ZodOptional<z.ZodString>;
package/dist/index.d.ts CHANGED
@@ -28,6 +28,8 @@ interface ICustomerDTO {
28
28
  avatarUrl: string;
29
29
  locale: string;
30
30
  referenceCode: string;
31
+ referralCode: string;
32
+ referredBy: string | null;
31
33
  blacklisted: {
32
34
  status: boolean;
33
35
  reason: string | null;
@@ -125,6 +127,10 @@ interface CreateCustomerOpts {
125
127
  referenceCode?: string;
126
128
  /** Prefix used when auto-generating. Defaults to 'CLT'. */
127
129
  referenceCodePrefix?: string;
130
+ /** Explicit referral code. Omit to auto-generate a random, workspace-unique one. */
131
+ referralCode?: string;
132
+ /** The referrer's referral code (from signup). Resolved to `referredBy` within the same workspace; ignored if it doesn't match a customer. */
133
+ referredByCode?: string;
128
134
  createdBy?: string | null;
129
135
  }
130
136
  interface UpdateCustomerOpts {
@@ -142,6 +148,17 @@ declare class CustomerModel {
142
148
  private readonly store;
143
149
  constructor(store: IStoreAdapter);
144
150
  private allocateCode;
151
+ /** A random referral code (crypto-random over the unambiguous alphabet). */
152
+ private randomReferralCode;
153
+ /**
154
+ * A referral code unique within the workspace. Random codes collide only
155
+ * astronomically rarely; we still pre-check and retry a few times, and the
156
+ * unique index is the final guard. Throws only if the space is somehow
157
+ * exhausted (not reachable in practice).
158
+ */
159
+ private allocateReferralCode;
160
+ /** Resolve a referral code to the referring customer's id, within a workspace. */
161
+ resolveReferralCode(workspaceId: string, code: string): Promise<string | null>;
145
162
  list(opts: ListCustomersOpts): Promise<ICustomer[]>;
146
163
  findById(id: string, workspaceId: string): Promise<ICustomer | null>;
147
164
  findDetail(id: string, workspaceId: string, depth: 2): Promise<ICustomerDetailD2 | null>;
@@ -251,6 +268,8 @@ declare const createCustomerSchema: z.ZodObject<{
251
268
  avatarUrl: z.ZodOptional<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodURL>>>;
252
269
  locale: z.ZodOptional<z.ZodNullable<z.ZodString>>;
253
270
  referenceCode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
271
+ referralCode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
272
+ referredByCode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
254
273
  }, z.core.$strip>;
255
274
  declare const updateCustomerSchema: z.ZodObject<{
256
275
  type: z.ZodOptional<z.ZodEnum<{
@@ -268,6 +287,8 @@ declare const updateCustomerSchema: z.ZodObject<{
268
287
  avatarUrl: z.ZodOptional<z.ZodNullable<z.ZodPipe<z.ZodString, z.ZodURL>>>;
269
288
  locale: z.ZodOptional<z.ZodNullable<z.ZodString>>;
270
289
  referenceCode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
290
+ referralCode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
291
+ referredByCode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
271
292
  }, z.core.$strip>;
272
293
  declare const blacklistSchema: z.ZodObject<{
273
294
  reason: z.ZodOptional<z.ZodString>;
package/dist/index.js CHANGED
@@ -13,6 +13,8 @@ var EVENT_KEYS = {
13
13
  customerUnblacklisted: "fonderie.customer.unblacklisted"
14
14
  };
15
15
  var DEFAULT_REFERENCE_CODE_PREFIX = "CLT";
16
+ var REFERRAL_CODE_ALPHABET = "23456789ABCDEFGHJKMNPQRSTUVWXYZ";
17
+ var REFERRAL_CODE_LENGTH = 8;
16
18
 
17
19
  // src/dtos/customer.ts
18
20
  import { arrayOrEmpty, booleanOrFalse, dateOrEmpty, stringOrEmpty } from "@fonderie/core";
@@ -28,6 +30,8 @@ function toCustomerDTO(c) {
28
30
  avatarUrl: stringOrEmpty(c.avatarUrl),
29
31
  locale: stringOrEmpty(c.locale),
30
32
  referenceCode: stringOrEmpty(c.referenceCode),
33
+ referralCode: stringOrEmpty(c.referralCode),
34
+ referredBy: c.referredBy ?? null,
31
35
  blacklisted: { status: booleanOrFalse(c.isBlacklisted), reason: c.blacklistReason ?? null },
32
36
  createdBy: stringOrEmpty(c.createdBy),
33
37
  createdAt: dateOrEmpty(c.createdAt),
@@ -149,6 +153,7 @@ function toCustomerTagDTO(t) {
149
153
  }
150
154
 
151
155
  // src/models/customer.model.ts
156
+ import { randomInt } from "crypto";
152
157
  function groupByCustomer(rows) {
153
158
  return rows.reduce((m, r) => {
154
159
  if (!m.has(r.customerId)) m.set(r.customerId, []);
@@ -167,6 +172,8 @@ var SELECT_CUSTOMER = `
167
172
  avatar_url AS "avatarUrl",
168
173
  locale,
169
174
  reference_code AS "referenceCode",
175
+ referral_code AS "referralCode",
176
+ referred_by AS "referredBy",
170
177
  is_blacklisted AS "isBlacklisted",
171
178
  blacklist_reason AS "blacklistReason",
172
179
  created_by AS "createdBy",
@@ -189,6 +196,39 @@ var CustomerModel = class {
189
196
  );
190
197
  return `${prefix}-${String(row.nextVal).padStart(4, "0")}`;
191
198
  }
199
+ /** A random referral code (crypto-random over the unambiguous alphabet). */
200
+ randomReferralCode() {
201
+ let out = "";
202
+ for (let i = 0; i < REFERRAL_CODE_LENGTH; i++) {
203
+ out += REFERRAL_CODE_ALPHABET[randomInt(REFERRAL_CODE_ALPHABET.length)];
204
+ }
205
+ return out;
206
+ }
207
+ /**
208
+ * A referral code unique within the workspace. Random codes collide only
209
+ * astronomically rarely; we still pre-check and retry a few times, and the
210
+ * unique index is the final guard. Throws only if the space is somehow
211
+ * exhausted (not reachable in practice).
212
+ */
213
+ async allocateReferralCode(workspaceId) {
214
+ for (let attempt = 0; attempt < 5; attempt++) {
215
+ const code = this.randomReferralCode();
216
+ const [hit] = await this.store.query(
217
+ `SELECT 1 AS one FROM fonderie_customers WHERE workspace_id = $1 AND referral_code = $2 LIMIT 1`,
218
+ [workspaceId, code]
219
+ );
220
+ if (!hit) return code;
221
+ }
222
+ throw new Error("could not allocate a unique referral code");
223
+ }
224
+ /** Resolve a referral code to the referring customer's id, within a workspace. */
225
+ async resolveReferralCode(workspaceId, code) {
226
+ const [row] = await this.store.query(
227
+ `SELECT id FROM fonderie_customers WHERE workspace_id = $1 AND referral_code = $2 LIMIT 1`,
228
+ [workspaceId, code]
229
+ );
230
+ return row?.id ?? null;
231
+ }
192
232
  async list(opts) {
193
233
  const conditions = ["workspace_id = $1"];
194
234
  const params = [opts.workspaceId];
@@ -504,10 +544,12 @@ var CustomerModel = class {
504
544
  }
505
545
  async create(opts) {
506
546
  const referenceCode = opts.referenceCode ?? await this.allocateCode(opts.workspaceId, opts.referenceCodePrefix ?? DEFAULT_REFERENCE_CODE_PREFIX);
547
+ const referralCode = opts.referralCode ?? await this.allocateReferralCode(opts.workspaceId);
548
+ const referredBy = opts.referredByCode ? await this.resolveReferralCode(opts.workspaceId, opts.referredByCode) : null;
507
549
  const [row] = await this.store.query(
508
550
  `INSERT INTO fonderie_customers
509
- (workspace_id, type, sex, first_name, last_name, company_name, avatar_url, locale, reference_code, created_by)
510
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
551
+ (workspace_id, type, sex, first_name, last_name, company_name, avatar_url, locale, reference_code, referral_code, referred_by, created_by)
552
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
511
553
  RETURNING ${SELECT_CUSTOMER}`,
512
554
  [
513
555
  opts.workspaceId,
@@ -519,6 +561,8 @@ var CustomerModel = class {
519
561
  opts.avatarUrl ?? null,
520
562
  opts.locale ?? "en-US",
521
563
  referenceCode,
564
+ referralCode,
565
+ referredBy,
522
566
  opts.createdBy ?? null
523
567
  ]
524
568
  );
@@ -1191,7 +1235,9 @@ var customerFields = {
1191
1235
  companyName: z.string().max(200).nullable().optional(),
1192
1236
  avatarUrl: z.string().trim().pipe(z.url()).nullable().optional(),
1193
1237
  locale: z.string().max(35).nullable().optional(),
1194
- referenceCode: z.string().max(100).nullable().optional()
1238
+ referenceCode: z.string().max(100).nullable().optional(),
1239
+ referralCode: z.string().max(100).nullable().optional(),
1240
+ referredByCode: z.string().max(100).nullable().optional()
1195
1241
  };
1196
1242
  var createCustomerSchema = z.object(customerFields);
1197
1243
  var updateCustomerSchema = z.object(customerFields).refine((o) => Object.values(o).some((v) => v !== void 0), "Provide at least one field");
@@ -1313,6 +1359,8 @@ function customerController(store, config = {}, bus) {
1313
1359
  const avatarUrl = body?.["avatarUrl"];
1314
1360
  const locale = body?.["locale"];
1315
1361
  const referenceCode = body?.["referenceCode"];
1362
+ const referralCode = body?.["referralCode"];
1363
+ const referredByCode = body?.["referredByCode"];
1316
1364
  if (type !== void 0 && type !== "individual" && type !== "business") {
1317
1365
  return setApiResponse(
1318
1366
  HTTP.UNPROCESSABLE,
@@ -1341,6 +1389,9 @@ function customerController(store, config = {}, bus) {
1341
1389
  // exactOptionalPropertyTypes: omit the key entirely when absent
1342
1390
  ...typeof referenceCode === "string" ? { referenceCode: referenceCode.toUpperCase() } : {},
1343
1391
  referenceCodePrefix: prefix,
1392
+ // referral: explicit code override is rare; referredByCode is the signup input
1393
+ ...typeof referralCode === "string" ? { referralCode: referralCode.toUpperCase() } : {},
1394
+ ...typeof referredByCode === "string" ? { referredByCode: referredByCode.toUpperCase() } : {},
1344
1395
  createdBy: ctx.user?.id ?? null
1345
1396
  });
1346
1397
  } catch (err) {