@stamprally/server 0.11.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.cjs +163 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -6
- package/dist/index.d.ts +42 -6
- package/dist/index.js +163 -106
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -9,9 +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 {
|
|
13
|
-
/**
|
|
14
|
-
|
|
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
|
+
}>;
|
|
15
46
|
acquireLock(rallyId: string, lockKey: string, ttlMs: number): Promise<boolean>;
|
|
16
47
|
releaseLock(rallyId: string, lockKey: string): Promise<void>;
|
|
17
48
|
getRewardStock(rallyId: string, rewardId: string): Promise<number | null>;
|
|
@@ -19,9 +50,6 @@ interface ServerPersistenceAdapter {
|
|
|
19
50
|
readonly success: boolean;
|
|
20
51
|
readonly remainingStock: number;
|
|
21
52
|
}>;
|
|
22
|
-
restoreRewardStock?(rallyId: string, rewardId: string, count?: number): Promise<void>;
|
|
23
|
-
rollbackUserState?(rallyId: string, userId: string, previousState: UserRallyState | null): Promise<void>;
|
|
24
|
-
rollbackUserClaim?(rallyId: string, userId: string, rewardId: string, ticketNumber: string): Promise<void>;
|
|
25
53
|
getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null>;
|
|
26
54
|
saveIdempotentResult<T>(rallyId: string, key: string, result: T, ttlMs: number): Promise<void>;
|
|
27
55
|
getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number>;
|
|
@@ -47,6 +75,14 @@ declare class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapt
|
|
|
47
75
|
remainingStock: number;
|
|
48
76
|
}>;
|
|
49
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
|
+
}>;
|
|
50
86
|
rollbackUserState(rallyId: string, userId: string, previousState: UserRallyState | null): Promise<void>;
|
|
51
87
|
getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null>;
|
|
52
88
|
saveIdempotentResult<T>(rallyId: string, key: string, result: T, ttlMs: number): Promise<void>;
|
|
@@ -136,4 +172,4 @@ interface ServerOptions {
|
|
|
136
172
|
readonly now?: () => string;
|
|
137
173
|
}
|
|
138
174
|
|
|
139
|
-
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,9 +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 {
|
|
13
|
-
/**
|
|
14
|
-
|
|
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
|
+
}>;
|
|
15
46
|
acquireLock(rallyId: string, lockKey: string, ttlMs: number): Promise<boolean>;
|
|
16
47
|
releaseLock(rallyId: string, lockKey: string): Promise<void>;
|
|
17
48
|
getRewardStock(rallyId: string, rewardId: string): Promise<number | null>;
|
|
@@ -19,9 +50,6 @@ interface ServerPersistenceAdapter {
|
|
|
19
50
|
readonly success: boolean;
|
|
20
51
|
readonly remainingStock: number;
|
|
21
52
|
}>;
|
|
22
|
-
restoreRewardStock?(rallyId: string, rewardId: string, count?: number): Promise<void>;
|
|
23
|
-
rollbackUserState?(rallyId: string, userId: string, previousState: UserRallyState | null): Promise<void>;
|
|
24
|
-
rollbackUserClaim?(rallyId: string, userId: string, rewardId: string, ticketNumber: string): Promise<void>;
|
|
25
53
|
getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null>;
|
|
26
54
|
saveIdempotentResult<T>(rallyId: string, key: string, result: T, ttlMs: number): Promise<void>;
|
|
27
55
|
getUserClaimCount(rallyId: string, userId: string, rewardId: string): Promise<number>;
|
|
@@ -47,6 +75,14 @@ declare class InMemoryServerPersistenceAdapter implements ServerPersistenceAdapt
|
|
|
47
75
|
remainingStock: number;
|
|
48
76
|
}>;
|
|
49
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
|
+
}>;
|
|
50
86
|
rollbackUserState(rallyId: string, userId: string, previousState: UserRallyState | null): Promise<void>;
|
|
51
87
|
getIdempotentResult<T>(rallyId: string, key: string): Promise<T | null>;
|
|
52
88
|
saveIdempotentResult<T>(rallyId: string, key: string, result: T, ttlMs: number): Promise<void>;
|
|
@@ -136,4 +172,4 @@ interface ServerOptions {
|
|
|
136
172
|
readonly now?: () => string;
|
|
137
173
|
}
|
|
138
174
|
|
|
139
|
-
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
|
@@ -58,6 +58,65 @@ var InMemoryServerPersistenceAdapter = class {
|
|
|
58
58
|
const current = this.#stocks.get(key);
|
|
59
59
|
if (current !== void 0) this.#stocks.set(key, current + Math.max(0, count));
|
|
60
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
|
+
}
|
|
61
120
|
async rollbackUserState(rallyId, userId, previousState) {
|
|
62
121
|
const key = `${rallyId}:${userId}`;
|
|
63
122
|
if (previousState === null) this.#states.delete(key);
|
|
@@ -365,16 +424,11 @@ var StampRallyServer = class {
|
|
|
365
424
|
}
|
|
366
425
|
}
|
|
367
426
|
async claimReward(request) {
|
|
368
|
-
if (this.#persistence.runTransaction !== void 0)
|
|
369
|
-
return this.#persistence.runTransaction(
|
|
370
|
-
request.rallyId,
|
|
371
|
-
(transaction) => this.#claimRewardMutation(request, transaction)
|
|
372
|
-
);
|
|
373
|
-
return this.#claimRewardMutation(request, this.#persistence);
|
|
374
|
-
}
|
|
375
|
-
async #claimRewardMutation(request, persistence) {
|
|
376
427
|
const key = `claim:${request.rallyId}:${request.userId}:${request.rewardId}:${request.idempotencyKey}`;
|
|
377
|
-
const previous = await persistence.getIdempotentResult(
|
|
428
|
+
const previous = await this.#persistence.getIdempotentResult(
|
|
429
|
+
request.rallyId,
|
|
430
|
+
key
|
|
431
|
+
);
|
|
378
432
|
if (previous !== null) return previous;
|
|
379
433
|
const reward = this.#config.rewards.find((item) => item.id === request.rewardId);
|
|
380
434
|
if (reward === void 0)
|
|
@@ -382,118 +436,125 @@ var StampRallyServer = class {
|
|
|
382
436
|
key,
|
|
383
437
|
{ ok: false, code: "REWARD_NOT_FOUND", message: "Reward was not found." },
|
|
384
438
|
request,
|
|
385
|
-
now(this.#options, request.now)
|
|
386
|
-
persistence
|
|
439
|
+
now(this.#options, request.now)
|
|
387
440
|
);
|
|
388
441
|
const lockKey = `reward:${request.rallyId}:${reward.id}`;
|
|
389
|
-
if (!await persistence.acquireLock(
|
|
442
|
+
if (!await this.#persistence.acquireLock(
|
|
443
|
+
request.rallyId,
|
|
444
|
+
lockKey,
|
|
445
|
+
this.#options.lockTtlMs ?? 5e3
|
|
446
|
+
))
|
|
390
447
|
return { ok: false, code: "CONFLICT", message: "The reward is being claimed." };
|
|
391
448
|
const timestamp = now(this.#options, request.now);
|
|
392
|
-
let decremented = false;
|
|
393
|
-
let previousState = null;
|
|
394
|
-
let recordedTicket = null;
|
|
395
|
-
let successAuditId = null;
|
|
396
449
|
try {
|
|
397
|
-
const checked = await persistence.getIdempotentResult(
|
|
398
|
-
if (checked !== null) return checked;
|
|
399
|
-
const storedState = await persistence.getUserState(request.rallyId, request.userId);
|
|
400
|
-
previousState = storedState;
|
|
401
|
-
const current = storedState ?? initialState(this.#config, request.userId, timestamp);
|
|
402
|
-
const claimCount = await persistence.getUserClaimCount(
|
|
450
|
+
const checked = await this.#persistence.getIdempotentResult(
|
|
403
451
|
request.rallyId,
|
|
404
|
-
|
|
405
|
-
reward.id
|
|
452
|
+
key
|
|
406
453
|
);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const currentReward = reward.redemptionMethod === "server_claim" && storedReward.status === "CONSUMED" && (reward.userClaimLimit === void 0 || claimCount < reward.userClaimLimit) ? { ...storedReward, status: "AVAILABLE" } : storedReward;
|
|
412
|
-
const local = consumeReward({
|
|
413
|
-
reward,
|
|
414
|
-
currentState: currentReward,
|
|
415
|
-
now: timestamp,
|
|
416
|
-
userRedemptionCount: claimCount,
|
|
417
|
-
...request.staffPasscode === void 0 ? {} : { inputPasscode: request.staffPasscode },
|
|
418
|
-
...request.staffId === void 0 ? {} : { staffId: request.staffId }
|
|
419
|
-
});
|
|
420
|
-
if (!local.ok)
|
|
421
|
-
return this.#rememberClaim(
|
|
422
|
-
key,
|
|
423
|
-
{ ok: false, code: local.error.code, message: "Reward cannot be claimed." },
|
|
424
|
-
request,
|
|
425
|
-
timestamp,
|
|
426
|
-
persistence
|
|
427
|
-
);
|
|
428
|
-
const stock = await persistence.decrementRewardStock(request.rallyId, reward.id);
|
|
429
|
-
if (!stock.success)
|
|
430
|
-
return this.#rememberClaim(
|
|
431
|
-
key,
|
|
432
|
-
{ ok: false, code: "OUT_OF_STOCK", message: "Reward is out of stock." },
|
|
433
|
-
request,
|
|
434
|
-
timestamp,
|
|
435
|
-
persistence
|
|
436
|
-
);
|
|
437
|
-
decremented = true;
|
|
438
|
-
const next = {
|
|
439
|
-
...current,
|
|
440
|
-
rewards: current.rewards.map((item) => item.rewardId === reward.id ? local.value : item),
|
|
441
|
-
updatedAt: timestamp
|
|
442
|
-
};
|
|
443
|
-
try {
|
|
444
|
-
await persistence.saveUserState(request.rallyId, request.userId, next);
|
|
445
|
-
const response = local.value.claimTicketNumber === void 0 ? { ok: true, state: next } : { ok: true, state: next, claimTicketNumber: local.value.claimTicketNumber };
|
|
446
|
-
recordedTicket = local.value.claimTicketNumber ?? "";
|
|
447
|
-
await persistence.recordUserClaim({
|
|
454
|
+
if (checked !== null) return checked;
|
|
455
|
+
const responseHolder = { value: null };
|
|
456
|
+
const result = await this.#persistence.executeClaimRewardTransaction(
|
|
457
|
+
{
|
|
448
458
|
rallyId: request.rallyId,
|
|
449
459
|
userId: request.userId,
|
|
450
460
|
rewardId: reward.id,
|
|
451
|
-
ticketNumber:
|
|
452
|
-
timestamp: Number.isNaN(Date.parse(timestamp)) ? Date.now() : Date.parse(timestamp)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
request.userId,
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
await persistence.saveIdempotentResult(
|
|
466
|
-
request.rallyId,
|
|
467
|
-
key,
|
|
468
|
-
response,
|
|
469
|
-
this.#options.idempotencyTtlMs ?? 864e5
|
|
470
|
-
);
|
|
471
|
-
return response;
|
|
472
|
-
} catch (error) {
|
|
473
|
-
if (recordedTicket !== null && persistence.rollbackUserClaim !== void 0)
|
|
474
|
-
await persistence.rollbackUserClaim(
|
|
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(
|
|
475
475
|
request.rallyId,
|
|
476
476
|
request.userId,
|
|
477
|
+
"CLAIM_REWARD",
|
|
477
478
|
reward.id,
|
|
478
|
-
|
|
479
|
+
request.idempotencyKey,
|
|
480
|
+
status,
|
|
481
|
+
timestamp,
|
|
482
|
+
code
|
|
479
483
|
);
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
+
};
|
|
487
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
|
+
};
|
|
488
550
|
} catch (error) {
|
|
489
|
-
if (decremented) await this.#restoreRewardStock(persistence, request.rallyId, reward.id);
|
|
490
551
|
return {
|
|
491
552
|
ok: false,
|
|
492
553
|
code: "PERSISTENCE_FAILED",
|
|
493
554
|
message: error instanceof Error ? error.message : "Reward claim failed."
|
|
494
555
|
};
|
|
495
556
|
} finally {
|
|
496
|
-
await persistence.releaseLock(request.rallyId, lockKey);
|
|
557
|
+
await this.#persistence.releaseLock(request.rallyId, lockKey);
|
|
497
558
|
}
|
|
498
559
|
}
|
|
499
560
|
async sync(rallyId, userId) {
|
|
@@ -533,8 +594,8 @@ var StampRallyServer = class {
|
|
|
533
594
|
);
|
|
534
595
|
return result;
|
|
535
596
|
}
|
|
536
|
-
async #rememberClaim(key, result, request, timestamp
|
|
537
|
-
await persistence.recordAuditLog(
|
|
597
|
+
async #rememberClaim(key, result, request, timestamp) {
|
|
598
|
+
await this.#persistence.recordAuditLog(
|
|
538
599
|
audit(
|
|
539
600
|
request.rallyId,
|
|
540
601
|
request.userId,
|
|
@@ -546,7 +607,7 @@ var StampRallyServer = class {
|
|
|
546
607
|
result.ok ? void 0 : result.code
|
|
547
608
|
)
|
|
548
609
|
);
|
|
549
|
-
await persistence.saveIdempotentResult(
|
|
610
|
+
await this.#persistence.saveIdempotentResult(
|
|
550
611
|
request.rallyId,
|
|
551
612
|
key,
|
|
552
613
|
result,
|
|
@@ -554,10 +615,6 @@ var StampRallyServer = class {
|
|
|
554
615
|
);
|
|
555
616
|
return result;
|
|
556
617
|
}
|
|
557
|
-
async #restoreRewardStock(persistence, rallyId, rewardId) {
|
|
558
|
-
if (persistence.restoreRewardStock !== void 0)
|
|
559
|
-
await persistence.restoreRewardStock(rallyId, rewardId);
|
|
560
|
-
}
|
|
561
618
|
};
|
|
562
619
|
|
|
563
620
|
export { InMemoryServerPersistenceAdapter, StampRallyServer };
|