@dappermountain/payload-plugin-achievements 0.2.15 → 0.3.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.
Files changed (41) hide show
  1. package/README.md +25 -10
  2. package/dist/collections/Metrics/index.d.ts.map +1 -1
  3. package/dist/collections/Metrics/index.js +5 -3
  4. package/dist/collections/fields/ruleFields.d.ts.map +1 -1
  5. package/dist/collections/fields/ruleFields.js +2 -48
  6. package/dist/collections/hooks/eachRuleLeaf.d.ts +5 -0
  7. package/dist/collections/hooks/eachRuleLeaf.d.ts.map +1 -1
  8. package/dist/collections/hooks/eachRuleLeaf.js +7 -1
  9. package/dist/collections/hooks/preventCatalogDelete.d.ts.map +1 -1
  10. package/dist/collections/hooks/preventCatalogDelete.js +1 -2
  11. package/dist/endpoints/me.d.ts +2 -1
  12. package/dist/endpoints/me.d.ts.map +1 -1
  13. package/dist/endpoints/me.js +7 -1
  14. package/dist/index.d.ts +3 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2 -1
  17. package/dist/seed/index.d.ts +1 -1
  18. package/dist/seed/index.d.ts.map +1 -1
  19. package/dist/seed/index.js +1 -1
  20. package/dist/services/achievement/buildUnlockRequirementLeaves.d.ts +48 -0
  21. package/dist/services/achievement/buildUnlockRequirementLeaves.d.ts.map +1 -0
  22. package/dist/services/achievement/buildUnlockRequirementLeaves.js +192 -0
  23. package/dist/services/achievement/builtInRules.d.ts.map +1 -1
  24. package/dist/services/achievement/builtInRules.js +0 -41
  25. package/dist/services/achievement/duration.d.ts +1 -0
  26. package/dist/services/achievement/duration.d.ts.map +1 -1
  27. package/dist/services/achievement/duration.js +8 -1
  28. package/dist/services/achievement/getUserProgress.d.ts +7 -0
  29. package/dist/services/achievement/getUserProgress.d.ts.map +1 -1
  30. package/dist/services/achievement/getUserProgress.js +11 -1
  31. package/dist/services/achievement/index.d.ts +5 -0
  32. package/dist/services/achievement/index.d.ts.map +1 -1
  33. package/dist/services/achievement/index.js +2 -0
  34. package/dist/services/achievement/loadUserProgressReviews.d.ts +23 -0
  35. package/dist/services/achievement/loadUserProgressReviews.d.ts.map +1 -0
  36. package/dist/services/achievement/loadUserProgressReviews.js +79 -0
  37. package/dist/services/achievement/validateRules.d.ts.map +1 -1
  38. package/dist/services/achievement/validateRules.js +0 -22
  39. package/dist/types.d.ts +4 -4
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +1 -1
package/README.md CHANGED
@@ -107,7 +107,7 @@ Unlock (tiers), eligibility, and completion (achievements) share the same rule t
107
107
 
108
108
  - Top-level empty list → everyone passes
109
109
  - Nested groups → AND (“all of these”) or OR (“any of these”)
110
- - Built-in leaves → `tier-at-least`, `achievement-complete`, `metric-minimum`, `event-count`, `elapsed-since`
110
+ - Built-in leaves → `tier-at-least`, `achievement-complete`, `metric-minimum`, `event-count`
111
111
  - Custom leaves → register via `extensions.ruleTypes`
112
112
 
113
113
  **Current tier** is computed by walking tiers in `rank` order and evaluating each tier’s unlock rules (`resolveCurrentTier`). Tiers with **`requiresReview`** only become current after an **approved tier request**. Turning **`requiresReview` off** approves pending tier requests and writes missing `tier.changed` logs for members who already meet unlock rules (including those who never had a pending request). **Next-tier fill** uses fractional progress on the same trees (`resolveTierProgress` / `evaluateRuleProgress`):
@@ -117,14 +117,18 @@ Unlock (tiers), eligibility, and completion (achievements) share the same rule t
117
117
  | AND | Equal average of *requirement* children |
118
118
  | OR | Best child (max) |
119
119
  | `tier-at-least` | Gate — skipped in AND averages |
120
- | `event-count` / `metric-minimum` / `elapsed-since` | Fraction of the target |
120
+ | `event-count` / `metric-minimum` | Fraction of the target |
121
121
  | `achievement-complete` | `1` if granted; otherwise rolls up that achievement’s completion (or eligibility) rules |
122
122
 
123
- **`elapsed-since`** — time since an anchor ≥ amount (unit: `days` | `hours` | `minutes`). Built-in anchors: user `createdAt`, or earliest log of an event type (`since: 'first-event'`). Hosts register more anchors via `extensions.metricAnchors` (same keys work on computed metrics).
123
+ **Computed elapsed metrics** — `kind: computed` + `compute: elapsed` + `unit` (`seconds` | `minutes` | `hours` | `days` | `years`) + `since` (built-in `user-created-at` / `first-event`, or a host `metricAnchors` key). Gate tenure with `metric-minimum` against that metric. Years use a fixed 365-day length (not calendar years). System seed only creates stored `points` — product computed metrics (e.g. `days`) are host-seeded after registering anchors.
124
124
 
125
- **Stored vs computed metrics** — `kind: stored` (default) totals live in `achievement-metric-balances`, updated by `recordMetricChange` (log + balance dual-write). Logs remain the audit/rebuild source; reconcile backfills balances. `kind: computed` + `compute: elapsed` exposes a live number (e.g. days since an anchor); use `metric-minimum` against it. System seed only creates stored `points` — product computed metrics (e.g. `days`) are host-seeded after registering anchors.
125
+ **Stored metrics** — totals live in `achievement-metric-balances`, updated by `recordMetricChange` (log + balance dual-write). Logs remain the audit/rebuild source; reconcile backfills balances.
126
126
 
127
- **Leaderboards** — `getMetricLeaderboard` / `GET /api/achievements/leaderboard?metric=points` ranks stored balances (authenticated). Disable with `endpoints.leaderboard: false`.
127
+ **Leaderboards** — `getMetricLeaderboard` / `GET /api/achievements/leaderboard?metric=points` ranks **stored** metric balances (authenticated, optional `scope`). Computed/elapsed metrics (e.g. tenure days) have no balance rows and are rejected — resolve a single user with `resolveMetricValue`, or materialize snapshots if you need a ranked computed board. Disable with `endpoints.leaderboard: false`. Hosts compose member directories (names, current tier, earned counts, who may appear) themselves; the plugin returns `{ user, value, rank }` only.
128
+
129
+ **Unlock checklist** — `buildUnlockRequirementLeaves` walks a rule tree (including Payload leaves that carry empty `rules: []`), evaluates met/progress per leaf, and returns catalog subjects (`relations`, `target`, `unit`, `since`). It does **not** build UI copy — hosts format labels. `achievement-complete` is omitted by default so those stay on catalog groups. `eachRuleLeaf` is also exported if you need the walker alone.
130
+
131
+ **Review snapshot** — `getUserProgress({ include: { reviews: true } })` or `loadUserProgressReviews` adds pending achievement keys and open/rejected tier requests so hosts do not re-query the engine for badge state.
128
132
 
129
133
  **System catalog** — by default the plugin runs `seedAchievementCatalog` on Payload `onInit` (opt out with `seedSystemCatalog: false`). Hosts still seed product catalog + locales via `seedAchievements`.
130
134
 
