@dappermountain/payload-plugin-achievements 0.4.2 → 0.5.0

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 (27) hide show
  1. package/README.md +2 -2
  2. package/dist/collections/EventTypes/index.d.ts.map +1 -1
  3. package/dist/collections/EventTypes/index.js +10 -0
  4. package/dist/collections/Logs/index.d.ts.map +1 -1
  5. package/dist/collections/Logs/index.js +24 -0
  6. package/dist/collections/fields/ruleFields.d.ts.map +1 -1
  7. package/dist/collections/fields/ruleFields.js +10 -0
  8. package/dist/seed/index.d.ts +2 -0
  9. package/dist/seed/index.d.ts.map +1 -1
  10. package/dist/seed/index.js +9 -0
  11. package/dist/services/achievement/actorTierSnapshot.d.ts +40 -0
  12. package/dist/services/achievement/actorTierSnapshot.d.ts.map +1 -0
  13. package/dist/services/achievement/actorTierSnapshot.js +138 -0
  14. package/dist/services/achievement/buildUnlockRequirementLeaves.d.ts +1 -0
  15. package/dist/services/achievement/buildUnlockRequirementLeaves.d.ts.map +1 -1
  16. package/dist/services/achievement/buildUnlockRequirementLeaves.js +1 -0
  17. package/dist/services/achievement/builtInRules.d.ts.map +1 -1
  18. package/dist/services/achievement/builtInRules.js +77 -22
  19. package/dist/services/achievement/contextFlags.d.ts +2 -0
  20. package/dist/services/achievement/contextFlags.d.ts.map +1 -1
  21. package/dist/services/achievement/contextFlags.js +2 -0
  22. package/dist/services/achievement/eventTypeRequiresActor.d.ts +2 -0
  23. package/dist/services/achievement/eventTypeRequiresActor.d.ts.map +1 -1
  24. package/dist/services/achievement/eventTypeRequiresActor.js +5 -0
  25. package/dist/services/achievement/recordEvent.d.ts.map +1 -1
  26. package/dist/services/achievement/recordEvent.js +10 -0
  27. package/package.json +1 -1
package/README.md CHANGED
@@ -100,7 +100,7 @@ Slugs are prefixed with `achievement-` by default so they don’t collide with y
100
100
  | `achievement.revoked` | Written when a grant is deleted |
101
101
  | `tier.changed` | Written when derived tier moves (including demotions when a tier request leaves `approved`) |
102
102
 
103
- Event types can require an **actor** and/or a **metric + change**. Admin only prompts for those fields when the type needs them.
103
+ Event types can require an **actor** and/or a **metric + change**. Admin only prompts for those fields when the type needs them. Optional **Snapshot actor tiers** stores the actor’s derived ladder rank on the log at write time (`resolveCurrentTier` only — no grants or tier requests). Unscoped logs stamp every ladder scope; scoped logs stamp that tenant plus any unscoped ranks. Use that with Event count → **Actor tier** to count only events whose actor held at least that step in the rule’s scope when the event was written. Leave Actor tier empty to count every matching event. Logs without a snapshot (legacy rows, or types that do not stamp) do not match an Actor tier filter.
104
104
 
105
105
  ### Rules engine
106
106
 
@@ -118,7 +118,7 @@ Unlock (tiers), eligibility, and completion (achievements) share the same rule t
118
118
  | AND | Equal average of *requirement* children |
119
119
  | OR | Best child (max) |
120
120
  | `tier-at-least` | Gate — skipped in AND averages |
121
- | `event-count` / `metric-minimum` | Fraction of the target |
121
+ | `event-count` / `metric-minimum` | Fraction of the target. `event-count` may set **Actor tier** (at-least, this scope) against the log’s actor-tier snapshot |
122
122
  | `achievement-complete` | `1` if granted; otherwise rolls up that achievement’s completion (or eligibility) rules |
123
123
 
124
124
  **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. System seed only creates stored `points` — product computed metrics (e.g. `days`) are host-seeded after registering anchors.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/EventTypes/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAqCtD,wBAAgB,yBAAyB,IAAI,gBAAgB,CAoE5D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/EventTypes/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAqCtD,wBAAgB,yBAAyB,IAAI,gBAAgB,CA+E5D"}
