@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.cjs +192 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -3
- package/dist/index.d.ts +48 -3
- package/dist/index.js +192 -72
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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 +
|
|
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
|
|
353
|
-
const
|
|
354
|
-
|
|
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:
|
|
400
|
-
timestamp: Number.isNaN(Date.parse(timestamp)) ? Date.now() : Date.parse(timestamp)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
410
|
-
timestamp
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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 };
|