All files / lib/partners code.ts

100% Statements 23/23
100% Branches 10/10
100% Functions 4/4
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106                                                          24x 24x                       62368x 62368x 62368x 66524x 66524x 66524x         7796x 62368x 7796x     24x                                     46x 43x         43x                           83x 77x 77x              
/**
 * Referral codes.
 *
 * The code is the most-handled string in the programme. It goes into a WhatsApp
 * message the partner forwards, into `go.interioring.com/e/<code>`, and back to
 * us as `ref <code>` when the homeowner replies — so it is read on a phone
 * screen, sometimes retyped, and occasionally read aloud over a call.
 *
 * ── The alphabet ─────────────────────────────────────────────────────────
 * 30 characters, uppercase, with `0 1 I L O U` removed:
 *   - 0/O and 1/I/L are the pairs people actually confuse when retyping.
 *   - U is dropped as well, which costs one character of entropy and removes
 *     the great majority of accidental English profanity from a generated
 *     code. A partner forwarding a rude five-letter string to a homeowner they
 *     are trying to impress is a real cost, and it is cheaper to avoid than to
 *     apologise for.
 *
 * ── The length ───────────────────────────────────────────────────────────
 * 8 characters: 30^8 ≈ 6.6e11. At the programme's plausible ceiling — say a
 * million referrals — the birthday probability of any collision at all is
 * under one in a thousand, and `referrals_code_uniq` turns the case we do hit
 * into a retry rather than into two homeowners sharing a link.
 *
 * Not sequential, and not derived from the partner id: the code appears in a
 * public URL, so anything guessable would let someone enumerate other
 * partners' referrals and read a homeowner's name off the page.
 */
 
/** No 0/1/I/L/O/U. */
const ALPHABET = "23456789ABCDEFGHJKMNPQRSTVWXYZ";
const CODE_LENGTH = 8;
 
/**
 * Rejection sampling rather than `% ALPHABET.length`.
 *
 * 256 is not a multiple of 30, so a plain modulo makes the first 16 characters
 * of the alphabet fractionally likelier than the rest. It would not matter for
 * a display id; it matters here only because this is also the thing standing
 * between a stranger and someone else's referral, and biased randomness is a
 * bad habit to establish in a file whose whole job is unguessability.
 */
function randomIndex(): number {
	const limit = 256 - (256 % ALPHABET.length); // 240
	const byte = new Uint8Array(1);
	for (;;) {
		crypto.getRandomValues(byte);
		const value = byte[0] as number;
		if (value < limit) return value % ALPHABET.length;
	}
}
 
export function generateReferralCode(): string {
	let out = "";
	for (let i = 0; i < CODE_LENGTH; i++) out += ALPHABET[randomIndex()];
	return out;
}
 
const CODE_RE = new RegExp(`^[${ALPHABET}]{${CODE_LENGTH}}$`);
 
/**
 * Canonicalises what a human typed, or null if it is not a code.
 *
 * There is deliberately NO character mapping here. The obvious feature — treat
 * a typed `O` as `0`, an `I` as `1` — has nothing to operate on, because the
 * alphabet excludes BOTH halves of every confusable pair. That is the whole
 * point of choosing it: ambiguity is designed out at generation rather than
 * guessed at on the way back in.
 *
 * A code containing `O` is not a code with a typo we can repair; it is either
 * misread (and `Q`, `D` and `0` are all equally likely intents) or invented.
 * Guessing would turn one person's mistyping into another person's referral,
 * so this refuses and the UI asks them to check.
 */
export function normaliseReferralCode(
	input: string | null | undefined,
): string | null {
	if (!input) return null;
	const cleaned = input
		.trim()
		.replace(/^ref\s+/i, "")
		.replace(/[\s-]/g, "")
		.toUpperCase();
	return CODE_RE.test(cleaned) ? cleaned : null;
}
 
/**
 * Pulls `ref <code>` out of an inbound WhatsApp message (B4, API-3).
 *
 * The homeowner's message is free text they may have edited, so the code can
 * sit anywhere in it and carry punctuation. Anchored on the `ref` keyword
 * rather than "any 8-char token" so an ordinary sentence cannot be mistaken
 * for a referral.
 */
export function extractReferralCode(
	text: string | null | undefined,
): string | null {
	if (!text) return null;
	const match = text.match(/\bref[:\s]+([A-Za-z0-9-]{6,14})\b/i);
	return match ? normaliseReferralCode(match[1]) : null;
}
 
export {
	ALPHABET as REFERRAL_CODE_ALPHABET,
	CODE_LENGTH as REFERRAL_CODE_LENGTH,
};