@dappermountain/payload-plugin-achievements 0.2.10 → 0.2.12
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 +2 -2
- package/dist/collections/TierRequests/hooks/prepareTierRequest.d.ts.map +1 -1
- package/dist/collections/TierRequests/hooks/prepareTierRequest.js +4 -7
- package/dist/collections/Tiers/hooks/afterRequiresReviewDisabled.d.ts +3 -2
- package/dist/collections/Tiers/hooks/afterRequiresReviewDisabled.d.ts.map +1 -1
- package/dist/collections/Tiers/hooks/afterRequiresReviewDisabled.js +3 -2
- package/dist/services/achievement/releaseReviewGate.d.ts +9 -0
- package/dist/services/achievement/releaseReviewGate.d.ts.map +1 -1
- package/dist/services/achievement/releaseReviewGate.js +65 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ Slugs are prefixed with `achievement-` by default so they don’t collide with y
|
|
|
97
97
|
| `metric.delta` | Metric change log type (requires metric + change amount) |
|
|
98
98
|
| `achievement.granted` | Written when a grant is created |
|
|
99
99
|
| `achievement.revoked` | Written when a grant is deleted |
|
|
100
|
-
| `tier.changed` | Written when derived tier moves |
|
|
100
|
+
| `tier.changed` | Written when derived tier moves (including demotions when a tier request leaves `approved`) |
|
|
101
101
|
|
|
102
102
|
Event types can require an **actor** and/or a **metric + change**. Admin only prompts for those fields when the type needs them.
|
|
103
103
|
|
|
@@ -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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prepareTierRequest.d.ts","sourceRoot":"","sources":["../../../../src/collections/TierRequests/hooks/prepareTierRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,4BAA4B,EAE7B,MAAM,SAAS,CAAA;AAYhB;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,4BAyChC,CAAA;AAED,eAAO,MAAM,sBAAsB,EAAE,
|
|
1
|
+
{"version":3,"file":"prepareTierRequest.d.ts","sourceRoot":"","sources":["../../../../src/collections/TierRequests/hooks/prepareTierRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,4BAA4B,EAE7B,MAAM,SAAS,CAAA;AAYhB;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,4BAyChC,CAAA;AAED,eAAO,MAAM,sBAAsB,EAAE,yBAoBpC,CAAA"}
|
|
@@ -46,17 +46,14 @@ export const prepareTierRequest = async ({ data, operation, req, originalDoc, })
|
|
|
46
46
|
}
|
|
47
47
|
return data;
|
|
48
48
|
};
|
|
49
|
-
export const afterTierRequestChange = async ({ doc,
|
|
50
|
-
const status = doc.status;
|
|
51
|
-
if (status !== 'approved')
|
|
52
|
-
return doc;
|
|
53
|
-
const previousStatus = operation === 'update' ? previousDoc?.status : undefined;
|
|
54
|
-
if (previousStatus === 'approved')
|
|
55
|
-
return doc;
|
|
49
|
+
export const afterTierRequestChange = async ({ doc, req, }) => {
|
|
56
50
|
const userId = relationId(doc.user);
|
|
57
51
|
if (!userId)
|
|
58
52
|
return doc;
|
|
59
53
|
const scopeId = scopeIdOf(doc);
|
|
54
|
+
// Any status transition (or create as pending/rejected/approved) can change the
|
|
55
|
+
// live ladder — including demotions when approved → pending/rejected. Sync is
|
|
56
|
+
// idempotent when the latest audit already matches resolveCurrentTier.
|
|
60
57
|
await syncTierChangedLog({
|
|
61
58
|
payload: req.payload,
|
|
62
59
|
req: req,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { CollectionAfterChangeHook } from 'payload';
|
|
2
2
|
/**
|
|
3
|
-
* When Requires review is turned off, approve pending tier requests
|
|
4
|
-
*
|
|
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
|
|
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
|
|
4
|
-
*
|
|
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,
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.12",
|
|
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",
|