@ibiliaze/stringman 3.15.0 → 3.17.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.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './seat';
2
+ import { fishyMatchesAll } from './ticket';
2
3
  /**
3
4
  * Clean up extra whitespace in a string.
4
5
  *
@@ -159,6 +160,7 @@ export declare const ticket: {
159
160
  dashed?: boolean;
160
161
  }) => string;
161
162
  validateTicketCode: (code: string) => boolean;
163
+ fishyMatchesAll: typeof fishyMatchesAll;
162
164
  };
163
165
  export declare const order: {
164
166
  createNumericOrderId: () => string;
package/dist/index.js CHANGED
@@ -306,5 +306,6 @@ exports.ticket = {
306
306
  getTicketId: ticket_1.getTicketId,
307
307
  buildTicketCode: ticket_1.buildTicketCode,
308
308
  validateTicketCode: ticket_1.validateTicketCode,
309
+ fishyMatchesAll: ticket_1.fishyMatchesAll,
309
310
  };
310
311
  exports.order = { createNumericOrderId: order_1.createNumericOrderId };
package/dist/ticket.d.ts CHANGED
@@ -5,36 +5,35 @@
5
5
  */
6
6
  export declare const luhn10: (digits: string) => string;
7
7
  /**
8
- * Compact, stable 2-digit number for a venue or any short code.
9
- * Uses a small SHA-1 based hash and modulo to fit the requested width.
8
+ * Compact, stable numeric block from a short venue code.
9
+ * Uses the first two bytes of a SHA-1 digest modulo 10^len.
10
10
  */
11
- export declare const venueBlockNum: (venueCode: string, len?: number) => string;
11
+ export declare const venueBlockNum: (venueCode: string, len?: 3) => string;
12
12
  /**
13
- * Compact, stable 2-digit number for a Fixture/Match ID string.
14
- * If you pass your DB fixtureId here, you’ll get a short numeric block.
13
+ * Compact, stable numeric block from a Fixture/Match ID string.
15
14
  */
16
- export declare const fixtureIdBlockNum: (fixtureId: string, len?: number) => string;
15
+ export declare const fixtureIdBlockNum: (fixtureId: string, len?: 3) => string;
17
16
  /**
18
- * Compact, stable 3-digit number for a specific seat (section + row + col).
17
+ * Compact, stable numeric block for a specific seat (section + row + col).
19
18
  */
20
- export declare const seatBlockNum: (sectionId: string, row: number, col: number, len?: number) => string;
19
+ export declare const seatBlockNum: (sectionId: string, row: number, col: number, len?: 3) => string;
21
20
  /**
22
- * 6-digit date/time block derived from local start time.
21
+ * 4-digit date/time block derived from local start time.
23
22
  * Input format expected: "YYYY-MM-DDThh:mm" (local, no 'Z').
24
23
  * Encodes YYMMDDhhmm then reduces modulo 10^len to keep width stable.
25
24
  */
26
- export declare const fixtureBlockNum: (fixtureLocalIso: string, len?: number) => string;
25
+ export declare const fixtureBlockNum: (fixtureLocalIso: string, len?: 4) => string;
27
26
  /**
28
27
  * Cryptographically strong random numeric block of the given length.
29
28
  */
30
- export declare const rndBlockNum: (len?: number) => string;
29
+ export declare const rndBlockNum: (len?: 6) => string;
31
30
  /**
32
31
  * Build a numeric ticket code with blocks ordered to match the picture *in reverse*:
33
- * Random (3) + Sector/Block/Col (3) + Date/Time (6) + Fixture/Match ID (2) + Luhn(1)
32
+ * Random (6) + Seat (3) + Date/Time (4) + Fixture/Match ID (3) + Luhn(1)
34
33
  *
35
- * Default dashed display groups: 3-3-6-2-1
34
+ * Default dashed display groups: 6-3-4-3-1
36
35
  *
37
- * Lengths are chosen to keep the overall size at 15 digits (incl. Luhn),
36
+ * Lengths are chosen to keep the overall size at 17 digits (incl. Luhn),
38
37
  * which plays nicely with scanners and manual entry.
39
38
  */
