@dappermountain/payload-plugin-achievements 0.4.2 → 0.5.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.
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 +103 -29
  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;AA4PlF,eAAO,MAAM,gBAAgB,EAAE,mBAAmB,EAiJjD,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';
@@ -19,6 +20,7 @@ async function resolveMetricId(args) {
19
20
  depth: 0,
20
21
  limit: 1,
21
22
  overrideAccess: true,
23
+ req: args.req,
22
24
  where: { slug: { equals: metricSlug } },
23
25
  });
24
26
  return relationId(found.docs[0]);
@@ -33,6 +35,7 @@ async function resolveEventTypeId(args) {
33
35
  depth: 0,
34
36
  limit: 1,
35
37
  overrideAccess: true,
38
+ req: args.req,
36
39
  where: { id: { equals: eventTypeId } },
37
40
  });
38
41
  if (relationId(byId.docs[0]))
@@ -48,10 +51,92 @@ async function resolveEventTypeId(args) {
48
51
  depth: 0,
49
52
  limit: 1,
50
53
  overrideAccess: true,
54
+ req: args.req,
51
55
  where: { slug: { equals: slugCandidate } },
52
56
  });
53
57
  return relationId(found.docs[0]);
54
58
  }
59
+ async function resolveActorTierMinimumRank(args) {
60
+ const fromRelation = relationId(args.rule.actorTier);
61
+ const slug = typeof args.rule.actorTierSlug === 'string' && args.rule.actorTierSlug
62
+ ? args.rule.actorTierSlug
63
+ : '';
64
+ if (!fromRelation && !slug)
65
+ return null;
66
+ if (fromRelation) {
67
+ try {
68
+ const doc = (await args.payload.findByID({
69
+ collection: collectionOf('tiers'),
70
+ id: fromRelation,
71
+ depth: 0,
72
+ overrideAccess: true,
73
+ req: args.req,
74
+ select: { rank: true },
75
+ }));
76
+ if (doc && typeof doc.rank === 'number' && Number.isFinite(doc.rank))
77
+ return doc.rank;
78
+ }
79
+ catch {
80
+ /* fall through to slug */
81
+ }
82
+ }
83
+ if (!slug)
84
+ return null;
85
+ const found = await args.payload.find({
86
+ collection: collectionOf('tiers'),
87
+ depth: 0,
88
+ limit: 1,
89
+ overrideAccess: true,
90
+ req: args.req,
91
+ where: { slug: { equals: slug } },
92
+ select: { rank: true },
93
+ });
94
+ const rank = found.docs[0]?.rank;
95
+ return typeof rank === 'number' && Number.isFinite(rank) ? rank : null;
96
+ }
97
+ async function countMatchingEventLogs(args) {
98
+ const eventTypeId = await resolveEventTypeId({
99
+ payload: args.payload,
100
+ req: args.req,
101
+ rule: args.rule,
102
+ });
103
+ if (!eventTypeId)
104
+ return 0;
105
+ const where = {
106
+ and: [
107
+ userScopeIncludingUnscopedWhere(args.userId, args.scopeId),
108
+ { type: { equals: eventTypeId } },
109
+ ],
110
+ };
111
+ const minimumRank = await resolveActorTierMinimumRank({
112
+ payload: args.payload,
113
+ req: args.req,
114
+ rule: args.rule,
115
+ });
116
+ if (minimumRank == null) {
117
+ const result = await args.payload.count({
118
+ collection: collectionOf('logs'),
119
+ overrideAccess: true,
120
+ req: args.req,
121
+ where,
122
+ });
123
+ return result.totalDocs;
124
+ }
125
+ const { docs } = await args.payload.find({
126
+ collection: collectionOf('logs'),
127
+ depth: 0,
128
+ overrideAccess: true,
129
+ pagination: false,
130
+ req: args.req,
131
+ select: { actorTiers: true },
132
+ where,
133
+ });
134
+ return docs.filter((doc) => actorSnapshotMeetsTier({
135
+ rows: doc.actorTiers,
136
+ scopeId: args.scopeId,
137
+ minimumRank,
138
+ })).length;
139
+ }
55
140
  async function resolveAchievementRef(args) {
56
141
  let requiredId = relationId(args.rule.achievement);
57
142
  let requiredSlug = typeof args.rule.achievementSlug === 'string' ? args.rule.achievementSlug : '';
@@ -63,6 +148,7 @@ async function resolveAchievementRef(args) {
63
148
  depth: 0,
64
149
  limit: 1,
65
150
  overrideAccess: true,
151
+ req: args.req,
66
152
  where: args.scopeId
67
153
  ? {
68
154
  and: [{ slug: { equals: requiredSlug } }, { scope: { equals: args.scopeId } }],
@@ -81,6 +167,7 @@ async function resolveAchievementRef(args) {
81
167
  id: requiredId,
82
168
  depth: 0,
83
169
  overrideAccess: true,
170
+ req: args.req,
84
171
  }));
85
172
  requiredSlug = requiredSlug || String(doc?.slug ?? '');
86
173
  eligibilityRules = eligibilityRules ?? doc?.eligibilityRules;
@@ -101,6 +188,7 @@ async function isAchievementGranted(args) {
101
188
  depth: args.requiredSlug && !args.requiredId ? 1 : 0,
102
189
  limit: args.requiredId ? 1 : 50,
103
190
  overrideAccess: true,
191
+ req: args.req,
104
192
  where: { and: grantClauses },
105
193
  });
