shufflecom-calculations 6.0.1 → 6.1.1

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];
@@ -3,6 +3,7 @@ import { Currency } from '../utils/currency';
3
3
  import {
4
4
  MatchStatusParts,
5
5
  Sports,
6
+ SportsBetCashoutButtonState,
6
7
  SportsBetLegType,
7
8
  SportsBetSelectionStatus,
8
9
  SportsBetStatus,
@@ -133,11 +134,38 @@ export interface SportsBetSettlementV3 {
133
134
  createdAt: string;
134
135
  }
135
136
 
137
+ /**
138
+ * @deprecated use SportsBetCashoutInfoV3
139
+ */
136
140
  export interface SportsBetCashoutAvailableV3 {
137
141
  canCashout: boolean;
138
142
  reason: SportsCashoutFailedReason | null;
139
143
  }
140
144
 
145
+ export interface SportsBetCashoutInfoV3 {
146
+ /** Non-null exactly when playerCashoutEnabled is ENABLED. */
147
+ cashoutOddsDecimal: string | null;
148
+ playerCashoutEnabled: SportsBetCashoutButtonState;
149
+ }
150
+
151
+ export interface CashoutSportsBetPayloadV2 {
152
+ id: string; // bs58
153
+ currency: Currency;
154
+ amount: string;
155
+ status: SportsBetStatus;
156
+ settlement: { payoutOddsDecimal: string; payout: string };
157
+ }
158
+
159
+ export interface SportsBetUpdatedPayloadV2 {
160
+ id: string; // bs58
161
+ status: SportsBetStatus;
162
+ settlement: { payout: string; payoutOddsDecimal: string } | null;
163
+ legs: {
164
+ displayStatus: SportsBetSelectionStatus;
165
+ selections: { id: string; status: SportsBetSelectionStatus }[];
166
+ }[];
167
+ }
168
+
141
169
  export interface SportsBetUserV3 {
142
170
  id: string;
143
171
  username: string;
@@ -157,9 +185,19 @@ export interface SportsBetPayloadV3 {
157
185
  updatedAt: string;
158
186
  createdAt: string;
159
187
  actualOddsDecimal: string;
188
+ cashoutInfo: SportsBetCashoutInfoV3;
189
+ /**
190
+ * @deprecated use cashoutInfo.cashoutOddsDecimal
191
+ */
160
192
  cashoutOddsDecimal: string | null;
193
+ /**
194
+ * @deprecated folded into cashoutInfo.playerCashoutEnabled as OUTRIGHT_NOT_ALLOWED_CASHOUT
195
+ */
161
196
  providerCashoutDisabled: boolean;
162
197
  settlement: SportsBetSettlementV3 | null;
198
+ /**
199
+ * @deprecated use cashoutInfo
200
+ */
163
201
  cashoutAvailable: SportsBetCashoutAvailableV3;
164
202
  user: SportsBetUserV3 | null;
165
203
  legs: SportsBetLegV3[];
@@ -97,6 +97,18 @@ export enum SportsCashoutFailedReason {
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
99
  CUSTOM_BET_NOT_ALLOWED_CASHOUT = 'CUSTOM_BET_NOT_ALLOWED_CASHOUT',
100
+ OTC_BET_NOT_ALLOWED_CASHOUT = 'OTC_BET_NOT_ALLOWED_CASHOUT',
101
+ CROSS_PROVIDERS_NOT_ALLOWED_CASHOUT = 'CROSS_PROVIDERS_NOT_ALLOWED_CASHOUT',
102
+ OUTRIGHT_NOT_ALLOWED_CASHOUT = 'OUTRIGHT_NOT_ALLOWED_CASHOUT',
103
+ SPORTS_CASHOUT_DISABLED = 'SPORTS_CASHOUT_DISABLED',
104
+ SPORTS_MAINTENANCE = 'SPORTS_MAINTENANCE',
105
+ CASHOUT_MARGIN_CAP_HIT = 'CASHOUT_MARGIN_CAP_HIT',
106
+ }
107
+
108
+ export enum SportsBetCashoutButtonState {
109
+ ENABLED = 'ENABLED',
110
+ SUSPENDED = 'SUSPENDED',
111
+ HIDDEN = 'HIDDEN',
100
112
  }
101
113
 
102
114
  export enum SportsBetType {
@@ -170,9 +182,7 @@ export interface SportsBetSelectionInterface {
170
182
  status: SportsBetSelectionStatus;
171
183
  oddsNumerator: BigNumber;
172
184
  oddsDenominator: BigNumber;
173
- // Present on persisted selections; used to detect multi-custom-leg bets that a flat selection
174
- // list cannot faithfully represent (see deriveLegsFromSelections).
175
- sportsBetLegId?: string | null;
185
+ sportsBetLegId: string;
176
186
  boostId?: string | null;
177
187
  }
178
188
 
@@ -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
- });