@dappermountain/payload-plugin-achievements 0.3.0 → 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.
package/README.md CHANGED
@@ -124,7 +124,11 @@ Unlock (tiers), eligibility, and completion (achievements) share the same rule t
124
124
 
125
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
 
@@ -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,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';
@@ -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,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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dappermountain/payload-plugin-achievements",
3
- "version": "0.3.0",
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",