shufflecom-calculations 3.3.18 → 3.3.20

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.
@@ -1,6 +1,6 @@
1
1
  import { Dayjs } from 'dayjs';
2
2
  import { VipBonusIssuanceModel, VipBonusPartial } from './vip-bonus.type';
3
- export declare function getReloadAvailableDate(activationTime: Dayjs | null, cadence: number, index: number, createdAt: Dayjs): Date;
4
- export declare function getReloadExpiryDate(activationTime: Dayjs | null, cadence: number, expiryPerOccurrence: number, index: number, createdAt: Dayjs, firstOccurrenceActivationPeriod: number): Date;
3
+ export declare function getReloadAvailableDate(activationTime: Dayjs | null, cadence: number, index: number, createdAt: Dayjs): Date | null;
4
+ export declare function getReloadExpiryDate(activationTime: Dayjs | null, cadence: number, expiryPerOccurrence: number, index: number, createdAt: Dayjs, firstOccurrenceActivationPeriod: number): Date | null;
5
5
  export declare function deriveBonusIssuances(vipBonus: VipBonusPartial, now?: Dayjs): VipBonusIssuanceModel[];
6
6
  export declare function updateBonusIssuanceStatus(issuance: VipBonusIssuanceModel, vipBonusCancelledAt: Date | null, now: Dayjs): VipBonusIssuanceModel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shufflecom-calculations",
3
- "version": "3.3.18",
3
+ "version": "3.3.20",
4
4
  "description": "",
5
5
  "types": "lib/index.d.ts",
6
6
  "main": "lib/index.js",
@@ -14,5 +14,5 @@
14
14
  },
15
15
  "author": "",
16
16
  "license": "ISC",
17
- "gitHead": "790409f0ad898b64030c8ebb943d0191f91e6626"
17
+ "gitHead": "680a93d4430f249efb6b00ac6a79421a9dcf71a1"
18
18
  }
@@ -1,7 +1,9 @@
1
1
  import BigNumber from 'bignumber.js';
2
- import { calculateSportsPayoutOdds, getSystemBetSubBetSelections } from './calculate-sports-payout-odds';
2
+ import { calculateSportsPayoutOdds, calculateSportsPayoutOddsFromLegs, getSystemBetSubBetSelections } from './calculate-sports-payout-odds';
3
3
  import {
4
4
  OutcomeResult,
5
+ SportsBetLegInterface,
6
+ SportsBetLegType,
5
7
  SportsBetSelectionInterface,
6
8
  SportsBetSelectionStatus,
7
9
  SportsMarketProvider,
@@ -565,6 +567,81 @@ describe('calculateSportsPayoutOdds', () => {
565
567
  });
566
568
  });
567
569
 
