shufflecom-calculations 6.0.0 → 6.1.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.
@@ -4,9 +4,7 @@ import {
4
4
  OutcomeResult,
5
5
  SportsBetLegInterface,
6
6
  SportsBetLegType,
7
- SportsBetSelectionInterface,
8
7
  SportsBetSelectionStatus,
9
- SportsBetType,
10
8
  SportsMarketSelectionInterface,
11
9
  } from './sports.types';
12
10
  import { SportsSystemBetType, SportsSystemBetTypeCombinations, SportsSystemBetTypeSelectionCountsToBetCounts } from './system-bet.types';
@@ -46,72 +44,6 @@ export function generateSystemBetIndexCombinations(length: number, size: number)
46
44
  return combinations;
47
45
  }
48
46
 
49
- export function getSystemBetSubBetSelectionsAndIndex(
50
- selections: SportsBetSelectionInterface[],
51
- sportsSystemBetType: SportsSystemBetType,
52
- ): { subBetSelection: SportsBetSelectionInterface[]; combinationIndex: string }[] {
53
- const systemBetSizes = SportsSystemBetTypeCombinations[sportsSystemBetType];
54
- if (!systemBetSizes) {
55
- throw new Error('Invalid system bet type');
56
- }
57
- if (!SportsSystemBetTypeSelectionCountsToBetCounts[selections.length]?.[sportsSystemBetType]) {
58
- throw new Error('Invalid selection count');
59
- }
60
- const combinations = systemBetSizes.flatMap(size => generateSystemBetIndexCombinations(selections.length, size));
61
- const combinationSelections = combinations.map(combination => {
62
- return {
63
- subBetSelection: combination.map(index => selections[index]),
64
- combinationIndex: combination.map(index => index + 1).join('/'),
65
- };
66
- });
67
-
68
- return combinationSelections;
69
- }
70
-
71
- export function getSystemBetSubBetSelections(
72
- selections: SportsBetSelectionInterface[],
73
- sportsSystemBetType: SportsSystemBetType,
74
- ): SportsBetSelectionInterface[][] {
75
- const subBetSelectionsAndIndex = getSystemBetSubBetSelectionsAndIndex(selections, sportsSystemBetType);
76
- return subBetSelectionsAndIndex.map(({ subBetSelection }) => subBetSelection);
77
- }
78
-
79
- // Selection-shaped façade over `calculateSportsPayoutOddsFromLegs`. Builds legs from the input
80
- // selections, maps subBetSelections to subBetLegs, and delegates. Callers that already have legs
81
- // deprecated function and is only used for cashout atm, as only regular bet / system bets are allowed for cashouts
82
- export function calculateSportsPayoutOdds(
83
- selections: SportsBetSelectionInterface[],
84
- marketSelections: SportsMarketSelectionInterface[],
85
- opt: {
86
- sportsCustomBetTotalOddsDecimal: BigNumber | null;
87
- subBetSelections: SportsBetSelectionInterface[][] | null;
88
- isEstimation: boolean; // treats PENDING leg as WON
89
- },
90
- ): BigNumber {
91
- const sportsBetType = opt.sportsCustomBetTotalOddsDecimal !== null ? SportsBetType.CUSTOM_BET : SportsBetType.REGULAR;
92
- const legs = deriveLegsFromSelections(sportsBetType, selections, opt.sportsCustomBetTotalOddsDecimal ?? new BigNumber(1));
93
- const subBetLegs = opt.subBetSelections ? mapSubBetSelectionsToLegs(opt.subBetSelections, legs) : null;
94
- return calculateSportsPayoutOddsFromLegs(legs, marketSelections, { subBetLegs, isEstimation: opt.isEstimation });
95
- }
96
-
97
- function mapSubBetSelectionsToLegs(subBetSelections: SportsBetSelectionInterface[][], legs: SportsBetLegInterface[]): SportsBetLegInterface[][] {
98
- const legByMarketSelectionId = new Map<string, SportsBetLegInterface>();
99
- for (const leg of legs) {
100
- for (const selection of leg.selections) {
101
- legByMarketSelectionId.set(selection.marketSelectionId, leg);
102
- }
103
- }
104
- return subBetSelections.map(subBet =>
105
- subBet.map(selection => {
106
- const leg = legByMarketSelectionId.get(selection.marketSelectionId);
107
- if (!leg) {
108
- throw new Error('Sub bet leg not found');
109
- }
110
- return leg;
111
- }),
112
- );
113
- }
114
-
115
47
  function calculateSelectionCashoutOdds(
116
48
  totalOddsDecimal: BigNumber,
117
49
  selectionOddsDecimal: BigNumber,
@@ -188,35 +120,6 @@ function calculateSelectionCashoutOdds(
188
120
  .dividedBy(oddsFractionToDecimal(marketSelection.oddsNumerator, marketSelection.oddsDenominator));
189
121
  }
190
122
 
191
- /**
192
- * Structural mapping from (bet type, selections) → legs.
193
- *
194
- * @deprecated Reconstructing legs from a flat selection list only works for regular/system bets and
195
- * single-custom-leg bets — it cannot represent a multi-custom-leg (multi-SGM) bet and throws for
196
- * one. Confined to the cashout odds path; anywhere persisted legs are available, pass them to
197
- * `calculateSportsPayoutOddsFromLegs` instead of calling this.
198
- */
199
- export function deriveLegsFromSelections(
200
- sportsBetType: SportsBetType,
201
- selections: SportsBetSelectionInterface[],
202
- customBetTotalOddsDecimal: BigNumber,
203
- ): SportsBetLegInterface[] {
204
- if (sportsBetType === SportsBetType.CUSTOM_BET) {
205
- const distinctLegIds = new Set(selections.map(selection => selection.sportsBetLegId).filter(Boolean));
206
- if (distinctLegIds.size > 1) {
207
- throw new Error(
208
- 'deriveLegsFromSelections cannot reconstruct a multi-custom-leg bet from a flat selection list; use persisted legs with calculateSportsPayoutOddsFromLegs',
209
- );
210
- }
211
- return [{ type: SportsBetLegType.CUSTOM, oddsDecimal: customBetTotalOddsDecimal, selections }];
212
- }
213
- return selections.map(selection => ({
214
- type: SportsBetLegType.REGULAR,
215
- oddsDecimal: oddsFractionToDecimal(selection.oddsNumerator, selection.oddsDenominator),
216
- selections: [selection],
217
- }));
218
- }
219
-
220
123
  export function getSystemBetSubBetLegs<TLeg>(legs: TLeg[], sportsSystemBetType: SportsSystemBetType): TLeg[][] {
221
124
  return getSystemBetSubBetLegsAndIndex(legs, sportsSystemBetType).map(({ subBetLegs }) => subBetLegs);
222
125
  }
@@ -292,7 +195,7 @@ function getLegPayoutOddsDecimal(
292
195
  return leg.oddsDecimal;
293
196
  }
294
197
 
295
- // REGULAR leg: exactly one selection. Mirrors the per-selection reducer in calculateSportsPayoutOdds.
198
+ // REGULAR leg: exactly one selection, so the leg resolves to that selection's own outcome.
296
199
  const [selection] = leg.selections;
297
200
  const selectionOddsDecimal = oddsFractionToDecimal(selection.oddsNumerator, selection.oddsDenominator);
298
201
  const marketSelection = marketSelectionMap[selection.marketSelectionId];
@@ -96,6 +96,7 @@ export enum SportsCashoutFailedReason {
96
96
  SPORTS_MARKET_CLOSED = 'SPORTS_MARKET_CLOSED',
97
97
  SPORTS_MARKET_CASHOUT_NOT_ENABLED = 'SPORTS_MARKET_CASHOUT_NOT_ENABLED',
98
98
  BOOSTED_BET_NOT_ALLOWED_CASHOUT = 'BOOSTED_BET_NOT_ALLOWED_CASHOUT',
99
+ CUSTOM_BET_NOT_ALLOWED_CASHOUT = 'CUSTOM_BET_NOT_ALLOWED_CASHOUT',
99
100
  }
100
101
 
101
102
  export enum SportsBetType {
@@ -169,9 +170,7 @@ export interface SportsBetSelectionInterface {
169
170
  status: SportsBetSelectionStatus;
170
171
  oddsNumerator: BigNumber;
171
172
  oddsDenominator: BigNumber;
172
- // Present on persisted selections; used to detect multi-custom-leg bets that a flat selection
173
- // list cannot faithfully represent (see deriveLegsFromSelections).
174
- sportsBetLegId?: string | null;
173
+ sportsBetLegId: string;
175
174
  boostId?: string | null;
176
175
  }
177
176
 
@@ -1,137 +0,0 @@
1
- import { FIAT_DOCUMENT_SPECS, FiatDocumentType, compactFiatDocument, formatFiatDocument, isFiatDocumentValid } from '.';
2
-
3
- const { CPF, CURP, CUIL_CUIT } = FiatDocumentType;
4
-
5
- describe('fiat documents', () => {
6
- describe('isFiatDocumentValid', () => {
7
- it('accepts each document formatted or bare', () => {
8
- expect(isFiatDocumentValid(CPF, '123.456.789-09')).toBe(true);
9
- expect(isFiatDocumentValid(CPF, '12345678909')).toBe(true);
10
- expect(isFiatDocumentValid(CUIL_CUIT, '20-32456789-6')).toBe(true);
11
- expect(isFiatDocumentValid(CUIL_CUIT, '20324567896')).toBe(true);
12
- expect(isFiatDocumentValid(CURP, 'BOXW310820HNERXN09')).toBe(true);
13
- });
14
-
15
- it('rejects wrong length, stray characters and bad prefixes', () => {
16
- expect(isFiatDocumentValid(CPF, '1234567890')).toBe(false);
17
- expect(isFiatDocumentValid(CPF, '1234567890a')).toBe(false);
18
- expect(isFiatDocumentValid(CUIL_CUIT, '19-12345678-9')).toBe(false);
19
- expect(isFiatDocumentValid(CUIL_CUIT, '2012345678')).toBe(false);
20
- expect(isFiatDocumentValid(CURP, 'ABCD123456HDFXYZ')).toBe(false);
21
- });
22
-
23
- it("tolerates a document's own separators in any position", () => {
24
- expect(isFiatDocumentValid(CPF, '123-456-789-09')).toBe(true);
25
- expect(isFiatDocumentValid(CUIL_CUIT, '20-3245678-96')).toBe(true);
26
- });
27
-
28
- it('rejects a separator the document does not use — a dotted CUIT is not a CUIT', () => {
29
- expect(isFiatDocumentValid(CUIL_CUIT, '20.32456789.6')).toBe(false);
30
- expect(isFiatDocumentValid(CURP, 'BOXW-310820-HNERXN09')).toBe(false);
31
- });
32
- });
33
-
34
- describe('compactFiatDocument', () => {
35
- it('strips separators and upper-cases, per document column', () => {
36
- expect(compactFiatDocument('cpf', '123.456.789-09')).toBe('12345678909');
37
- expect(compactFiatDocument('cuilCuit', '20-32456789-6')).toBe('20324567896');
38
- expect(compactFiatDocument('curp', 'boxw310820hnerxn09')).toBe('BOXW310820HNERXN09');
39
- });
40
-
41
- it('is idempotent on an already-canonical value', () => {
42
- expect(compactFiatDocument('cpf', '12345678909')).toBe('12345678909');
43
- expect(compactFiatDocument('curp', 'BOXW310820HNERXN09')).toBe('BOXW310820HNERXN09');
44
- });
45
-
46
- it('passes a non-document column through untouched', () => {
47
- expect(compactFiatDocument('email', 'Fiat.User@Example.TEST')).toBe('Fiat.User@Example.TEST');
48
- expect(compactFiatDocument('phoneNumber', '+14165550100')).toBe('+14165550100');
49
- });
50
- });
51
-
52
- describe('formatFiatDocument', () => {
53
- it('re-punctuates a stored document into the shape the product spec names', () => {
54
- expect(formatFiatDocument('cpf', '12345678909')).toBe('123.456.789-09');
55
- expect(formatFiatDocument('cuilCuit', '20324567896')).toBe('20-32456789-6');
56
- expect(formatFiatDocument('curp', 'BOXW310820HNERXN09')).toBe('BOXW310820HNERXN09');
57
- });
58
-
59
- it('round-trips against compactFiatDocument and is idempotent', () => {
60
- for (const [column, stored] of [
61
- ['cpf', '12345678909'],
62
- ['cuilCuit', '20324567896'],
63
- ['curp', 'BOXW310820HNERXN09'],
64
- ] as const) {
65
- const formatted = formatFiatDocument(column, stored);
66
- expect(compactFiatDocument(column, formatted)).toBe(stored);
67
- expect(formatFiatDocument(column, formatted)).toBe(formatted);
68
- }
69
- });
70
-
71
- it('passes a non-document column through untouched', () => {
72
- expect(formatFiatDocument('email', 'a@b.test')).toBe('a@b.test');
73
- });
74
- });
75
-
76
- describe('CUIT prefixes', () => {
77
- // AFIP check digit over base '<prefix>32456789', so each case is a genuinely valid CUIT.
78
- const W = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
79
- const validCuitFor = (prefix: string) => {
80
- const base = `${prefix}32456789`;
81
- const sum = [...base].reduce((acc, d, i) => acc + Number(d) * W[i], 0);
82
- const remainder = 11 - (sum % 11);
83
- return base + (remainder === 11 ? 0 : remainder === 10 ? 9 : remainder);
84
- };
85
-
86
- // AFIP: 20 men, 27 women, 23-26 either (when the usual prefix is already taken), 30/33
87
- // legal persons. 34 is not in AFIP's summary but is in stdnum's list and long-standing
88
- // practice, so we keep it — a superset can only fail at the PSP, never lock a user out.
89
- // stdnum's own list omits 25 and 26, which is why we compute the check digit ourselves.
90
- it.each(['20', '23', '24', '25', '26', '27', '30', '33', '34'])('accepts a valid CUIT with the %s prefix', prefix => {
91
- expect(isFiatDocumentValid(CUIL_CUIT, validCuitFor(prefix))).toBe(true);
92
- });
93
-
94
- // An individual's 8 middle digits are their DNI, zero-padded when shorter.
95
- it('accepts a zero-padded DNI', () => {
96
- expect(isFiatDocumentValid(CUIL_CUIT, '20051234562')).toBe(true);
97
- expect(isFiatDocumentValid(CUIL_CUIT, '27000512341')).toBe(true);
98
- });
99
-
100
- it('still rejects a prefix that is not issued at all', () => {
101
- expect(isFiatDocumentValid(CUIL_CUIT, validCuitFor('19'))).toBe(false);
102
- expect(isFiatDocumentValid(CUIL_CUIT, validCuitFor('28'))).toBe(false);
103
- });
104
- });
105
-
106
- describe('issuer checksums', () => {
107
- // Each value below is well-formed but carries wrong check digits — a mistyped document,
108
- // which is exactly what the checksum exists to catch before it reaches the PSP.
109
- it.each([
110
- [CUIL_CUIT, '20-32456789-4'],
111
- [CURP, 'BOXW310820HNERXN08'],
112
- [CPF, '12345678900'],
113
- ])('rejects a well-formed but checksum-invalid %s', (documentType, value) => {
114
- expect(isFiatDocumentValid(documentType, value)).toBe(false);
115
- });
116
-
117
- it('rejects an all-zero DNI, which the CUIT check digit alone would let through', () => {
118
- // 20-00000000-1 and 27-00000000-6 both carry correct check digits.
119
- expect(isFiatDocumentValid(CUIL_CUIT, '20000000001')).toBe(false);
120
- expect(isFiatDocumentValid(CUIL_CUIT, '27000000006')).toBe(false);
121
- // A real DNI that merely starts with zeros still passes.
122
- expect(isFiatDocumentValid(CUIL_CUIT, '27000512341')).toBe(true);
123
- });
124
-
125
- it('rejects every repeated-digit CPF, which mod-11 alone would let through', () => {
126
- for (let digit = 0; digit <= 9; digit++) {
127
- expect(isFiatDocumentValid(CPF, String(digit).repeat(11))).toBe(false);
128
- }
129
- });
130
-
131
- it('keeps a checksum on every document, so a new one is a deliberate omission', () => {
132
- for (const spec of Object.values(FIAT_DOCUMENT_SPECS)) {
133
- expect(typeof spec.checksum).toBe('function');
134
- }
135
- });
136
- });
137
- });