sahab-utils 1.7.4 → 1.7.6

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.
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WHEEL_PRIZE_EXPIRY_DAYS = void 0;
4
+ exports.wheelPrizeExpiryDate = wheelPrizeExpiryDate;
5
+ exports.cycleKeyFor = cycleKeyFor;
6
+ exports.isCampaignLive = isCampaignLive;
7
+ exports.pickWeightedSegment = pickWeightedSegment;
8
+ exports.wheelPrizeDiscount = wheelPrizeDiscount;
9
+ /** Wheel-of-fortune — the shared rules. Pure (no Firestore/clock/ids); the cloud function owns
10
+ * reading the campaign, generating the random draw, and writing the spin + prize atomically. */
11
+ /** A prize is redeemable for this long from the moment it's won, independent of the campaign's own
12
+ * window — a customer who spins on the campaign's last day still gets a fair chance to use it. */
13
+ exports.WHEEL_PRIZE_EXPIRY_DAYS = 14;
14
+ function wheelPrizeExpiryDate(issuedAt) {
15
+ const out = new Date(issuedAt.getTime());
16
+ out.setDate(out.getDate() + exports.WHEEL_PRIZE_EXPIRY_DAYS);
17
+ return out;
18
+ }
19
+ /** Which cycle `at` falls into for a campaign's cadence — the same key twice within one grant
20
+ * period, a different key across it. `'once'` has a single, constant cycle for the campaign's
21
+ * whole life; the doc-id built from this is what makes "already spun this cycle" a direct read. */
22
+ function cycleKeyFor(campaign, at) {
23
+ if (campaign.cadence === 'daily') {
24
+ return at.toISOString().slice(0, 10);
25
+ }
26
+ if (campaign.cadence === 'weekly') {
27
+ return isoWeekKey(at);
28
+ }
29
+ return 'once';
30
+ }
31
+ /** ISO-8601 week key (`YYYY-Www`), Thursday-anchored so the week a year "belongs to" matches the
32
+ * ISO standard rather than the calendar year of Jan 1st. */
33
+ function isoWeekKey(at) {
34
+ const date = new Date(Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate()));
35
+ const dayNum = date.getUTCDay() || 7;
36
+ date.setUTCDate(date.getUTCDate() + 4 - dayNum);
37
+ const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
38
+ const week = Math.ceil(((date.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
39
+ return `${date.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
40
+ }
41
+ /** Whether a campaign is currently offering spins. `validFrom`/`validTo` are passed pre-converted
42
+ * (see this package's `TimestampType` gotcha — converting Firestore timestamps is the caller's job). */
43
+ function isCampaignLive(campaign, at, window) {
44
+ if (campaign.active === false)
45
+ return false;
46
+ if (window.validFrom && at < window.validFrom)
47
+ return false;
48
+ if (window.validTo && at > window.validTo)
49
+ return false;
50
+ return true;
51
+ }
52
+ /** Weighted random pick, given the caller's own `[0,1)` draw so the choice itself stays pure and
53
+ * testable — `Math.random()` is the cloud function's job, never this module's. A segment with
54
+ * `weight` ≤ 0 can never be picked. */
55
+ function pickWeightedSegment(segments, randomUnit) {
56
+ const live = segments.filter((s) => { var _a; return ((_a = s.weight) !== null && _a !== void 0 ? _a : 0) > 0; });
57
+ if (!live.length)
58
+ throw new Error('pickWeightedSegment: no segment has a positive weight');
59
+ const totalWeight = live.reduce((sum, s) => sum + s.weight, 0);
60
+ const target = Math.max(0, Math.min(randomUnit, 0.9999999999)) * totalWeight;
61
+ let cumulative = 0;
62
+ for (const segment of live) {
63
+ cumulative += segment.weight;
64
+ if (target < cumulative)
65
+ return segment;
66
+ }
67
+ return live[live.length - 1];
68
+ }
69
+ /** The prize for a winning spin — an ordinary auto-applied `Discount`, mirroring `giftDiscount`.
70
+ * Hardcoded shape (not passed in) so no call site can misfund it or leave it typeable-in. */
71
+ function wheelPrizeDiscount(campaign, segment, winnerUid, promoCodeId, dates) {
72
+ var _a, _b;
73
+ return {
74
+ promoCodeId,
75
+ amount: segment.amount,
76
+ isPercentage: segment.isPercentage,
77
+ maxAmount: segment.maxAmount,
78
+ clientIds: [winnerUid],
79
+ providerUids: campaign.providerUids,
80
+ allowedOnce: true,
81
+ usedBy: [],
82
+ // Never typed in as a code — the app finds it via `wheel.campaignId`, not a promoCode string.
83
+ isAutoApplied: true,
84
+ createdByProvider: false,
85
+ sahabPercentage: (_a = campaign.sahabPercentage) !== null && _a !== void 0 ? _a : 100,
86
+ providerPercentage: (_b = campaign.providerPercentage) !== null && _b !== void 0 ? _b : 0,
87
+ validFrom: dates.validFrom,
88
+ validTo: dates.validTo,
89
+ wheel: { campaignId: campaign.campaignId, segmentId: segment.segmentId },
90
+ isTest: campaign.isTest,
91
+ };
92
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "sahab-utils",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "description": "This package contains types and utils functions",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
9
  "deploy": "npm run build && npm publish",
10
- "test": "npm run build && node tests/loyalty.test.js && node tests/gifts.test.js && node tests/cashback.test.js && node tests/utils.test.js"
10
+ "test": "npm run build && node tests/loyalty.test.js && node tests/gifts.test.js && node tests/cashback.test.js && node tests/utils.test.js && node tests/wheel.test.js && node tests/calcs.test.js"
11
11
  },
12
12
  "repository": {
13
13
  "type": "git",