@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 +2 -0
- package/dist/index.js +1 -0
- package/dist/ticket.d.ts +27 -19
- package/dist/ticket.js +76 -27
- package/package.json +2 -2
- package/src/index.ts +2 -1
- package/src/ticket.ts +79 -28
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
|
|
9
|
-
* Uses a
|
|
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?:
|
|
11
|
+
export declare const venueBlockNum: (venueCode: string, len?: 3) => string;
|
|
12
12
|
/**
|
|
13
|
-
* Compact, stable
|
|
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?:
|
|
15
|
+
export declare const fixtureIdBlockNum: (fixtureId: string, len?: 3) => string;
|
|
17
16
|
/**
|
|
18
|
-
* Compact, stable
|
|
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?:
|
|
19
|
+
export declare const seatBlockNum: (sectionId: string, row: number, col: number, len?: 3) => string;
|
|
21
20
|
/**
|
|
22
|
-
*
|
|
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?:
|
|
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?:
|
|
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 (
|
|
32
|
+
* Random (6) + Seat (3) + Date/Time (4) + Fixture/Match ID (3) + Luhn(1)
|
|
34
33
|
*
|
|
35
|
-
* Default dashed display groups:
|
|
34
|
+
* Default dashed display groups: 6-3-4-3-1
|
|
36
35
|
*
|
|
37
|
-
* Lengths are chosen to keep the overall size at
|
|
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
|
|
50
|
+
/** Optional: your DB fixtureId. If present, becomes the 3-digit “Fixture/Match ID” block. */
|
|
52
51
|
fixtureId?: string;
|
|
53
|
-
/**
|
|
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 (
|
|
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
|
|
91
|
+
/** Optional: your DB fixtureId. If present, becomes the 3-digit “Fixture/Match ID” block. */
|
|
84
92
|
fixtureId?: string;
|
|
85
|
-
/**
|
|
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
|
|
33
|
-
* Uses a
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
78
|
+
* Compact, stable numeric block for a specific seat (section + row + col).
|
|
63
79
|
*/
|
|
64
|
-
const seatBlockNum = (sectionId, row, col, len =
|
|
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
|
-
*
|
|
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 =
|
|
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 =
|
|
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 (
|
|
128
|
+
* Random (6) + Seat (3) + Date/Time (4) + Fixture/Match ID (3) + Luhn(1)
|
|
112
129
|
*
|
|
113
|
-
* Default dashed display groups:
|
|
130
|
+
* Default dashed display groups: 6-3-4-3-1
|
|
114
131
|
*
|
|
115
|
-
* Lengths are chosen to keep the overall size at
|
|
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)(
|
|
122
|
-
const S = (0, exports.seatBlockNum)(args.sectionId, args.row, args.col); // Seat (3)
|
|
123
|
-
const D = (0, exports.fixtureBlockNum)(args.fixtureLocalIso,
|
|
124
|
-
// Fixture/Match ID (
|
|
125
|
-
const F = args.fixtureId
|
|
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}`; //
|
|
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
|
|
148
|
+
const numeric = `${body}${C}`; // total 17 digits
|
|
130
149
|
if (args.dashed === false)
|
|
131
150
|
return numeric;
|
|
132
|
-
// Readability grouping for scanners/ushers:
|
|
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 (
|
|
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
|
|
148
|
-
return false;
|
|
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.
|
|
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
|
|
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
|
|
31
|
-
* Uses a
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
*
|
|
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 =
|
|
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 =
|
|
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 (
|
|
123
|
+
* Random (6) + Seat (3) + Date/Time (4) + Fixture/Match ID (3) + Luhn(1)
|
|
104
124
|
*
|
|
105
|
-
* Default dashed display groups:
|
|
125
|
+
* Default dashed display groups: 6-3-4-3-1
|
|
106
126
|
*
|
|
107
|
-
* Lengths are chosen to keep the overall size at
|
|
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
|
|
141
|
+
/** Optional: your DB fixtureId. If present, becomes the 3-digit “Fixture/Match ID” block. */
|
|
122
142
|
fixtureId?: string;
|
|
123
|
-
/**
|
|
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(
|
|
131
|
-
const S = seatBlockNum(args.sectionId, args.row, args.col); // Seat (3)
|
|
132
|
-
const D = fixtureBlockNum(args.fixtureLocalIso,
|
|
133
|
-
// Fixture/Match ID (
|
|
134
|
-
const F = args.fixtureId
|
|
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}`; //
|
|
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
|
|
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:
|
|
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 (
|
|
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
|
|
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.)
|