@stackedapp/utils 2.0.0 → 2.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.
- package/dist/conditions/helpers.js +6 -1
- package/dist/conditions/index.js +107 -4
- package/dist/template.d.ts +7 -1
- package/dist/template.js +23 -0
- package/package.json +1 -1
|
@@ -48,9 +48,14 @@ const calculateDynamicGroupPercent = (dynamicObj, group) => {
|
|
|
48
48
|
if (!group?.conditions?.length)
|
|
49
49
|
return 100;
|
|
50
50
|
const percentages = group.conditions.map((c) => (0, exports.calculateDynamicConditionPercent)(dynamicObj, c));
|
|
51
|
-
if (!group.links?.length) {
|
|
51
|
+
if (!group.links?.length || group.links.every((l) => l === 'AND')) {
|
|
52
52
|
return percentages.reduce((sum, p) => sum + p, 0) / percentages.length;
|
|
53
53
|
}
|
|
54
|
+
if (group.links.every((l) => l === 'AND NOT')) {
|
|
55
|
+
const adjusted = [percentages[0], ...percentages.slice(1).map((p) => 100 - p)];
|
|
56
|
+
return adjusted.reduce((sum, p) => sum + p, 0) / adjusted.length;
|
|
57
|
+
}
|
|
58
|
+
// Mixed links: fall back to pairwise evaluation
|
|
54
59
|
let result = percentages[0];
|
|
55
60
|
for (let i = 0; i < group.links.length; i++) {
|
|
56
61
|
const nextPct = percentages[i + 1];
|
package/dist/conditions/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.meetsCompletionConditionsBeforeExpiry = exports.meetsCompletionConditions = exports.offerMeetsCompletionConditions = exports.meetsLinkedEntityOffersCondition = exports.hasCompletionConditions = exports.meetsBaseConditions = exports.getMaxClaimsForDynamicGroup = exports.getMaxClaimsForDynamicCondition = exports.meetsDynamicConditions = void 0;
|
|
4
4
|
exports.meetsClaimableConditions = meetsClaimableConditions;
|
|
5
5
|
const handlers_1 = require("./handlers");
|
|
6
|
+
const template_1 = require("../template");
|
|
6
7
|
var helpers_1 = require("./helpers");
|
|
7
8
|
Object.defineProperty(exports, "meetsDynamicConditions", { enumerable: true, get: function () { return helpers_1.meetsDynamicConditions; } });
|
|
8
9
|
Object.defineProperty(exports, "getMaxClaimsForDynamicCondition", { enumerable: true, get: function () { return helpers_1.getMaxClaimsForDynamicCondition; } });
|
|
@@ -76,8 +77,51 @@ const meetsBaseConditions = ({ conditions, playerSnap, addDetails, playerOffer,
|
|
|
76
77
|
shouldScale: false,
|
|
77
78
|
};
|
|
78
79
|
for (const cond of conditions) {
|
|
79
|
-
if (cond.kind === 'nested')
|
|
80
|
-
|
|
80
|
+
if (cond.kind === 'nested') {
|
|
81
|
+
const nested = cond;
|
|
82
|
+
const evalNested = (conds) => (0, exports.meetsBaseConditions)({ conditions: conds, playerSnap, addDetails, playerOffer, additionalData });
|
|
83
|
+
let nestedMet = false;
|
|
84
|
+
let nestedPercent = 0;
|
|
85
|
+
let children;
|
|
86
|
+
if (nested.operator === 'AND') {
|
|
87
|
+
const result = evalNested(nested.conditions);
|
|
88
|
+
nestedMet = result.isValid;
|
|
89
|
+
nestedPercent = result.percentCompleted;
|
|
90
|
+
children = result.conditionData;
|
|
91
|
+
}
|
|
92
|
+
else if (nested.operator === 'OR') {
|
|
93
|
+
let bestPercent = 0;
|
|
94
|
+
const allChildren = [];
|
|
95
|
+
for (const child of nested.conditions) {
|
|
96
|
+
const result = evalNested([child]);
|
|
97
|
+
if (result.isValid)
|
|
98
|
+
nestedMet = true;
|
|
99
|
+
bestPercent = Math.max(bestPercent, result.percentCompleted);
|
|
100
|
+
if (result.conditionData)
|
|
101
|
+
allChildren.push(...result.conditionData);
|
|
102
|
+
}
|
|
103
|
+
nestedPercent = nestedMet ? 100 : bestPercent;
|
|
104
|
+
children = allChildren.length > 0 ? allChildren : undefined;
|
|
105
|
+
}
|
|
106
|
+
else if (nested.operator === 'NOT') {
|
|
107
|
+
const result = evalNested(nested.conditions);
|
|
108
|
+
nestedMet = !result.isValid;
|
|
109
|
+
nestedPercent = nestedMet ? 100 : 0;
|
|
110
|
+
children = result.conditionData;
|
|
111
|
+
}
|
|
112
|
+
if (addDetails) {
|
|
113
|
+
const text = nested.template && children?.length
|
|
114
|
+
? (0, template_1.renderNestedTemplate)(nested.template, children)
|
|
115
|
+
: children?.map((c) => c.text).join(` ${nested.operator} `) || nested.operator;
|
|
116
|
+
conditionData.push({ isMet: nestedMet, kind: 'nested', percentCompleted: nestedPercent, text, operator: nested.operator, children });
|
|
117
|
+
if (!nestedMet)
|
|
118
|
+
isValid = false;
|
|
119
|
+
}
|
|
120
|
+
else if (!nestedMet) {
|
|
121
|
+
return { isValid: false, isComplete: false, percentCompleted: 0 };
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
81
125
|
const result = evaluateBaseCondition(cond, ctx);
|
|
82
126
|
if (!result)
|
|
83
127
|
continue; // not a base kind, skip
|
|
@@ -156,8 +200,54 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
|
|
|
156
200
|
offerCreatedAt: playerOffer?.createdAt,
|
|
157
201
|
};
|
|
158
202
|
for (const cond of completionConditions) {
|
|
159
|
-
if (cond.kind === 'nested')
|
|
160
|
-
|
|
203
|
+
if (cond.kind === 'nested') {
|
|
204
|
+
const nested = cond;
|
|
205
|
+
const evalNested = (conds) => (0, exports.meetsCompletionConditions)({
|
|
206
|
+
completionConditions: conds, completionTrackers, playerSnap, playerOffer,
|
|
207
|
+
addDetails, maxClaimCount, additionalData,
|
|
208
|
+
});
|
|
209
|
+
let nestedMet = false;
|
|
210
|
+
let nestedPercent = 0;
|
|
211
|
+
let children;
|
|
212
|
+
if (nested.operator === 'AND') {
|
|
213
|
+
const result = evalNested(nested.conditions);
|
|
214
|
+
nestedMet = result.isValid;
|
|
215
|
+
nestedPercent = result.percentCompleted;
|
|
216
|
+
children = result.conditionData?.length ? result.conditionData : undefined;
|
|
217
|
+
}
|
|
218
|
+
else if (nested.operator === 'OR') {
|
|
219
|
+
let bestPercent = 0;
|
|
220
|
+
const allChildren = [];
|
|
221
|
+
for (const child of nested.conditions) {
|
|
222
|
+
const result = evalNested([child]);
|
|
223
|
+
if (result.isValid)
|
|
224
|
+
nestedMet = true;
|
|
225
|
+
bestPercent = Math.max(bestPercent, result.percentCompleted);
|
|
226
|
+
if (result.conditionData)
|
|
227
|
+
allChildren.push(...result.conditionData);
|
|
228
|
+
}
|
|
229
|
+
nestedPercent = nestedMet ? 100 : bestPercent;
|
|
230
|
+
children = allChildren.length > 0 ? allChildren : undefined;
|
|
231
|
+
}
|
|
232
|
+
else if (nested.operator === 'NOT') {
|
|
233
|
+
const result = evalNested(nested.conditions);
|
|
234
|
+
nestedMet = !result.isValid;
|
|
235
|
+
nestedPercent = nestedMet ? 100 : 0;
|
|
236
|
+
children = result.conditionData?.length ? result.conditionData : undefined;
|
|
237
|
+
}
|
|
238
|
+
if (addDetails) {
|
|
239
|
+
const text = nested.template && children?.length
|
|
240
|
+
? (0, template_1.renderNestedTemplate)(nested.template, children)
|
|
241
|
+
: children?.map((c) => c.text).join(` ${nested.operator} `) || nested.operator;
|
|
242
|
+
conditionData.push({ isMet: nestedMet, kind: 'nested', percentCompleted: nestedPercent, text, operator: nested.operator, children });
|
|
243
|
+
if (!nestedMet)
|
|
244
|
+
isValid = false;
|
|
245
|
+
}
|
|
246
|
+
else if (!nestedMet) {
|
|
247
|
+
return { isValid: false, isComplete: false, percentCompleted: 0, availableClaimsNow: 0 };
|
|
248
|
+
}
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
161
251
|
// Try completion-only handler first, then base handler
|
|
162
252
|
let result = evaluateCompletionOnlyCondition(cond, ctx);
|
|
163
253
|
if (!result)
|
|
@@ -308,6 +398,19 @@ const meetsCompletionConditionsBeforeExpiry = ({ completionConditions, completio
|
|
|
308
398
|
}
|
|
309
399
|
}
|
|
310
400
|
break;
|
|
401
|
+
case 'nested': {
|
|
402
|
+
const nested = cond;
|
|
403
|
+
const childResult = (0, exports.meetsCompletionConditionsBeforeExpiry)({
|
|
404
|
+
completionConditions: nested.conditions,
|
|
405
|
+
completionTrackers,
|
|
406
|
+
playerSnap,
|
|
407
|
+
playerOffer,
|
|
408
|
+
maxClaimCount,
|
|
409
|
+
});
|
|
410
|
+
if (!childResult)
|
|
411
|
+
return false;
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
311
414
|
}
|
|
312
415
|
}
|
|
313
416
|
return true;
|
package/dist/template.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IDynamicCondition } from '@stackedapp/types';
|
|
1
|
+
import { IDynamicCondition, ConditionDetail } from '@stackedapp/types';
|
|
2
2
|
export declare function extractTemplateKeys(template: string | undefined): Set<string>;
|
|
3
3
|
/**
|
|
4
4
|
* This replaces {keyName} keys from the template with corresponding values from the dynamic object.
|
|
@@ -11,6 +11,12 @@ export declare function renderTemplate(template: string | undefined, dynamic: Re
|
|
|
11
11
|
* eg. a condition high_score_pet-{{surfacerPlayerId}} with high_score_pet-12345
|
|
12
12
|
*/
|
|
13
13
|
export declare function replaceDynamicConditionKey(key: string, trackers: Record<string, any> | undefined): string;
|
|
14
|
+
/**
|
|
15
|
+
* Renders a nested condition template using children's data.
|
|
16
|
+
* Supports {[0].text}, {[0].current}, {[0].goal}, {[0].percent}, {[0].met}
|
|
17
|
+
* where current = trackerAmount, goal = trackerGoal, percent = percentCompleted, met = isMet
|
|
18
|
+
*/
|
|
19
|
+
export declare function renderNestedTemplate(template: string, children: ConditionDetail[]): string;
|
|
14
20
|
/** this replaces all of the dynamic conditions.keys by calling replaceDynamicConditionKey */
|
|
15
21
|
export declare function replaceDynamicConditionKeys(conditions: Array<IDynamicCondition>, trackers: Record<string, any> | undefined): {
|
|
16
22
|
key: string;
|
package/dist/template.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.extractTemplateKeys = extractTemplateKeys;
|
|
4
4
|
exports.renderTemplate = renderTemplate;
|
|
5
5
|
exports.replaceDynamicConditionKey = replaceDynamicConditionKey;
|
|
6
|
+
exports.renderNestedTemplate = renderNestedTemplate;
|
|
6
7
|
exports.replaceDynamicConditionKeys = replaceDynamicConditionKeys;
|
|
7
8
|
const keyPattern = /\{([a-zA-Z_][a-zA-Z0-9_-]*)\}/g;
|
|
8
9
|
// extract {key} placeholders from template string
|
|
@@ -48,6 +49,28 @@ function replaceDynamicConditionKey(key, trackers) {
|
|
|
48
49
|
return value !== undefined ? String(value) : match;
|
|
49
50
|
});
|
|
50
51
|
}
|
|
52
|
+
const nestedKeyPattern = /\{(\[(\d+)\])\.([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
53
|
+
/**
|
|
54
|
+
* Renders a nested condition template using children's data.
|
|
55
|
+
* Supports {[0].text}, {[0].current}, {[0].goal}, {[0].percent}, {[0].met}
|
|
56
|
+
* where current = trackerAmount, goal = trackerGoal, percent = percentCompleted, met = isMet
|
|
57
|
+
*/
|
|
58
|
+
function renderNestedTemplate(template, children) {
|
|
59
|
+
return template.replace(nestedKeyPattern, (_match, _bracket, index, field) => {
|
|
60
|
+
const child = children[Number(index)];
|
|
61
|
+
if (!child)
|
|
62
|
+
return '{?}';
|
|
63
|
+
switch (field) {
|
|
64
|
+
case 'text': return child.text;
|
|
65
|
+
case 'current': return String(child.trackerAmount ?? 0);
|
|
66
|
+
case 'goal': return String(child.trackerGoal ?? 0);
|
|
67
|
+
case 'percent': return String(Math.round(child.percentCompleted));
|
|
68
|
+
case 'met': return child.isMet ? '✓' : '✗';
|
|
69
|
+
case 'kind': return child.kind || '';
|
|
70
|
+
default: return '{?}';
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
51
74
|
/** this replaces all of the dynamic conditions.keys by calling replaceDynamicConditionKey */
|
|
52
75
|
function replaceDynamicConditionKeys(conditions, trackers) {
|
|
53
76
|
return conditions.map((condition) => ({
|