@stackedapp/utils 2.0.1 → 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.
@@ -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
- continue; // handled separately if needed
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
- continue; // TODO: handle nested
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;
@@ -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) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackedapp/utils",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Public utilities for Stacked platform SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",