@dappermountain/payload-plugin-achievements 0.2.10 → 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
  | --- | --- |
@@ -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,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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dappermountain/payload-plugin-achievements",
3
- "version": "0.2.10",
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",