106
194
  if (args.requiredId)
@@ -126,6 +214,7 @@ export const builtInRuleTypes = [
126
214
  id: tierId,
127
215
  depth: 0,
128
216
  overrideAccess: true,
217
+ req,
129
218
  }));
130
219
  requiredRank = tier?.rank;
131
220
  }
@@ -138,6 +227,7 @@ export const builtInRuleTypes = [
138
227
  depth: 0,
139
228
  limit: 1,
140
229
  overrideAccess: true,
230
+ req,
141
231
  where: scopeId
142
232
  ? { and: [{ slug: { equals: tierSlug } }, { scope: { equals: scopeId } }] }
143
233
  : { slug: { equals: tierSlug } },
@@ -162,10 +252,11 @@ export const builtInRuleTypes = [
162
252
  },
163
253
  {
164
254
  type: 'achievement-complete',
165
- async evaluate({ payload, rule, userId, scopeId }) {
166
- const ref = await resolveAchievementRef({ payload, rule, scopeId });
255
+ async evaluate({ payload, req, rule, userId, scopeId }) {
256
+ const ref = await resolveAchievementRef({ payload, req, rule, scopeId });
167
257
  return isAchievementGranted({
168
258
  payload,
259
+ req,
169
260
  userId,
170
261
  scopeId,
171
262
  requiredId: ref.id,
@@ -174,11 +265,12 @@ export const builtInRuleTypes = [
174
265
  },
175
266
  async progress(args) {
176
267
  const { payload, req, rule, userId, scopeId, ladderRank, progressVisited } = args;
177
- const ref = await resolveAchievementRef({ payload, rule, scopeId });
268
+ const ref = await resolveAchievementRef({ payload, req, rule, scopeId });
178
269
  if (!ref.id && !ref.slug)
179
270
  return 0;
180
271
  const granted = await isAchievementGranted({
181
272
  payload,
273
+ req,
182
274
  userId,
183
275
  scopeId,
184
276
  requiredId: ref.id,
@@ -211,7 +303,7 @@ export const builtInRuleTypes = [
211
303
  {
212
304
  type: 'metric-minimum',
213
305
  async evaluate({ payload, req, rule, userId, scopeId }) {
214
- const resolvedId = await resolveMetricId({ payload, rule });
306
+ const resolvedId = await resolveMetricId({ payload, req, rule });
215
307
  if (!resolvedId)
216
308
  return false;
217
309
  const minimum = Number(rule.minimum ?? 0);
@@ -225,7 +317,7 @@ export const builtInRuleTypes = [
225
317
  return value >= minimum;
226
318
  },
227
319
  async progress({ payload, req, rule, userId, scopeId }) {
228
- const resolvedId = await resolveMetricId({ payload, rule });
320
+ const resolvedId = await resolveMetricId({ payload, req, rule });
229
321
  if (!resolvedId)
230
322
  return 0;
231
323
  const minimum = Number(rule.minimum ?? 0);
@@ -243,35 +335,17 @@ export const builtInRuleTypes = [
243
335
  },
244
336
  {
245
337
  type: 'event-count',
246
- async evaluate({ payload, rule, userId, scopeId }) {
247
- const eventTypeId = await resolveEventTypeId({ payload, rule });
248
- if (!eventTypeId)
249
- return false;
338
+ async evaluate({ payload, req, rule, userId, scopeId }) {
250
339
  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;
340
+ const total = await countMatchingEventLogs({ payload, req, rule, userId, scopeId });
341
+ return total >= needed;
259
342
  },
260
- async progress({ payload, rule, userId, scopeId }) {
261
- const eventTypeId = await resolveEventTypeId({ payload, rule });
262
- if (!eventTypeId)
263
- return 0;
343
+ async progress({ payload, req, rule, userId, scopeId }) {
264
344
  const needed = Number(rule.count ?? 1);
265
345
  if (!(needed > 0))
266
346
  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);
347
+ const total = await countMatchingEventLogs({ payload, req, rule, userId, scopeId });
348
+ return clamp01(total / needed);
275
349
  },
276
350
  },
277
351
  ];
@@ -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.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",