@ibiliaze/stringman 3.14.1 → 3.16.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 +1 -0
- package/dist/ticket.d.ts +63 -4
- package/dist/ticket.js +98 -27
- package/package.json +2 -2
- package/src/ticket.ts +112 -29
package/dist/index.d.ts
CHANGED
package/dist/ticket.d.ts
CHANGED
|
@@ -1,30 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute a Luhn mod-10 check digit for a numeric string.
|
|
3
|
+
* @param digits Numeric string (no separators).
|
|
4
|
+
* @returns Single check digit as a string.
|
|
5
|
+
*/
|
|
1
6
|
export declare const luhn10: (digits: string) => string;
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
/**
|
|
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
|
+
*/
|
|
11
|
+
export declare const venueBlockNum: (venueCode: string, len?: 3) => string;
|
|
12
|
+
/**
|
|
13
|
+
* Compact, stable numeric block from a Fixture/Match ID string.
|
|
14
|
+
*/
|
|
15
|
+
export declare const fixtureIdBlockNum: (fixtureId: string, len?: 3) => string;
|
|
16
|
+
/**
|
|
17
|
+
* Compact, stable numeric block for a specific seat (section + row + col).
|
|
18
|
+
*/
|
|
19
|
+
export declare const seatBlockNum: (sectionId: string, row: number, col: number, len?: 3) => string;
|
|
20
|
+
/**
|
|
21
|
+
* 4-digit date/time block derived from local start time.
|
|
22
|
+
* Input format expected: "YYYY-MM-DDThh:mm" (local, no 'Z').
|
|
23
|
+
* Encodes YYMMDDhhmm then reduces modulo 10^len to keep width stable.
|
|
24
|
+
*/
|
|
25
|
+
export declare const fixtureBlockNum: (fixtureLocalIso: string, len?: 4) => string;
|
|
26
|
+
/**
|
|
27
|
+
* Cryptographically strong random numeric block of the given length.
|
|
28
|
+
*/
|
|
29
|
+
export declare const rndBlockNum: (len?: 6) => string;
|
|
30
|
+
/**
|
|
31
|
+
* Build a numeric ticket code with blocks ordered to match the picture *in reverse*:
|
|
32
|
+
* Random (6) + Seat (3) + Date/Time (4) + Fixture/Match ID (3) + Luhn(1)
|
|
33
|
+
*
|
|
34
|
+
* Default dashed display groups: 6-3-4-3-1
|
|
35
|
+
*
|
|
36
|
+
* Lengths are chosen to keep the overall size at 17 digits (incl. Luhn),
|
|
37
|
+
* which plays nicely with scanners and manual entry.
|
|
38
|
+
*/
|
|
6
39
|
export declare const buildTicketCodeNumeric: (args: {
|
|
40
|
+
/** Human/ops venue short code (e.g. "SB"). Used only if fixtureId is not provided. */
|
|
7
41
|
venueCode: string;
|
|
42
|
+
/** Local fixture start time, "YYYY-MM-DDThh:mm" (no 'Z'). */
|
|
8
43
|
fixtureLocalIso: string;
|
|
44
|
+
/** Section identifier as used in your DB. */
|
|
9
45
|
sectionId: string;
|
|
46
|
+
/** Seat row number. */
|
|
10
47
|
row: number;
|
|
48
|
+
/** Seat column/number. */
|
|
11
49
|
col: number;
|
|
50
|
+
/** Optional: your DB fixtureId. If present, becomes the 3-digit “Fixture/Match ID” block. */
|
|
51
|
+
fixtureId?: string;
|
|
52
|
+
/** Optional override for random block length (defaults to 6). */
|
|
12
53
|
rndLen?: number;
|
|
54
|
+
/** Show dashes for readability (default true). */
|
|
13
55
|
dashed?: boolean;
|
|
14
56
|
}) => string;
|
|
57
|
+
/**
|
|
58
|
+
* Validate a code built by buildTicketCodeNumeric (dashes optional).
|
|
59
|
+
* Checks: numeric, correct total length (17), and Luhn checksum.
|
|
60
|
+
*/
|
|
15
61
|
export declare const validateTicketCodeNumeric: (code: string) => boolean;
|
|
62
|
+
/**
|
|
63
|
+
* A stable, human-readable internal ID for a seat within a fixture.
|
|
64
|
+
* (Not used in the printed/scanned code — just handy elsewhere.)
|
|
65
|
+
*/
|
|
16
66
|
export declare const getTicketId: ({ fixtureId, sectionName, seatName, }: {
|
|
17
67
|
fixtureId: string;
|
|
18
68
|
sectionName: string;
|
|
19
69
|
seatName: string;
|
|
20
70
|
}) => string;
|
|
21
71
|
export declare const buildTicketCode: (args: {
|
|
72
|
+
/** Human/ops venue short code (e.g. "SB"). Used only if fixtureId is not provided. */
|
|
22
73
|
venueCode: string;
|
|
74
|
+
/** Local fixture start time, "YYYY-MM-DDThh:mm" (no 'Z'). */
|
|
23
75
|
fixtureLocalIso: string;
|
|
76
|
+
/** Section identifier as used in your DB. */
|
|
24
77
|
sectionId: string;
|
|
78
|
+
/** Seat row number. */
|
|
25
79
|
row: number;
|
|
80
|
+
/** Seat column/number. */
|
|
26
81
|
col: number;
|
|
82
|
+
/** Optional: your DB fixtureId. If present, becomes the 3-digit “Fixture/Match ID” block. */
|
|
83
|
+
fixtureId?: string;
|
|
84
|
+
/** Optional override for random block length (defaults to 6). */
|
|
27
85
|
rndLen?: number;
|
|
86
|
+
/** Show dashes for readability (default true). */
|
|
28
87
|
dashed?: boolean;
|
|
29
88
|
}) => string;
|
|
30
89
|
export declare const validateTicketCode: (code: string) => boolean;
|
package/dist/ticket.js
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
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
|
-
exports.validateTicketCode = exports.buildTicketCode = exports.getTicketId = exports.validateTicketCodeNumeric = exports.buildTicketCodeNumeric = exports.rndBlockNum = exports.fixtureBlockNum = exports.seatBlockNum = exports.venueBlockNum = exports.luhn10 = void 0;
|
|
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;
|
|
4
8
|
const node_crypto_1 = require("node:crypto");
|
|
5
|
-
// ----------
|
|
9
|
+
// ---------- config: block lengths (easy to tweak in one place) ----------
|
|
10
|
+
const CODE_LEN = {
|
|
11
|
+
rnd: 6, // Random block
|
|
12
|
+
seat: 3, // Seat identity (section + row + col hashed to 3 digits)
|
|
13
|
+
date: 4, // Fixture local datetime compacted to 4 digits
|
|
14
|
+
fix: 3, // Fixture/Match ID block
|
|
15
|
+
check: 1, // Luhn check digit
|
|
16
|
+
};
|
|
17
|
+
const EXPECTED_TOTAL_LEN = CODE_LEN.rnd + CODE_LEN.seat + CODE_LEN.date + CODE_LEN.fix + CODE_LEN.check; // 17
|
|
18
|
+
// =====================================================================================
|
|
19
|
+
// Helpers
|
|
20
|
+
// =====================================================================================
|
|
21
|
+
/**
|
|
22
|
+
* Left-pad a positive integer with zeros to a fixed width.
|
|
23
|
+
*/
|
|
6
24
|
const pad = (n, len) => n.toString().padStart(len, '0');
|
|
7
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Compute a Luhn mod-10 check digit for a numeric string.
|
|
27
|
+
* @param digits Numeric string (no separators).
|
|
28
|
+
* @returns Single check digit as a string.
|
|
29
|
+
*/
|
|
8
30
|
const luhn10 = (digits) => {
|
|
9
31
|
let sum = 0;
|
|
10
32
|
let dbl = false;
|
|
@@ -22,11 +44,13 @@ const luhn10 = (digits) => {
|
|
|
22
44
|
return String(check);
|
|
23
45
|
};
|
|
24
46
|
exports.luhn10 = luhn10;
|
|
25
|
-
|
|
26
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Compact, stable numeric block from a short venue code.
|
|
49
|
+
* Uses the first two bytes of a SHA-1 digest modulo 10^len.
|
|
50
|
+
*/
|
|
51
|
+
const venueBlockNum = (venueCode, len = CODE_LEN.fix) => {
|
|
27
52
|
try {
|
|
28
|
-
|
|
29
|
-
const h = (0, node_crypto_1.createHash)('sha1').update(venueCode.toUpperCase()).digest(); // 20 bytes
|
|
53
|
+
const h = (0, node_crypto_1.createHash)('sha1').update(venueCode.toUpperCase()).digest();
|
|
30
54
|
const n = ((h[0] << 8) | h[1]) % 10 ** len;
|
|
31
55
|
return pad(n, len);
|
|
32
56
|
}
|
|
@@ -35,9 +59,25 @@ const venueBlockNum = (venueCode, len = 2) => {
|
|
|
35
59
|
}
|
|
36
60
|
};
|
|
37
61
|
exports.venueBlockNum = venueBlockNum;
|
|
38
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Compact, stable numeric block from a Fixture/Match ID string.
|
|
64
|
+
*/
|
|
65
|
+
const fixtureIdBlockNum = (fixtureId, len = CODE_LEN.fix) => {
|
|
66
|
+
try {
|
|
67
|
+
const h = (0, node_crypto_1.createHash)('sha1').update(fixtureId).digest();
|
|
68
|
+
const n = ((h[0] << 8) | h[1]) % 10 ** len;
|
|
69
|
+
return pad(n, len);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return ''.padStart(len, '0');
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
exports.fixtureIdBlockNum = fixtureIdBlockNum;
|
|
76
|
+
/**
|
|
77
|
+
* Compact, stable numeric block for a specific seat (section + row + col).
|
|
78
|
+
*/
|
|
79
|
+
const seatBlockNum = (sectionId, row, col, len = CODE_LEN.seat) => {
|
|
39
80
|
try {
|
|
40
|
-
// Hash -> 16-bit -> modulo 10^len
|
|
41
81
|
const h = (0, node_crypto_1.createHash)('sha1').update(`${sectionId}:${row}:${col}`).digest();
|
|
42
82
|
const n16 = (h[0] << 8) | h[1];
|
|
43
83
|
const n = n16 % 10 ** len;
|
|
@@ -48,16 +88,19 @@ const seatBlockNum = (sectionId, row, col, len = 3) => {
|
|
|
48
88
|
}
|
|
49
89
|
};
|
|
50
90
|
exports.seatBlockNum = seatBlockNum;
|
|
51
|
-
|
|
91
|
+
/**
|
|
92
|
+
* 4-digit date/time block derived from local start time.
|
|
93
|
+
* Input format expected: "YYYY-MM-DDThh:mm" (local, no 'Z').
|
|
94
|
+
* Encodes YYMMDDhhmm then reduces modulo 10^len to keep width stable.
|
|
95
|
+
*/
|
|
96
|
+
const fixtureBlockNum = (fixtureLocalIso, len = CODE_LEN.date) => {
|
|
52
97
|
try {
|
|
53
|
-
// Expect "YYYY-MM-DDThh:mm" (local, no Z).
|
|
54
98
|
const m = fixtureLocalIso.match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})/);
|
|
55
99
|
if (!m)
|
|
56
100
|
throw new Error('Bad fixtureLocalIso');
|
|
57
101
|
const [_, y, M, d, h, mnt] = m;
|
|
58
|
-
// YYMMDDhhmm is 10 digits; compress to len digits via mod 10^len.
|
|
59
102
|
const compact = Number(`${y.slice(2)}${M}${d}${h}${mnt}`); // e.g. 2510312000
|
|
60
|
-
const n = compact % 10 ** len;
|
|
103
|
+
const n = compact % 10 ** len; // now 4 digits by default
|
|
61
104
|
return pad(n, len);
|
|
62
105
|
}
|
|
63
106
|
catch {
|
|
@@ -65,9 +108,11 @@ const fixtureBlockNum = (fixtureLocalIso, len = 6) => {
|
|
|
65
108
|
}
|
|
66
109
|
};
|
|
67
110
|
exports.fixtureBlockNum = fixtureBlockNum;
|
|
68
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Cryptographically strong random numeric block of the given length.
|
|
113
|
+
*/
|
|
114
|
+
const rndBlockNum = (len = CODE_LEN.rnd) => {
|
|
69
115
|
try {
|
|
70
|
-
// Uniform cryptographic integer in [0, 10^len)
|
|
71
116
|
const max = 10 ** len;
|
|
72
117
|
return pad((0, node_crypto_1.randomInt)(0, max), len);
|
|
73
118
|
}
|
|
@@ -76,31 +121,52 @@ const rndBlockNum = (len = 3) => {
|
|
|
76
121
|
}
|
|
77
122
|
};
|
|
78
123
|
exports.rndBlockNum = rndBlockNum;
|
|
79
|
-
//
|
|
124
|
+
// =====================================================================================
|
|
125
|
+
/**
|
|
126
|
+
* Build a numeric ticket code with blocks ordered to match the picture *in reverse*:
|
|
127
|
+
* Random (6) + Seat (3) + Date/Time (4) + Fixture/Match ID (3) + Luhn(1)
|
|
128
|
+
*
|
|
129
|
+
* Default dashed display groups: 6-3-4-3-1
|
|
130
|
+
*
|
|
131
|
+
* Lengths are chosen to keep the overall size at 17 digits (incl. Luhn),
|
|
132
|
+
* which plays nicely with scanners and manual entry.
|
|
133
|
+
*/
|
|
80
134
|
const buildTicketCodeNumeric = (args) => {
|
|
81
135
|
try {
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
const S = (0, exports.seatBlockNum)(args.sectionId, args.row, args.col); // 3
|
|
85
|
-
const
|
|
86
|
-
|
|
136
|
+
// --- individual blocks (new lengths) ---
|
|
137
|
+
const R = (0, exports.rndBlockNum)(6); // Random (6)
|
|
138
|
+
const S = (0, exports.seatBlockNum)(args.sectionId, args.row, args.col, CODE_LEN.seat); // Seat (3)
|
|
139
|
+
const D = (0, exports.fixtureBlockNum)(args.fixtureLocalIso, CODE_LEN.date); // Date/Time (4)
|
|
140
|
+
// Fixture/Match ID (3): prefer fixtureId; fallback to venue code hash (also 3)
|
|
141
|
+
const F = args.fixtureId
|
|
142
|
+
? (0, exports.fixtureIdBlockNum)(args.fixtureId, CODE_LEN.fix)
|
|
143
|
+
: (0, exports.venueBlockNum)(args.venueCode, CODE_LEN.fix);
|
|
144
|
+
// --- assemble (REVERSED order vs the picture’s left-to-right) ---
|
|
145
|
+
const body = `${R}${S}${D}${F}`; // 6+3+4+3 = 16
|
|
87
146
|
const C = (0, exports.luhn10)(body); // 1 digit
|
|
88
|
-
const numeric = `${body}${C}`; //
|
|
147
|
+
const numeric = `${body}${C}`; // total 17 digits
|
|
89
148
|
if (args.dashed === false)
|
|
90
149
|
return numeric;
|
|
91
|
-
//
|
|
92
|
-
return `${
|
|
150
|
+
// Readability grouping for scanners/ushers: 6-3-4-3-1
|
|
151
|
+
return `${R}-${S}-${D}-${F}-${C}`;
|
|
93
152
|
}
|
|
94
153
|
catch {
|
|
95
154
|
return '';
|
|
96
155
|
}
|
|
97
156
|
};
|
|
98
157
|
exports.buildTicketCodeNumeric = buildTicketCodeNumeric;
|
|
158
|
+
// =====================================================================================
|
|
159
|
+
/**
|
|
160
|
+
* Validate a code built by buildTicketCodeNumeric (dashes optional).
|
|
161
|
+
* Checks: numeric, correct total length (17), and Luhn checksum.
|
|
162
|
+
*/
|
|
99
163
|
const validateTicketCodeNumeric = (code) => {
|
|
100
164
|
try {
|
|
101
165
|
const clean = code.replace(/-/g, '');
|
|
102
|
-
if (!/^\d
|
|
103
|
-
return false;
|
|
166
|
+
if (!/^\d+$/.test(clean))
|
|
167
|
+
return false;
|
|
168
|
+
if (clean.length !== EXPECTED_TOTAL_LEN)
|
|
169
|
+
return false; // 17
|
|
104
170
|
const body = clean.slice(0, -1);
|
|
105
171
|
const check = clean.slice(-1);
|
|
106
172
|
return (0, exports.luhn10)(body) === check;
|
|
@@ -110,8 +176,13 @@ const validateTicketCodeNumeric = (code) => {
|
|
|
110
176
|
}
|
|
111
177
|
};
|
|
112
178
|
exports.validateTicketCodeNumeric = validateTicketCodeNumeric;
|
|
179
|
+
// =====================================================================================
|
|
180
|
+
/**
|
|
181
|
+
* A stable, human-readable internal ID for a seat within a fixture.
|
|
182
|
+
* (Not used in the printed/scanned code — just handy elsewhere.)
|
|
183
|
+
*/
|
|
113
184
|
const getTicketId = ({ fixtureId, sectionName, seatName, }) => `${sectionName}-${seatName}-${fixtureId}`;
|
|
114
185
|
exports.getTicketId = getTicketId;
|
|
115
|
-
//
|
|
186
|
+
// Compatibility aliases (if other code imports these names already)
|
|
116
187
|
exports.buildTicketCode = exports.buildTicketCodeNumeric;
|
|
117
188
|
exports.validateTicketCode = exports.validateTicketCodeNumeric;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ibiliaze/stringman",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.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.16.0 -m '3.16.0'; git push origin 3.16.0; git push",
|
|
11
11
|
"push": "npm run build; npm run git; npm run pub"
|
|
12
12
|
},
|
|
13
13
|
"author": "Ibi Hasanli",
|
package/src/ticket.ts
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
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
|
|
|
3
|
-
// ----------
|
|
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
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Left-pad a positive integer with zeros to a fixed width.
|
|
25
|
+
*/
|
|
4
26
|
const pad = (n: number, len: number) => n.toString().padStart(len, '0');
|
|
5
27
|
|
|
6
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Compute a Luhn mod-10 check digit for a numeric string.
|
|
30
|
+
* @param digits Numeric string (no separators).
|
|
31
|
+
* @returns Single check digit as a string.
|
|
32
|
+
*/
|
|
7
33
|
export const luhn10 = (digits: string): string => {
|
|
8
34
|
let sum = 0;
|
|
9
35
|
let dbl = false;
|
|
@@ -20,11 +46,13 @@ export const luhn10 = (digits: string): string => {
|
|
|
20
46
|
return String(check);
|
|
21
47
|
};
|
|
22
48
|
|
|
23
|
-
|
|
24
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Compact, stable numeric block from a short venue code.
|
|
51
|
+
* Uses the first two bytes of a SHA-1 digest modulo 10^len.
|
|
52
|
+
*/
|
|
53
|
+
export const venueBlockNum = (venueCode: string, len = CODE_LEN.fix) => {
|
|
25
54
|
try {
|
|
26
|
-
|
|
27
|
-
const h = createHash('sha1').update(venueCode.toUpperCase()).digest(); // 20 bytes
|
|
55
|
+
const h = createHash('sha1').update(venueCode.toUpperCase()).digest();
|
|
28
56
|
const n = ((h[0] << 8) | h[1]) % 10 ** len;
|
|
29
57
|
return pad(n, len);
|
|
30
58
|
} catch {
|
|
@@ -32,9 +60,24 @@ export const venueBlockNum = (venueCode: string, len = 2) => {
|
|
|
32
60
|
}
|
|
33
61
|
};
|
|
34
62
|
|
|
35
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Compact, stable numeric block from a Fixture/Match ID string.
|
|
65
|
+
*/
|
|
66
|
+
export const fixtureIdBlockNum = (fixtureId: string, len = CODE_LEN.fix) => {
|
|
67
|
+
try {
|
|
68
|
+
const h = createHash('sha1').update(fixtureId).digest();
|
|
69
|
+
const n = ((h[0] << 8) | h[1]) % 10 ** len;
|
|
70
|
+
return pad(n, len);
|
|
71
|
+
} catch {
|
|
72
|
+
return ''.padStart(len, '0');
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Compact, stable numeric block for a specific seat (section + row + col).
|
|
78
|
+
*/
|
|
79
|
+
export const seatBlockNum = (sectionId: string, row: number, col: number, len = CODE_LEN.seat) => {
|
|
36
80
|
try {
|
|
37
|
-
// Hash -> 16-bit -> modulo 10^len
|
|
38
81
|
const h = createHash('sha1').update(`${sectionId}:${row}:${col}`).digest();
|
|
39
82
|
const n16 = (h[0] << 8) | h[1];
|
|
40
83
|
const n = n16 % 10 ** len;
|
|
@@ -44,24 +87,29 @@ export const seatBlockNum = (sectionId: string, row: number, col: number, len =
|
|
|
44
87
|
}
|
|
45
88
|
};
|
|
46
89
|
|
|
47
|
-
|
|
90
|
+
/**
|
|
91
|
+
* 4-digit date/time block derived from local start time.
|
|
92
|
+
* Input format expected: "YYYY-MM-DDThh:mm" (local, no 'Z').
|
|
93
|
+
* Encodes YYMMDDhhmm then reduces modulo 10^len to keep width stable.
|
|
94
|
+
*/
|
|
95
|
+
export const fixtureBlockNum = (fixtureLocalIso: string, len = CODE_LEN.date) => {
|
|
48
96
|
try {
|
|
49
|
-
// Expect "YYYY-MM-DDThh:mm" (local, no Z).
|
|
50
97
|
const m = fixtureLocalIso.match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})/);
|
|
51
98
|
if (!m) throw new Error('Bad fixtureLocalIso');
|
|
52
99
|
const [_, y, M, d, h, mnt] = m;
|
|
53
|
-
// YYMMDDhhmm is 10 digits; compress to len digits via mod 10^len.
|
|
54
100
|
const compact = Number(`${y.slice(2)}${M}${d}${h}${mnt}`); // e.g. 2510312000
|
|
55
|
-
const n = compact % 10 ** len;
|
|
101
|
+
const n = compact % 10 ** len; // now 4 digits by default
|
|
56
102
|
return pad(n, len);
|
|
57
103
|
} catch {
|
|
58
104
|
return ''.padStart(len, '0');
|
|
59
105
|
}
|
|
60
106
|
};
|
|
61
107
|
|
|
62
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Cryptographically strong random numeric block of the given length.
|
|
110
|
+
*/
|
|
111
|
+
export const rndBlockNum = (len = CODE_LEN.rnd) => {
|
|
63
112
|
try {
|
|
64
|
-
// Uniform cryptographic integer in [0, 10^len)
|
|
65
113
|
const max = 10 ** len;
|
|
66
114
|
return pad(randomInt(0, max), len);
|
|
67
115
|
} catch {
|
|
@@ -69,38 +117,68 @@ export const rndBlockNum = (len = 3) => {
|
|
|
69
117
|
}
|
|
70
118
|
};
|
|
71
119
|
|
|
72
|
-
//
|
|
120
|
+
// =====================================================================================
|
|
121
|
+
/**
|
|
122
|
+
* Build a numeric ticket code with blocks ordered to match the picture *in reverse*:
|
|
123
|
+
* Random (6) + Seat (3) + Date/Time (4) + Fixture/Match ID (3) + Luhn(1)
|
|
124
|
+
*
|
|
125
|
+
* Default dashed display groups: 6-3-4-3-1
|
|
126
|
+
*
|
|
127
|
+
* Lengths are chosen to keep the overall size at 17 digits (incl. Luhn),
|
|
128
|
+
* which plays nicely with scanners and manual entry.
|
|
129
|
+
*/
|
|
73
130
|
export const buildTicketCodeNumeric = (args: {
|
|
74
|
-
|
|
75
|
-
|
|
131
|
+
/** Human/ops venue short code (e.g. "SB"). Used only if fixtureId is not provided. */
|
|
132
|
+
venueCode: string;
|
|
133
|
+
/** Local fixture start time, "YYYY-MM-DDThh:mm" (no 'Z'). */
|
|
134
|
+
fixtureLocalIso: string;
|
|
135
|
+
/** Section identifier as used in your DB. */
|
|
76
136
|
sectionId: string;
|
|
137
|
+
/** Seat row number. */
|
|
77
138
|
row: number;
|
|
139
|
+
/** Seat column/number. */
|
|
78
140
|
col: number;
|
|
79
|
-
|
|
80
|
-
|
|
141
|
+
/** Optional: your DB fixtureId. If present, becomes the 3-digit “Fixture/Match ID” block. */
|
|
142
|
+
fixtureId?: string;
|
|
143
|
+
/** Optional override for random block length (defaults to 6). */
|
|
144
|
+
rndLen?: number;
|
|
145
|
+
/** Show dashes for readability (default true). */
|
|
146
|
+
dashed?: boolean;
|
|
81
147
|
}) => {
|
|
82
148
|
try {
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const S = seatBlockNum(args.sectionId, args.row, args.col); // 3
|
|
86
|
-
const
|
|
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);
|
|
87
157
|
|
|
88
|
-
|
|
158
|
+
// --- assemble (REVERSED order vs the picture’s left-to-right) ---
|
|
159
|
+
const body = `${R}${S}${D}${F}`; // 6+3+4+3 = 16
|
|
89
160
|
const C = luhn10(body); // 1 digit
|
|
90
|
-
const numeric = `${body}${C}`; //
|
|
161
|
+
const numeric = `${body}${C}`; // total 17 digits
|
|
91
162
|
|
|
92
163
|
if (args.dashed === false) return numeric;
|
|
93
|
-
|
|
94
|
-
|
|
164
|
+
|
|
165
|
+
// Readability grouping for scanners/ushers: 6-3-4-3-1
|
|
166
|
+
return `${R}-${S}-${D}-${F}-${C}`;
|
|
95
167
|
} catch {
|
|
96
168
|
return '';
|
|
97
169
|
}
|
|
98
170
|
};
|
|
99
171
|
|
|
172
|
+
// =====================================================================================
|
|
173
|
+
/**
|
|
174
|
+
* Validate a code built by buildTicketCodeNumeric (dashes optional).
|
|
175
|
+
* Checks: numeric, correct total length (17), and Luhn checksum.
|
|
176
|
+
*/
|
|
100
177
|
export const validateTicketCodeNumeric = (code: string) => {
|
|
101
178
|
try {
|
|
102
179
|
const clean = code.replace(/-/g, '');
|
|
103
|
-
if (!/^\d
|
|
180
|
+
if (!/^\d+$/.test(clean)) return false;
|
|
181
|
+
if (clean.length !== EXPECTED_TOTAL_LEN) return false; // 17
|
|
104
182
|
const body = clean.slice(0, -1);
|
|
105
183
|
const check = clean.slice(-1);
|
|
106
184
|
return luhn10(body) === check;
|
|
@@ -109,6 +187,11 @@ export const validateTicketCodeNumeric = (code: string) => {
|
|
|
109
187
|
}
|
|
110
188
|
};
|
|
111
189
|
|
|
190
|
+
// =====================================================================================
|
|
191
|
+
/**
|
|
192
|
+
* A stable, human-readable internal ID for a seat within a fixture.
|
|
193
|
+
* (Not used in the printed/scanned code — just handy elsewhere.)
|
|
194
|
+
*/
|
|
112
195
|
export const getTicketId = ({
|
|
113
196
|
fixtureId,
|
|
114
197
|
sectionName,
|
|
@@ -119,6 +202,6 @@ export const getTicketId = ({
|
|
|
119
202
|
seatName: string;
|
|
120
203
|
}) => `${sectionName}-${seatName}-${fixtureId}`;
|
|
121
204
|
|
|
122
|
-
//
|
|
205
|
+
// Compatibility aliases (if other code imports these names already)
|
|
123
206
|
export const buildTicketCode = buildTicketCodeNumeric;
|
|
124
207
|
export const validateTicketCode = validateTicketCodeNumeric;
|