crazy-odds-bet-shared 1.0.42 → 1.0.45
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/bookmakers/specifiers/index.js +11 -0
- package/bookmakers/specifiers/mrbitSpecifiers.js +165 -0
- package/bookmakers/specifiers/specifiersHelperBase.js +173 -0
- package/bookmakers/specifiers/superbetSpecifiers.js +125 -0
- package/bookmakers/specifiers/unibetSpecifiers.js +183 -0
- package/package.json +7 -3
- package/players/index.js +24 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const SpecifiersHelperBase = require('./specifiersHelperBase');
|
|
2
|
+
const SuperbetSpecifiersHelper = require('./superbetSpecifiers');
|
|
3
|
+
const UnibetSpecifiersHelper = require('./unibetSpecifiers');
|
|
4
|
+
const MrBitSpecifiersHelper = require('./mrbitSpecifiers');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
SpecifiersHelperBase,
|
|
8
|
+
SuperbetSpecifiersHelper,
|
|
9
|
+
UnibetSpecifiersHelper,
|
|
10
|
+
MrBitSpecifiersHelper
|
|
11
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const { logger } = require('../../utils');
|
|
2
|
+
const SpecifiersHelperBase = require('./specifiersHelperBase');
|
|
3
|
+
|
|
4
|
+
class MrBitSpecifiersHelper extends SpecifiersHelperBase {
|
|
5
|
+
constructor(bookmaker, fixture, template, marketOutcome, resources) {
|
|
6
|
+
super(bookmaker, fixture, template, marketOutcome, resources);
|
|
7
|
+
this.marketsData = null;
|
|
8
|
+
this.childMarket = null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
getSpecifiers() {
|
|
12
|
+
const specifiers = {};
|
|
13
|
+
|
|
14
|
+
if (this.template?.externalRefs?.[this.bookmaker.slug]?.specifierKeys) {
|
|
15
|
+
for (const [keyGroup, mappings] of Object.entries(this.template.externalRefs[this.bookmaker.slug].specifierKeys)) {
|
|
16
|
+
if (!mappings || typeof mappings !== 'object') {
|
|
17
|
+
logger.warn('Invalid mappings for key group', { keyGroup, mappings });
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
specifiers[keyGroup] = {};
|
|
22
|
+
|
|
23
|
+
for (const [key, path] of Object.entries(mappings)) {
|
|
24
|
+
if (key === 'offset' && keyGroup === 'total') {
|
|
25
|
+
specifiers[keyGroup][key] = path;
|
|
26
|
+
logger.debug('Set total offset specifier', { keyGroup, key, value: path });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (typeof path === 'string') {
|
|
30
|
+
const filterString = 'marketChild:';
|
|
31
|
+
const sourceObject = path.startsWith(filterString) ? this.childMarket : this.marketOutcome;
|
|
32
|
+
const value = this.extractAttributeValue(
|
|
33
|
+
path.startsWith(filterString) ? path.slice(filterString.length) : path,
|
|
34
|
+
sourceObject
|
|
35
|
+
);
|
|
36
|
+
if (value !== undefined) {
|
|
37
|
+
specifiers[keyGroup][key] = value;
|
|
38
|
+
logger.debug('Extracted specifier value', { keyGroup, key, value });
|
|
39
|
+
} else {
|
|
40
|
+
logger.warn('Failed to extract specifier value - path not found in market data', { keyGroup, key, path, marketOutcome: this.marketOutcome });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return specifiers;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async processExternalRefSpecifiers() {
|
|
51
|
+
const externalRefSpecifiers = this.getSpecifiers();
|
|
52
|
+
const templateKeyOrder = this.template?.specifierKeys ? Object.keys(this.template.specifierKeys) : [];
|
|
53
|
+
|
|
54
|
+
const specifiers = {};
|
|
55
|
+
|
|
56
|
+
for (const keyGroup of templateKeyOrder) {
|
|
57
|
+
if (externalRefSpecifiers[keyGroup] === undefined) continue;
|
|
58
|
+
|
|
59
|
+
const properties = externalRefSpecifiers[keyGroup];
|
|
60
|
+
switch (keyGroup) {
|
|
61
|
+
case 'player':
|
|
62
|
+
specifiers[keyGroup] = await this.getPlayer(properties);
|
|
63
|
+
break;
|
|
64
|
+
case 'total':
|
|
65
|
+
specifiers[keyGroup] = this.getTotal(properties);
|
|
66
|
+
break;
|
|
67
|
+
case 'team':
|
|
68
|
+
specifiers[keyGroup] = this.getTeam(properties);
|
|
69
|
+
break;
|
|
70
|
+
case 'quarter':
|
|
71
|
+
specifiers[keyGroup] = this.getQuarter(properties);
|
|
72
|
+
break;
|
|
73
|
+
case 'half':
|
|
74
|
+
specifiers[keyGroup] = this.getHalf(properties);
|
|
75
|
+
break;
|
|
76
|
+
default:
|
|
77
|
+
specifiers[keyGroup] = properties;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return specifiers;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getTotal(externalRef) {
|
|
85
|
+
const { value, offset } = externalRef;
|
|
86
|
+
if (offset !== undefined && offset !== null) {
|
|
87
|
+
const numValue = parseFloat(String(value).replace(/\+/g, '')) || 0;
|
|
88
|
+
const numOffset = parseFloat(offset) || 0;
|
|
89
|
+
return { value: String(numValue + numOffset) };
|
|
90
|
+
}
|
|
91
|
+
return { value: String(value).replace(/\+/g, '') };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async getPlayer(externalRef) {
|
|
95
|
+
const { name: externalFullName, id: externalRawId } = externalRef;
|
|
96
|
+
const externalId = this.extractExternalPlayerId(externalRawId);
|
|
97
|
+
const currentPlayer = await this.getPlayerMapped(externalId, externalFullName);
|
|
98
|
+
|
|
99
|
+
return currentPlayer;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getQuarter(externalRef) {
|
|
103
|
+
const { value } = externalRef;
|
|
104
|
+
return { value: String(this.extractQuarterValue(value)) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getHalf(externalRef) {
|
|
108
|
+
const { value } = externalRef;
|
|
109
|
+
return { value: String(this.extractHalfValue(value)) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
extractAttributeValue(path, sourceData) {
|
|
113
|
+
const parts = path.split('.');
|
|
114
|
+
let current = sourceData;
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < parts.length; i++) {
|
|
117
|
+
const part = parts[i];
|
|
118
|
+
if (current && typeof current === 'object' && part in current) {
|
|
119
|
+
current = current[part];
|
|
120
|
+
} else {
|
|
121
|
+
logger.warn('Path traversal failed - missing property', { path, failedAtPart: part, failedAtIndex: i });
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return current;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
extractExternalPlayerId(externalRawId) {
|
|
130
|
+
const externalCompetitionName = this.marketsData?.champ?.name || '';
|
|
131
|
+
const externalCompetitorId = this.marketOutcome?.competitorId;
|
|
132
|
+
const externalCompetitorName = this.marketsData?.competitors.find((c) => c.id === externalCompetitorId)?.name || '';
|
|
133
|
+
const str = `${externalRawId}-${externalCompetitionName.slice(0, 3)}-${externalCompetitorName}`;
|
|
134
|
+
return str
|
|
135
|
+
.trim()
|
|
136
|
+
.replace(/\s*\(([^)]*)\)\s*/g, '-$1-')
|
|
137
|
+
.replace(/[-]+/g, '-')
|
|
138
|
+
.replace(/^-|-$/g, '')
|
|
139
|
+
.replace(/\s+/g, '-')
|
|
140
|
+
.replace(/\./g, '-')
|
|
141
|
+
.replace(/'/g, '')
|
|
142
|
+
.replace(/[-]+/g, '-')
|
|
143
|
+
.toLowerCase();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
extractQuarterValue(value) {
|
|
147
|
+
if (value.includes('Sfertul primul') || value.includes('Sfertul întâi')) return '1';
|
|
148
|
+
if (value.includes('Sfertul al doilea')) return '2';
|
|
149
|
+
if (value.includes('Sfertul al treilea')) return '3';
|
|
150
|
+
if (value.includes('Sfertul al patrulea')) return '4';
|
|
151
|
+
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
extractHalfValue(value) {
|
|
156
|
+
if (value.includes('Prima repriză')) return '1';
|
|
157
|
+
if (value.includes('A 2-a repriză')) return '2';
|
|
158
|
+
|
|
159
|
+
const s = value.match(/Repriza\s+(\d+)/i);
|
|
160
|
+
|
|
161
|
+
return s ? String(s[1]) : null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = MrBitSpecifiersHelper;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
const { logger } = require('../../utils');
|
|
2
|
+
const PlayersHelper = require('../../players');
|
|
3
|
+
|
|
4
|
+
class SpecifiersHelperBase {
|
|
5
|
+
constructor(bookmaker, fixture, template, marketOutcome, resources) {
|
|
6
|
+
this.bookmaker = bookmaker;
|
|
7
|
+
this.fixture = fixture;
|
|
8
|
+
this.template = template;
|
|
9
|
+
this.marketOutcome = marketOutcome;
|
|
10
|
+
this.resources = resources;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async getPlayerMapped(externalId, externalFullName) {
|
|
14
|
+
const externalFullNameNotEmpty = typeof externalFullName === 'string' && externalFullName.trim().length > 0;
|
|
15
|
+
|
|
16
|
+
if (!externalFullNameNotEmpty) {
|
|
17
|
+
throw new Error(`Player name is empty for ${this.bookmaker.slug} ID: ${externalId}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Preview mode - return placeholder player
|
|
21
|
+
if (this.resources?.preview) {
|
|
22
|
+
return {
|
|
23
|
+
id: '__preview__',
|
|
24
|
+
name: externalFullName || '(preview)',
|
|
25
|
+
externalBookmakerId: String(externalId),
|
|
26
|
+
teamId: null,
|
|
27
|
+
teamName: '(preview — not resolved)',
|
|
28
|
+
teamAbbreviation: '—',
|
|
29
|
+
_preview: true
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let currentPlayer;
|
|
34
|
+
|
|
35
|
+
// Find by external ref id (supports array or single object)
|
|
36
|
+
currentPlayer = this.resources.players.find((p) => {
|
|
37
|
+
const val = p.externalRefs?.[this.bookmaker.slug];
|
|
38
|
+
if (!val) return false;
|
|
39
|
+
const refs = Array.isArray(val) ? val : [val];
|
|
40
|
+
return refs.some((r) => String(r.id) === String(externalId));
|
|
41
|
+
});
|
|
42
|
+
if (currentPlayer) {
|
|
43
|
+
logger.debug('Found player by ID', { bookmaker: this.bookmaker.slug, externalId, playerName: currentPlayer.fullName });
|
|
44
|
+
|
|
45
|
+
return this.mapCurrentPlayer(currentPlayer);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const separator = externalFullName.includes(',') ? ',' : ' ';
|
|
49
|
+
const { firstName, lastName } = PlayersHelper.parseLastFirstName(externalFullName, separator);
|
|
50
|
+
|
|
51
|
+
// Find by name and fixture teams (if available)
|
|
52
|
+
currentPlayer = this.resources.players.find(
|
|
53
|
+
(p) =>
|
|
54
|
+
(p.team._id === this.fixture.homeTeamId || p.team._id === this.fixture.awayTeamId) &&
|
|
55
|
+
((p.firstName === firstName && p.lastName === lastName) || (p.firstName === lastName && p.lastName === firstName))
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (currentPlayer) {
|
|
59
|
+
logger.debug('Found player by name match', { bookmaker: this.bookmaker.slug, externalId, playerName: currentPlayer.fullName });
|
|
60
|
+
await this.addExternalRefToPlayer(currentPlayer, { id: externalId, name: externalFullName });
|
|
61
|
+
return this.mapCurrentPlayer(currentPlayer);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.resources.debugQueue.queuePlayerDebug(externalFullName, externalId);
|
|
65
|
+
|
|
66
|
+
throw new Error(`Player not found for ${this.bookmaker.slug} ID: ${externalId}, name: ${externalFullName}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
mapCurrentPlayer(currentPlayer) {
|
|
70
|
+
if (!currentPlayer.team._id) {
|
|
71
|
+
logger.error('Player team not populated - cannot map player', { playerId: currentPlayer._id, playerName: currentPlayer.fullName });
|
|
72
|
+
throw new Error(`Player team not populated for ${currentPlayer.fullName}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
id: currentPlayer._id,
|
|
77
|
+
name: currentPlayer.fullName,
|
|
78
|
+
teamId: currentPlayer.team._id,
|
|
79
|
+
teamName: currentPlayer.team?.name,
|
|
80
|
+
teamAbbreviation: currentPlayer.team?.metadata?.abbreviation ?? currentPlayer.team?.name.slice(0, 3).toUpperCase()
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async addExternalRefToPlayer(player, playerData) {
|
|
85
|
+
if (this.resources?.preview) return;
|
|
86
|
+
await player.addExternalRef(this.bookmaker.slug, playerData.id, {
|
|
87
|
+
name: playerData.name,
|
|
88
|
+
isActive: true,
|
|
89
|
+
metadata: playerData.metadata || {}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getTeam(externalRef) {
|
|
94
|
+
const home = this.fixture.homeTeam;
|
|
95
|
+
const away = this.fixture.awayTeam;
|
|
96
|
+
if (!home?._id || !away?._id) {
|
|
97
|
+
logger.warn('getTeam: homeTeam or awayTeam not populated on fixture');
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const externalNeedle = externalRef ? String(externalRef.side ?? '').trim() : '';
|
|
102
|
+
if (!externalNeedle) {
|
|
103
|
+
logger.warn('getTeam: empty external team label', { externalRef });
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const homeNames = this.teamMatchNames(home);
|
|
107
|
+
const awayNames = this.teamMatchNames(away);
|
|
108
|
+
|
|
109
|
+
const homeHit = homeNames.some((n) => this.externalTeamLabelMatches(externalNeedle, n));
|
|
110
|
+
if (homeHit) {
|
|
111
|
+
logger.debug('getTeam matched home', { externalNeedle, homeNames });
|
|
112
|
+
return this.mapTeamSpecifier(home, 'home');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const awayHit = awayNames.some((n) => this.externalTeamLabelMatches(externalNeedle, n));
|
|
116
|
+
if (awayHit) {
|
|
117
|
+
logger.debug('getTeam matched away', { externalNeedle, awayNames });
|
|
118
|
+
return this.mapTeamSpecifier(away, 'away');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
logger.warn('getTeam: no home/away match for label', { externalNeedle, homeNames, awayNames });
|
|
122
|
+
if (this.resources?.preview) {
|
|
123
|
+
return {
|
|
124
|
+
id: '__preview__',
|
|
125
|
+
name: externalNeedle,
|
|
126
|
+
abbreviation: '—',
|
|
127
|
+
_preview: true
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getBookmakerTeamRefs(team) {
|
|
134
|
+
if (!team?.externalRefs || typeof team.externalRefs !== 'object') return [];
|
|
135
|
+
const val = team.externalRefs[this.bookmaker.slug];
|
|
136
|
+
if (val == null) return [];
|
|
137
|
+
return Array.isArray(val) ? val : [val];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
teamMatchNames(team) {
|
|
141
|
+
const refs = this.getBookmakerTeamRefs(team);
|
|
142
|
+
const fromRefs = refs.map((r) => String(r?.name ?? '').trim()).filter(Boolean);
|
|
143
|
+
if (fromRefs.length > 0) return fromRefs;
|
|
144
|
+
if (team?.name) return [String(team.name).trim()];
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
externalTeamLabelMatches(externalStr, candidate) {
|
|
149
|
+
const a = String(externalStr ?? '').trim().toLowerCase();
|
|
150
|
+
const b = String(candidate ?? '').trim().toLowerCase();
|
|
151
|
+
if (!a || !b) return false;
|
|
152
|
+
return a.includes(b) || b.includes(a);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
mapTeamSpecifier(teamDoc, side) {
|
|
156
|
+
const name = teamDoc?.name ?? '';
|
|
157
|
+
const abbr =
|
|
158
|
+
teamDoc?.metadata?.abbreviation ??
|
|
159
|
+
(typeof name === 'string' && name.length > 0 ? name.slice(0, 3).toUpperCase() : '');
|
|
160
|
+
return {
|
|
161
|
+
id: teamDoc._id,
|
|
162
|
+
name,
|
|
163
|
+
abbreviation: abbr,
|
|
164
|
+
side
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
startsWithNumber(str) {
|
|
169
|
+
return /^\d/.test(String(str));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = SpecifiersHelperBase;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const { logger } = require('../../utils');
|
|
2
|
+
const { generateSlug } = require('../../models');
|
|
3
|
+
const SpecifiersHelperBase = require('./specifiersHelperBase');
|
|
4
|
+
|
|
5
|
+
class SuperbetSpecifiersHelper extends SpecifiersHelperBase {
|
|
6
|
+
getSpecifiers() {
|
|
7
|
+
const specifiers = {};
|
|
8
|
+
const externalRefSpecifiers = this.template?.externalRefs?.[this.bookmaker.slug]?.specifierKeys;
|
|
9
|
+
|
|
10
|
+
if (externalRefSpecifiers) {
|
|
11
|
+
for (const [keyGroup, mappings] of Object.entries(externalRefSpecifiers)) {
|
|
12
|
+
if (!mappings || typeof mappings !== 'object') {
|
|
13
|
+
logger.warn('Invalid mappings for key group', { keyGroup, mappings });
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
specifiers[keyGroup] = {};
|
|
18
|
+
|
|
19
|
+
for (const [key, path] of Object.entries(mappings)) {
|
|
20
|
+
if (key === 'offset' && keyGroup === 'total') {
|
|
21
|
+
specifiers[keyGroup][key] = path;
|
|
22
|
+
logger.debug('Set total offset specifier', { keyGroup, key, value: path });
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (typeof path === 'string') {
|
|
26
|
+
const value = this.extractAttributeValue(path);
|
|
27
|
+
if (value !== undefined) {
|
|
28
|
+
specifiers[keyGroup][key] = value;
|
|
29
|
+
logger.debug('Extracted specifier value', { keyGroup, key, value });
|
|
30
|
+
} else {
|
|
31
|
+
logger.warn('Failed to extract specifier value - path not found in market data', { keyGroup, key, path, availableKeys: Object.keys(this.marketOutcome) });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return specifiers;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
extractAttributeValue(path) {
|
|
42
|
+
const parts = path.split('.');
|
|
43
|
+
let current = this.marketOutcome;
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < parts.length; i++) {
|
|
46
|
+
const part = parts[i];
|
|
47
|
+
if (current && typeof current === 'object' && part in current) {
|
|
48
|
+
current = current[part];
|
|
49
|
+
} else {
|
|
50
|
+
logger.debug('Path traversal failed - missing property', { path, failedAtPart: part, failedAtIndex: i });
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return current;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async processExternalRefSpecifiers() {
|
|
59
|
+
const externalRefSpecifiers = this.getSpecifiers();
|
|
60
|
+
const templateKeyOrder = this.template?.specifierKeys ? Object.keys(this.template.specifierKeys) : [];
|
|
61
|
+
|
|
62
|
+
const specifiers = {};
|
|
63
|
+
|
|
64
|
+
for (const keyGroup of templateKeyOrder) {
|
|
65
|
+
if (externalRefSpecifiers[keyGroup] === undefined) continue;
|
|
66
|
+
|
|
67
|
+
const properties = externalRefSpecifiers[keyGroup];
|
|
68
|
+
switch (keyGroup) {
|
|
69
|
+
case 'player':
|
|
70
|
+
specifiers[keyGroup] = await this.getPlayer(properties);
|
|
71
|
+
break;
|
|
72
|
+
case 'total':
|
|
73
|
+
specifiers[keyGroup] = this.getTotal(properties);
|
|
74
|
+
break;
|
|
75
|
+
case 'team':
|
|
76
|
+
specifiers[keyGroup] = this.getTeam(properties);
|
|
77
|
+
break;
|
|
78
|
+
case 'quarter':
|
|
79
|
+
specifiers[keyGroup] = this.getQuarter(properties);
|
|
80
|
+
break;
|
|
81
|
+
case 'half':
|
|
82
|
+
specifiers[keyGroup] = this.getHalf(properties);
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
specifiers[keyGroup] = properties;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return specifiers;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getTotal(externalRef) {
|
|
93
|
+
const { value, offset } = externalRef;
|
|
94
|
+
if (offset !== undefined && offset !== null) {
|
|
95
|
+
return { value: String(Number(value) + Number(offset)) };
|
|
96
|
+
}
|
|
97
|
+
return { value: String(value) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async getPlayer(externalRef) {
|
|
101
|
+
const { name: externalFullName, id: externalRawId } = externalRef;
|
|
102
|
+
const externalId = externalRawId ? String(externalRawId) : generateSlug(`auto-generated-${this.fixture.sport.slug}-${externalFullName}`);
|
|
103
|
+
const currentPlayer = await this.getPlayerMapped(externalId, externalFullName);
|
|
104
|
+
|
|
105
|
+
return currentPlayer;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getQuarter(externalRef) {
|
|
109
|
+
const s = String(externalRef?.value ?? '').trim();
|
|
110
|
+
const m = s.match(/Sfertul\s*([0-9])/i);
|
|
111
|
+
if (!m) return { value: null };
|
|
112
|
+
const n = Number(m[1]);
|
|
113
|
+
if (!Number.isFinite(n) || n < 1 || n > 4) return { value: null };
|
|
114
|
+
return { value: String(n) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getHalf(externalRef) {
|
|
118
|
+
const s = String(externalRef?.value ?? '').trim().toLowerCase();
|
|
119
|
+
if (/^prima\b/.test(s)) return { value: '1' };
|
|
120
|
+
if (/^a\s+doua\b/.test(s)) return { value: '2' };
|
|
121
|
+
return { value: null };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = SuperbetSpecifiersHelper;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const { logger } = require('../../utils');
|
|
2
|
+
const SpecifiersHelperBase = require('./specifiersHelperBase');
|
|
3
|
+
|
|
4
|
+
class UnibetSpecifiersHelper extends SpecifiersHelperBase {
|
|
5
|
+
getSpecifiers(market) {
|
|
6
|
+
const specifiers = {};
|
|
7
|
+
const bookmakerSpecifierKeys = this.template?.externalRefs?.[this.bookmaker.slug]?.specifierKeys;
|
|
8
|
+
|
|
9
|
+
if (bookmakerSpecifierKeys) {
|
|
10
|
+
for (const [keyGroup, mappings] of Object.entries(bookmakerSpecifierKeys)) {
|
|
11
|
+
if (!mappings || typeof mappings !== 'object') {
|
|
12
|
+
logger.warn('Invalid mappings for key group', { keyGroup, mappings });
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
specifiers[keyGroup] = {};
|
|
17
|
+
|
|
18
|
+
for (const [key, path] of Object.entries(mappings)) {
|
|
19
|
+
if (key === 'offset' && keyGroup === 'total') {
|
|
20
|
+
specifiers[keyGroup][key] = path;
|
|
21
|
+
logger.debug('Set total offset specifier', { keyGroup, key, value: path });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (typeof path === 'string') {
|
|
25
|
+
const sourceObject = path.startsWith('market:') ? market : this.marketOutcome;
|
|
26
|
+
const value = this.extractAttributeValue(
|
|
27
|
+
path.startsWith('market:') ? path.slice(7) : path,
|
|
28
|
+
sourceObject
|
|
29
|
+
);
|
|
30
|
+
if (value !== undefined) {
|
|
31
|
+
specifiers[keyGroup][key] = value;
|
|
32
|
+
logger.debug('Extracted specifier value', { keyGroup, key, value });
|
|
33
|
+
} else {
|
|
34
|
+
logger.warn('Failed to extract specifier value - path not found in market data', { keyGroup, key, path, marketOutcome: this.marketOutcome });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return specifiers;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
extractAttributeValue(path, sourceData) {
|
|
45
|
+
const parts = path.split('.');
|
|
46
|
+
let current = sourceData;
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < parts.length; i++) {
|
|
49
|
+
const part = parts[i];
|
|
50
|
+
if (current && typeof current === 'object' && part in current) {
|
|
51
|
+
current = current[part];
|
|
52
|
+
} else {
|
|
53
|
+
logger.warn('Path traversal failed - missing property', { path, failedAtPart: part, failedAtIndex: i });
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return current;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async processExternalRefSpecifiers(market, displayType) {
|
|
62
|
+
const externalRefSpecifiers = this.getSpecifiers(market);
|
|
63
|
+
const templateKeyOrder = this.template?.specifierKeys ? Object.keys(this.template.specifierKeys) : [];
|
|
64
|
+
|
|
65
|
+
const specifiers = {};
|
|
66
|
+
|
|
67
|
+
for (const keyGroup of templateKeyOrder) {
|
|
68
|
+
if (externalRefSpecifiers[keyGroup] === undefined) continue;
|
|
69
|
+
|
|
70
|
+
const properties = externalRefSpecifiers[keyGroup];
|
|
71
|
+
switch (keyGroup) {
|
|
72
|
+
case 'player':
|
|
73
|
+
specifiers[keyGroup] = await this.getPlayer(properties, displayType);
|
|
74
|
+
break;
|
|
75
|
+
case 'total':
|
|
76
|
+
specifiers[keyGroup] = this.getTotal(properties, displayType);
|
|
77
|
+
break;
|
|
78
|
+
case 'team':
|
|
79
|
+
specifiers[keyGroup] = this.getTeam(properties);
|
|
80
|
+
break;
|
|
81
|
+
case 'quarter':
|
|
82
|
+
specifiers[keyGroup] = this.getQuarter(properties);
|
|
83
|
+
break;
|
|
84
|
+
case 'half':
|
|
85
|
+
specifiers[keyGroup] = this.getHalf(properties);
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
specifiers[keyGroup] = properties;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return specifiers;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getTotal(externalRef, displayType) {
|
|
96
|
+
const { value: rawValue, offset } = externalRef;
|
|
97
|
+
const value = this.extractTotalValue(rawValue, displayType);
|
|
98
|
+
|
|
99
|
+
if (offset !== undefined && offset !== null) {
|
|
100
|
+
return { value: String(Number(value) + Number(offset)) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { value };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async getPlayer(externalRef, displayType) {
|
|
107
|
+
const { name: externalRawName, id: externalRawId } = externalRef;
|
|
108
|
+
const externalFullName = this.extractExternalPlayerName(externalRawName, displayType);
|
|
109
|
+
const externalId = this.extractExternalPlayerId(externalRawId, displayType);
|
|
110
|
+
const currentPlayer = await this.getPlayerMapped(externalId, externalFullName);
|
|
111
|
+
|
|
112
|
+
return currentPlayer;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getQuarter(externalRef) {
|
|
116
|
+
const { value: rawValue } = externalRef;
|
|
117
|
+
const value = this.extractQuarterValue(rawValue);
|
|
118
|
+
return { value };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getHalf(externalRef) {
|
|
122
|
+
const { value: rawValue } = externalRef;
|
|
123
|
+
const value = this.extractHalfValue(rawValue);
|
|
124
|
+
return { value };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
extractTotalValue(rawValue, displayType) {
|
|
128
|
+
const split = rawValue.split('_');
|
|
129
|
+
switch (displayType) {
|
|
130
|
+
case 'MultiParticipant':
|
|
131
|
+
return split.length === 3 ? split[1] : rawValue;
|
|
132
|
+
default:
|
|
133
|
+
return rawValue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
extractExternalPlayerName(rawName, displayType) {
|
|
138
|
+
const split = rawName.split(' - ');
|
|
139
|
+
const match = rawName.match(/^(.+?)\s+(\d+\+)$/);
|
|
140
|
+
|
|
141
|
+
switch (displayType) {
|
|
142
|
+
case 'OverUnder':
|
|
143
|
+
return split.length === 2 ? split[1] : null;
|
|
144
|
+
case 'MultiParticipant':
|
|
145
|
+
return match ? match[1] : null;
|
|
146
|
+
default:
|
|
147
|
+
return rawName;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
extractExternalPlayerId(rawId, displayType) {
|
|
152
|
+
const split = rawId.split('_');
|
|
153
|
+
const ids = split[0].split(':');
|
|
154
|
+
switch (displayType) {
|
|
155
|
+
case 'OverUnder':
|
|
156
|
+
return ids.length === 2 ? ids[1] : null;
|
|
157
|
+
case 'MultiParticipant':
|
|
158
|
+
return split.length === 3 ? split[0] : null;
|
|
159
|
+
default:
|
|
160
|
+
return rawId;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
extractQuarterValue(rawValue) {
|
|
165
|
+
const split = rawValue.split(':');
|
|
166
|
+
const quarterRaw = split[1];
|
|
167
|
+
if (this.startsWithNumber(quarterRaw)) {
|
|
168
|
+
return String(quarterRaw.slice(0, 1));
|
|
169
|
+
}
|
|
170
|
+
return quarterRaw;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
extractHalfValue(rawValue) {
|
|
174
|
+
const split = rawValue.split(':');
|
|
175
|
+
const halfRaw = split[1];
|
|
176
|
+
if (this.startsWithNumber(halfRaw)) {
|
|
177
|
+
return String(halfRaw.slice(0, 1));
|
|
178
|
+
}
|
|
179
|
+
return halfRaw;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = UnibetSpecifiersHelper;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crazy-odds-bet-shared",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.45",
|
|
4
4
|
"description": "Shared MongoDB models and utilities for odds project",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
@@ -9,14 +9,18 @@
|
|
|
9
9
|
"./models": "./models/index.js",
|
|
10
10
|
"./models/*": "./models/*.js",
|
|
11
11
|
"./utils": "./utils/index.js",
|
|
12
|
-
"./files": "./files/index.js"
|
|
12
|
+
"./files": "./files/index.js",
|
|
13
|
+
"./players": "./players/index.js",
|
|
14
|
+
"./bookmakers/specifiers": "./bookmakers/specifiers/index.js"
|
|
13
15
|
},
|
|
14
16
|
"files": [
|
|
15
17
|
"index.js",
|
|
16
18
|
"db",
|
|
17
19
|
"models",
|
|
18
20
|
"utils",
|
|
19
|
-
"files"
|
|
21
|
+
"files",
|
|
22
|
+
"players",
|
|
23
|
+
"bookmakers"
|
|
20
24
|
],
|
|
21
25
|
"scripts": {
|
|
22
26
|
"lint": "echo \"Lint not configured yet\"",
|
package/players/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class PlayersHelper {
|
|
2
|
+
/**
|
|
3
|
+
* Parse last name and first name from a raw name string.
|
|
4
|
+
* Never returns null: when parsing fails (no separator or empty part), returns a fallback object.
|
|
5
|
+
* @param {string} rawName
|
|
6
|
+
* @param {string} separator
|
|
7
|
+
* @returns {{firstName: string, lastName: string}}
|
|
8
|
+
*/
|
|
9
|
+
static parseLastFirstName(rawName, separator = ',') {
|
|
10
|
+
const trimmed = (rawName || '').trim();
|
|
11
|
+
if (!trimmed) {
|
|
12
|
+
return { firstName: '', lastName: '' };
|
|
13
|
+
}
|
|
14
|
+
if (!trimmed.includes(separator)) {
|
|
15
|
+
return { firstName: trimmed, lastName: '' };
|
|
16
|
+
}
|
|
17
|
+
const parts = trimmed.split(separator).map((s) => s.trim());
|
|
18
|
+
const lastName = parts[0] || '';
|
|
19
|
+
const firstName = parts[1] || '';
|
|
20
|
+
return { firstName, lastName };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = PlayersHelper;
|