@dappermountain/payload-plugin-achievements 0.2.9 → 0.2.11

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
@@ -110,7 +110,7 @@ Unlock (tiers), eligibility, and completion (achievements) share the same rule t
110
110
  - Built-in leaves → `tier-at-least`, `achievement-complete`, `metric-minimum`, `event-count`, `elapsed-since`
111
111
  - Custom leaves → register via `extensions.ruleTypes`
112
112
 
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**. **Next-tier fill** uses fractional progress on the same trees (`resolveTierProgress` / `evaluateRuleProgress`):
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`):
114
114
 
115
115
  | Combinator / leaf | Progress behavior |
116
116
  | --- | --- |
@@ -186,7 +186,7 @@ achievementPlugin({
186
186
  | `canReview` | _(denied)_ | **Important.** Without this, users cannot manage catalogs or approve requests. Receives `(user, scopeId)` where `scopeId` may be `null`. |
187
187
  | `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
188
  | `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 `(ctx) => Date | null` resolvers for computed elapsed metrics / `elapsed-since`. Keys appear in Admin `since` selects. |
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. |
190
190
 
191
191
  ### `collections`
192
192
 
@@ -245,9 +245,13 @@ achievementPlugin({
245
245
  canReview,
246
246
  extensions: {
247
247
  metricAnchors: {
248
- 'membership-granted': async ({ payload, userId, scopeId }) => {
249
- // return a Date from host data, or null
250
- return null
248
+ 'membership-granted': {
249
+ // Admin UI i18n (Payload label), not a CMS localized field
250
+ label: ({ t }) => t('custom:achievements:anchors:membershipGranted'),
251
+ resolve: async ({ payload, userId, scopeId }) => {
252
+ // return a Date from host data, or null
253
+ return null
254
+ },
251
255
  },
252
256
  },
253
257
  ruleTypes: [
@@ -1,7 +1,8 @@
1
1
  import type { CollectionAfterChangeHook } from 'payload';
2
2
  /**
3
- * When Requires review is turned off, approve pending tier requests for this step
4
- * so each waiting user gets a tier.changed audit via existing request hooks.
3
+ * When Requires review is turned off, approve pending tier requests and sync
4
+ * `tier.changed` for members who already met unlock rules (including those with
5
+ * no pending request — otherwise the UI advances with no audit until Repair).
5
6
  */
6
7
  export declare const afterRequiresReviewDisabled: CollectionAfterChangeHook;
7
8
  //# sourceMappingURL=afterRequiresReviewDisabled.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"afterRequiresReviewDisabled.d.ts","sourceRoot":"","sources":["../../../../src/collections/Tiers/hooks/afterRequiresReviewDisabled.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AAOxD;;;GAGG;AACH,eAAO,MAAM,2BAA2B,EAAE,yBAmBzC,CAAA"}
1
+ {"version":3,"file":"afterRequiresReviewDisabled.d.ts","sourceRoot":"","sources":["../../../../src/collections/Tiers/hooks/afterRequiresReviewDisabled.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AAOxD;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,EAAE,yBAmBzC,CAAA"}
@@ -1,7 +1,8 @@
1
1
  import { didDisableRequiresReview, releasePendingTierReviews, } from '../../../services/achievement/releaseReviewGate.js';
2
2
  /**
3
- * When Requires review is turned off, approve pending tier requests for this step
4
- * so each waiting user gets a tier.changed audit via existing request hooks.
3
+ * When Requires review is turned off, approve pending tier requests and sync
4
+ * `tier.changed` for members who already met unlock rules (including those with
5
+ * no pending request — otherwise the UI advances with no audit until Repair).
5
6
  */
6
7
  export const afterRequiresReviewDisabled = async ({ doc, operation, previousDoc, req, }) => {
7
8
  if (operation !== 'update')
@@ -1,7 +1,9 @@
1
- /** Admin select options for elapsed `since` (built-ins + host `metricAnchors` keys). */
2
- export declare function elapsedSinceSelectOptions(): Array<{
3
- label: string;
4
- value: string;
5
- }>;
1
+ import type { Option } from 'payload';
2
+ /**
3
+ * Admin select options for elapsed `since`.
4
+ * Built-in labels are English (same as other plugin Admin chrome).
5
+ * Host anchors pass through Payload {@link OptionLabel} (string, locale map, or `({ t }) => t(…)`).
6
+ */
7
+ export declare function elapsedSinceSelectOptions(): Option[];
6
8
  export declare function isBuiltinElapsedSince(since: string): boolean;
7
9
  //# sourceMappingURL=elapsedSinceOptions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"elapsedSinceOptions.d.ts","sourceRoot":"","sources":["../../src/extensions/elapsedSinceOptions.ts"],"names":[],"mappings":"AAOA,wFAAwF;AACxF,wBAAgB,yBAAyB,IAAI,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAMnF;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE5D"}
1
+ {"version":3,"file":"elapsedSinceOptions.d.ts","sourceRoot":"","sources":["../../src/extensions/elapsedSinceOptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AASrC;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,EAAE,CAKpD;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE5D"}
@@ -1,14 +1,17 @@
1
- import { getAchievementOptions } from '../options-store.js';
1
+ import { getMetricAnchorEntries } from './metricAnchors.js';
2
2
  const BUILTIN_ELAPSED_SINCE_OPTIONS = [
3
3
  { label: 'User created at', value: 'user-created-at' },
4
4
  { label: 'First matching event log', value: 'first-event' },
5
5
  ];
6
- /** Admin select options for elapsed `since` (built-ins + host `metricAnchors` keys). */
6
+ /**
7
+ * Admin select options for elapsed `since`.
8
+ * Built-in labels are English (same as other plugin Admin chrome).
9
+ * Host anchors pass through Payload {@link OptionLabel} (string, locale map, or `({ t }) => t(…)`).
10
+ */
7
11
  export function elapsedSinceSelectOptions() {
8
- const custom = Object.keys(getAchievementOptions().extensions?.metricAnchors ?? {});
9
12
  return [
10
- ...BUILTIN_ELAPSED_SINCE_OPTIONS.map((option) => ({ ...option })),
11
- ...custom.map((key) => ({ label: key, value: key })),
13
+ ...BUILTIN_ELAPSED_SINCE_OPTIONS,
14
+ ...getMetricAnchorEntries().map(({ key, label }) => ({ label, value: key })),
12
15
  ];
13
16
  }
14
17
  export function isBuiltinElapsedSince(since) {
@@ -0,0 +1,13 @@
1
+ import type { OptionLabel } from 'payload';
2
+ import type { AchievementMetricAnchor, AchievementMetricAnchorEntry } from '../types.js';
3
+ export declare function resolveMetricAnchorEntry(entry: AchievementMetricAnchorEntry, key: string): {
4
+ label: OptionLabel;
5
+ resolve: AchievementMetricAnchor;
6
+ };
7
+ export declare function getMetricAnchorEntries(): Array<{
8
+ key: string;
9
+ label: OptionLabel;
10
+ resolve: AchievementMetricAnchor;
11
+ }>;
12
+ export declare function getMetricAnchorResolver(key: string): AchievementMetricAnchor | null;
13
+ //# sourceMappingURL=metricAnchors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metricAnchors.d.ts","sourceRoot":"","sources":["../../src/extensions/metricAnchors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAE1C,OAAO,KAAK,EACV,uBAAuB,EAEvB,4BAA4B,EAC7B,MAAM,aAAa,CAAA;AAOpB,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,4BAA4B,EACnC,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,uBAAuB,CAAA;CAAE,CAM1D;AAED,wBAAgB,sBAAsB,IAAI,KAAK,CAAC;IAC9C,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,WAAW,CAAA;IAClB,OAAO,EAAE,uBAAuB,CAAA;CACjC,CAAC,CAMD;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,uBAAuB,GAAG,IAAI,CAInF"}
@@ -0,0 +1,24 @@
1
+ import { getAchievementOptions } from '../options-store.js';
2
+ function isAnchorConfig(entry) {
3
+ return typeof entry === 'object' && entry != null && 'resolve' in entry;
4
+ }
5
+ export function resolveMetricAnchorEntry(entry, key) {
6
+ if (isAnchorConfig(entry)) {
7
+ return { label: entry.label, resolve: entry.resolve };
8
+ }
9
+ // Bare resolver: Admin shows the key until the host supplies a Payload label.
10
+ return { label: key, resolve: entry };
11
+ }
12
+ export function getMetricAnchorEntries() {
13
+ const anchors = getAchievementOptions().extensions?.metricAnchors ?? {};
14
+ return Object.entries(anchors).map(([key, entry]) => {
15
+ const resolved = resolveMetricAnchorEntry(entry, key);
16
+ return { key, ...resolved };
17
+ });
18
+ }
19
+ export function getMetricAnchorResolver(key) {
20
+ const entry = getAchievementOptions().extensions?.metricAnchors?.[key];
21
+ if (!entry)
22
+ return null;
23
+ return resolveMetricAnchorEntry(entry, key).resolve;
24
+ }
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { seedAchievementCatalog, seedAchievements } from './seed/index.js';
5
5
  export type { AchievementSeedAchievement, AchievementSeedCatalogRow, AchievementSeedInput, AchievementSeedLocalizedText, AchievementSeedTier, } from './seed/index.js';
6
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
7
  export type { ResolvedTier, RuleGroup, TierLadderProgress, CatalogProgressGroup, CatalogProgressItem, CatalogRef, TransitionLogData, MetricLeaderboardResult, MetricLeaderboardRow, ReconcileProgressionArgs, ReconcileProgressionResult, ReleaseReviewGateResult, } from './services/achievement/index.js';
8
- export type { BuiltInEventType, BuiltInRuleType, AchievementCollectionKey, AchievementCollectionOverride, AchievementCollectionsOptions, AchievementEndpointsOptions, AchievementMetricAnchor, AchievementPluginOptions, AchievementRuleType, AchievementScopeConfig, AchievementUsersOptions, } from './types.js';
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';
@@ -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,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,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,6 +1,8 @@
1
1
  import type { PayloadRequest } from 'payload';
2
2
  export type ReleaseReviewGateResult = {
3
3
  requestsApproved: number;
4
+ /** `tier.changed` rows written for users who newly resolve past the released gate. */
5
+ tierLogsWritten?: number;
4
6
  };
5
7
  /**
6
8
  * When a definition stops requiring review, approve each pending request for that
@@ -10,6 +12,13 @@ export declare function releasePendingAchievementReviews(args: {
10
12
  req: PayloadRequest;
11
13
  achievementId: string;
12
14
  }): Promise<ReleaseReviewGateResult>;
15
+ /**
16
+ * When a tier stops requiring review:
17
+ * 1. Approve pending tier requests (their afterChange hooks sync `tier.changed`).
18
+ * 2. Sync `tier.changed` for every (user, scope) that may now resolve to this rung —
19
+ * including members who already met unlock rules but never had a pending request
20
+ * (UI would otherwise show the new rank with no audit log until Repair).
21
+ */
13
22
  export declare function releasePendingTierReviews(args: {
14
23
  req: PayloadRequest;
15
24
  tierId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"releaseReviewGate.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/releaseReviewGate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAK7C,MAAM,MAAM,uBAAuB,GAAG;IACpC,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AAED;;;GAGG;AACH,wBAAsB,gCAAgC,CAAC,IAAI,EAAE;IAC3D,GAAG,EAAE,cAAc,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;CACtB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAgCnC;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE;IACpD,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA6BnC;AAED,uEAAuE;AACvE,wBAAgB,wBAAwB,CACtC,WAAW,EAAE;IAAE,cAAc,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACnE,GAAG,EAAE;IAAE,cAAc,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAC1D,OAAO,CAET"}
1
+ {"version":3,"file":"releaseReviewGate.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/releaseReviewGate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAS,MAAM,SAAS,CAAA;AAMpD,MAAM,MAAM,uBAAuB,GAAG;IACpC,gBAAgB,EAAE,MAAM,CAAA;IACxB,sFAAsF;IACtF,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAcD;;;GAGG;AACH,wBAAsB,gCAAgC,CAAC,IAAI,EAAE;IAC3D,GAAG,EAAE,cAAc,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;CACtB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAgCnC;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAAC,IAAI,EAAE;IACpD,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA0FnC;AAED,uEAAuE;AACvE,wBAAgB,wBAAwB,CACtC,WAAW,EAAE;IAAE,cAAc,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACnE,GAAG,EAAE;IAAE,cAAc,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAC1D,OAAO,CAET"}
@@ -1,5 +1,13 @@
1
1
  import { collectionOf } from '../../collections/helpers.js';
2
+ import { syncTierChangedLog } from './recordEvent.js';
2
3
  import { relationId } from './relationId.js';
4
+ function addUserScopePair(pairs, userId, scopeId) {
5
+ if (!userId)
6
+ return;
7
+ const key = `${userId}::${scopeId ?? ''}`;
8
+ if (!pairs.has(key))
9
+ pairs.set(key, { userId, scopeId });
10
+ }
3
11
  /**
4
12
  * When a definition stops requiring review, approve each pending request for that
5
13
  * definition so existing grant / tier.changed hooks run for that user + scope only.
@@ -36,6 +44,13 @@ export async function releasePendingAchievementReviews(args) {
36
44
  }
37
45
  return { requestsApproved };
38
46
  }
47
+ /**
48
+ * When a tier stops requiring review:
49
+ * 1. Approve pending tier requests (their afterChange hooks sync `tier.changed`).
50
+ * 2. Sync `tier.changed` for every (user, scope) that may now resolve to this rung —
51
+ * including members who already met unlock rules but never had a pending request
52
+ * (UI would otherwise show the new rank with no audit log until Repair).
53
+ */
39
54
  export async function releasePendingTierReviews(args) {
40
55
  const pending = await args.req.payload.find({
41
56
  collection: collectionOf('tierRequests'),
@@ -63,7 +78,56 @@ export async function releasePendingTierReviews(args) {
63
78
  });
64
79
  requestsApproved += 1;
65
80
  }
66
- return { requestsApproved };
81
+ const tier = (await args.req.payload.findByID({
82
+ collection: collectionOf('tiers'),
83
+ id: args.tierId,
84
+ depth: 0,
85
+ overrideAccess: true,
86
+ req: args.req,
87
+ }));
88
+ const tierScopeId = relationId(tier?.scope);
89
+ const pairs = new Map();
90
+ const allRequests = await args.req.payload.find({
91
+ collection: collectionOf('tierRequests'),
92
+ depth: 0,
93
+ limit: 5000,
94
+ pagination: false,
95
+ overrideAccess: true,
96
+ req: args.req,
97
+ where: { tier: { equals: args.tierId } },
98
+ });
99
+ for (const doc of allRequests.docs) {
100
+ addUserScopePair(pairs, relationId(doc.user), tierScopeId ?? relationId(doc.scope));
101
+ }
102
+ const grantWhere = tierScopeId
103
+ ? { scope: { equals: tierScopeId } }
104
+ : {
105
+ or: [{ scope: { exists: false } }, { scope: { equals: null } }],
106
+ };
107
+ const grants = await args.req.payload.find({
108
+ collection: collectionOf('grants'),
109
+ depth: 0,
110
+ limit: 5000,
111
+ pagination: false,
112
+ overrideAccess: true,
113
+ req: args.req,
114
+ where: grantWhere,
115
+ });
116
+ for (const doc of grants.docs) {
117
+ addUserScopePair(pairs, relationId(doc.user), tierScopeId ?? relationId(doc.scope));
118
+ }
119
+ let tierLogsWritten = 0;
120
+ for (const pair of pairs.values()) {
121
+ const log = await syncTierChangedLog({
122
+ payload: args.req.payload,
123
+ req: args.req,
124
+ userId: pair.userId,
125
+ scopeId: pair.scopeId,
126
+ });
127
+ if (log)
128
+ tierLogsWritten += 1;
129
+ }
130
+ return { requestsApproved, tierLogsWritten };
67
131
  }
68
132
  /** True when `requiresReview` flipped from required → not required. */
69
133
  export function didDisableRequiresReview(previousDoc, doc) {
@@ -1 +1 @@
1
- {"version":3,"file":"resolveElapsedAnchor.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/resolveElapsedAnchor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AA8CtD;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CA0DvB"}
1
+ {"version":3,"file":"resolveElapsedAnchor.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/resolveElapsedAnchor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AA+CtD;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CA0DvB"}
@@ -1,5 +1,6 @@
1
1
  import { collectionOf } from '../../collections/helpers.js';
2
2
  import { getAchievementOptions } from '../../options-store.js';
3
+ import { getMetricAnchorResolver } from '../../extensions/metricAnchors.js';
3
4
  import { parseDate } from './duration.js';
4
5
  import { relationId } from './relationId.js';
5
6
  import { userScopeIncludingUnscopedWhere } from './where.js';
@@ -81,7 +82,7 @@ export async function resolveElapsedAnchor(args) {
81
82
  });
82
83
  return parseDate(logs.docs[0]?.createdAt);
83
84
  }
84
- const hostAnchor = getAchievementOptions().extensions?.metricAnchors?.[since];
85
+ const hostAnchor = getMetricAnchorResolver(since);
85
86
  if (!hostAnchor)
86
87
  return null;
87
88
  try {
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CollectionConfig, Payload, PayloadRequest, TypedUser } from 'payload';
1
+ import type { CollectionConfig, OptionLabel, Payload, PayloadRequest, TypedUser } from 'payload';
2
2
  /** Host-registered date anchors for computed elapsed metrics / `elapsed-since` rules. */
3
3
  export type AchievementMetricAnchor = (args: {
4
4
  payload: Payload;
@@ -6,6 +6,16 @@ export type AchievementMetricAnchor = (args: {
6
6
  userId: string;
7
7
  scopeId: string | null;
8
8
  }) => Date | null | Promise<Date | null>;
9
+ /**
10
+ * Anchor with an Admin label.
11
+ * `label` is a Payload Admin UI string ({@link OptionLabel}): plain string, `{ en, es, … }`,
12
+ * or `({ t }) => t('custom:…')` — not CMS `localization`.
13
+ */
14
+ export type AchievementMetricAnchorConfig = {
15
+ label: OptionLabel;
16
+ resolve: AchievementMetricAnchor;
17
+ };
18
+ export type AchievementMetricAnchorEntry = AchievementMetricAnchor | AchievementMetricAnchorConfig;
9
19
  export type AchievementScopeConfig = {
10
20
  collection: string;
11
21
  relationField?: string;
@@ -118,7 +128,7 @@ export type AchievementPluginOptions = {
118
128
  * Named date anchors for computed `elapsed` metrics and `elapsed-since` rules.
119
129
  * Keys appear in Admin `since` selects; missing keys resolve to null at eval time.
120
130
  */
121
- metricAnchors?: Record<string, AchievementMetricAnchor>;
131
+ metricAnchors?: Record<string, AchievementMetricAnchorEntry>;
122
132
  };
123
133
  /**
124
134
  * When true (default), ensure system metrics + event types exist on Payload `onInit`
@@ -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.9",
3
+ "version": "0.2.11",
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",