@stamprally/server 0.10.0 → 0.12.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.
package/dist/index.d.cts CHANGED
@@ -9,7 +9,40 @@ interface UserClaimRecord {
9
9
  readonly ticketNumber: string;
10
10
  readonly timestamp: number;
11
11
  }
12
+ interface ClaimRewardTransactionParams {
13
+ readonly rallyId: string;
14
+ readonly userId: string;
15
+ readonly rewardId: string;
16
+ readonly ticketNumber: string;
17
+ readonly timestamp: number;
18
+ readonly idempotencyKey?: string;
19
+ readonly proofData?: unknown;
20
+ readonly idempotencyTtlMs?: number;
21
+ /** Used when the first claim is made before a user state has been stored. */
22
+ readonly initialUserState?: UserRallyState;
23
+ }
24
+ interface ClaimRewardTransactionMutation {
25
+ readonly nextStock: number | null;
26
+ readonly nextUserState: UserRallyState;
27
+ readonly auditLog: AuditLog;
28
+ /** The server response is persisted by the adapter with the idempotency key. */
29
+ readonly result?: unknown;
30
+ /** A domain rejection is committed as an audit/idempotency record without mutations. */
31
+ readonly error?: string;
32
+ }
12
33
  interface ServerPersistenceAdapter {
34
+ /**
35
+ * Atomically commits stock, user state, claim count, audit log, and idempotency data.
36
+ * Adapters must roll back every write when the callback or any commit operation fails.
37
+ */
38
+ executeClaimRewardTransaction(params: ClaimRewardTransactionParams, mutation: (current: {
39
+ readonly stock: number | null;
40
+ readonly claimCount: number;
41
+ readonly userState: UserRallyState;
42
+ }) => ClaimRewardTransactionMutation): Promise<{
43
+ readonly success: boolean;
44
+ readonly error?: string;
45
+ }>;
13
46
  acquireLock(rallyId: string, lockKey: string, ttlMs: number): Promise<boolean>;
14
47
  releaseLock(rallyId: string, lockKey: string): Promise<void>;
15
48
  getRewardStock(rallyId: string, rewardId: string): Promise<number | null>;
@@ -17,7 +50,6 @@ interface ServerPersistenceAdapter {
17
50
  readonly success: boolean;
18
51
  readonly remainingStock: number;
19
52
  }>;
20
- restoreRewardStock?(rallyId: string, rewardId: string): Promise<void>;
21
53
  getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null>;
22
54
  saveIdempotentResult<T>(rallyId: string, key: string, result: T, ttlMs: number): Promise<void>;
23
55
  getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number>;
@@ -27,6 +59,7 @@ interface ServerPersistenceAdapter {
27
59
  getUserState(rallyId: string, userId: string): Promise<UserRallyState | null>;
28
60
  saveUserState(rallyId: string, userId: string, state: UserRallyState): Promise<void>;
29
61
  recordAuditLog(log: AuditLog): Promise<void>;
62
+ removeAuditLog?(auditId: string): Promise<void>;
30
63
  }
31
64
  interface InMemoryServerPersistenceOptions {
32
65
  readonly stocks?: Readonly<Record<string, number>>;
@@ -41,20 +74,32 @@ declare class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapt
41
74
  success: boolean;
42
75
  remainingStock: number;
43
76
  }>;
44
- restoreRewardStock(rallyId: string, rewardId: string): Promise<void>;
77
+ restoreRewardStock(rallyId: string, rewardId: string, count?: number): Promise<void>;
78
+ executeClaimRewardTransaction(params: ClaimRewardTransactionParams, mutation: (current: {
79
+ readonly stock: number | null;
80
+ readonly claimCount: number;
81
+ readonly userState: UserRallyState;
82
+ }) => ClaimRewardTransactionMutation): Promise<{
83
+ readonly success: boolean;
84
+ readonly error?: string;
85
+ }>;
86
+ rollbackUserState(rallyId: string, userId: string, previousState: UserRallyState | null): Promise<void>;
45
87
  getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null>;
46
88
  saveIdempotentResult<T>(rallyId: string, key: string, result: T, ttlMs: number): Promise<void>;
47
89
  getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number>;
48
90
  getUserState(rallyId: string, userId: string): Promise<UserRallyState | null>;
49
91
  saveUserState(rallyId: string, userId: string, state: UserRallyState): Promise<void>;
50
92
  recordAuditLog(log: AuditLog): Promise<void>;
93
+ removeAuditLog(auditId: string): Promise<void>;
51
94
  getAuditLogs(): ReadonlyArray<AuditLog>;
52
95
  recordUserClaim(params: UserClaimRecord): Promise<void>;
96
+ rollbackUserClaim(rallyId: string, userId: string, rewardId: string, ticketNumber: string): Promise<void>;
53
97
  incrementUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<void>;
54
98
  getClaimRecords(): ReadonlyArray<UserClaimRecord>;
55
99
  /** @deprecated Use recordUserClaim with a ticket number and timestamp. */
56
100
  recordClaim(params: UserClaimRecord): Promise<void>;
57
101
  recordClaim(rallyId: string, userId: string, rewardId: string): Promise<void>;
102
+ runTransaction<T>(_rallyId: string, operation: (transaction: ServerPersistenceAdapter) => Promise<T>): Promise<T>;
58
103
  }
