fix(housekeeping): coordinate all rank mutation paths
This commit is contained in:
1 parent
3d53321575
commit
866f38818b
14 files changed
+1070
-317
No files matched your search
+61
-1
@@ -1,5 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import crypto from "node:crypto";
|
||||
import { and, eq, max, sql } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -7,7 +8,12 @@ import { auth } from "@/lib/auth";
|
||||
import { db, User, UsersBadges, WebsiteShopArticles } from "@/lib/db";
|
||||
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||
import { logServerError } from "@/lib/server-log";
|
||||
import { logAudit } from "@/lib/services/audit";
|
||||
import { creditsPerUnit } from "@/lib/services/paypal";
|
||||
import {
|
||||
lockConfiguredRank,
|
||||
rankAssignmentCoordinator,
|
||||
} from "@/lib/services/rank-assignment";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
import { currencyDb, sendCurrency } from "@/lib/services/send-currency";
|
||||
|
||||
@@ -118,8 +124,38 @@ export async function buyShopArticle(formData: FormData): Promise<void> {
|
||||
outcome = "credits";
|
||||
} else {
|
||||
const badgeCodes = parseBadgeCodes(article.badges);
|
||||
let rankChanged = false;
|
||||
const rankRecoveryId = crypto.randomUUID();
|
||||
const rankAudit = {
|
||||
userId,
|
||||
action: "system.external-sync",
|
||||
target: "rcon.set-rank",
|
||||
targetId: userId,
|
||||
correlationId: rankRecoveryId,
|
||||
domain: "system" as const,
|
||||
after: {
|
||||
kind: "set-rank",
|
||||
operation: "rcon.set-rank",
|
||||
userId,
|
||||
rank: article.giveRank,
|
||||
},
|
||||
};
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
if (
|
||||
article.giveRank != null &&
|
||||
article.giveRank > 0 &&
|
||||
!(await lockConfiguredRank(tx, article.giveRank))
|
||||
) {
|
||||
throw new Error("Package rank no longer exists");
|
||||
}
|
||||
const [lockedBuyer] = await tx
|
||||
.select({ credits: User.credits, rank: User.rank })
|
||||
.from(User)
|
||||
.where(eq(User.id, userId))
|
||||
.for("update");
|
||||
if (!lockedBuyer || lockedBuyer.credits < price)
|
||||
throw new Error("Insufficient credits");
|
||||
if (price > 0) {
|
||||
await tx
|
||||
.update(User)
|
||||
@@ -130,12 +166,14 @@ export async function buyShopArticle(formData: FormData): Promise<void> {
|
||||
if (
|
||||
article.giveRank != null &&
|
||||
article.giveRank > 0 &&
|
||||
article.giveRank > buyer.rank
|
||||
article.giveRank > lockedBuyer.rank
|
||||
) {
|
||||
await tx
|
||||
.update(User)
|
||||
.set({ rank: article.giveRank })
|
||||
.where(eq(User.id, userId));
|
||||
rankChanged = true;
|
||||
await logAudit({ ...rankAudit, outcome: "intent" }, tx);
|
||||
}
|
||||
|
||||
for (const code of badgeCodes) {
|
||||
@@ -164,6 +202,28 @@ export async function buyShopArticle(formData: FormData): Promise<void> {
|
||||
}
|
||||
});
|
||||
|
||||
if (rankChanged) {
|
||||
try {
|
||||
const delivery =
|
||||
await rankAssignmentCoordinator.synchronize(userId);
|
||||
await logAudit({
|
||||
...rankAudit,
|
||||
after: {
|
||||
...rankAudit.after,
|
||||
...(delivery.status === "superseded"
|
||||
? { superseded: true }
|
||||
: { rank: delivery.rank }),
|
||||
},
|
||||
outcome:
|
||||
delivery.status === "pending" ? "partial" : "success",
|
||||
});
|
||||
} catch (error) {
|
||||
logServerError("shop.rank_sync_pending", error, {
|
||||
userId,
|
||||
correlationId: rankRecoveryId,
|
||||
});
|
||||
}
|
||||
}
|
||||
await sendCurrency(
|
||||
{ rcon, db: currencyDb },
|
||||
userId,
|
||||
|
||||
Reference in new issue
Block a user