40
39
  export declare const buildTicketCodeNumeric: (args: {
@@ -48,18 +47,27 @@ export declare const buildTicketCodeNumeric: (args: {
48
47
  row: number;
49
48
  /** Seat column/number. */
50
49
  col: number;
51
- /** Optional: your DB fixtureId. If present, becomes the 2-digit “Fixture/Match ID” block. */
50
+ /** Optional: your DB fixtureId. If present, becomes the 3-digit “Fixture/Match ID” block. */
52
51
  fixtureId?: string;
53
- /** Random block length (default 3). */
52
+ /** Optional override for random block length (defaults to 6). */
54
53
  rndLen?: number;
55
54
  /** Show dashes for readability (default true). */
56
55
  dashed?: boolean;
57
56
  }) => string;
58
57
  /**
59
58
  * Validate a code built by buildTicketCodeNumeric (dashes optional).
60
- * Checks: numeric, correct total length (15), and Luhn checksum.
59
+ * Checks: numeric, correct total length (17), and Luhn checksum.
61
60
  */
62
61
  export declare const validateTicketCodeNumeric: (code: string) => boolean;
62
+ /**
63
+ * Check if a fishy ticket's core digits (last 6 before the final digit)
64
+ * match ALL ticket codes in the list.
65
+ *
66
+ * @param {string[]} tickets - list of ticket codes
67
+ * @param {string} fishyCode - the suspect ticket code
68
+ * @returns {boolean} true if fishyCode matches all, false otherwise
69
+ */
70
+ export declare function fishyMatchesAll(tickets: string[], fishyCode: string): boolean;
63
71
  /**
64
72
  * A stable, human-readable internal ID for a seat within a fixture.
65
73
  * (Not used in the printed/scanned code — just handy elsewhere.)
@@ -80,9 +88,9 @@ export declare const buildTicketCode: (args: {
80
88
  row: number;
81
89
  /** Seat column/number. */
82
90
  col: number;
83
- /** Optional: your DB fixtureId. If present, becomes the 2-digit “Fixture/Match ID” block. */
91
+ /** Optional: your DB fixtureId. If present, becomes the 3-digit “Fixture/Match ID” block. */
84
92
  fixtureId?: string;
85
- /** Random block length (default 3). */
93
+ /** Optional override for random block length (defaults to 6). */
86
94
  rndLen?: number;
87
95
  /** Show dashes for readability (default true). */
88
96
  dashed?: boolean;
package/dist/ticket.js CHANGED
@@ -1,7 +1,24 @@
1
1
  "use strict";
2
+ // =====================================================================================
3
+ // Ticket Code Utilities — Format: 6-3-4-3-1 (Random • Seat • Date/Time • Fixture • Luhn)
4
+ // Total length = 17 digits (including Luhn). Dashes optional for readability.
5
+ // =====================================================================================
2
6
  Object.defineProperty(exports, "__esModule", { value: true });
3
7
  exports.validateTicketCode = exports.buildTicketCode = exports.getTicketId = exports.validateTicketCodeNumeric = exports.buildTicketCodeNumeric = exports.rndBlockNum = exports.fixtureBlockNum = exports.seatBlockNum = exports.fixtureIdBlockNum = exports.venueBlockNum = exports.luhn10 = void 0;
8
+ exports.fishyMatchesAll = fishyMatchesAll;
4
9
  const node_crypto_1 = require("node:crypto");
10
+ // ---------- config: block lengths (easy to tweak in one place) ----------
11
+ const CODE_LEN = {
12
+ rnd: 6, // Random block
13
+ seat: 3, // Seat identity (section + row + col hashed to 3 digits)
14
+ date: 4, // Fixture local datetime compacted to 4 digits
15
+ fix: 3, // Fixture/Match ID block
16
+ check: 1, // Luhn check digit
17
+ };
18
+ const EXPECTED_TOTAL_LEN = CODE_LEN.rnd + CODE_LEN.seat + CODE_LEN.date + CODE_LEN.fix + CODE_LEN.check; // 17
19
+ // =====================================================================================
20
+ // Helpers
21
+ // =====================================================================================
5
22
  /**
6
23
  * Left-pad a positive integer with zeros to a fixed width.
7
24
  */
@@ -29,10 +46,10 @@ const luhn10 = (digits) => {
29
46
  };
30
47
  exports.luhn10 = luhn10;
31
48
  /**
32
- * Compact, stable 2-digit number for a venue or any short code.
33
- * Uses a small SHA-1 based hash and modulo to fit the requested width.
49
+ * Compact, stable numeric block from a short venue code.
50
+ * Uses the first two bytes of a SHA-1 digest modulo 10^len.
34
51
  */
35
- const venueBlockNum = (venueCode, len = 2) => {
52
+ const venueBlockNum = (venueCode, len = CODE_LEN.fix) => {
36
53
  try {
37
54
  const h = (0, node_crypto_1.createHash)('sha1').update(venueCode.toUpperCase()).digest();
38
55
  const n = ((h[0] << 8) | h[1]) % 10 ** len;
@@ -44,10 +61,9 @@ const venueBlockNum = (venueCode, len = 2) => {
44
61
  };
45
62
  exports.venueBlockNum = venueBlockNum;
46
63
  /**
47
- * Compact, stable 2-digit number for a Fixture/Match ID string.
48
- * If you pass your DB fixtureId here, you’ll get a short numeric block.
64
+ * Compact, stable numeric block from a Fixture/Match ID string.
49
65
  */
50
- const fixtureIdBlockNum = (fixtureId, len = 2) => {
66
+ const fixtureIdBlockNum = (fixtureId, len = CODE_LEN.fix) => {
51
67
  try {
52
68
  const h = (0, node_crypto_1.createHash)('sha1').update(fixtureId).digest();
53
69
  const n = ((h[0] << 8) | h[1]) % 10 ** len;
@@ -59,9 +75,9 @@ const fixtureIdBlockNum = (fixtureId, len = 2) => {
59
75
  };
60
76
  exports.fixtureIdBlockNum = fixtureIdBlockNum;
61
77
  /**
62
- * Compact, stable 3-digit number for a specific seat (section + row + col).
78
+ * Compact, stable numeric block for a specific seat (section + row + col).
63
79
  */
64
- const seatBlockNum = (sectionId, row, col, len = 3) => {
80
+ const seatBlockNum = (sectionId, row, col, len = CODE_LEN.seat) => {
65
81
  try {
66
82
  const h = (0, node_crypto_1.createHash)('sha1').update(`${sectionId}:${row}:${col}`).digest();
67
83
  const n16 = (h[0] << 8) | h[1];
@@ -74,18 +90,18 @@ const seatBlockNum = (sectionId, row, col, len = 3) => {
74
90
  };
75
91
  exports.seatBlockNum = seatBlockNum;
76
92
  /**
77
- * 6-digit date/time block derived from local start time.
93
+ * 4-digit date/time block derived from local start time.
78
94
  * Input format expected: "YYYY-MM-DDThh:mm" (local, no 'Z').
79
95
  * Encodes YYMMDDhhmm then reduces modulo 10^len to keep width stable.
80
96
  */
81
- const fixtureBlockNum = (fixtureLocalIso, len = 6) => {
97
+ const fixtureBlockNum = (fixtureLocalIso, len = CODE_LEN.date) => {
82
98
  try {
83
99
  const m = fixtureLocalIso.match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})/);
84
100
  if (!m)
85
101
  throw new Error('Bad fixtureLocalIso');
86
102
  const [_, y, M, d, h, mnt] = m;
87
103
  const compact = Number(`${y.slice(2)}${M}${d}${h}${mnt}`); // e.g. 2510312000
88
- const n = compact % 10 ** len;
104
+ const n = compact % 10 ** len; // now 4 digits by default
89
105
  return pad(n, len);
90
106
  }
91
107
  catch {
@@ -96,7 +112,7 @@ exports.fixtureBlockNum = fixtureBlockNum;
96
112
  /**
97
113
  * Cryptographically strong random numeric block of the given length.
98
114
  */
99
- const rndBlockNum = (len = 3) => {
115
+ const rndBlockNum = (len = CODE_LEN.rnd) => {
100
116
  try {
101
117
  const max = 10 ** len;
102
118
  return pad((0, node_crypto_1.randomInt)(0, max), len);
@@ -106,30 +122,33 @@ const rndBlockNum = (len = 3) => {
106
122
  }
107
123
  };
108
124
  exports.rndBlockNum = rndBlockNum;
125
+ // =====================================================================================
109
126
  /**
110
127
  * Build a numeric ticket code with blocks ordered to match the picture *in reverse*:
111
- * Random (3) + Sector/Block/Col (3) + Date/Time (6) + Fixture/Match ID (2) + Luhn(1)
128
+ * Random (6) + Seat (3) + Date/Time (4) + Fixture/Match ID (3) + Luhn(1)
112
129
  *
113
- * Default dashed display groups: 3-3-6-2-1
130
+ * Default dashed display groups: 6-3-4-3-1
114
131
  *
115
- * Lengths are chosen to keep the overall size at 15 digits (incl. Luhn),
132
+ * Lengths are chosen to keep the overall size at 17 digits (incl. Luhn),
116
133
  * which plays nicely with scanners and manual entry.
117
134
  */
118
135
  const buildTicketCodeNumeric = (args) => {
119
136
  try {
120
- // --- individual blocks ---
121
- const R = (0, exports.rndBlockNum)(args.rndLen ?? 3); // Random (3)
122
- const S = (0, exports.seatBlockNum)(args.sectionId, args.row, args.col); // Seat (3)
123
- const D = (0, exports.fixtureBlockNum)(args.fixtureLocalIso, 6); // Date/Time (6)
124
- // Fixture/Match ID (2): prefer fixtureId; fallback to venue code hash
125
- const F = args.fixtureId ? (0, exports.fixtureIdBlockNum)(args.fixtureId, 2) : (0, exports.venueBlockNum)(args.venueCode, 2);
137
+ // --- individual blocks (new lengths) ---
138
+ const R = (0, exports.rndBlockNum)(6); // Random (6)
139
+ const S = (0, exports.seatBlockNum)(args.sectionId, args.row, args.col, CODE_LEN.seat); // Seat (3)
140
+ const D = (0, exports.fixtureBlockNum)(args.fixtureLocalIso, CODE_LEN.date); // Date/Time (4)
141
+ // Fixture/Match ID (3): prefer fixtureId; fallback to venue code hash (also 3)
142
+ const F = args.fixtureId
143
+ ? (0, exports.fixtureIdBlockNum)(args.fixtureId, CODE_LEN.fix)
144
+ : (0, exports.venueBlockNum)(args.venueCode, CODE_LEN.fix);
126
145
  // --- assemble (REVERSED order vs the picture’s left-to-right) ---
127
- const body = `${R}${S}${D}${F}`; // 3+3+6+2 = 14
146
+ const body = `${R}${S}${D}${F}`; // 6+3+4+3 = 16
128
147
  const C = (0, exports.luhn10)(body); // 1 digit
129
- const numeric = `${body}${C}`; // total 15 digits
148
+ const numeric = `${body}${C}`; // total 17 digits
130
149
  if (args.dashed === false)
131
150
  return numeric;
132
- // Readability grouping for scanners/ushers: 3-3-6-2-1
151
+ // Readability grouping for scanners/ushers: 6-3-4-3-1
133
152
  return `${R}-${S}-${D}-${F}-${C}`;
134
153
  }
135
154
  catch {
@@ -137,15 +156,18 @@ const buildTicketCodeNumeric = (args) => {
137
156
  }
138
157
  };
139
158
  exports.buildTicketCodeNumeric = buildTicketCodeNumeric;
159
+ // =====================================================================================
140
160
  /**
141
161
  * Validate a code built by buildTicketCodeNumeric (dashes optional).
142
- * Checks: numeric, correct total length (15), and Luhn checksum.
162
+ * Checks: numeric, correct total length (17), and Luhn checksum.
143
163
  */
144
164
  const validateTicketCodeNumeric = (code) => {
145
165
  try {
146
166
  const clean = code.replace(/-/g, '');
147
- if (!/^\d{15}$/.test(clean))
148
- return false; // 3+3+6+2+1
167
+ if (!/^\d+$/.test(clean))
168
+ return false;
169
+ if (clean.length !== EXPECTED_TOTAL_LEN)
170
+ return false; // 17
149
171
  const body = clean.slice(0, -1);
150
172
  const check = clean.slice(-1);
151
173
  return (0, exports.luhn10)(body) === check;
@@ -155,6 +177,33 @@ const validateTicketCodeNumeric = (code) => {
155
177
  }
156
178
  };
157
179
  exports.validateTicketCodeNumeric = validateTicketCodeNumeric;
180
+ /**
181
+ * Check if a fishy ticket's core digits (last 6 before the final digit)
182
+ * match ALL ticket codes in the list.
183
+ *
184
+ * @param {string[]} tickets - list of ticket codes
185
+ * @param {string} fishyCode - the suspect ticket code
186
+ * @returns {boolean} true if fishyCode matches all, false otherwise
187
+ */
188
+ function fishyMatchesAll(tickets, fishyCode) {
189
+ try {
190
+ // Need at least 7 chars to have "XXXXXX" + last digit
191
+ if (typeof fishyCode !== 'string' || fishyCode.length < 7)
192
+ return false;
193
+ const fishyCore = fishyCode.slice(-7, -1); // last 6 chars before very last
194
+ return tickets.every(code => {
195
+ if (typeof code !== 'string' || code.length < 7)
196
+ return false;
197
+ const core = code.slice(-7, -1);
198
+ return core === fishyCore;
199
+ });
200
+ }
201
+ catch (e) {
202
+ console.error(e);
203
+ return false;
204
+ }
205
+ }
206
+ // =====================================================================================
158
207
  /**
159
208
  * A stable, human-readable internal ID for a seat within a fixture.
160
209
  * (Not used in the printed/scanned code — just handy elsewhere.)
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@ibiliaze/stringman",
3
- "version": "3.15.0",
3
+ "version": "3.17.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
9
  "pub": "npm publish --access public",
10
- "git": "git add .; git commit -m 'changes'; git tag -a v3.14.1 -m 'v3.14.1'; git push origin v3.14.1; git push",
10
+ "git": "git add .; git commit -m 'changes'; git tag -a 3.17.0 -m '3.17.0'; git push origin 3.17.0; git push",
11
11
  "push": "npm run build; npm run git; npm run pub"
12
12
  },
13
13
  "author": "Ibi Hasanli",
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from './seat';
2
- import { buildTicketCode, getTicketId, validateTicketCode } from './ticket';
2
+ import { buildTicketCode, getTicketId, validateTicketCode, fishyMatchesAll } from './ticket';
3
3
  import { createNumericOrderId } from './order';
4
4
 
5
5
  /**
@@ -282,6 +282,7 @@ export const ticket = {
282
282
  getTicketId,
283
283
  buildTicketCode,
284
284
  validateTicketCode,
285
+ fishyMatchesAll,
285
286
  };
286
287
 
287
288
  export const order = { createNumericOrderId };
package/src/ticket.ts CHANGED
@@ -1,5 +1,25 @@
1
+ // =====================================================================================
2
+ // Ticket Code Utilities — Format: 6-3-4-3-1 (Random • Seat • Date/Time • Fixture • Luhn)
3
+ // Total length = 17 digits (including Luhn). Dashes optional for readability.
4
+ // =====================================================================================
5
+
1
6
  import { createHash, randomInt } from 'node:crypto';
2
7
 
8
+ // ---------- config: block lengths (easy to tweak in one place) ----------
9
+ const CODE_LEN = {
10
+ rnd: 6, // Random block
11
+ seat: 3, // Seat identity (section + row + col hashed to 3 digits)
12
+ date: 4, // Fixture local datetime compacted to 4 digits
13
+ fix: 3, // Fixture/Match ID block
14
+ check: 1, // Luhn check digit
15
+ } as const;
16
+
17
+ const EXPECTED_TOTAL_LEN = CODE_LEN.rnd + CODE_LEN.seat + CODE_LEN.date + CODE_LEN.fix + CODE_LEN.check; // 17
18
+
19
+ // =====================================================================================
20
+ // Helpers
21
+ // =====================================================================================
22
+
3
23
  /**
4
24
  * Left-pad a positive integer with zeros to a fixed width.
5
25
  */
@@ -27,10 +47,10 @@ export const luhn10 = (digits: string): string => {
27
47
  };
28
48
 
29
49
  /**
30
- * Compact, stable 2-digit number for a venue or any short code.
31
- * Uses a small SHA-1 based hash and modulo to fit the requested width.
50
+ * Compact, stable numeric block from a short venue code.
51
+ * Uses the first two bytes of a SHA-1 digest modulo 10^len.
32
52
  */
33
- export const venueBlockNum = (venueCode: string, len = 2) => {
53
+ export const venueBlockNum = (venueCode: string, len = CODE_LEN.fix) => {
34
54
  try {
35
55
  const h = createHash('sha1').update(venueCode.toUpperCase()).digest();
36
56
  const n = ((h[0] << 8) | h[1]) % 10 ** len;
@@ -41,10 +61,9 @@ export const venueBlockNum = (venueCode: string, len = 2) => {
41
61
  };
42
62
 
43
63
  /**
44
- * Compact, stable 2-digit number for a Fixture/Match ID string.
45
- * If you pass your DB fixtureId here, you’ll get a short numeric block.
64
+ * Compact, stable numeric block from a Fixture/Match ID string.
46
65
  */
47
- export const fixtureIdBlockNum = (fixtureId: string, len = 2) => {
66
+ export const fixtureIdBlockNum = (fixtureId: string, len = CODE_LEN.fix) => {
48
67
  try {
49
68
  const h = createHash('sha1').update(fixtureId).digest();
50
69
  const n = ((h[0] << 8) | h[1]) % 10 ** len;
@@ -55,9 +74,9 @@ export const fixtureIdBlockNum = (fixtureId: string, len = 2) => {
55
74
  };
56
75
 
57
76
  /**
58
- * Compact, stable 3-digit number for a specific seat (section + row + col).
77
+ * Compact, stable numeric block for a specific seat (section + row + col).
59
78
  */
60
- export const seatBlockNum = (sectionId: string, row: number, col: number, len = 3) => {
79
+ export const seatBlockNum = (sectionId: string, row: number, col: number, len = CODE_LEN.seat) => {
61
80
  try {
62
81
  const h = createHash('sha1').update(`${sectionId}:${row}:${col}`).digest();
63
82
  const n16 = (h[0] << 8) | h[1];
@@ -69,17 +88,17 @@ export const seatBlockNum = (sectionId: string, row: number, col: number, len =
69
88
  };
70
89
 
71
90
  /**
72
- * 6-digit date/time block derived from local start time.
91
+ * 4-digit date/time block derived from local start time.
73
92
  * Input format expected: "YYYY-MM-DDThh:mm" (local, no 'Z').
74
93
  * Encodes YYMMDDhhmm then reduces modulo 10^len to keep width stable.
75
94
  */
76
- export const fixtureBlockNum = (fixtureLocalIso: string, len = 6) => {
95
+ export const fixtureBlockNum = (fixtureLocalIso: string, len = CODE_LEN.date) => {
77
96
  try {
78
97
  const m = fixtureLocalIso.match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})/);
79
98
  if (!m) throw new Error('Bad fixtureLocalIso');
80
99
  const [_, y, M, d, h, mnt] = m;
81
100
  const compact = Number(`${y.slice(2)}${M}${d}${h}${mnt}`); // e.g. 2510312000
82
- const n = compact % 10 ** len;
101
+ const n = compact % 10 ** len; // now 4 digits by default
83
102
  return pad(n, len);
84
103
  } catch {
85
104
  return ''.padStart(len, '0');
@@ -89,7 +108,7 @@ export const fixtureBlockNum = (fixtureLocalIso: string, len = 6) => {
89
108
  /**
90
109
  * Cryptographically strong random numeric block of the given length.
91
110
  */
92
- export const rndBlockNum = (len = 3) => {
111
+ export const rndBlockNum = (len = CODE_LEN.rnd) => {
93
112
  try {
94
113
  const max = 10 ** len;
95
114
  return pad(randomInt(0, max), len);
@@ -98,13 +117,14 @@ export const rndBlockNum = (len = 3) => {
98
117
  }
99
118
  };
100
119
 
120
+ // =====================================================================================
101
121
  /**
102
122
  * Build a numeric ticket code with blocks ordered to match the picture *in reverse*:
103
- * Random (3) + Sector/Block/Col (3) + Date/Time (6) + Fixture/Match ID (2) + Luhn(1)
123
+ * Random (6) + Seat (3) + Date/Time (4) + Fixture/Match ID (3) + Luhn(1)
104
124
  *
105
- * Default dashed display groups: 3-3-6-2-1
125
+ * Default dashed display groups: 6-3-4-3-1
106
126
  *
107
- * Lengths are chosen to keep the overall size at 15 digits (incl. Luhn),
127
+ * Lengths are chosen to keep the overall size at 17 digits (incl. Luhn),
108
128
  * which plays nicely with scanners and manual entry.
109
129
  */
110
130
  export const buildTicketCodeNumeric = (args: {
@@ -118,43 +138,47 @@ export const buildTicketCodeNumeric = (args: {
118
138
  row: number;
119
139
  /** Seat column/number. */
120
140
  col: number;
121
- /** Optional: your DB fixtureId. If present, becomes the 2-digit “Fixture/Match ID” block. */
141
+ /** Optional: your DB fixtureId. If present, becomes the 3-digit “Fixture/Match ID” block. */
122
142
  fixtureId?: string;
123
- /** Random block length (default 3). */
143
+ /** Optional override for random block length (defaults to 6). */
124
144
  rndLen?: number;
125
145
  /** Show dashes for readability (default true). */
126
146
  dashed?: boolean;
127
147
  }) => {
128
148
  try {
129
- // --- individual blocks ---
130
- const R = rndBlockNum(args.rndLen ?? 3); // Random (3)
131
- const S = seatBlockNum(args.sectionId, args.row, args.col); // Seat (3)
132
- const D = fixtureBlockNum(args.fixtureLocalIso, 6); // Date/Time (6)
133
- // Fixture/Match ID (2): prefer fixtureId; fallback to venue code hash
134
- const F = args.fixtureId ? fixtureIdBlockNum(args.fixtureId, 2) : venueBlockNum(args.venueCode, 2);
149
+ // --- individual blocks (new lengths) ---
150
+ const R = rndBlockNum(6); // Random (6)
151
+ const S = seatBlockNum(args.sectionId, args.row, args.col, CODE_LEN.seat); // Seat (3)
152
+ const D = fixtureBlockNum(args.fixtureLocalIso, CODE_LEN.date); // Date/Time (4)
153
+ // Fixture/Match ID (3): prefer fixtureId; fallback to venue code hash (also 3)
154
+ const F = args.fixtureId
155
+ ? fixtureIdBlockNum(args.fixtureId, CODE_LEN.fix)
156
+ : venueBlockNum(args.venueCode, CODE_LEN.fix);
135
157
 
136
158
  // --- assemble (REVERSED order vs the picture’s left-to-right) ---
137
- const body = `${R}${S}${D}${F}`; // 3+3+6+2 = 14
159
+ const body = `${R}${S}${D}${F}`; // 6+3+4+3 = 16
138
160
  const C = luhn10(body); // 1 digit
139
- const numeric = `${body}${C}`; // total 15 digits
161
+ const numeric = `${body}${C}`; // total 17 digits
140
162
 
141
163
  if (args.dashed === false) return numeric;
142
164
 
143
- // Readability grouping for scanners/ushers: 3-3-6-2-1
165
+ // Readability grouping for scanners/ushers: 6-3-4-3-1
144
166
  return `${R}-${S}-${D}-${F}-${C}`;
145
167
  } catch {
146
168
  return '';
147
169
  }
148
170
  };
149
171
 
172
+ // =====================================================================================
150
173
  /**
151
174
  * Validate a code built by buildTicketCodeNumeric (dashes optional).
152
- * Checks: numeric, correct total length (15), and Luhn checksum.
175
+ * Checks: numeric, correct total length (17), and Luhn checksum.
153
176
  */
154
177
  export const validateTicketCodeNumeric = (code: string) => {
155
178
  try {
156
179
  const clean = code.replace(/-/g, '');
157
- if (!/^\d{15}$/.test(clean)) return false; // 3+3+6+2+1
180
+ if (!/^\d+$/.test(clean)) return false;
181
+ if (clean.length !== EXPECTED_TOTAL_LEN) return false; // 17
158
182
  const body = clean.slice(0, -1);
159
183
  const check = clean.slice(-1);
160
184
  return luhn10(body) === check;
@@ -163,6 +187,33 @@ export const validateTicketCodeNumeric = (code: string) => {
163
187
  }
164
188
  };
165
189
 
190
+ /**
191
+ * Check if a fishy ticket's core digits (last 6 before the final digit)
192
+ * match ALL ticket codes in the list.
193
+ *
194
+ * @param {string[]} tickets - list of ticket codes
195
+ * @param {string} fishyCode - the suspect ticket code
196
+ * @returns {boolean} true if fishyCode matches all, false otherwise
197
+ */
198
+ export function fishyMatchesAll(tickets: string[], fishyCode: string): boolean {
199
+ try {
200
+ // Need at least 7 chars to have "XXXXXX" + last digit
201
+ if (typeof fishyCode !== 'string' || fishyCode.length < 7) return false;
202
+
203
+ const fishyCore = fishyCode.slice(-7, -1); // last 6 chars before very last
204
+
205
+ return tickets.every(code => {
206
+ if (typeof code !== 'string' || code.length < 7) return false;
207
+ const core = code.slice(-7, -1);
208
+ return core === fishyCore;
209
+ });
210
+ } catch (e) {
211
+ console.error(e);
212
+ return false;
213
+ }
214
+ }
215
+
216
+ // =====================================================================================
166
217
  /**
167
218
  * A stable, human-readable internal ID for a seat within a fixture.
168
219
  * (Not used in the printed/scanned code — just handy elsewhere.)