59
104
 
60
105
  type ErrorResponse = {
@@ -127,4 +172,4 @@ interface ServerOptions {
127
172
  readonly now?: () => string;
128
173
  }
129
174
 
130
- export { type AuditLog, type CheckInRequest, type CheckInResponse, type ClaimRewardRequest, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type ServerOptions, type ServerPersistenceAdapter, StampRallyServer, type UserClaimRecord };
175
+ export { type AuditLog, type CheckInRequest, type CheckInResponse, type ClaimRewardRequest, type ClaimRewardTransactionMutation, type ClaimRewardTransactionParams, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type ServerOptions, type ServerPersistenceAdapter, StampRallyServer, type UserClaimRecord };
package/dist/index.d.ts CHANGED
@@ -9,7 +9,40 @@ interface UserClaimRecord {
9
9
  readonly ticketNumber: string;
10
10
  readonly timestamp: number;
11
11
  }
12
+ interface ClaimRewardTransactionParams {
13
+ readonly rallyId: string;
14
+ readonly userId: string;
15
+ readonly rewardId: string;
16
+ readonly ticketNumber: string;
17
+ readonly timestamp: number;
18
+ readonly idempotencyKey?: string;
19
+ readonly proofData?: unknown;
20
+ readonly idempotencyTtlMs?: number;
21
+ /** Used when the first claim is made before a user state has been stored. */
22
+ readonly initialUserState?: UserRallyState;
23
+ }
24
+ interface ClaimRewardTransactionMutation {
25
+ readonly nextStock: number | null;
26
+ readonly nextUserState: UserRallyState;
27
+ readonly auditLog: AuditLog;
28
+ /** The server response is persisted by the adapter with the idempotency key. */
29
+ readonly result?: unknown;
30
+ /** A domain rejection is committed as an audit/idempotency record without mutations. */
31
+ readonly error?: string;
32
+ }
12
33
  interface ServerPersistenceAdapter {
34
+ /**
35
+ * Atomically commits stock, user state, claim count, audit log, and idempotency data.
36
+ * Adapters must roll back every write when the callback or any commit operation fails.
37
+ */
38
+ executeClaimRewardTransaction(params: ClaimRewardTransactionParams, mutation: (current: {
39
+ readonly stock: number | null;
40
+ readonly claimCount: number;
41
+ readonly userState: UserRallyState;
42
+ }) => ClaimRewardTransactionMutation): Promise<{
43
+ readonly success: boolean;
44
+ readonly error?: string;
45
+ }>;
13
46
  acquireLock(rallyId: string, lockKey: string, ttlMs: number): Promise<boolean>;
14
47
  releaseLock(rallyId: string, lockKey: string): Promise<void>;
15
48
  getRewardStock(rallyId: string, rewardId: string): Promise<number | null>;
@@ -17,7 +50,6 @@ interface ServerPersistenceAdapter {
17
50
  readonly success: boolean;
18
51
  readonly remainingStock: number;
19
52
  }>;
20
- restoreRewardStock?(rallyId: string, rewardId: string): Promise<void>;
21
53
  getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null>;
22
54
  saveIdempotentResult<T>(rallyId: string, key: string, result: T, ttlMs: number): Promise<void>;
23
55
  getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number>;
@@ -27,6 +59,7 @@ interface ServerPersistenceAdapter {
27
59
  getUserState(rallyId: string, userId: string): Promise<UserRallyState | null>;
28
60
  saveUserState(rallyId: string, userId: string, state: UserRallyState): Promise<void>;
29
61
  recordAuditLog(log: AuditLog): Promise<void>;
62
+ removeAuditLog?(auditId: string): Promise<void>;
30
63
  }
31
64
  interface InMemoryServerPersistenceOptions {
32
65
  readonly stocks?: Readonly<Record<string, number>>;
@@ -41,20 +74,32 @@ declare class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapt
41
74
  success: boolean;
42
75
  remainingStock: number;
43
76
  }>;
44
- restoreRewardStock(rallyId: string, rewardId: string): Promise<void>;
77
+ restoreRewardStock(rallyId: string, rewardId: string, count?: number): Promise<void>;
78
+ executeClaimRewardTransaction(params: ClaimRewardTransactionParams, mutation: (current: {
79
+ readonly stock: number | null;
80
+ readonly claimCount: number;
81
+ readonly userState: UserRallyState;
82
+ }) => ClaimRewardTransactionMutation): Promise<{
83
+ readonly success: boolean;
84
+ readonly error?: string;
85
+ }>;
86
+ rollbackUserState(rallyId: string, userId: string, previousState: UserRallyState | null): Promise<void>;
45
87
  getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null>;
46
88
  saveIdempotentResult<T>(rallyId: string, key: string, result: T, ttlMs: number): Promise<void>;
47
89
  getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number>;
48
90
  getUserState(rallyId: string, userId: string): Promise<UserRallyState | null>;
49
91
  saveUserState(rallyId: string, userId: string, state: UserRallyState): Promise<void>;
50
92
  recordAuditLog(log: AuditLog): Promise<void>;
93
+ removeAuditLog(auditId: string): Promise<void>;
51
94
  getAuditLogs(): ReadonlyArray<AuditLog>;
52
95
  recordUserClaim(params: UserClaimRecord): Promise<void>;
96
+ rollbackUserClaim(rallyId: string, userId: string, rewardId: string, ticketNumber: string): Promise<void>;
53
97
  incrementUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<void>;
54
98
  getClaimRecords(): ReadonlyArray<UserClaimRecord>;
55
99
  /** @deprecated Use recordUserClaim with a ticket number and timestamp. */
56
100
  recordClaim(params: UserClaimRecord): Promise<void>;
57
101
  recordClaim(rallyId: string, userId: string, rewardId: string): Promise<void>;
102
+ runTransaction<T>(_rallyId: string, operation: (transaction: ServerPersistenceAdapter) => Promise<T>): Promise<T>;
58
103
  }
59
104
 
60
105
  type ErrorResponse = {
@@ -127,4 +172,4 @@ interface ServerOptions {
127
172
  readonly now?: () => string;
128
173
  }
129
174
 
130
- export { type AuditLog, type CheckInRequest, type CheckInResponse, type ClaimRewardRequest, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type ServerOptions, type ServerPersistenceAdapter, StampRallyServer, type UserClaimRecord };
175
+ export { type AuditLog, type CheckInRequest, type CheckInResponse, type ClaimRewardRequest, type ClaimRewardTransactionMutation, type ClaimRewardTransactionParams, InMemoryServerPersistenceAdapter, type InMemoryServerPersistenceOptions, type ServerOptions, type ServerPersistenceAdapter, StampRallyServer, type UserClaimRecord };
package/dist/index.js CHANGED
@@ -53,10 +53,74 @@ var InMemoryServerPersistenceAdapter = class {
53
53
  this.#stocks.set(key, current - 1);
54
54
  return { success: true, remainingStock: current - 1 };
55
55
  }
56
- async restoreRewardStock(rallyId, rewardId) {
56
+ async restoreRewardStock(rallyId, rewardId, count = 1) {
57
57
  const key = this.#stockKey(rallyId, rewardId);
58
58
  const current = this.#stocks.get(key);
59
- if (current !== void 0) this.#stocks.set(key, current + 1);
59
+ if (current !== void 0) this.#stocks.set(key, current + Math.max(0, count));
60
+ }
61
+ async executeClaimRewardTransaction(params, mutation) {
62
+ try {
63
+ return await this.runTransaction(params.rallyId, async () => {
64
+ const userState = await this.getUserState(params.rallyId, params.userId) ?? params.initialUserState;
65
+ if (userState === void 0)
66
+ return { success: false, error: "A user state is required for this transaction." };
67
+ const mutationResult = mutation({
68
+ stock: await this.getRewardStock(params.rallyId, params.rewardId),
69
+ claimCount: await this.getUserClaimCount(params.rallyId, params.userId, params.rewardId),
70
+ userState
71
+ });
72
+ const idempotencyKey = params.idempotencyKey;
73
+ if (mutationResult.error !== void 0) {
74
+ await this.recordAuditLog(mutationResult.auditLog);
75
+ if (idempotencyKey !== void 0 && mutationResult.result !== void 0)
76
+ await this.saveIdempotentResult(
77
+ params.rallyId,
78
+ idempotencyKey,
79
+ mutationResult.result,
80
+ params.idempotencyTtlMs ?? 864e5
81
+ );
82
+ return { success: false, error: mutationResult.error };
83
+ }
84
+ const currentStock = await this.getRewardStock(params.rallyId, params.rewardId);
85
+ if (currentStock !== null && (mutationResult.nextStock === null || mutationResult.nextStock < 0))
86
+ throw new Error("The transaction produced an invalid stock value.");
87
+ if (currentStock === null && mutationResult.nextStock !== null)
88
+ throw new Error("The transaction changed an unlimited stock to a limited stock.");
89
+ if (mutationResult.nextStock !== null)
90
+ this.#stocks.set(
91
+ this.#stockKey(params.rallyId, params.rewardId),
92
+ mutationResult.nextStock
93
+ );
94
+ await this.saveUserState(params.rallyId, params.userId, mutationResult.nextUserState);
95
+ const reward = mutationResult.nextUserState.rewards.find(
96
+ (item) => item.rewardId === params.rewardId
97
+ );
98
+ if (reward?.claimTicketNumber !== void 0)
99
+ await this.recordUserClaim({
100
+ rallyId: params.rallyId,
101
+ userId: params.userId,
102
+ rewardId: params.rewardId,
103
+ ticketNumber: reward.claimTicketNumber,
104
+ timestamp: params.timestamp
105
+ });
106
+ await this.recordAuditLog(mutationResult.auditLog);
107
+ if (idempotencyKey !== void 0 && mutationResult.result !== void 0)
108
+ await this.saveIdempotentResult(
109
+ params.rallyId,
110
+ idempotencyKey,
111
+ mutationResult.result,
112
+ params.idempotencyTtlMs ?? 864e5
113
+ );
114
+ return { success: true };
115
+ });
116
+ } catch (error) {
117
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
118
+ }
119
+ }
120
+ async rollbackUserState(rallyId, userId, previousState) {
121
+ const key = `${rallyId}:${userId}`;
122
+ if (previousState === null) this.#states.delete(key);
123
+ else this.#states.set(key, structuredClone(previousState));
60
124
  }
61
125
  async getIdempotentResult(rallyId, key) {
62
126
  const value = this.#idempotent.get(this.#key(rallyId, key));
@@ -85,6 +149,10 @@ var InMemoryServerPersistenceAdapter = class {
85
149
  async recordAuditLog(log) {
86
150
  this.#auditLogs.push(structuredClone(log));
87
151
  }
152
+ async removeAuditLog(auditId) {
153
+ const index = this.#auditLogs.findIndex((log) => log.id === auditId);
154
+ if (index >= 0) this.#auditLogs.splice(index, 1);
155
+ }
88
156
  getAuditLogs() {
89
157
  return structuredClone(this.#auditLogs);
90
158
  }
@@ -94,6 +162,16 @@ var InMemoryServerPersistenceAdapter = class {
94
162
  this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);
95
163
  this.#claimRecords.push(structuredClone(params));
96
164
  }
165
+ async rollbackUserClaim(rallyId, userId, rewardId, ticketNumber) {
166
+ const key = `${rallyId}:${userId}:${rewardId}`;
167
+ const count = this.#claims.get(key) ?? 0;
168
+ if (count <= 1) this.#claims.delete(key);
169
+ else this.#claims.set(key, count - 1);
170
+ const index = this.#claimRecords.findIndex(
171
+ (record) => record.rallyId === rallyId && record.userId === userId && record.rewardId === rewardId && record.ticketNumber === ticketNumber
172
+ );
173
+ if (index >= 0) this.#claimRecords.splice(index, 1);
174
+ }
97
175
  async incrementUserClaimCount(rallyId, userId, rewardId) {
98
176
  const key = `${rallyId}:${userId}:${rewardId}`;
99
177
  this.#claims.set(key, (this.#claims.get(key) ?? 0) + 1);
@@ -111,6 +189,32 @@ var InMemoryServerPersistenceAdapter = class {
111
189
  } : paramsOrRallyId;
112
190
  return this.recordUserClaim(params);
113
191
  }
192
+ async runTransaction(_rallyId, operation) {
193
+ const snapshot = {
194
+ stocks: new Map(this.#stocks),
195
+ idempotent: new Map(this.#idempotent),
196
+ states: new Map(this.#states),
197
+ claims: new Map(this.#claims),
198
+ claimRecords: structuredClone(this.#claimRecords),
199
+ auditLogs: structuredClone(this.#auditLogs)
200
+ };
201
+ try {
202
+ return await operation(this);
203
+ } catch (error) {
204
+ this.#stocks.clear();
205
+ for (const [key, value] of snapshot.stocks) this.#stocks.set(key, value);
206
+ this.#idempotent.clear();
207
+ for (const [key, value] of snapshot.idempotent)
208
+ this.#idempotent.set(key, structuredClone(value));
209
+ this.#states.clear();
210
+ for (const [key, value] of snapshot.states) this.#states.set(key, structuredClone(value));
211
+ this.#claims.clear();
212
+ for (const [key, value] of snapshot.claims) this.#claims.set(key, value);
213
+ this.#claimRecords.splice(0, this.#claimRecords.length, ...snapshot.claimRecords);
214
+ this.#auditLogs.splice(0, this.#auditLogs.length, ...snapshot.auditLogs);
215
+ throw error;
216
+ }
217
+ }
114
218
  };
115
219
  function json(body, status = 200) {
116
220
  return new Response(JSON.stringify(body), {
@@ -342,88 +446,108 @@ var StampRallyServer = class {
342
446
  ))
343
447
  return { ok: false, code: "CONFLICT", message: "The reward is being claimed." };
344
448
  const timestamp = now(this.#options, request.now);
345
- let decremented = false;
346
449
  try {
347
450
  const checked = await this.#persistence.getIdempotentResult(
348
451
  request.rallyId,
349
452
  key
350
453
  );
351
454
  if (checked !== null) return checked;
352
- const current = await this.#persistence.getUserState(request.rallyId, request.userId) ?? initialState(this.#config, request.userId, timestamp);
353
- const claimCount = await this.#persistence.getUserClaimCount(
354
- request.rallyId,
355
- request.userId,
356
- reward.id
357
- );
358
- const storedReward = current.rewards.find((item) => item.rewardId === reward.id) ?? {
359
- rewardId: reward.id,
360
- status: "LOCKED"
361
- };
362
- const currentReward = reward.redemptionMethod === "server_claim" && storedReward.status === "CONSUMED" && (reward.userClaimLimit === void 0 || claimCount < reward.userClaimLimit) ? { ...storedReward, status: "AVAILABLE" } : storedReward;
363
- const local = consumeReward({
364
- reward,
365
- currentState: currentReward,
366
- now: timestamp,
367
- userRedemptionCount: claimCount,
368
- ...request.staffPasscode === void 0 ? {} : { inputPasscode: request.staffPasscode },
369
- ...request.staffId === void 0 ? {} : { staffId: request.staffId }
370
- });
371
- if (!local.ok)
372
- return this.#rememberClaim(
373
- key,
374
- { ok: false, code: local.error.code, message: "Reward cannot be claimed." },
375
- request,
376
- timestamp
377
- );
378
- const stock = await this.#persistence.decrementRewardStock(request.rallyId, reward.id);
379
- if (!stock.success)
380
- return this.#rememberClaim(
381
- key,
382
- { ok: false, code: "OUT_OF_STOCK", message: "Reward is out of stock." },
383
- request,
384
- timestamp
385
- );
386
- decremented = true;
387
- const next = {
388
- ...current,
389
- rewards: current.rewards.map((item) => item.rewardId === reward.id ? local.value : item),
390
- updatedAt: timestamp
391
- };
392
- try {
393
- await this.#persistence.saveUserState(request.rallyId, request.userId, next);
394
- const response = local.value.claimTicketNumber === void 0 ? { ok: true, state: next } : { ok: true, state: next, claimTicketNumber: local.value.claimTicketNumber };
395
- await this.#persistence.recordUserClaim({
455
+ const responseHolder = { value: null };
456
+ const result = await this.#persistence.executeClaimRewardTransaction(
457
+ {
396
458
  rallyId: request.rallyId,
397
459
  userId: request.userId,
398
460
  rewardId: reward.id,
399
- ticketNumber: local.value.claimTicketNumber ?? "",
400
- timestamp: Number.isNaN(Date.parse(timestamp)) ? Date.now() : Date.parse(timestamp)
401
- });
402
- await this.#persistence.recordAuditLog(
403
- audit(
461
+ ticketNumber: request.idempotencyKey,
462
+ timestamp: Number.isNaN(Date.parse(timestamp)) ? Date.now() : Date.parse(timestamp),
463
+ idempotencyKey: key,
464
+ ...request.staffPasscode === void 0 ? {} : { proofData: request.staffPasscode },
465
+ ...this.#options.idempotencyTtlMs === void 0 ? {} : { idempotencyTtlMs: this.#options.idempotencyTtlMs },
466
+ initialUserState: initialState(this.#config, request.userId, timestamp)
467
+ },
468
+ ({ stock, claimCount, userState }) => {
469
+ const storedReward = userState.rewards.find((item) => item.rewardId === reward.id) ?? {
470
+ rewardId: reward.id,
471
+ status: "LOCKED"
472
+ };
473
+ const currentReward = reward.redemptionMethod === "server_claim" && storedReward.status === "CONSUMED" && (reward.userClaimLimit === void 0 || claimCount < reward.userClaimLimit) ? { ...storedReward, status: "AVAILABLE" } : storedReward;
474
+ const makeAudit = (status, code) => audit(
404
475
  request.rallyId,
405
476
  request.userId,
406
477
  "CLAIM_REWARD",
407
478
  reward.id,
408
479
  request.idempotencyKey,
409
- "SUCCESS",
410
- timestamp
411
- )
412
- );
413
- await this.#persistence.saveIdempotentResult(
414
- request.rallyId,
415
- key,
416
- response,
417
- this.#options.idempotencyTtlMs ?? 864e5
418
- );
419
- return response;
420
- } catch (error) {
421
- await this.#restoreRewardStock(request.rallyId, reward.id);
422
- decremented = false;
423
- throw error;
480
+ status,
481
+ timestamp,
482
+ code
483
+ );
484
+ if (stock !== null && stock <= 0) {
485
+ responseHolder.value = {
486
+ ok: false,
487
+ code: "OUT_OF_STOCK",
488
+ message: "Reward is out of stock."
489
+ };
490
+ return {
491
+ nextStock: stock,
492
+ nextUserState: userState,
493
+ auditLog: makeAudit("REJECTED", "OUT_OF_STOCK"),
494
+ result: responseHolder.value,
495
+ error: "OUT_OF_STOCK"
496
+ };
497
+ }
498
+ const local = consumeReward({
499
+ reward,
500
+ currentState: currentReward,
501
+ now: timestamp,
502
+ userRedemptionCount: claimCount,
503
+ ...request.staffPasscode === void 0 ? {} : { inputPasscode: request.staffPasscode },
504
+ ...request.staffId === void 0 ? {} : { staffId: request.staffId }
505
+ });
506
+ if (!local.ok) {
507
+ responseHolder.value = {
508
+ ok: false,
509
+ code: local.error.code,
510
+ message: "Reward cannot be claimed."
511
+ };
512
+ return {
513
+ nextStock: stock,
514
+ nextUserState: userState,
515
+ auditLog: makeAudit("REJECTED", local.error.code),
516
+ result: responseHolder.value,
517
+ error: local.error.code
518
+ };
519
+ }
520
+ const nextRewards = userState.rewards.some((item) => item.rewardId === reward.id) ? userState.rewards.map((item) => item.rewardId === reward.id ? local.value : item) : [...userState.rewards, local.value];
521
+ const next = {
522
+ ...userState,
523
+ rewards: nextRewards,
524
+ updatedAt: timestamp
525
+ };
526
+ responseHolder.value = local.value.claimTicketNumber === void 0 ? { ok: true, state: next } : { ok: true, state: next, claimTicketNumber: local.value.claimTicketNumber };
527
+ return {
528
+ nextStock: stock === null ? null : stock - 1,
529
+ nextUserState: next,
530
+ auditLog: makeAudit("SUCCESS"),
531
+ result: responseHolder.value
532
+ };
533
+ }
534
+ );
535
+ const response = responseHolder.value;
536
+ if (!result.success) {
537
+ if (response !== null && !response.ok && response.code === result.error) return response;
538
+ return {
539
+ ok: false,
540
+ code: "PERSISTENCE_FAILED",
541
+ message: result.error ?? "Reward claim failed."
542
+ };
424
543
  }
544
+ if (response !== null && result.success) return response;
545
+ return {
546
+ ok: false,
547
+ code: "PERSISTENCE_FAILED",
548
+ message: result.error ?? "Reward claim failed."
549
+ };
425
550
  } catch (error) {
426
- if (decremented) await this.#restoreRewardStock(request.rallyId, reward.id);
427
551
  return {
428
552
  ok: false,
429
553
  code: "PERSISTENCE_FAILED",
@@ -491,10 +615,6 @@ var StampRallyServer = class {
491
615
  );
492
616
  return result;
493
617
  }
494
- async #restoreRewardStock(rallyId, rewardId) {
495
- if (this.#persistence.restoreRewardStock !== void 0)
496
- await this.#persistence.restoreRewardStock(rallyId, rewardId);
497
- }
498
618
  };
499
619
 
500
620
  export { InMemoryServerPersistenceAdapter, StampRallyServer };