@@ -41,6 +41,7 @@ export function buildEventTypesCollection() {
41
41
  'name',
42
42
  'slug',
43
43
  'requiresActor',
44
+ 'snapshotActorTiers',
44
45
  'requiresMetric',
45
46
  ...(subjectFields.length > 0 ? ['requiresSubject'] : []),
46
47
  'system',
@@ -66,6 +67,15 @@ export function buildEventTypesCollection() {
66
67
  description: 'Turn this on when someone else caused the entry (for example a peer nomination). We’ll ask for that person when it’s logged.',
67
68
  },
68
69
  },
70
+ {
71
+ name: 'snapshotActorTiers',
72
+ type: 'checkbox',
73
+ defaultValue: false,
74
+ label: 'Snapshot actor tiers',
75
+ admin: {
76
+ description: 'When this entry is logged, store the actor’s derived ladder rank at write time. Unscoped logs stamp every ladder; scoped logs stamp that tenant plus unscoped ranks. Event count can then require “actor was at least this tier” in the rule’s scope. Does not grant or open requests.',
77
+ },
78
+ },
69
79
  {
70
80
  name: 'requiresMetric',
71
81
  type: 'checkbox',
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Logs/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAqCtD,+EAA+E;AAC/E,wBAAgB,mBAAmB,IAAI,gBAAgB,CAuHtD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Logs/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAqCtD,+EAA+E;AAC/E,wBAAgB,mBAAmB,IAAI,gBAAgB,CAgJtD"}
@@ -119,6 +119,30 @@ export function buildLogsCollection() {
119
119
  ],
120
120
  },
121
121
  ...(subjectField ? [subjectField] : []),
122
+ {
123
+ name: 'actorTiers',
124
+ type: 'array',
125
+ admin: {
126
+ readOnly: true,
127
+ description: 'Derived ladder rank of the actor when this entry was written. Filled only when the event type snapshots actor tiers.',
128
+ },
129
+ fields: [
130
+ ...optionalFields(scopeField()),
131
+ {
132
+ name: 'tier',
133
+ type: 'relationship',
134
+ relationTo: collectionOf('tiers'),
135
+ required: true,
136
+ admin: { description: 'Which ladder step they held.' },
137
+ },
138
+ {
139
+ name: 'rank',
140
+ type: 'number',
141
+ required: true,
142
+ admin: { description: 'Ladder rank at write time (higher is further along).' },
143
+ },
144
+ ],
145
+ },
122
146
  {
123
147
  type: 'collapsible',
124
148
  label: 'Details',
@@ -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;AAqGpC;;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;AAgHpC;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,KAAK,EAAE,CAqB7C;AAED,mDAAmD;AACnD,wBAAgB,kBAAkB,IAAI,KAAK,EAAE,CAE5C"}
@@ -87,6 +87,16 @@ function ruleParamFields() {
87
87
  description: 'How many times it needs to have happened.',
88
88
  },
89
89
  },
90
+ {
91
+ name: 'actorTier',
92
+ type: 'relationship',
93
+ relationTo: collectionOf('tiers'),
94
+ filterOptions: catalogFilterOptions,
95
+ admin: {
96
+ condition: (_, sibling) => sibling?.type === 'event-count',
97
+ description: 'Only count events whose actor held at least this tier in this scope when the event was written. Requires the event type to snapshot actor tiers. Leave empty to count every matching event.',
98
+ },
99
+ },
90
100
  ];
91
101
  }