@@ -186,7 +190,7 @@ achievementPlugin({
186
190
  | `canReview` | _(denied)_ | **Important.** Without this, users cannot manage catalogs or approve requests. Receives `(user, scopeId)` where `scopeId` may be `null`. |
187
191
  | `scope` | _(none)_ | Multi-tenant hook-up. `collection` is the relation target (e.g. `'tenants'`). `relationField` defaults to `'scope'`. Adds an optional scope field across plugin collections. |
188
192
  | `mediaCollection` | _(none)_ | When set (e.g. `'media'`), tiers get an optional upload field for ladder badge images. Icon string keys still work without this. |
189
- | `extensions.metricAnchors` | _(none)_ | Named date resolvers for computed elapsed / `elapsed-since`. Prefer `{ label, resolve }` where `label` is a Payload Admin {@link OptionLabel} (string, `{ en, es }`, or `({ t }) => t('custom:…')`). Not CMS localization. |
193
+ | `extensions.metricAnchors` | _(none)_ | Named date resolvers for computed elapsed metrics. Prefer `{ label, resolve }` where `label` is a Payload Admin {@link OptionLabel} (string, `{ en, es }`, or `({ t }) => t('custom:…')`). Not CMS localization. |
190
194
 
191
195
  ### `collections`
192
196
 
@@ -230,11 +234,11 @@ achievementPlugin({
230
234
 
231
235
  | Option | Default | Notes |
232
236
  | --- | --- | --- |
233
- | `me` | `'/achievements/me'` | Registers `GET /api/achievements/me` for the signed-in user’s grants snapshot (+ derived tiers and lean requests). Pass `false` to skip. Having review access does **not** widen this endpoint to other users. |
234
- | `leaderboard` | `'/achievements/leaderboard'` | Registers `GET /api/achievements/leaderboard?metric=<slug>` for authenticated users (stored metrics only). Pass `false` to skip. |
237
+ | `me` | `'/achievements/me'` | Registers `GET /api/achievements/me` for the signed-in user’s grants snapshot (+ derived tiers and lean requests). Pass `false` to skip. Having review access does **not** widen this endpoint to other users. Optional `?reviews=1` adds `reviews`. |
238
+ | `leaderboard` | `'/achievements/leaderboard'` | Registers `GET /api/achievements/leaderboard?metric=<slug>` for authenticated users (**stored** metrics only). Pass `false` to skip. |
235
239
  | `reconcile` | `'/achievements/reconcile'` | Registers `POST /api/achievements/reconcile` for reviewers. Pass `false` to skip. |
236
240
 
237
- Query params (`me`): `scope`, `limit`, `page`, `requestsLimit`. Leaderboard: `metric` (required), `scope`, `limit`, `page`.
241
+ Query params (`me`): `scope`, `limit`, `page`, `requestsLimit`, `reviews`. Leaderboard: `metric` (required), `scope`, `limit`, `page`.
238
242
 
239
243
  ### `extensions.ruleTypes` / `metricAnchors`
240
244
 
@@ -285,14 +289,25 @@ import {
285
289
  resolveCurrentTier,
286
290
  resolveTierProgress,
287
291
  getUserProgress,
292
+ loadUserProgressReviews,
293
+ buildUnlockRequirementLeaves,
288
294
  submitAchievementRequest,
289
295
  reviewAchievementRequest,
290
296
  } from '@dappermountain/payload-plugin-achievements'
291
297
 
292
298
  await recordLog({ payload, userId, scopeId, type: 'host.custom-kind', actorId })
293
299
  await recordMetricChange({ payload, userId, scopeId, metric: 'points', change: 10 })
300
+ const points = await resolveMetricValue({ payload, userId, scopeId, metric: 'points' })
294
301
  const board = await getMetricLeaderboard({ payload, metric: 'points', scopeId, limit: 20 })
295
302
  const tier = await resolveCurrentTier({ payload, userId, scopeId })
303
+ const progress = await getUserProgress({ payload, userId, scopeId, include: { reviews: true } })
304
+ const unlockLeaves = await buildUnlockRequirementLeaves({
305
+ payload,
306
+ userId,
307
+ scopeId,
308
+ rules: nextTier.unlockRules,
309
+ ladderRank: tier?.rank ?? null,
310
+ })
296
311
 
297
312
  // Repair users whose grants predate side-effect hooks (idempotent)
298
313
  await reconcileProgression({ payload })
@@ -334,7 +349,7 @@ Turning **Requires review** off on a definition or tier **approves pending reque
334
349
 
335
350
  | Goal | Request |
336
351
  | --- | --- |
337
- | Current user snapshot | `GET /api/achievements/me?scope=<scopeId>&limit=10&page=1` |
352
+ | Current user snapshot | `GET /api/achievements/me?scope=<scopeId>&limit=10&page=1` (`&reviews=1` for pending/rejected review keys) |
338
353
  | Stored metric leaderboard | `GET /api/achievements/leaderboard?metric=points&scope=<scopeId>&limit=20&page=1` |
339
354
  | Repair progression | `POST /api/achievements/reconcile` (reviewer; optional `{ userId, scopeId, achievementId\|achievementSlug, tierId\|tierSlug, limit }`) |
340
355
  | List grants | `GET /api/achievement-grants?where[user][equals]=<userId>` |
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Metrics/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAS/C,wBAAgB,sBAAsB,IAAI,gBAAgB,CA+FzD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Metrics/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAS/C,wBAAgB,sBAAsB,IAAI,gBAAgB,CAkGzD"}
@@ -54,13 +54,15 @@ export function buildMetricsCollection() {
54
54
  defaultValue: 'days',
55
55
  enumName: 'ach_metric_unit',
56
56
  options: [
57
- { label: 'Days', value: 'days' },
58
- { label: 'Hours', value: 'hours' },
57
+ { label: 'Seconds', value: 'seconds' },
59
58
  { label: 'Minutes', value: 'minutes' },
59
+ { label: 'Hours', value: 'hours' },
60
+ { label: 'Days', value: 'days' },
61
+ { label: 'Years', value: 'years' },
60
62
  ],
61
63
  admin: {
62
64
  condition: (_, sibling) => sibling?.kind === 'computed' && sibling?.compute === 'elapsed',
63
- description: 'Unit of the exposed number (e.g. whole days since the anchor).',
65
+ description: 'Unit of the exposed number (whole units since the anchor). Years use a fixed 365-day length.',
64
66
  },
65
67
  },
66
68
  {
@@ -1 +1 @@
1
- {"version":3,"file":"ruleFields.d.ts","sourceRoot":"","sources":["../../../src/collections/fields/ruleFields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAqJpC;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,KAAK,EAAE,CAqB7C;AAED,mDAAmD;AACnD,wBAAgB,kBAAkB,IAAI,KAAK,EAAE,CAE5C"}
1
+ {"version":3,"file":"ruleFields.d.ts","sourceRoot":"","sources":["../../../src/collections/fields/ruleFields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAqGpC;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,KAAK,EAAE,CAqB7C;AAED,mDAAmD;AACnD,wBAAgB,kBAAkB,IAAI,KAAK,EAAE,CAE5C"}
@@ -1,6 +1,5 @@
1
1
  import { catalogFilterOptions } from './catalogFilter.js';
2
2
  import { collectionOf } from '../helpers.js';
3
- import { elapsedSinceSelectOptions } from '../../extensions/elapsedSinceOptions.js';
4
3
  function combinatorField(visibleWhenGroup) {
5
4
  return {
6
5
  name: 'combinator',
@@ -26,7 +25,6 @@ function ruleTypeSelect(includeGroup) {
26
25
  { label: 'Achievement complete', value: 'achievement-complete' },
27
26
  { label: 'Metric minimum', value: 'metric-minimum' },
28
27
  { label: 'Event count', value: 'event-count' },
29
- { label: 'Elapsed since', value: 'elapsed-since' },
30
28
  ...(includeGroup ? [{ label: 'Group', value: 'group' }] : []),
31
29
  ],
32
30
  };
@@ -77,9 +75,8 @@ function ruleParamFields() {
77
75
  relationTo: collectionOf('eventTypes'),
78
76
  filterOptions: catalogFilterOptions,
79
77
  admin: {
80
- condition: (_, sibling) => sibling?.type === 'event-count' ||
81
- (sibling?.type === 'elapsed-since' && sibling?.since === 'first-event'),
82
- description: 'Event type to count, or (for elapsed since) the type whose first log starts the clock.',
78
+ condition: (_, sibling) => sibling?.type === 'event-count',
79
+ description: 'Event type to count.',
83
80
  },
84
81
  },
85
82
  {
@@ -90,49 +87,6 @@ function ruleParamFields() {
90
87
  description: 'How many times it needs to have happened.',
91
88
  },
92
89
  },
93
- {
94
- name: 'since',
95
- type: 'select',
96
- // Nested under eligibility/completion/unlock → rules → rules; keep PG enum ≤63 chars.
97
- enumName: 'ach_elapsed_since',
98
- defaultValue: 'user-created-at',
99
- options: elapsedSinceSelectOptions(),
100
- admin: {
101
- condition: (_, sibling) => sibling?.type === 'elapsed-since',
102
- description: 'Which timestamp starts the clock.',
103
- },
104
- },
105
- {
106
- type: 'row',
107
- admin: {
108
- condition: (_, sibling) => sibling?.type === 'elapsed-since',
109
- },
110
- fields: [
111
- {
112
- name: 'amount',
113
- type: 'number',
114
- admin: {
115
- width: '50%',
116
- description: 'How much time must pass.',
117
- },
118
- },
119
- {
120
- name: 'unit',
121
- type: 'select',
122
- enumName: 'ach_elapsed_unit',
123
- defaultValue: 'days',
124
- options: [
125
- { label: 'Days', value: 'days' },
126
- { label: 'Hours', value: 'hours' },
127
- { label: 'Minutes', value: 'minutes' },
128
- ],
129
- admin: {
130
- width: '50%',
131
- description: 'Unit for the amount.',
132
- },
133
- },
134
- ],
135
- },
136
90
  ];
137
91
  }
138
92
  function buildRuleLeafFields() {
@@ -1,3 +1,8 @@
1
1
  export type LeafVisitor = (rule: Record<string, unknown>) => void;
2
+ /**
3
+ * Visit every leaf in a rule tree.
4
+ * Payload array rows often include empty `rules: []` and a default `combinator` even on
5
+ * leaves — those must still be visited (do not treat them as groups).
6
+ */
2
7
  export declare function eachRuleLeaf(node: unknown, visit: LeafVisitor): void;
3
8
  //# sourceMappingURL=eachRuleLeaf.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"eachRuleLeaf.d.ts","sourceRoot":"","sources":["../../../src/collections/hooks/eachRuleLeaf.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;AAEjE,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAgBpE"}
1
+ {"version":3,"file":"eachRuleLeaf.d.ts","sourceRoot":"","sources":["../../../src/collections/hooks/eachRuleLeaf.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;AAEjE;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAiBpE"}
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Visit every leaf in a rule tree.
3
+ * Payload array rows often include empty `rules: []` and a default `combinator` even on
4
+ * leaves — those must still be visited (do not treat them as groups).
5
+ */
1
6
  export function eachRuleLeaf(node, visit) {
2
7
  if (Array.isArray(node)) {
3
8
  for (const item of node)
@@ -11,7 +16,8 @@ export function eachRuleLeaf(node, visit) {
11
16
  eachRuleLeaf(rec.rules, visit);
12
17
  return;
13
18
  }
14
- if (Array.isArray(rec.rules) && (rec.combinator === 'and' || rec.combinator === 'or' || !rec.type)) {
19
+ // Top-level `{ combinator, rules }` with no leaf type
20
+ if (!rec.type && Array.isArray(rec.rules)) {
15
21
  eachRuleLeaf(rec.rules, visit);
16
22
  return;
17
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"preventCatalogDelete.d.ts","sourceRoot":"","sources":["../../../src/collections/hooks/preventCatalogDelete.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAA;AA6CzD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,IAAI,EAAE,WAAW,GAAG,QAAQ,CAAA;CAC7B,GAAG,0BAA0B,CA+D7B"}
1
+ {"version":3,"file":"preventCatalogDelete.d.ts","sourceRoot":"","sources":["../../../src/collections/hooks/preventCatalogDelete.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAA;AA6CzD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,IAAI,EAAE,WAAW,GAAG,QAAQ,CAAA;CAC7B,GAAG,0BAA0B,CA8D7B"}
@@ -61,8 +61,7 @@ export function preventCatalogDelete(args) {
61
61
  parts.push(`${logs.totalDocs} log row(s)`);
62
62
  const rules = await countMatchingLeaves({
63
63
  req,
64
- match: (rule) => (rule.type === 'event-count' || rule.type === 'elapsed-since') &&
65
- relationId(rule.eventType) === id,
64
+ match: (rule) => rule.type === 'event-count' && relationId(rule.eventType) === id,
66
65
  });
67
66
  if (rules > 0)
68
67
  parts.push(`${rules} rule(s)`);
@@ -1,7 +1,8 @@
1
1
  import type { Endpoint } from 'payload';
2
2
  /**
3
3
  * `GET` current user’s snapshot. Body is Payload `find`-shaped for **grants**,
4
- * plus ladder-derived `tiers` and lean `requests`. Optional `?scope=` / `limit` / `page`.
4
+ * plus ladder-derived `tiers` and lean `requests`. Optional `?scope=` / `limit` / `page`
5
+ * / `reviews=1` (pending achievement keys + open/rejected tier requests).
5
6
  */
6
7
  export declare function buildMeEndpoint(path: string): Endpoint;
7
8
  //# sourceMappingURL=me.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../src/endpoints/me.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAkB,MAAM,SAAS,CAAA;AA+BvD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAqBtD"}
1
+ {"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../src/endpoints/me.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAkB,MAAM,SAAS,CAAA;AAoCvD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAsBtD"}
@@ -24,9 +24,14 @@ function pagingFromRequest(req) {
24
24
  requestsLimit: positiveInt(params?.get('requestsLimit') ?? null, 20),
25
25
  };
26
26
  }
27
+ function reviewsFromRequest(req) {
28
+ const raw = requestUrl(req)?.searchParams.get('reviews');
29
+ return raw === '1' || raw === 'true';
30
+ }
27
31
  /**
28
32
  * `GET` current user’s snapshot. Body is Payload `find`-shaped for **grants**,
29
- * plus ladder-derived `tiers` and lean `requests`. Optional `?scope=` / `limit` / `page`.
33
+ * plus ladder-derived `tiers` and lean `requests`. Optional `?scope=` / `limit` / `page`
34
+ * / `reviews=1` (pending achievement keys + open/rejected tier requests).
30
35
  */
31
36
  export function buildMeEndpoint(path) {
32
37
  return {
@@ -43,6 +48,7 @@ export function buildMeEndpoint(path) {
43
48
  userId,
44
49
  scopeId: scopeFromRequest(req),
45
50
  paging: pagingFromRequest(req),
51
+ include: reviewsFromRequest(req) ? { reviews: true } : undefined,
46
52
  });
47
53
  return Response.json(body);
48
54
  },
package/dist/index.d.ts CHANGED
@@ -3,12 +3,13 @@ export type { EngineEventTypeSlug, EngineMetricSlug } from './catalog.js';
3
3
  export { achievementPlugin } from './plugin.js';
4
4
  export { seedAchievementCatalog, seedAchievements } from './seed/index.js';
5
5
  export type { AchievementSeedAchievement, AchievementSeedCatalogRow, AchievementSeedInput, AchievementSeedLocalizedText, AchievementSeedTier, } from './seed/index.js';
6
- export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, getUserProgress, getMetricLeaderboard, grantAchievement, loadMetricDoc, recordEvent, recordLog, recordMetricChange, rebuildMetricBalance, rebuildMetricBalancesForUser, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, resolveElapsedAnchor, resolveMetricValue, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, catalogRef, transitionLogData, transitionToId, validateRuleGroup, validateRuleNode, } from './services/achievement/index.js';
7
- export type { ResolvedTier, RuleGroup, TierLadderProgress, CatalogProgressGroup, CatalogProgressItem, CatalogRef, TransitionLogData, MetricLeaderboardResult, MetricLeaderboardRow, ReconcileProgressionArgs, ReconcileProgressionResult, ReleaseReviewGateResult, } from './services/achievement/index.js';
6
+ export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, buildUnlockRequirementLeaves, getUserProgress, loadUserProgressReviews, getMetricLeaderboard, grantAchievement, loadMetricDoc, recordEvent, recordLog, recordMetricChange, rebuildMetricBalance, rebuildMetricBalancesForUser, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, resolveElapsedAnchor, resolveMetricValue, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, catalogRef, transitionLogData, transitionToId, validateRuleGroup, validateRuleNode, } from './services/achievement/index.js';
7
+ export type { ResolvedTier, RuleGroup, TierLadderProgress, CatalogProgressGroup, CatalogProgressItem, UnlockRequirementLeaf, UnlockRequirementRelation, GetUserProgressInclude, UserProgressMeResponse, UserProgressReviews, UserProgressTierRequest, CatalogRef, TransitionLogData, MetricLeaderboardResult, MetricLeaderboardRow, ReconcileProgressionArgs, ReconcileProgressionResult, ReleaseReviewGateResult, } from './services/achievement/index.js';
8
8
  export type { BuiltInEventType, BuiltInRuleType, AchievementCollectionKey, AchievementCollectionOverride, AchievementCollectionsOptions, AchievementEndpointsOptions, AchievementMetricAnchor, AchievementMetricAnchorConfig, AchievementMetricAnchorEntry, AchievementPluginOptions, AchievementRuleType, AchievementScopeConfig, AchievementUsersOptions, } from './types.js';
9
9
  export { DEFAULT_COLLECTION_BASE_SLUGS, DEFAULT_COLLECTION_PREFIX, DEFAULT_ADMIN_GROUP } from './collections/slugs.js';
10
10
  export { DEFAULT_ME_ENDPOINT_PATH, DEFAULT_LEADERBOARD_ENDPOINT_PATH, DEFAULT_RECONCILE_ENDPOINT_PATH, } from './defaults.js';
11
11
  export { collectionOf, slugOf } from './collections/helpers.js';
12
+ export { eachRuleLeaf } from './collections/hooks/eachRuleLeaf.js';
12
13
  export { isAchievementPluginEnabled } from './enabled.js';
13
14
  export { descriptionToPlaintext } from './fields/descriptionPlaintext.js';
14
15
  export { plainTextToLexical } from './fields/plainTextToLexical.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,+BAA+B,EAC/B,kBAAkB,EAClB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAC1E,YAAY,EACV,0BAA0B,EAC1B,yBAAyB,EACzB,oBAAoB,EACpB,4BAA4B,EAC5B,mBAAmB,GACpB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,8BAA8B,EAC9B,0BAA0B,EAC1B,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,4BAA4B,EAC5B,oBAAoB,EACpB,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,iCAAiC,CAAA;AACxC,YAAY,EACV,YAAY,EACZ,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,UAAU,EACV,iBAAiB,EACjB,uBAAuB,EACvB,oBAAoB,EACpB,wBAAwB,EACxB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,iCAAiC,CAAA;AACxC,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,2BAA2B,EAC3B,uBAAuB,EACvB,6BAA6B,EAC7B,4BAA4B,EAC5B,wBAAwB,EACxB,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,6BAA6B,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AACtH,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,+BAA+B,GAChC,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,+BAA+B,EAC/B,kBAAkB,EAClB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAC1E,YAAY,EACV,0BAA0B,EAC1B,yBAAyB,EACzB,oBAAoB,EACpB,4BAA4B,EAC5B,mBAAmB,GACpB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,8BAA8B,EAC9B,0BAA0B,EAC1B,4BAA4B,EAC5B,eAAe,EACf,uBAAuB,EACvB,oBAAoB,EACpB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,4BAA4B,EAC5B,oBAAoB,EACpB,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,iCAAiC,CAAA;AACxC,YAAY,EACV,YAAY,EACZ,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,uBAAuB,EACvB,UAAU,EACV,iBAAiB,EACjB,uBAAuB,EACvB,oBAAoB,EACpB,wBAAwB,EACxB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,iCAAiC,CAAA;AACxC,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,2BAA2B,EAC3B,uBAAuB,EACvB,6BAA6B,EAC7B,4BAA4B,EAC5B,wBAAwB,EACxB,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,6BAA6B,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AACtH,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,+BAA+B,GAChC,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAClE,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA"}
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  export { DEFAULT_ACHIEVEMENT_METRIC, DEFAULT_ACHIEVEMENT_METRIC_SLUG, ENGINE_EVENT_TYPES, ENGINE_METRICS, } from './catalog.js';
2
2
  export { achievementPlugin } from './plugin.js';
3
3
  export { seedAchievementCatalog, seedAchievements } from './seed/index.js';
4
- export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, getUserProgress, getMetricLeaderboard, grantAchievement, loadMetricDoc, recordEvent, recordLog, recordMetricChange, rebuildMetricBalance, rebuildMetricBalancesForUser, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, resolveElapsedAnchor, resolveMetricValue, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, catalogRef, transitionLogData, transitionToId, validateRuleGroup, validateRuleNode, } from './services/achievement/index.js';
4
+ export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, buildUnlockRequirementLeaves, getUserProgress, loadUserProgressReviews, getMetricLeaderboard, grantAchievement, loadMetricDoc, recordEvent, recordLog, recordMetricChange, rebuildMetricBalance, rebuildMetricBalancesForUser, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, resolveElapsedAnchor, resolveMetricValue, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, catalogRef, transitionLogData, transitionToId, validateRuleGroup, validateRuleNode, } from './services/achievement/index.js';
5
5
  export { DEFAULT_COLLECTION_BASE_SLUGS, DEFAULT_COLLECTION_PREFIX, DEFAULT_ADMIN_GROUP } from './collections/slugs.js';
6
6
  export { DEFAULT_ME_ENDPOINT_PATH, DEFAULT_LEADERBOARD_ENDPOINT_PATH, DEFAULT_RECONCILE_ENDPOINT_PATH, } from './defaults.js';
7
7
  export { collectionOf, slugOf } from './collections/helpers.js';
8
+ export { eachRuleLeaf } from './collections/hooks/eachRuleLeaf.js';
8
9
  export { isAchievementPluginEnabled } from './enabled.js';
9
10
  export { descriptionToPlaintext } from './fields/descriptionPlaintext.js';
10
11
  export { plainTextToLexical } from './fields/plainTextToLexical.js';
@@ -48,7 +48,7 @@ export type AchievementSeedCatalogRow = {
48
48
  /** Computed metrics — v1 only `elapsed`. */
49
49
  compute?: 'elapsed';
50
50
  /** Computed elapsed — unit of the exposed number. */
51
- unit?: 'days' | 'hours' | 'minutes';
51
+ unit?: 'seconds' | 'minutes' | 'hours' | 'days' | 'years';
52
52
  /** Computed elapsed — built-in or host `metricAnchors` key. */
53
53
  since?: string;
54
54
  /** Computed elapsed with `since: first-event` — event type slug. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/seed/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/seed/index.ts"],"names":[],"mappings":"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"}
@@ -136,7 +136,7 @@ async function resolveRuleNode(payload, rule) {
136
136
  delete next.achievementSlug;
137
137
  }
138
138
  }
139
- if (next.type === 'event-count' || next.type === 'elapsed-since') {
139
+ if (next.type === 'event-count') {
140
140
  const slug = typeof next.eventTypeSlug === 'string'
141
141
  ? next.eventTypeSlug
142
142
  : typeof next.eventType === 'string'
@@ -0,0 +1,48 @@
1
+ import type { Payload, PayloadRequest } from 'payload';
2
+ export declare const UNLOCK_REQUIREMENT_RELATION_FIELDS: {
3
+ readonly tier: "tiers";
4
+ readonly achievement: "achievements";
5
+ readonly metric: "metrics";
6
+ readonly eventType: "eventTypes";
7
+ };
8
+ export type UnlockRequirementCollectionKey = (typeof UNLOCK_REQUIREMENT_RELATION_FIELDS)[keyof typeof UNLOCK_REQUIREMENT_RELATION_FIELDS];
9
+ export type UnlockRequirementRelation = {
10
+ field: keyof typeof UNLOCK_REQUIREMENT_RELATION_FIELDS;
11
+ collectionKey: UnlockRequirementCollectionKey;
12
+ id?: string;
13
+ slug?: string;
14
+ name?: string;
15
+ };
16
+ /**
17
+ * Structured unlock/eligibility leaf — catalog names only, no host UI copy.
18
+ * Hosts format `label` / `detail` from these fields.
19
+ */
20
+ export type UnlockRequirementLeaf = {
21
+ id: string;
22
+ type: string;
23
+ met: boolean;
24
+ /** 0..1 from {@link evaluateRuleProgress}. */
25
+ progress: number;
26
+ relations: UnlockRequirementRelation[];
27
+ target?: number;
28
+ unit?: string;
29
+ since?: string;
30
+ };
31
+ /**
32
+ * Walk every leaf in a rule tree, evaluate met/progress, and return catalog-backed
33
+ * subjects. Payload rows often carry empty `rules: []` + default combinator — those
34
+ * are still leaves ({@link eachRuleLeaf}).
35
+ *
36
+ * `achievement-complete` is omitted by default so hosts can keep those on catalog groups.
37
+ */
38
+ export declare function buildUnlockRequirementLeaves(args: {
39
+ payload: Payload;
40
+ req?: PayloadRequest;
41
+ userId: string;
42
+ scopeId: string | null;
43
+ rules: unknown;
44
+ ladderRank?: number | null;
45
+ locale?: string;
46
+ includeAchievementComplete?: boolean;
47
+ }): Promise<UnlockRequirementLeaf[]>;
48
+ //# sourceMappingURL=buildUnlockRequirementLeaves.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildUnlockRequirementLeaves.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/buildUnlockRequirementLeaves.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAOtD,eAAO,MAAM,kCAAkC;;;;;CAKyC,CAAA;AAExF,MAAM,MAAM,8BAA8B,GACxC,CAAC,OAAO,kCAAkC,CAAC,CAAC,MAAM,OAAO,kCAAkC,CAAC,CAAA;AAE9F,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,MAAM,OAAO,kCAAkC,CAAA;IACtD,aAAa,EAAE,8BAA8B,CAAA;IAC7C,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,OAAO,CAAA;IACZ,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,yBAAyB,EAAE,CAAA;IACtC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAqHD;;;;;;GAMG;AACH,wBAAsB,4BAA4B,CAAC,IAAI,EAAE;IACvD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,KAAK,EAAE,OAAO,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0BAA0B,CAAC,EAAE,OAAO,CAAA;CACrC,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC,CA8EnC"}
@@ -0,0 +1,192 @@
1
+ import { eachRuleLeaf } from '../../collections/hooks/eachRuleLeaf.js';
2
+ import { collectionOf } from '../../collections/helpers.js';
3
+ import { evaluateRuleProgress, evaluateRules } from './evaluateRules.js';
4
+ import { relationId } from './relationId.js';
5
+ export const UNLOCK_REQUIREMENT_RELATION_FIELDS = {
6
+ tier: 'tiers',
7
+ achievement: 'achievements',
8
+ metric: 'metrics',
9
+ eventType: 'eventTypes',
10
+ };
11
+ function relationName(value) {
12
+ if (value && typeof value === 'object' && 'name' in value) {
13
+ const name = value.name;
14
+ if (typeof name === 'string' && name)
15
+ return name;
16
+ }
17
+ return null;
18
+ }
19
+ function relationSlug(value) {
20
+ if (value && typeof value === 'object' && 'slug' in value) {
21
+ const slug = value.slug;
22
+ if (typeof slug === 'string' && slug)
23
+ return slug;
24
+ }
25
+ return null;
26
+ }
27
+ function slugField(rule, relationKey) {
28
+ const raw = rule[`${relationKey}Slug`];
29
+ return typeof raw === 'string' && raw ? raw : null;
30
+ }
31
+ function numericTarget(rule) {
32
+ for (const key of ['amount', 'minimum', 'count']) {
33
+ const value = rule[key];
34
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
35
+ return value;
36
+ }
37
+ return null;
38
+ }
39
+ async function resolveCatalogName(args) {
40
+ const named = relationName(args.value);
41
+ const slug = args.slug || relationSlug(args.value);
42
+ const id = relationId(args.value);
43
+ if (named) {
44
+ return {
45
+ ...(id ? { id } : {}),
46
+ ...(slug ? { slug } : {}),
47
+ name: named,
48
+ };
49
+ }
50
+ if (slug) {
51
+ const cached = args.cache.get(`slug:${args.collectionKey}:${slug}`);
52
+ if (cached)
53
+ return { ...(id ? { id } : {}), slug, name: cached };
54
+ }
55
+ if (id) {
56
+ const cached = args.cache.get(`id:${args.collectionKey}:${id}`);
57
+ if (cached) {
58
+ return { id, ...(slug ? { slug } : {}), name: cached };
59
+ }
60
+ try {
61
+ const doc = (await args.payload.findByID({
62
+ collection: collectionOf(args.collectionKey),
63
+ id,
64
+ depth: 0,
65
+ overrideAccess: true,
66
+ req: args.req,
67
+ ...(args.locale ? { locale: args.locale } : {}),
68
+ }));
69
+ const name = (typeof doc?.name === 'string' && doc.name) ||
70
+ (typeof doc?.slug === 'string' && doc.slug) ||
71
+ null;
72
+ if (name) {
73
+ args.cache.set(`id:${args.collectionKey}:${id}`, name);
74
+ if (doc?.slug)
75
+ args.cache.set(`slug:${args.collectionKey}:${doc.slug}`, name);
76
+ return { id, ...(doc?.slug ? { slug: doc.slug } : slug ? { slug } : {}), name };
77
+ }
78
+ }
79
+ catch {
80
+ /* fall through */
81
+ }
82
+ }
83
+ if (slug && !id) {
84
+ try {
85
+ const found = await args.payload.find({
86
+ collection: collectionOf(args.collectionKey),
87
+ depth: 0,
88
+ limit: 1,
89
+ overrideAccess: true,
90
+ req: args.req,
91
+ where: { slug: { equals: slug } },
92
+ ...(args.locale ? { locale: args.locale } : {}),
93
+ });
94
+ const doc = found.docs[0];
95
+ const resolvedId = relationId(doc);
96
+ const name = (typeof doc?.name === 'string' && doc.name) ||
97
+ (typeof doc?.slug === 'string' && doc.slug) ||
98
+ slug;
99
+ if (resolvedId)
100
+ args.cache.set(`id:${args.collectionKey}:${resolvedId}`, name);
101
+ args.cache.set(`slug:${args.collectionKey}:${slug}`, name);
102
+ return { ...(resolvedId ? { id: resolvedId } : {}), slug, name };
103
+ }
104
+ catch {
105
+ /* fall through */
106
+ }
107
+ }
108
+ return {
109
+ ...(id ? { id } : {}),
110
+ ...(slug ? { slug } : {}),
111
+ ...(slug || id ? { name: slug || id || undefined } : {}),
112
+ };
113
+ }
114
+ /**
115
+ * Walk every leaf in a rule tree, evaluate met/progress, and return catalog-backed
116
+ * subjects. Payload rows often carry empty `rules: []` + default combinator — those
117
+ * are still leaves ({@link eachRuleLeaf}).
118
+ *
119
+ * `achievement-complete` is omitted by default so hosts can keep those on catalog groups.
120
+ */
121
+ export async function buildUnlockRequirementLeaves(args) {
122
+ const req = args.req ?? { payload: args.payload };
123
+ const cache = new Map();
124
+ const leaves = [];
125
+ eachRuleLeaf(args.rules, (rule) => {
126
+ const type = String(rule.type ?? '');
127
+ if (!type)
128
+ return;
129
+ if (type === 'achievement-complete' && !args.includeAchievementComplete)
130
+ return;
131
+ leaves.push(rule);
132
+ });
133
+ const out = [];
134
+ for (let index = 0; index < leaves.length; index += 1) {
135
+ const rule = leaves[index];
136
+ const type = String(rule.type);
137
+ const leafRules = [rule];
138
+ const met = await evaluateRules({
139
+ payload: args.payload,
140
+ req,
141
+ rules: leafRules,
142
+ userId: args.userId,
143
+ scopeId: args.scopeId,
144
+ ladderRank: args.ladderRank,
145
+ });
146
+ const progress = await evaluateRuleProgress({
147
+ payload: args.payload,
148
+ req,
149
+ rules: leafRules,
150
+ userId: args.userId,
151
+ scopeId: args.scopeId,
152
+ ladderRank: args.ladderRank,
153
+ });
154
+ const relations = [];
155
+ for (const [field, collectionKey] of Object.entries(UNLOCK_REQUIREMENT_RELATION_FIELDS)) {
156
+ const key = field;
157
+ if (rule[key] == null && !slugField(rule, key))
158
+ continue;
159
+ const resolved = await resolveCatalogName({
160
+ payload: args.payload,
161
+ req,
162
+ locale: args.locale,
163
+ collectionKey,
164
+ value: rule[key],
165
+ slug: slugField(rule, key),
166
+ cache,
167
+ });
168
+ if (!resolved.id && !resolved.slug && !resolved.name)
169
+ continue;
170
+ relations.push({ field: key, collectionKey, ...resolved });
171
+ }
172
+ const target = numericTarget(rule);
173
+ const unit = typeof rule.unit === 'string' && rule.unit ? rule.unit : undefined;
174
+ // `since` / `unit` only matter when a duration/count target is set (defaults pollute other leaves).
175
+ const since = target != null && typeof rule.since === 'string' && rule.since ? rule.since : undefined;
176
+ const subjectKey = relations
177
+ .map((row) => row.name || row.slug || row.id)
178
+ .filter(Boolean)
179
+ .join(':') || 'leaf';
180
+ out.push({
181
+ id: `${type}:${index}:${subjectKey}`,
182
+ type,
183
+ met,
184
+ progress,
185
+ relations,
186
+ ...(target != null ? { target } : {}),
187
+ ...(unit ? { unit } : {}),
188
+ ...(since ? { since } : {}),
189
+ });
190
+ }
191
+ return out;
192
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"builtInRules.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/builtInRules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA2B,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAuJlF,eAAO,MAAM,gBAAgB,EAAE,mBAAmB,EA+LjD,CAAA"}
1
+ {"version":3,"file":"builtInRules.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/builtInRules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA2B,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAqJlF,eAAO,MAAM,gBAAgB,EAAE,mBAAmB,EA6JjD,CAAA"}
@@ -1,7 +1,5 @@
1
1
  import { collectionOf } from '../../collections/helpers.js';
2
- import { durationMs } from './duration.js';
3
2
  import { relationId } from './relationId.js';
4
- import { resolveElapsedAnchor } from './resolveElapsedAnchor.js';
5
3
  import { resolveMetricValue } from './resolveMetricValue.js';
6
4
  import { userScopeIncludingUnscopedWhere } from './where.js';
7
5
  function clamp01(value) {
@@ -276,43 +274,4 @@ export const builtInRuleTypes = [
276
274
  return clamp01(result.totalDocs / needed);
277
275
  },
278
276
  },
279
- {
280
- type: 'elapsed-since',
281
- async evaluate({ payload, req, rule, userId, scopeId }) {
282
- const needed = durationMs(Number(rule.amount ?? NaN), rule.unit);
283
- if (needed == null)
284
- return false;
285
- const anchor = await resolveElapsedAnchor({
286
- payload,
287
- req,
288
- since: rule.since,
289
- eventType: rule.eventType,
290
- eventTypeSlug: rule.eventTypeSlug,
291
- userId,
292
- scopeId,
293
- });
294
- if (!anchor)
295
- return false;
296
- return Date.now() - anchor.getTime() >= needed;
297
- },
298
- async progress({ payload, req, rule, userId, scopeId }) {
299
- const needed = durationMs(Number(rule.amount ?? NaN), rule.unit);
300
- if (needed == null)
301
- return 0;
302
- if (!(needed > 0))
303
- return 1;
304
- const anchor = await resolveElapsedAnchor({
305
- payload,
306
- req,
307
- since: rule.since,
308
- eventType: rule.eventType,
309
- eventTypeSlug: rule.eventTypeSlug,
310
- userId,
311
- scopeId,
312
- });
313
- if (!anchor)
314
- return 0;
315
- return clamp01((Date.now() - anchor.getTime()) / needed);
316
- },
317
- },
318
277
  ];
@@ -1,3 +1,4 @@
1
+ export type ElapsedUnit = 'seconds' | 'minutes' | 'hours' | 'days' | 'years';
1
2
  export declare function durationMs(amount: number, unit: unknown): number | null;
2
3
  export declare function unitMs(unit: unknown): number | null;
3
4
  export declare function parseDate(value: unknown): Date | null;
@@ -1 +1 @@
1
- {"version":3,"file":"duration.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/duration.ts"],"names":[],"mappings":"AAIA,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAevE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAEnD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAOrD;AAED,wDAAwD;AACxD,wBAAgB,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,GAAG,IAAI,CAM/F"}
1
+ {"version":3,"file":"duration.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/duration.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAA;AAE5E,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAmBvE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAEnD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAOrD;AAED,wDAAwD;AACxD,wBAAgB,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,GAAG,IAAI,CAM/F"}
@@ -1,14 +1,21 @@
1
- const MS_PER_MINUTE = 60_000;
1
+ const MS_PER_SECOND = 1_000;
2
+ const MS_PER_MINUTE = 60 * MS_PER_SECOND;
2
3
  const MS_PER_HOUR = 60 * MS_PER_MINUTE;
3
4
  const MS_PER_DAY = 24 * MS_PER_HOUR;
5
+ /** Fixed-length year for elapsed floors (not calendar years). */
6
+ const MS_PER_YEAR = 365 * MS_PER_DAY;
4
7
  export function durationMs(amount, unit) {
5
8
  if (!(typeof amount === 'number') || !Number.isFinite(amount) || amount < 0)
6
9
  return null;
7
10
  switch (unit) {
11
+ case 'seconds':
12
+ return amount * MS_PER_SECOND;
8
13
  case 'minutes':
9
14
  return amount * MS_PER_MINUTE;
10
15
  case 'hours':
11
16
  return amount * MS_PER_HOUR;
17
+ case 'years':
18
+ return amount * MS_PER_YEAR;
12
19
  case 'days':
13
20
  case undefined:
14
21
  case null:
@@ -1,4 +1,5 @@
1
1
  import type { PaginatedDocs, Payload, PayloadRequest } from 'payload';
2
+ import { type UserProgressReviews } from './loadUserProgressReviews.js';
2
3
  /** Catalog slice — `description` matches the collection (Lexical JSON, or legacy string). */
3
4
  type LeanCatalog = {
4
5
  id: string;
@@ -32,10 +33,15 @@ declare function leanRequest(doc: Record<string, unknown>): {
32
33
  updatedAt: {} | null;
33
34
  createdAt: {} | null;
34
35
  };
36
+ export type GetUserProgressInclude = {
37
+ /** Pending achievement keys + open/rejected tier requests. */
38
+ reviews?: boolean;
39
+ };
35
40
  export type UserProgressMeResponse = PaginatedDocs<ReturnType<typeof leanGrant>> & {
36
41
  /** Ladder-derived current tier(s). */
37
42
  tiers: LeanTier[];
38
43
  requests: ReturnType<typeof leanRequest>[];
44
+ reviews?: UserProgressReviews;
39
45
  };
40
46
  /**
41
47
  * Current-user snapshot. Top-level Payload `find` shape for **grants**;
@@ -49,6 +55,7 @@ export declare function getUserProgress(args: {
49
55
  paging?: GetUserProgressPaging;
50
56
  /** Content locale for localized achievement / tier names (falls back to `req.locale`). */
51
57
  locale?: string;
58
+ include?: GetUserProgressInclude;
52
59
  }): Promise<UserProgressMeResponse>;
53
60
  export {};
54
61
  //# sourceMappingURL=getUserProgress.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"getUserProgress.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/getUserProgress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAOrE,6FAA6F;AAC7F,KAAK,WAAW,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AACtF,KAAK,QAAQ,GAAG,WAAW,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AAExE,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAmBD,iBAAS,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;;EAU9C;AAED,iBAAS,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;EAShD;AAoBD,MAAM,MAAM,sBAAsB,GAAG,aAAa,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,GAAG;IACjF,sCAAsC;IACtC,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,QAAQ,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,EAAE,CAAA;CAC3C,CAAA;AAkED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,CAAC,EAAE,qBAAqB,CAAA;IAC9B,0FAA0F;IAC1F,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAiDlC"}
1
+ {"version":3,"file":"getUserProgress.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/getUserProgress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAGrE,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,8BAA8B,CAAA;AAKhG,6FAA6F;AAC7F,KAAK,WAAW,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AACtF,KAAK,QAAQ,GAAG,WAAW,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AAExE,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAmBD,iBAAS,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;;EAU9C;AAED,iBAAS,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;EAShD;AAoBD,MAAM,MAAM,sBAAsB,GAAG;IACnC,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG,aAAa,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,GAAG;IACjF,sCAAsC;IACtC,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,QAAQ,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,EAAE,CAAA;IAC1C,OAAO,CAAC,EAAE,mBAAmB,CAAA;CAC9B,CAAA;AAkED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,CAAC,EAAE,qBAAqB,CAAA;IAC9B,0FAA0F;IAC1F,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,sBAAsB,CAAA;CACjC,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA0DlC"}
@@ -1,4 +1,5 @@
1
1
  import { collectionOf } from '../../collections/helpers.js';
2
+ import { loadUserProgressReviews } from './loadUserProgressReviews.js';
2
3
  import { relationId } from './relationId.js';
3
4
  import { resolveCurrentTier } from './resolveCurrentTier.js';
4
5
  import { userScopeWhere } from './where.js';
@@ -125,7 +126,7 @@ export async function getUserProgress(args) {
125
126
  const page = args.paging?.page ?? 1;
126
127
  const requestsLimit = args.paging?.requestsLimit ?? 20;
127
128
  const locale = args.locale ?? (args.req?.locale || undefined);
128
- const [grants, requests, tiers] = await Promise.all([
129
+ const [grants, requests, tiers, reviews] = await Promise.all([
129
130
  args.payload.find({
130
131
  collection: collectionOf('grants'),
131
132
  depth: 1,
@@ -155,10 +156,19 @@ export async function getUserProgress(args) {
155
156
  scopeId,
156
157
  locale,
157
158
  }),
159
+ args.include?.reviews
160
+ ? loadUserProgressReviews({
161
+ payload: args.payload,
162
+ req: args.req,
163
+ userId: args.userId,
164
+ scopeId,
165
+ })
166
+ : Promise.resolve(undefined),
158
167
  ]);
159
168
  return {
160
169
  ...mapPaginatedDocs(grants, (doc) => leanGrant(doc)),
161
170
  tiers,
162
171
  requests: requests.docs.map((doc) => leanRequest(doc)),
172
+ ...(reviews ? { reviews } : {}),
163
173
  };
164
174
  }
@@ -2,7 +2,12 @@ export { evaluateRules, evaluateRuleProgress, normalizeRuleGroup, collectAchieve
2
2
  export type { RuleGroup, AchievementCompleteRef } from './evaluateRules.js';
3
3
  export { buildCatalogProgressGroups } from './buildCatalogProgressGroups.js';
4
4
  export type { CatalogProgressGroup, CatalogProgressItem } from './buildCatalogProgressGroups.js';
5
+ export { buildUnlockRequirementLeaves } from './buildUnlockRequirementLeaves.js';
6
+ export type { UnlockRequirementCollectionKey, UnlockRequirementLeaf, UnlockRequirementRelation, } from './buildUnlockRequirementLeaves.js';
5
7
  export { getUserProgress } from './getUserProgress.js';
8
+ export type { GetUserProgressInclude, GetUserProgressPaging, UserProgressMeResponse } from './getUserProgress.js';
9
+ export { loadUserProgressReviews } from './loadUserProgressReviews.js';
10
+ export type { UserProgressReviews, UserProgressTierRequest } from './loadUserProgressReviews.js';
6
11
  export { getMetricLeaderboard } from './getMetricLeaderboard.js';
7
12
  export type { MetricLeaderboardResult, MetricLeaderboardRow } from './getMetricLeaderboard.js';
8
13
  export { applyMetricBalanceDelta, findMetricBalance, metricBalanceKey, rebuildMetricBalance, rebuildMetricBalancesForUser, } from './metricBalances.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAA;AAC5H,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAA;AAChG,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,YAAY,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAC9F,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,4BAA4B,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAC3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACjG,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAClE,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAC1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACxE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA;AAC/E,YAAY,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAA;AAC1F,OAAO,EACL,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,GAC1B,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AACrE,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAA;AAC5H,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAA;AAChG,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAA;AAChF,YAAY,EACV,8BAA8B,EAC9B,qBAAqB,EACrB,yBAAyB,GAC1B,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AACjH,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AACtE,YAAY,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAChG,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,YAAY,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAC9F,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,4BAA4B,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAC3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACjG,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAClE,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAC1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACxE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA;AAC/E,YAAY,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAA;AAC1F,OAAO,EACL,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,GAC1B,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AACrE,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA"}
@@ -1,6 +1,8 @@
1
1
  export { evaluateRules, evaluateRuleProgress, normalizeRuleGroup, collectAchievementCompleteRefs } from './evaluateRules.js';
2
2
  export { buildCatalogProgressGroups } from './buildCatalogProgressGroups.js';
3
+ export { buildUnlockRequirementLeaves } from './buildUnlockRequirementLeaves.js';
3
4
  export { getUserProgress } from './getUserProgress.js';
5
+ export { loadUserProgressReviews } from './loadUserProgressReviews.js';
4
6
  export { getMetricLeaderboard } from './getMetricLeaderboard.js';
5
7
  export { applyMetricBalanceDelta, findMetricBalance, metricBalanceKey, rebuildMetricBalance, rebuildMetricBalancesForUser, } from './metricBalances.js';
6
8
  export { resolveMetricValue, loadMetricDoc } from './resolveMetricValue.js';
@@ -0,0 +1,23 @@
1
+ import type { Payload, PayloadRequest } from 'payload';
2
+ export type UserProgressTierRequest = {
3
+ id: string;
4
+ status: 'pending' | 'rejected';
5
+ tierId: string | null;
6
+ };
7
+ export type UserProgressReviews = {
8
+ /** Achievement definition ids and slugs with a pending request. */
9
+ pendingAchievementKeys: string[];
10
+ /** Open or rejected tier requests (host decides which badge wins). */
11
+ tierRequests: UserProgressTierRequest[];
12
+ };
13
+ /**
14
+ * Review-queue snapshot for member UI (pending achievement keys + open/rejected tier requests).
15
+ * Does not decide ready/rejected presentation — hosts apply ladder context themselves.
16
+ */
17
+ export declare function loadUserProgressReviews(args: {
18
+ payload: Payload;
19
+ req?: PayloadRequest;
20
+ userId: string;
21
+ scopeId?: string | null;
22
+ }): Promise<UserProgressReviews>;
23
+ //# sourceMappingURL=loadUserProgressReviews.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadUserProgressReviews.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/loadUserProgressReviews.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAS,MAAM,SAAS,CAAA;AAQ7D,MAAM,MAAM,uBAAuB,GAAG;IACpC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,SAAS,GAAG,UAAU,CAAA;IAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,mEAAmE;IACnE,sBAAsB,EAAE,MAAM,EAAE,CAAA;IAChC,sEAAsE;IACtE,YAAY,EAAE,uBAAuB,EAAE,CAAA;CACxC,CAAA;AAyBD;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA+C/B"}
@@ -0,0 +1,79 @@
1
+ import { collectionOf } from '../../collections/helpers.js';
2
+ import { relationId } from './relationId.js';
3
+ const PENDING_ACHIEVEMENT_REQUESTS_LIMIT = 200;
4
+ const TIER_REQUESTS_LIMIT = 50;
5
+ function relationSlug(value) {
6
+ if (value && typeof value === 'object' && 'slug' in value) {
7
+ const slug = value.slug;
8
+ if (typeof slug === 'string' && slug)
9
+ return slug;
10
+ }
11
+ return null;
12
+ }
13
+ function pendingAchievementWhere(userId, scopeId) {
14
+ const clauses = [{ user: { equals: userId } }, { status: { equals: 'pending' } }];
15
+ if (scopeId)
16
+ clauses.push({ scope: { equals: scopeId } });
17
+ return { and: clauses };
18
+ }
19
+ function openOrRejectedTierRequestWhere(userId, scopeId) {
20
+ const clauses = [
21
+ { user: { equals: userId } },
22
+ { status: { in: ['pending', 'rejected'] } },
23
+ ];
24
+ if (scopeId)
25
+ clauses.push({ scope: { equals: scopeId } });
26
+ return { and: clauses };
27
+ }
28
+ /**
29
+ * Review-queue snapshot for member UI (pending achievement keys + open/rejected tier requests).
30
+ * Does not decide ready/rejected presentation — hosts apply ladder context themselves.
31
+ */
32
+ export async function loadUserProgressReviews(args) {
33
+ const scopeId = args.scopeId ?? null;
34
+ const [pendingAchievementRequests, tierRequests] = await Promise.all([
35
+ args.payload.find({
36
+ collection: collectionOf('achievementRequests'),
37
+ depth: 1,
38
+ limit: PENDING_ACHIEVEMENT_REQUESTS_LIMIT,
39
+ pagination: false,
40
+ overrideAccess: true,
41
+ req: args.req,
42
+ where: pendingAchievementWhere(args.userId, scopeId),
43
+ }),
44
+ scopeId
45
+ ? args.payload.find({
46
+ collection: collectionOf('tierRequests'),
47
+ depth: 0,
48
+ limit: TIER_REQUESTS_LIMIT,
49
+ pagination: false,
50
+ overrideAccess: true,
51
+ req: args.req,
52
+ where: openOrRejectedTierRequestWhere(args.userId, scopeId),
53
+ })
54
+ : Promise.resolve({ docs: [] }),
55
+ ]);
56
+ const pendingAchievementKeys = new Set();
57
+ for (const doc of pendingAchievementRequests.docs) {
58
+ const id = relationId(doc.achievement);
59
+ const slug = relationSlug(doc.achievement);
60
+ if (id)
61
+ pendingAchievementKeys.add(id);
62
+ if (slug)
63
+ pendingAchievementKeys.add(slug);
64
+ }
65
+ const reviews = [];
66
+ for (const doc of tierRequests.docs) {
67
+ const status = doc.status === 'rejected' ? 'rejected' : doc.status === 'pending' ? 'pending' : null;
68
+ if (!status)
69
+ continue;
70
+ const id = typeof doc.id === 'string' && doc.id ? doc.id : relationId(doc.id);
71
+ if (!id)
72
+ continue;
73
+ reviews.push({ id, status, tierId: relationId(doc.tier) });
74
+ }
75
+ return {
76
+ pendingAchievementKeys: [...pendingAchievementKeys],
77
+ tierRequests: reviews,
78
+ };
79
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"validateRules.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/validateRules.ts"],"names":[],"mappings":"AAKA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEvE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CA2E7E;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,GAC9D,MAAM,GAAG,IAAI,CAOf;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,EAC/D,KAAK,EAAE,MAAM,GACZ,IAAI,CAKN"}
1
+ {"version":3,"file":"validateRules.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/validateRules.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEvE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CAuD7E;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,GAC9D,MAAM,GAAG,IAAI,CAOf;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,EAC/D,KAAK,EAAE,MAAM,GACZ,IAAI,CAKN"}
@@ -1,6 +1,4 @@
1
1
  import { APIError } from 'payload';
2
- import { isBuiltinElapsedSince } from '../../extensions/elapsedSinceOptions.js';
3
- import { getMetricAnchorResolver } from '../../extensions/metricAnchors.js';
4
2
  import { relationId } from './relationId.js';
5
3
  import { normalizeRuleGroup } from './evaluateRules.js';
6
4
  /**
@@ -51,26 +49,6 @@ export function validateRuleNode(rule) {
51
49
  }
52
50
  return null;
53
51
  }
54
- if (type === 'elapsed-since') {
55
- const since = String(rule.since ?? 'user-created-at');
56
- if (since === 'first-event') {
57
- if (!relationId(rule.eventType) && typeof rule.eventTypeSlug !== 'string') {
58
- return 'Elapsed since (first event) needs an event type.';
59
- }
60
- }
61
- else if (!isBuiltinElapsedSince(since) && !getMetricAnchorResolver(since)) {
62
- // Host anchors (e.g. membership-granted) are registered via extensions.metricAnchors.
63
- return 'Elapsed since needs a valid since source.';
64
- }
65
- if (typeof rule.amount !== 'number' || Number.isNaN(rule.amount) || rule.amount < 0) {
66
- return 'Elapsed since needs a non-negative amount.';
67
- }
68
- const unit = rule.unit ?? 'days';
69
- if (unit !== 'days' && unit !== 'hours' && unit !== 'minutes') {
70
- return 'Elapsed since unit must be days, hours, or minutes.';
71
- }
72
- return null;
73
- }
74
52
  if (!type) {
75
53
  return 'Each rule needs a type.';
76
54
  }
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CollectionConfig, OptionLabel, Payload, PayloadRequest, TypedUser } from 'payload';
2
- /** Host-registered date anchors for computed elapsed metrics / `elapsed-since` rules. */
2
+ /** Host-registered date anchors for computed elapsed metrics. */
3
3
  export type AchievementMetricAnchor = (args: {
4
4
  payload: Payload;
5
5
  req?: PayloadRequest;
@@ -125,8 +125,8 @@ export type AchievementPluginOptions = {
125
125
  /** Custom rule evaluators merged with built-ins. */
126
126
  ruleTypes?: AchievementRuleType[];
127
127
  /**
128
- * Named date anchors for computed `elapsed` metrics and `elapsed-since` rules.
129
- * Keys appear in Admin `since` selects; missing keys resolve to null at eval time.
128
+ * Named date anchors for computed `elapsed` metrics.
129
+ * Keys appear in Admin `since` selects; missing keys resolve to null at eval time.
130
130
  */
131
131
  metricAnchors?: Record<string, AchievementMetricAnchorEntry>;
132
132
  };
@@ -150,7 +150,7 @@ export type AchievementPluginOptions = {
150
150
  users?: AchievementUsersOptions;
151
151
  usersCollectionSlug?: string;
152
152
  };
153
- export type BuiltInRuleType = 'achievement-complete' | 'elapsed-since' | 'event-count' | 'metric-minimum' | 'tier-at-least';
153
+ export type BuiltInRuleType = 'achievement-complete' | 'event-count' | 'metric-minimum' | 'tier-at-least';
154
154
  /** Engine log-type catalog slugs (seeded as system catalog rows). */
155
155
  export type BuiltInEventType = 'achievement.granted' | 'achievement.revoked' | 'metric.delta' | 'tier.changed';
156
156
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dappermountain/payload-plugin-achievements",
3
- "version": "0.2.15",
3
+ "version": "0.3.1",
4
4
  "description": "A robust, Admin-configured achievements engine for Payload: composable unlock rules, derived rank progress, request/review grants, metrics, and scoped multi-tenant support.",
5
5
  "license": "MIT",
6
6
  "type": "module",