570
+ // Multi-of-SGMs: a CUSTOM_BET with more than one CUSTOM leg. These go through
571
+ // calculateSportsPayoutOddsFromLegs with the real legs, so each leg's odds/void/loss is
572
+ // isolated — unlike the flat selection façade, which collapses every selection into one
573
+ // custom leg and would return 1 for the whole ticket on any void.
574
+ describe('multi-SGM (multiple custom legs)', () => {
575
+ it('multiplies each custom leg odds when all legs win', () => {
576
+ const legs: SportsBetLegInterface[] = [
577
+ {
578
+ type: SportsBetLegType.CUSTOM,
579
+ oddsDecimal: new BigNumber(3),
580
+ selections: [createSelection('a1', SportsBetSelectionStatus.WON, 150, 100), createSelection('a2', SportsBetSelectionStatus.WON, 200, 100)],
581
+ },
582
+ {
583
+ type: SportsBetLegType.CUSTOM,
584
+ oddsDecimal: new BigNumber(2),
585
+ selections: [createSelection('b1', SportsBetSelectionStatus.WON, 150, 100), createSelection('b2', SportsBetSelectionStatus.WON, 200, 100)],
586
+ },
587
+ ];
588
+ const marketSelections = [
589
+ createMarketSelection('a1', 150, 100),
590
+ createMarketSelection('a2', 200, 100),
591
+ createMarketSelection('b1', 150, 100),
592
+ createMarketSelection('b2', 200, 100),
593
+ ];
594
+
595
+ const result = calculateSportsPayoutOddsFromLegs(legs, marketSelections, { subBetLegs: null, isEstimation: false });
596
+
597
+ expect(result.toFixed(4)).toBe('6.0000');
598
+ });
599
+
600
+ it('voids only the affected custom leg and keeps the other legs odds', () => {
601
+ const legs: SportsBetLegInterface[] = [
602
+ {
603
+ type: SportsBetLegType.CUSTOM,
604
+ oddsDecimal: new BigNumber(3),
605
+ selections: [createSelection('a1', SportsBetSelectionStatus.WON, 150, 100), createSelection('a2', SportsBetSelectionStatus.WON, 250, 100)],
606
+ },
607
+ {
608
+ type: SportsBetLegType.CUSTOM,
609
+ oddsDecimal: new BigNumber(2),
610
+ selections: [
611
+ createSelection('b1', SportsBetSelectionStatus.WON, 150, 100),
612
+ createSelection('b2', SportsBetSelectionStatus.VOIDED, 200, 100),
613
+ ],
614
+ },
615
+ ];
616
+ const marketSelections = [createMarketSelection('a1', 150, 100), createMarketSelection('a2', 250, 100), createMarketSelection('b1', 150, 100), createMarketSelection('b2', 200, 100)];
617
+
618
+ // Leg B voids (→ odds 1); leg A survives at 3.0. The flat façade would return 1.0 for the whole ticket.
619
+ const result = calculateSportsPayoutOddsFromLegs(legs, marketSelections, { subBetLegs: null, isEstimation: false });
620
+
621
+ expect(result.toFixed(4)).toBe('3.0000');
622
+ });
623
+
624
+ it('returns 0 when a custom leg loses', () => {
625
+ const legs: SportsBetLegInterface[] = [
626
+ {
627
+ type: SportsBetLegType.CUSTOM,
628
+ oddsDecimal: new BigNumber(3),
629
+ selections: [createSelection('a1', SportsBetSelectionStatus.WON, 150, 100)],
630
+ },
631
+ {
632
+ type: SportsBetLegType.CUSTOM,
633
+ oddsDecimal: new BigNumber(2),
634
+ selections: [createSelection('b1', SportsBetSelectionStatus.WON, 150, 100), createSelection('b2', SportsBetSelectionStatus.LOST, 200, 100)],
635
+ },
636
+ ];
637
+ const marketSelections = [createMarketSelection('a1', 150, 100), createMarketSelection('b1', 150, 100), createMarketSelection('b2', 200, 100)];
638
+
639
+ const result = calculateSportsPayoutOddsFromLegs(legs, marketSelections, { subBetLegs: null, isEstimation: false });
640
+
641
+ expect(result.toFixed(4)).toBe('0.0000');
642
+ });
643
+ });
644
+
568
645
  describe('system bet', () => {
569
646
  it('can calculate payout odds correctly', () => {
570
647
  const selections = [
@@ -18,8 +18,6 @@ export const BPS_DIVISOR = 10_000;
18
18
  export const SPORTS_BET_CASHOUT_MARGIN = 500; // in bps
19
19
  export const SPORTS_BET_CASHOUT_PENDING_SELECTION_PENALTY = 300; // in bps
20
20
 
21
-
22
-
23
21
  export const CUSTOM_BET_VOIDED_STATUSES = [
24
22
  SportsBetSelectionStatus.VOIDED,
25
23
  SportsBetSelectionStatus.PROVIDER_VOIDED,
@@ -76,14 +74,13 @@ export function getSystemBetSubBetSelections(
76
74
  selections: SportsBetSelectionInterface[],
77
75
  sportsSystemBetType: SportsSystemBetType,
78
76
  ): SportsBetSelectionInterface[][] {
79
-
80
77
  const subBetSelectionsAndIndex = getSystemBetSubBetSelectionsAndIndex(selections, sportsSystemBetType);
81
78
  return subBetSelectionsAndIndex.map(({ subBetSelection }) => subBetSelection);
82
79
  }
83
80
 
84
81
  // Selection-shaped façade over `calculateSportsPayoutOddsFromLegs`. Builds legs from the input
85
82
  // selections, maps subBetSelections to subBetLegs, and delegates. Callers that already have legs
86
- // should prefer `calculateSportsPayoutOddsFromLegs` directly to skip the bridging cost.
83
+ // deprecated function and is only used for cashout atm, as only regular bet / system bets are allowed for cashouts
87
84
  export function calculateSportsPayoutOdds(
88
85
  selections: SportsBetSelectionInterface[],
89
86
  marketSelections: SportsMarketSelectionInterface[],
@@ -99,10 +96,7 @@ export function calculateSportsPayoutOdds(
99
96
  return calculateSportsPayoutOddsFromLegs(legs, marketSelections, { subBetLegs, isEstimation: opt.isEstimation });
100
97
  }
101
98
 
102
- function mapSubBetSelectionsToLegs(
103
- subBetSelections: SportsBetSelectionInterface[][],
104
- legs: SportsBetLegInterface[],
105
- ): SportsBetLegInterface[][] {
99
+ function mapSubBetSelectionsToLegs(subBetSelections: SportsBetSelectionInterface[][], legs: SportsBetLegInterface[]): SportsBetLegInterface[][] {
106
100
  const legByMarketSelectionId = new Map<string, SportsBetLegInterface>();
107
101
  for (const leg of legs) {
108
102
  for (const selection of leg.selections) {
@@ -196,14 +190,26 @@ function calculateSelectionCashoutOdds(
196
190
  .dividedBy(oddsFractionToDecimal(marketSelection.oddsNumerator, marketSelection.oddsDenominator));
197
191
  }
198
192
 
199
- // Structural mapping from (bet type, selections) → legs. The placement writer and the backfill
200
- // service both consume this so that the leg-shape rule for each bet type lives in one place.
193
+ /**
194
+ * Structural mapping from (bet type, selections) legs.
195
+ *
196
+ * @deprecated Reconstructing legs from a flat selection list only works for regular/system bets and
197
+ * single-custom-leg bets — it cannot represent a multi-custom-leg (multi-SGM) bet and throws for
198
+ * one. Confined to the cashout odds path; anywhere persisted legs are available, pass them to
199
+ * `calculateSportsPayoutOddsFromLegs` instead of calling this.
200
+ */
201
201
  export function deriveLegsFromSelections(
202
202
  sportsBetType: SportsBetType,
203
203
  selections: SportsBetSelectionInterface[],
204
204
  customBetTotalOddsDecimal: BigNumber,
205
205
  ): SportsBetLegInterface[] {
206
206
  if (sportsBetType === SportsBetType.CUSTOM_BET) {
207
+ const distinctLegIds = new Set(selections.map(selection => selection.sportsBetLegId).filter(Boolean));
208
+ if (distinctLegIds.size > 1) {
209
+ throw new Error(
210
+ 'deriveLegsFromSelections cannot reconstruct a multi-custom-leg bet from a flat selection list; use persisted legs with calculateSportsPayoutOddsFromLegs',
211
+ );
212
+ }
207
213
  return [{ type: SportsBetLegType.CUSTOM, oddsDecimal: customBetTotalOddsDecimal, selections }];
208
214
  }
209
215
  return selections.map(selection => ({
@@ -214,6 +220,13 @@ export function deriveLegsFromSelections(
214
220
  }
215
221
 
216
222
  export function getSystemBetSubBetLegs<TLeg>(legs: TLeg[], sportsSystemBetType: SportsSystemBetType): TLeg[][] {
223
+ return getSystemBetSubBetLegsAndIndex(legs, sportsSystemBetType).map(({ subBetLegs }) => subBetLegs);
224
+ }
225
+
226
+ export function getSystemBetSubBetLegsAndIndex<TLeg>(
227
+ legs: TLeg[],
228
+ sportsSystemBetType: SportsSystemBetType,
229
+ ): { subBetLegs: TLeg[]; combinationIndex: string }[] {
217
230
  const systemBetSizes = SportsSystemBetTypeCombinations[sportsSystemBetType];
218
231
  if (!systemBetSizes) {
219
232
  throw new Error('Invalid system bet type');
@@ -222,7 +235,10 @@ export function getSystemBetSubBetLegs<TLeg>(legs: TLeg[], sportsSystemBetType:
222
235
  throw new Error('Invalid selection count');
223
236
  }
224
237
  const combinations = systemBetSizes.flatMap(size => generateSystemBetIndexCombinations(legs.length, size));
225
- return combinations.map(combination => combination.map(index => legs[index]));
238
+ return combinations.map(combination => ({
239
+ subBetLegs: combination.map(index => legs[index]),
240
+ combinationIndex: combination.map(index => index + 1).join('/'),
241
+ }));
226
242
  }
227
243
 
228
244
  export function calculateSportsPayoutOddsFromLegs(
@@ -256,7 +272,10 @@ export function calculateSportsPayoutOddsFromLegs(
256
272
  }
257
273
 
258
274
  return limitTotalOddsDecimalPlaces(
259
- legs.reduce((totalOddsDecimal, leg) => totalOddsDecimal.multipliedBy(getLegPayoutOddsDecimal(leg, marketSelectionMap, opt.isEstimation)), new BigNumber(1)),
275
+ legs.reduce(
276
+ (totalOddsDecimal, leg) => totalOddsDecimal.multipliedBy(getLegPayoutOddsDecimal(leg, marketSelectionMap, opt.isEstimation)),
277
+ new BigNumber(1),
278
+ ),
260
279
  );
261
280
  }
262
281
 
@@ -119,6 +119,7 @@ export interface SportsBetLegV3 {
119
119
  id: string;
120
120
  type: SportsBetLegType;
121
121
  oddsDecimal: string;
122
+ displayStatus: SportsBetSelectionStatus;
122
123
  updatedAt: string;
123
124
  createdAt: string;
124
125
  selections: SportsBetSelectionV3[];
@@ -13,3 +13,7 @@ export class SportsContentFeaturedEvents extends SportsContentBase {
13
13
  pinned: boolean;
14
14
  })[];
15
15
  }
16
+
17
+ export class SportsContentFeaturedOutright extends SportsContentBase {
18
+ fixture: SportsFixtureInfoWithCompetition;
19
+ }
@@ -144,6 +144,9 @@ export interface SportsBetSelectionInterface {
144
144
  status: SportsBetSelectionStatus;
145
145
  oddsNumerator: BigNumber;
146
146
  oddsDenominator: BigNumber;
147
+ // Present on persisted selections; used to detect multi-custom-leg bets that a flat selection
148
+ // list cannot faithfully represent (see deriveLegsFromSelections).
149
+ sportsBetLegId?: string | null;
147
150
  }
148
151
 
149
152
  export enum SportsBetLegType {