92
102
  function buildRuleLeafFields() {
@@ -59,6 +59,8 @@ export type AchievementSeedCatalogRow = {
59
59
  requiresMetric?: boolean;
60
60
  /** Event types only — when true, logging the event requires a polymorphic `subject`. */
61
61
  requiresSubject?: boolean;
62
+ /** Event types only — stamp the actor’s derived ladder ranks on the log at write time. */
63
+ snapshotActorTiers?: boolean;
62
64
  /**
63
65
  * Event types only — allowed host collections for `subject.relationTo`.
64
66
  * Empty / omit = any collection from plugin `subjects.collections`.
@@ -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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"}
@@ -75,6 +75,7 @@ async function upsertCatalog(payload, key, row) {
75
75
  requiresActor: row.requiresActor ?? false,
76
76
  requiresMetric: row.requiresMetric ?? false,
77
77
  requiresSubject: row.requiresSubject ?? false,
78
+ snapshotActorTiers: row.snapshotActorTiers ?? false,
78
79
  ...(row.subjectRelationTo && row.subjectRelationTo.length > 0
79
80
  ? { subjectRelationTo: row.subjectRelationTo }
80
81
  : { subjectRelationTo: [] }),
@@ -153,6 +154,14 @@ async function resolveRuleNode(payload, rule) {
153
154
  delete next.eventTypeSlug;
154
155
  }
155
156
  }
157
+ // Optional at-least floor against the log’s actor-tier snapshot.
158
+ if (!next.actorTier && typeof next.actorTierSlug === 'string') {
159
+ const id = await findIdBySlug(payload, 'tiers', next.actorTierSlug);
160
+ if (id) {
161
+ next.actorTier = id;
162
+ delete next.actorTierSlug;
163
+ }
164
+ }
156
165
  }
157
166
  if (next.type === 'metric-minimum') {
158
167
  const slug = typeof next.metricSlug === 'string' ? next.metricSlug : typeof next.metric === 'string' ? next.metric : '';
@@ -0,0 +1,40 @@
1
+ import type { Payload, PayloadRequest } from 'payload';
2
+ export type ActorTierSnapshotRow = {
3
+ scope?: string | null;
4
+ tier: string;
5
+ rank: number;
6
+ };
7
+ /**
8
+ * Unscoped logs stamp every ladder (a later scoped count can still see the actor’s rank).
9
+ * Scoped logs stamp that tenant plus any unscoped ranks.
10
+ */
11
+ export declare function scopeIdsForActorSnapshot(args: {
12
+ logScopeId: string | null;
13
+ ladderScopeIds: Array<string | null>;
14
+ }): Array<string | null>;
15
+ /** Normalize log `actorTiers` rows for matching. */
16
+ export declare function readActorTierSnapshotRows(value: unknown): Array<{
17
+ scopeId: string | null;
18
+ rank: number;
19
+ }>;
20
+ /**
21
+ * True when a snapshot includes this scope at rank ≥ `minimumRank`.
22
+ * Missing snapshot (legacy logs) never matches — the event was not stamped.
23
+ */
24
+ export declare function actorSnapshotMeetsTier(args: {
25
+ rows: unknown;
26
+ scopeId: string | null;
27
+ minimumRank: number;
28
+ }): boolean;
29
+ /**
30
+ * Read-only derived ladder ranks for an actor across configured scopes.
31
+ * Does not reconcile, grant, or open tier requests.
32
+ */
33
+ export declare function snapshotActorDerivedTiers(args: {
34
+ payload: Payload;
35
+ req?: PayloadRequest;
36
+ userId: string;
37
+ /** Log `scope` — `null` stamps every ladder scope. */
38
+ logScopeId?: string | null;
39
+ }): Promise<ActorTierSnapshotRow[]>;
40
+ //# sourceMappingURL=actorTierSnapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actorTierSnapshot.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/actorTierSnapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAQtD,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAyBD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,cAAc,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACrC,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAcvB;AAED,oDAAoD;AACpD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAAC;IAC/D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;CACb,CAAC,CAWD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;CACpB,GAAG,OAAO,CAIV;AAmDD;;;GAGG;AACH,wBAAsB,yBAAyB,CAAC,IAAI,EAAE;IACpD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CA0BlC"}
@@ -0,0 +1,138 @@
1
+ import { collectionOf } from '../../collections/helpers.js';
2
+ import { getAchievementOptions } from '../../options-store.js';
3
+ import { CTX_ACTOR_TIER_RESOLVE } from './contextFlags.js';
4
+ import { relationId } from './relationId.js';
5
+ import { resolveCurrentTier } from './resolveCurrentTier.js';
6
+ function resolveCache(req) {
7
+ if (!req)
8
+ return null;
9
+ const context = (req.context ?? {});
10
+ const existing = context[CTX_ACTOR_TIER_RESOLVE];
11
+ if (existing instanceof Map)
12
+ return existing;
13
+ const created = new Map();
14
+ req.context = { ...req.context, [CTX_ACTOR_TIER_RESOLVE]: created };
15
+ return created;
16
+ }
17
+ function cacheKey(userId, scopeId) {
18
+ return `${userId}:${scopeId ?? ''}`;
19
+ }
20
+ function pushUniqueScope(out, seen, id) {
21
+ const key = id ?? '';
22
+ if (seen.has(key))
23
+ return;
24
+ seen.add(key);
25
+ out.push(id);
26
+ }
27
+ /**
28
+ * Unscoped logs stamp every ladder (a later scoped count can still see the actor’s rank).
29
+ * Scoped logs stamp that tenant plus any unscoped ranks.
30
+ */
31
+ export function scopeIdsForActorSnapshot(args) {
32
+ const out = [];
33
+ const seen = new Set();
34
+ if (args.logScopeId == null) {
35
+ for (const id of args.ladderScopeIds)
36
+ pushUniqueScope(out, seen, id);
37
+ return out;
38
+ }
39
+ pushUniqueScope(out, seen, args.logScopeId);
40
+ for (const id of args.ladderScopeIds) {
41
+ if (id == null)
42
+ pushUniqueScope(out, seen, null);
43
+ }
44
+ return out;
45
+ }
46
+ /** Normalize log `actorTiers` rows for matching. */
47
+ export function readActorTierSnapshotRows(value) {
48
+ if (!Array.isArray(value))
49
+ return [];
50
+ const out = [];
51
+ for (const row of value) {
52
+ if (!row || typeof row !== 'object')
53
+ continue;
54
+ const rec = row;
55
+ const rank = Number(rec.rank);
56
+ if (!Number.isFinite(rank))
57
+ continue;
58
+ out.push({ scopeId: relationId(rec.scope) ?? null, rank });
59
+ }
60
+ return out;
61
+ }
62
+ /**
63
+ * True when a snapshot includes this scope at rank ≥ `minimumRank`.
64
+ * Missing snapshot (legacy logs) never matches — the event was not stamped.
65
+ */
66
+ export function actorSnapshotMeetsTier(args) {
67
+ return readActorTierSnapshotRows(args.rows).some((row) => row.scopeId === args.scopeId && row.rank >= args.minimumRank);
68
+ }
69
+ async function listLadderScopeIds(args) {
70
+ if (!getAchievementOptions().scope?.collection)
71
+ return [null];
72
+ const { docs } = await args.payload.find({
73
+ collection: collectionOf('tiers'),
74
+ depth: 0,
75
+ overrideAccess: true,
76
+ pagination: false,
77
+ req: args.req,
78
+ select: { scope: true },
79
+ });
80
+ const ids = new Set();
81
+ let unscoped = false;
82
+ for (const doc of docs) {
83
+ const scopeId = relationId(doc.scope);
84
+ if (scopeId)
85
+ ids.add(scopeId);
86
+ else
87
+ unscoped = true;
88
+ }
89
+ const out = [...ids];
90
+ if (unscoped || out.length === 0)
91
+ out.push(null);
92
+ return out;
93
+ }
94
+ async function currentTierMemoized(args) {
95
+ const cache = resolveCache(args.req);
96
+ const key = cacheKey(args.userId, args.scopeId);
97
+ const hit = cache?.get(key);
98
+ if (hit)
99
+ return hit;
100
+ const pending = resolveCurrentTier({
101
+ payload: args.payload,
102
+ req: args.req,
103
+ userId: args.userId,
104
+ scopeId: args.scopeId,
105
+ });
106
+ cache?.set(key, pending);
107
+ return pending;
108
+ }
109
+ /**
110
+ * Read-only derived ladder ranks for an actor across configured scopes.
111
+ * Does not reconcile, grant, or open tier requests.
112
+ */
113
+ export async function snapshotActorDerivedTiers(args) {
114
+ if (!args.userId)
115
+ return [];
116
+ const ladderScopeIds = await listLadderScopeIds({ payload: args.payload, req: args.req });
117
+ const scopeIds = scopeIdsForActorSnapshot({
118
+ logScopeId: args.logScopeId ?? null,
119
+ ladderScopeIds,
120
+ });
121
+ const rows = [];
122
+ for (const scopeId of scopeIds) {
123
+ const current = await currentTierMemoized({
124
+ payload: args.payload,
125
+ req: args.req,
126
+ userId: args.userId,
127
+ scopeId,
128
+ });
129
+ if (!current)
130
+ continue;
131
+ rows.push({
132
+ ...(scopeId ? { scope: scopeId } : {}),
133
+ tier: current.id,
134
+ rank: current.rank,
135
+ });
136
+ }
137
+ return rows;
138
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Payload, PayloadRequest } from 'payload';
2
2
  export declare const UNLOCK_REQUIREMENT_RELATION_FIELDS: {
3
3
  readonly tier: "tiers";
4
+ readonly actorTier: "tiers";
4
5
  readonly achievement: "achievements";
5
6
  readonly metric: "metrics";
6
7
  readonly eventType: "eventTypes";
@@ -1 +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"}
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;;;;;;CAMyC,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"}
@@ -4,6 +4,7 @@ import { evaluateRuleProgress, evaluateRules } from './evaluateRules.js';
4
4
  import { relationId } from './relationId.js';
5
5
  export const UNLOCK_REQUIREMENT_RELATION_FIELDS = {
6
6
  tier: 'tiers',
7
+ actorTier: 'tiers',
7
8
  achievement: 'achievements',
8
9
  metric: 'metrics',
9
10
  eventType: 'eventTypes',
@@ -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;AAqJlF,eAAO,MAAM,gBAAgB,EAAE,mBAAmB,EA6JjD,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;AA4OlF,eAAO,MAAM,gBAAgB,EAAE,mBAAmB,EA6IjD,CAAA"}
@@ -1,4 +1,5 @@
1
1
  import { collectionOf } from '../../collections/helpers.js';
2
+ import { actorSnapshotMeetsTier } from './actorTierSnapshot.js';
2
3
  import { relationId } from './relationId.js';
3
4
  import { resolveMetricValue } from './resolveMetricValue.js';
4
5
  import { userScopeIncludingUnscopedWhere } from './where.js';
@@ -52,6 +53,78 @@ async function resolveEventTypeId(args) {
52
53
  });
53
54
  return relationId(found.docs[0]);
54
55
  }
56
+ async function resolveActorTierMinimumRank(args) {
57
+ const fromRelation = relationId(args.rule.actorTier);
58
+ const slug = typeof args.rule.actorTierSlug === 'string' && args.rule.actorTierSlug
59
+ ? args.rule.actorTierSlug
60
+ : '';
61
+ if (!fromRelation && !slug)
62
+ return null;
63
+ if (fromRelation) {
64
+ try {
65
+ const doc = (await args.payload.findByID({
66
+ collection: collectionOf('tiers'),
67
+ id: fromRelation,
68
+ depth: 0,
69
+ overrideAccess: true,
70
+ select: { rank: true },
71
+ }));
72
+ if (doc && typeof doc.rank === 'number' && Number.isFinite(doc.rank))
73
+ return doc.rank;
74
+ }
75
+ catch {
76
+ /* fall through to slug */
77
+ }
78
+ }
79
+ if (!slug)
80
+ return null;
81
+ const found = await args.payload.find({
82
+ collection: collectionOf('tiers'),
83
+ depth: 0,
84
+ limit: 1,
85
+ overrideAccess: true,
86
+ where: { slug: { equals: slug } },
87
+ select: { rank: true },
88
+ });
89
+ const rank = found.docs[0]?.rank;
90
+ return typeof rank === 'number' && Number.isFinite(rank) ? rank : null;
91
+ }
92
+ async function countMatchingEventLogs(args) {
93
+ const eventTypeId = await resolveEventTypeId({ payload: args.payload, rule: args.rule });
94
+ if (!eventTypeId)
95
+ return 0;
96
+ const where = {
97
+ and: [
98
+ userScopeIncludingUnscopedWhere(args.userId, args.scopeId),
99
+ { type: { equals: eventTypeId } },
100
+ ],
101
+ };
102
+ const minimumRank = await resolveActorTierMinimumRank({
103
+ payload: args.payload,
104
+ rule: args.rule,
105
+ });
106
+ if (minimumRank == null) {
107
+ const result = await args.payload.count({
108
+ collection: collectionOf('logs'),
109
+ overrideAccess: true,
110
+ where,
111
+ });
112
+ return result.totalDocs;
113
+ }
114
+ const { docs } = await args.payload.find({
115
+ collection: collectionOf('logs'),
116
+ depth: 0,
117
+ overrideAccess: true,
118
+ pagination: false,
119
+ select: { actorTiers: true },
120
+ where,
121
+ });
122
+ return docs.filter((doc) => actorSnapshotMeetsTier({
123
+ rows: doc.actorTiers,
124
+ scopeId: args.scopeId,
125
+ minimumRank,
126
+ })).length;
127
+ }
55
128
  async function resolveAchievementRef(args) {
56
129
  let requiredId = relationId(args.rule.achievement);
57
130
  let requiredSlug = typeof args.rule.achievementSlug === 'string' ? args.rule.achievementSlug : '';
@@ -244,34 +317,16 @@ export const builtInRuleTypes = [
244
317
  {
245
318
  type: 'event-count',
246
319
  async evaluate({ payload, rule, userId, scopeId }) {
247
- const eventTypeId = await resolveEventTypeId({ payload, rule });
248
- if (!eventTypeId)
249
- return false;
250
320
  const needed = Number(rule.count ?? 1);
251
- const result = await payload.count({
252
- collection: collectionOf('logs'),
253
- overrideAccess: true,
254
- where: {
255
- and: [userScopeIncludingUnscopedWhere(userId, scopeId), { type: { equals: eventTypeId } }],
256
- },
257
- });
258
- return result.totalDocs >= needed;
321
+ const total = await countMatchingEventLogs({ payload, rule, userId, scopeId });
322
+ return total >= needed;
259
323
  },
260
324
  async progress({ payload, rule, userId, scopeId }) {
261
- const eventTypeId = await resolveEventTypeId({ payload, rule });
262
- if (!eventTypeId)
263
- return 0;
264
325
  const needed = Number(rule.count ?? 1);
265
326
  if (!(needed > 0))
266
327
  return 1;
267
- const result = await payload.count({
268
- collection: collectionOf('logs'),
269
- overrideAccess: true,
270
- where: {
271
- and: [userScopeIncludingUnscopedWhere(userId, scopeId), { type: { equals: eventTypeId } }],
272
- },
273
- });
274
- return clamp01(result.totalDocs / needed);
328
+ const total = await countMatchingEventLogs({ payload, rule, userId, scopeId });
329
+ return clamp01(total / needed);
275
330
  },
276
331
  },
277
332
  ];
@@ -2,4 +2,6 @@
2
2
  export declare const CTX_SKIP_GRANT_SIDE_EFFECTS = "achievementSkipGrantSideEffects";
3
3
  export declare const CTX_CASCADING_REVOKE = "achievementCascadingRevoke";
4
4
  export declare const CTX_SYSTEM_REQUEST = "achievementSystemRequest";
5
+ /** Memo for read-only {@link resolveCurrentTier} during actor-tier snapshots. */
6
+ export declare const CTX_ACTOR_TIER_RESOLVE = "achievementActorTierResolve";
5
7
  //# sourceMappingURL=contextFlags.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contextFlags.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/contextFlags.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,eAAO,MAAM,2BAA2B,oCAAoC,CAAA;AAC5E,eAAO,MAAM,oBAAoB,+BAA+B,CAAA;AAChE,eAAO,MAAM,kBAAkB,6BAA6B,CAAA"}
1
+ {"version":3,"file":"contextFlags.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/contextFlags.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,eAAO,MAAM,2BAA2B,oCAAoC,CAAA;AAC5E,eAAO,MAAM,oBAAoB,+BAA+B,CAAA;AAChE,eAAO,MAAM,kBAAkB,6BAA6B,CAAA;AAC5D,iFAAiF;AACjF,eAAO,MAAM,sBAAsB,gCAAgC,CAAA"}
@@ -2,3 +2,5 @@
2
2
  export const CTX_SKIP_GRANT_SIDE_EFFECTS = 'achievementSkipGrantSideEffects';
3
3
  export const CTX_CASCADING_REVOKE = 'achievementCascadingRevoke';
4
4
  export const CTX_SYSTEM_REQUEST = 'achievementSystemRequest';
5
+ /** Memo for read-only {@link resolveCurrentTier} during actor-tier snapshots. */
6
+ export const CTX_ACTOR_TIER_RESOLVE = 'achievementActorTierResolve';
@@ -3,6 +3,8 @@ export type EventTypeFlags = {
3
3
  requiresActor: boolean;
4
4
  requiresMetric: boolean;
5
5
  requiresSubject: boolean;
6
+ /** Stamp the actor's derived ladder ranks on the log at write time. */
7
+ snapshotActorTiers: boolean;
6
8
  /** Allowed host collections for `subject.relationTo` (empty = any plugin allowlist entry). */
7
9
  subjectRelationTo: string[];
8
10
  };
@@ -1 +1 @@
1
- {"version":3,"file":"eventTypeRequiresActor.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/eventTypeRequiresActor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAMtD,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,EAAE,OAAO,CAAA;IACtB,cAAc,EAAE,OAAO,CAAA;IACvB,eAAe,EAAE,OAAO,CAAA;IACxB,8FAA8F;IAC9F,iBAAiB,EAAE,MAAM,EAAE,CAAA;CAC5B,CAAA;AAkBD,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAsBtE;AAMD,wDAAwD;AACxD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,cAAc,CAAC,CAwC1B;AAED,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED,wBAAsB,wBAAwB,CAAC,IAAI,EAAE;IACnD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED;;;GAGG;AACH,wBAAsB,0BAA0B,CAAC,IAAI,EAAE;IACrD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAOpB"}
1
+ {"version":3,"file":"eventTypeRequiresActor.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/eventTypeRequiresActor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAMtD,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,EAAE,OAAO,CAAA;IACtB,cAAc,EAAE,OAAO,CAAA;IACvB,eAAe,EAAE,OAAO,CAAA;IACxB,uEAAuE;IACvE,kBAAkB,EAAE,OAAO,CAAA;IAC3B,8FAA8F;IAC9F,iBAAiB,EAAE,MAAM,EAAE,CAAA;CAC5B,CAAA;AAmBD,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAyBtE;AAMD,wDAAwD;AACxD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,cAAc,CAAC,CA2C1B;AAED,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED,wBAAsB,wBAAwB,CAAC,IAAI,EAAE;IACnD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED;;;GAGG;AACH,wBAAsB,0BAA0B,CAAC,IAAI,EAAE;IACrD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAOpB"}
@@ -5,6 +5,7 @@ const emptyFlags = {
5
5
  requiresActor: false,
6
6
  requiresMetric: false,
7
7
  requiresSubject: false,
8
+ snapshotActorTiers: false,
8
9
  subjectRelationTo: [],
9
10
  };
10
11
  function readSubjectRelationTo(value) {
@@ -25,6 +26,7 @@ export function readEventTypeFlags(doc) {
25
26
  const hasFlagKeys = 'requiresActor' in row ||
26
27
  'requiresMetric' in row ||
27
28
  'requiresSubject' in row ||
29
+ 'snapshotActorTiers' in row ||
28
30
  'subjectRelationTo' in row ||
29
31
  typeof row.slug === 'string';
30
32
  if (!hasFlagKeys)
@@ -33,6 +35,7 @@ export function readEventTypeFlags(doc) {
33
35
  requiresActor: Boolean(row.requiresActor),
34
36
  requiresMetric: Boolean(row.requiresMetric) || row.slug === 'metric.delta',
35
37
  requiresSubject: Boolean(row.requiresSubject),
38
+ snapshotActorTiers: Boolean(row.snapshotActorTiers),
36
39
  subjectRelationTo: readSubjectRelationTo(row.subjectRelationTo),
37
40
  };
38
41
  }
@@ -47,6 +50,7 @@ export async function getEventTypeFlags(args) {
47
50
  if ('requiresActor' in row ||
48
51
  'requiresMetric' in row ||
49
52
  'requiresSubject' in row ||
53
+ 'snapshotActorTiers' in row ||
50
54
  'subjectRelationTo' in row ||
51
55
  'slug' in row) {
52
56
  return embedded;
@@ -65,6 +69,7 @@ export async function getEventTypeFlags(args) {
65
69
  requiresActor: true,
66
70
  requiresMetric: true,
67
71
  requiresSubject: true,
72
+ snapshotActorTiers: true,
68
73
  subjectRelationTo: true,
69
74
  slug: true,
70
75
  },
@@ -1 +1 @@
1
- {"version":3,"file":"recordEvent.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/recordEvent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAUtD,OAAO,EAAE,KAAK,aAAa,EAAkB,MAAM,iBAAiB,CAAA;AAQpE,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CACtC,CAAA;AAgBD,6DAA6D;AAC7D,wBAAsB,SAAS,CAAC,IAAI,EAAE,cAAc,wEA0EnD;AAED,4CAA4C;AAC5C,eAAO,MAAM,WAAW,kBAAY,CAAA;AAEpC;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,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,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,wEAsDA;AAED,6FAA6F;AAC7F,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,iFA0DA"}
1
+ {"version":3,"file":"recordEvent.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/recordEvent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAUtD,OAAO,EAAE,KAAK,aAAa,EAAkB,MAAM,iBAAiB,CAAA;AASpE,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CACtC,CAAA;AAgBD,6DAA6D;AAC7D,wBAAsB,SAAS,CAAC,IAAI,EAAE,cAAc,wEAqFnD;AAED,4CAA4C;AAC5C,eAAO,MAAM,WAAW,kBAAY,CAAA;AAEpC;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,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,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,wEAsDA;AAED,6FAA6F;AAC7F,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,iFA0DA"}
@@ -8,6 +8,7 @@ import { applyMetricBalanceDelta } from './metricBalances.js';
8
8
  import { transitionLogData, transitionToId } from './logData.js';
9
9
  import { loadMetricDoc } from './resolveMetricValue.js';
10
10
  import { resolveCatalogId } from './resolveCatalog.js';
11
+ import { snapshotActorDerivedTiers } from './actorTierSnapshot.js';
11
12
  import { resolveCurrentTier } from './resolveCurrentTier.js';
12
13
  import { userScopeWhere } from './where.js';
13
14
  async function catalogId(args) {
@@ -65,6 +66,14 @@ export async function recordLog(args) {
65
66
  throw new APIError(`This log type only allows subjects from: ${allowed.join(', ')}.`, 400);
66
67
  }
67
68
  }
69
+ const actorTiers = flags.snapshotActorTiers && args.actorId
70
+ ? await snapshotActorDerivedTiers({
71
+ payload: args.payload,
72
+ req: args.req,
73
+ userId: args.actorId,
74
+ logScopeId: scopeId,
75
+ })
76
+ : undefined;
68
77
  return args.payload.create({
69
78
  collection: collectionOf('logs'),
70
79
  data: {
@@ -76,6 +85,7 @@ export async function recordLog(args) {
76
85
  change: args.change,
77
86
  reason: args.reason,
78
87
  ...(subject ? { subject } : {}),
88
+ ...(actorTiers && actorTiers.length > 0 ? { actorTiers } : {}),
79
89
  data: args.data ?? undefined,
80
90
  },
81
91
  overrideAccess: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dappermountain/payload-plugin-achievements",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
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",