This commit is contained in:
End Nightshade 2026-03-03 12:17:01 -07:00
parent 5d4f12d4b6
commit dde9a8f27f
No known key found for this signature in database
2 changed files with 424 additions and 80 deletions

226
backend/dist/index.js vendored
View file

@ -4,25 +4,43 @@ var __getProtoOf = Object.getPrototypeOf;
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty; var __hasOwnProp = Object.prototype.hasOwnProperty;
function __accessProp(key) {
return this[key];
}
var __toESMCache_node;
var __toESMCache_esm;
var __toESM = (mod, isNodeMode, target) => { var __toESM = (mod, isNodeMode, target) => {
var canCache = mod != null && typeof mod === "object";
if (canCache) {
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
var cached = cache.get(mod);
if (cached)
return cached;
}
target = mod != null ? __create(__getProtoOf(mod)) : {}; target = mod != null ? __create(__getProtoOf(mod)) : {};
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target; const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
for (let key of __getOwnPropNames(mod)) for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key)) if (!__hasOwnProp.call(to, key))
__defProp(to, key, { __defProp(to, key, {
get: () => mod[key], get: __accessProp.bind(mod, key),
enumerable: true enumerable: true
}); });
if (canCache)
cache.set(mod, to);
return to; return to;
}; };
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => { var __export = (target, all) => {
for (var name in all) for (var name in all)
__defProp(target, name, { __defProp(target, name, {
get: all[name], get: all[name],
enumerable: true, enumerable: true,
configurable: true, configurable: true,
set: (newValue) => all[name] = () => newValue set: __exportSetter.bind(all, name)
}); });
}; };
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res); var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@ -14501,7 +14519,6 @@ __export(exports_type3, {
// node_modules/@sinclair/typebox/build/esm/type/type/index.mjs // node_modules/@sinclair/typebox/build/esm/type/type/index.mjs
var Type = exports_type3; var Type = exports_type3;
// node_modules/@sinclair/typebox/build/esm/errors/function.mjs // node_modules/@sinclair/typebox/build/esm/errors/function.mjs
function DefaultErrorFunction(error) { function DefaultErrorFunction(error) {
switch (error.errorType) { switch (error.errorType) {
@ -15779,6 +15796,7 @@ function Errors(...args) {
const iterator = args.length === 3 ? Visit6(args[0], args[1], "", args[2]) : Visit6(args[0], [], "", args[1]); const iterator = args.length === 3 ? Visit6(args[0], args[1], "", args[2]) : Visit6(args[0], [], "", args[1]);
return new ValueErrorIterator(iterator); return new ValueErrorIterator(iterator);
} }
// node_modules/@sinclair/typebox/build/esm/value/assert/assert.mjs // node_modules/@sinclair/typebox/build/esm/value/assert/assert.mjs
var __classPrivateFieldSet = function(receiver, state, value, kind, f) { var __classPrivateFieldSet = function(receiver, state, value, kind, f) {
if (kind === "m") if (kind === "m")
@ -31159,7 +31177,11 @@ your scraps project *<${projectUrl}|${projectName}>* is currently waiting for a
elements: [ elements: [
{ {
type: "button", type: "button",
text: { type: "plain_text", text: ":scraps: view your project", emoji: true }, text: {
type: "plain_text",
text: ":scraps: view your project",
emoji: true
},
url: projectUrl, url: projectUrl,
action_id: "view_project" action_id: "view_project"
} }
@ -31215,7 +31237,11 @@ keep building and ship again for more scraps! :blobhaj_party:`
elements: [ elements: [
{ {
type: "button", type: "button",
text: { type: "plain_text", text: ":scraps: view your project", emoji: true }, text: {
type: "plain_text",
text: ":scraps: view your project",
emoji: true
},
url: projectUrl, url: projectUrl,
action_id: "view_project" action_id: "view_project"
} }
@ -31223,8 +31249,7 @@ keep building and ship again for more scraps! :blobhaj_party:`
} }
]; ];
} else if (action === "denied") { } else if (action === "denied") {
const reviewerMention = reviewerSlackId ? `<@${reviewerSlackId}>` : "a reviewer"; fallbackText = `:scraps: hey <@${userSlackId}>! your scraps project ${projectName} needs some changes before it can be approved for scraps. here's some feedback from your reviewer: ${feedbackForAuthor}`;
fallbackText = `:scraps: hey <@${userSlackId}>! your scraps project ${projectName} needs some changes before it can be approved for scraps. here's some feedback from your reviewer, ${reviewerMention}: ${feedbackForAuthor}`;
blocks = [ blocks = [
{ {
type: "section", type: "section",
@ -31232,7 +31257,7 @@ keep building and ship again for more scraps! :blobhaj_party:`
type: "mrkdwn", type: "mrkdwn",
text: `:scraps: hey <@${userSlackId}>! :scraps: text: `:scraps: hey <@${userSlackId}>! :scraps:
your scraps project *<${projectUrl}|${projectName}>* needs some changes before it can be approved for scraps. here's some feedback from your reviewer, ${reviewerMention}: your scraps project *<${projectUrl}|${projectName}>* needs some changes before it can be approved for scraps. here's some feedback from your reviewer:
> ${feedbackForAuthor} > ${feedbackForAuthor}
@ -31244,7 +31269,11 @@ don't worry \u2014 make the requested changes and resubmit! :scraps:`
elements: [ elements: [
{ {
type: "button", type: "button",
text: { type: "plain_text", text: ":scraps: view your project", emoji: true }, text: {
type: "plain_text",
text: ":scraps: view your project",
emoji: true
},
url: projectUrl, url: projectUrl,
action_id: "view_project" action_id: "view_project"
} }
@ -31252,9 +31281,8 @@ don't worry \u2014 make the requested changes and resubmit! :scraps:`
} }
]; ];
} else if (action === "permanently_rejected") { } else if (action === "permanently_rejected") {
const reviewerMention = reviewerSlackId ? `<@${reviewerSlackId}>` : "a reviewer";
const adminMentions = adminSlackIds.length > 0 ? adminSlackIds.map((id) => `<@${id}>`).join(", ") : "an admin"; const adminMentions = adminSlackIds.length > 0 ? adminSlackIds.map((id) => `<@${id}>`).join(", ") : "an admin";
fallbackText = `:scraps: hey <@${userSlackId}>! unfortunately, your scraps project ${projectName} has been permanently rejected by ${reviewerMention}. reason: ${feedbackForAuthor}. if you have any questions, please reach out to an admin: ${adminMentions}`; fallbackText = `:scraps: hey <@${userSlackId}>! unfortunately, your scraps project ${projectName} has been permanently rejected. reason: ${feedbackForAuthor}. if you have any questions, please reach out to an admin: ${adminMentions}`;
blocks = [ blocks = [
{ {
type: "section", type: "section",
@ -31262,7 +31290,7 @@ don't worry \u2014 make the requested changes and resubmit! :scraps:`
type: "mrkdwn", type: "mrkdwn",
text: `:scraps: hey <@${userSlackId}>! :scraps: text: `:scraps: hey <@${userSlackId}>! :scraps:
unfortunately, your scraps project *<${projectUrl}|${projectName}>* has been *permanently rejected* by ${reviewerMention}. unfortunately, your scraps project *<${projectUrl}|${projectName}>* has been *permanently rejected*.
*reason:* *reason:*
> ${feedbackForAuthor} > ${feedbackForAuthor}
@ -31275,7 +31303,11 @@ if you have any questions about this decision, please reach out to one of our ad
elements: [ elements: [
{ {
type: "button", type: "button",
text: { type: "plain_text", text: ":scraps: view your project", emoji: true }, text: {
type: "plain_text",
text: ":scraps: view your project",
emoji: true
},
url: projectUrl, url: projectUrl,
action_id: "view_project" action_id: "view_project"
} }
@ -31968,13 +32000,18 @@ projects.get("/:id", async ({ params, headers }) => {
if (!project[0]) if (!project[0])
return { error: "Not found" }; return { error: "Not found" };
const isOwner = project[0].userId === user.id; const isOwner = project[0].userId === user.id;
const isStaff = user.role === "admin" || user.role === "reviewer";
if (!isOwner && project[0].status !== "shipped" && project[0].status !== "in_progress" && project[0].status !== "waiting_for_review" && project[0].status !== "pending_admin_approval") { if (!isOwner && project[0].status !== "shipped" && project[0].status !== "in_progress" && project[0].status !== "waiting_for_review" && project[0].status !== "pending_admin_approval") {
return { error: "Not found" }; return { error: "Not found" };
} }
if (!isOwner) { if (!isOwner) {
await db.update(projectsTable).set({ views: sql`${projectsTable.views} + 1` }).where(eq(projectsTable.id, parseInt(params.id))); await db.update(projectsTable).set({ views: sql`${projectsTable.views} + 1` }).where(eq(projectsTable.id, parseInt(params.id)));
} }
const projectOwner = await db.select({ id: usersTable.id, username: usersTable.username, avatar: usersTable.avatar }).from(usersTable).where(eq(usersTable.id, project[0].userId)).limit(1); const projectOwner = await db.select({
id: usersTable.id,
username: usersTable.username,
avatar: usersTable.avatar
}).from(usersTable).where(eq(usersTable.id, project[0].userId)).limit(1);
let activity = []; let activity = [];
const reviews = await db.select({ const reviews = await db.select({
id: reviewsTable.id, id: reviewsTable.id,
@ -31986,7 +32023,11 @@ projects.get("/:id", async ({ params, headers }) => {
const reviewerIds = reviews.map((r) => r.reviewerId); const reviewerIds = reviews.map((r) => r.reviewerId);
let reviewers = []; let reviewers = [];
if (reviewerIds.length > 0) { if (reviewerIds.length > 0) {
reviewers = await db.select({ id: usersTable.id, username: usersTable.username, avatar: usersTable.avatar }).from(usersTable).where(inArray(usersTable.id, reviewerIds)); reviewers = await db.select({
id: usersTable.id,
username: usersTable.username,
avatar: usersTable.avatar
}).from(usersTable).where(inArray(usersTable.id, reviewerIds));
} }
const isPendingAdmin = project[0].status === "pending_admin_approval"; const isPendingAdmin = project[0].status === "pending_admin_approval";
for (const r of reviews) { for (const r of reviews) {
@ -31997,7 +32038,7 @@ projects.get("/:id", async ({ params, headers }) => {
action: r.action, action: r.action,
feedbackForAuthor: r.feedbackForAuthor, feedbackForAuthor: r.feedbackForAuthor,
createdAt: r.createdAt, createdAt: r.createdAt,
reviewer: reviewers.find((rv) => rv.id === r.reviewerId) || null reviewer: isStaff ? reviewers.find((rv) => rv.id === r.reviewerId) || null : null
}); });
} }
const activityEntries = await db.select({ const activityEntries = await db.select({
@ -32104,7 +32145,10 @@ projects.post("/", async ({ body, headers }) => {
projectId: newProject[0].id, projectId: newProject[0].id,
action: "project_created" action: "project_created"
}); });
return { ...newProject[0], hackatimeProject: stripHackatimeIds(newProject[0].hackatimeProject) }; return {
...newProject[0],
hackatimeProject: stripHackatimeIds(newProject[0].hackatimeProject)
};
}); });
projects.put("/:id", async ({ params, body, headers }) => { projects.put("/:id", async ({ params, body, headers }) => {
const user = await getUserFromSession(headers); const user = await getUserFromSession(headers);
@ -32145,7 +32189,10 @@ projects.put("/:id", async ({ params, body, headers }) => {
const syncResult = await syncSingleProject(updated[0].id); const syncResult = await syncSingleProject(updated[0].id);
updated[0].hours = syncResult.hours; updated[0].hours = syncResult.hours;
} }
return { ...updated[0], hackatimeProject: stripHackatimeIds(updated[0].hackatimeProject) }; return {
...updated[0],
hackatimeProject: stripHackatimeIds(updated[0].hackatimeProject)
};
}); });
projects.delete("/:id", async ({ params, headers }) => { projects.delete("/:id", async ({ params, headers }) => {
const user = await getUserFromSession(headers); const user = await getUserFromSession(headers);
@ -32169,7 +32216,9 @@ projects.post("/:id/unsubmit", async ({ params, headers }) => {
if (!project[0]) if (!project[0])
return { error: "Not found" }; return { error: "Not found" };
if (project[0].status !== "waiting_for_review" && project[0].status !== "pending_admin_approval") { if (project[0].status !== "waiting_for_review" && project[0].status !== "pending_admin_approval") {
return { error: "Project can only be unsubmitted while waiting for review" }; return {
error: "Project can only be unsubmitted while waiting for review"
};
} }
if (project[0].status === "pending_admin_approval") { if (project[0].status === "pending_admin_approval") {
await db.delete(reviewsTable).where(and(eq(reviewsTable.projectId, parseInt(params.id)), eq(reviewsTable.action, "approved"))); await db.delete(reviewsTable).where(and(eq(reviewsTable.projectId, parseInt(params.id)), eq(reviewsTable.action, "approved")));
@ -32183,7 +32232,10 @@ projects.post("/:id/unsubmit", async ({ params, headers }) => {
projectId: updated[0].id, projectId: updated[0].id,
action: "project_unsubmitted" action: "project_unsubmitted"
}); });
return { ...updated[0], hackatimeProject: stripHackatimeIds(updated[0].hackatimeProject) }; return {
...updated[0],
hackatimeProject: stripHackatimeIds(updated[0].hackatimeProject)
};
}); });
projects.post("/:id/submit", async ({ params, headers, body }) => { projects.post("/:id/submit", async ({ params, headers, body }) => {
const user = await getUserFromSession(headers); const user = await getUserFromSession(headers);
@ -32196,7 +32248,10 @@ projects.post("/:id/submit", async ({ params, headers, body }) => {
const { identity } = meResponse; const { identity } = meResponse;
await db.update(usersTable).set({ verificationStatus: identity.verification_status }).where(eq(usersTable.id, user.id)); await db.update(usersTable).set({ verificationStatus: identity.verification_status }).where(eq(usersTable.id, user.id));
if (identity.verification_status === "ineligible") { if (identity.verification_status === "ineligible") {
return { error: "ineligible", redirectTo: "/auth/error?reason=not-eligible" }; return {
error: "ineligible",
redirectTo: "/auth/error?reason=not-eligible"
};
} }
} }
} }
@ -32241,12 +32296,16 @@ projects.post("/:id/submit", async ({ params, headers, body }) => {
console.error("Failed to send Slack submission notification:", slackErr); console.error("Failed to send Slack submission notification:", slackErr);
} }
} }
return { ...updated[0], hackatimeProject: stripHackatimeIds(updated[0].hackatimeProject) }; return {
...updated[0],
hackatimeProject: stripHackatimeIds(updated[0].hackatimeProject)
};
}); });
projects.get("/:id/reviews", async ({ params, headers }) => { projects.get("/:id/reviews", async ({ params, headers }) => {
const user = await getUserFromSession(headers); const user = await getUserFromSession(headers);
if (!user) if (!user)
return { error: "Unauthorized" }; return { error: "Unauthorized" };
const isStaff = user.role === "admin" || user.role === "reviewer";
const project = await db.select().from(projectsTable).where(and(eq(projectsTable.id, parseInt(params.id)), eq(projectsTable.userId, user.id))).limit(1); const project = await db.select().from(projectsTable).where(and(eq(projectsTable.id, parseInt(params.id)), eq(projectsTable.userId, user.id))).limit(1);
if (!project[0]) if (!project[0])
return { error: "Not found" }; return { error: "Not found" };
@ -32259,8 +32318,12 @@ projects.get("/:id/reviews", async ({ params, headers }) => {
}).from(reviewsTable).where(eq(reviewsTable.projectId, parseInt(params.id))); }).from(reviewsTable).where(eq(reviewsTable.projectId, parseInt(params.id)));
const reviewerIds = reviews.map((r) => r.reviewerId); const reviewerIds = reviews.map((r) => r.reviewerId);
let reviewers = []; let reviewers = [];
if (reviewerIds.length > 0) { if (isStaff && reviewerIds.length > 0) {
reviewers = await db.select({ id: usersTable.id, username: usersTable.username, avatar: usersTable.avatar }).from(usersTable).where(inArray(usersTable.id, reviewerIds)); reviewers = await db.select({
id: usersTable.id,
username: usersTable.username,
avatar: usersTable.avatar
}).from(usersTable).where(inArray(usersTable.id, reviewerIds));
} }
const filteredReviews = project[0].status === "pending_admin_approval" ? reviews.filter((r) => r.action !== "approved") : reviews; const filteredReviews = project[0].status === "pending_admin_approval" ? reviews.filter((r) => r.action !== "approved") : reviews;
return filteredReviews.map((r) => ({ return filteredReviews.map((r) => ({
@ -32268,7 +32331,7 @@ projects.get("/:id/reviews", async ({ params, headers }) => {
action: r.action, action: r.action,
feedbackForAuthor: r.feedbackForAuthor, feedbackForAuthor: r.feedbackForAuthor,
createdAt: r.createdAt, createdAt: r.createdAt,
reviewer: reviewers.find((rv) => rv.id === r.reviewerId) || null reviewer: isStaff ? reviewers.find((rv) => rv.id === r.reviewerId) || null : null
})); }));
}); });
var projects_default = projects; var projects_default = projects;
@ -35566,6 +35629,32 @@ admin.patch("/orders/:id", async ({ params, body, headers, status: status2 }) =>
if (orderStatus && !validStatuses.includes(orderStatus)) { if (orderStatus && !validStatuses.includes(orderStatus)) {
return status2(400, { error: "Invalid status" }); return status2(400, { error: "Invalid status" });
} }
const orderId = parseInt(params.id);
const orderBeforeUpdate = await db.select({
id: shopOrdersTable.id,
status: shopOrdersTable.status,
orderType: shopOrdersTable.orderType,
shopItemId: shopOrdersTable.shopItemId,
quantity: shopOrdersTable.quantity
}).from(shopOrdersTable).where(eq(shopOrdersTable.id, orderId)).limit(1);
if (orderStatus && orderBeforeUpdate[0] && (orderBeforeUpdate[0].orderType === "purchase" || orderBeforeUpdate[0].orderType === "luck_win")) {
const wasInactive = orderBeforeUpdate[0].status === "cancelled" || orderBeforeUpdate[0].status === "deleted";
const willBeInactive = orderStatus === "cancelled" || orderStatus === "deleted";
const qty = orderBeforeUpdate[0].quantity ?? 1;
if (!wasInactive && willBeInactive) {
await db.update(shopItemsTable).set({
count: sql`${shopItemsTable.count} + ${qty}`,
updatedAt: new Date
}).where(eq(shopItemsTable.id, orderBeforeUpdate[0].shopItemId));
console.log(`[ADMIN] Order #${orderId} status\u2192${orderStatus}: restored ${qty} stock to item #${orderBeforeUpdate[0].shopItemId}`);
} else if (wasInactive && !willBeInactive) {
await db.update(shopItemsTable).set({
count: sql`GREATEST(${shopItemsTable.count} - ${qty}, 0)`,
updatedAt: new Date
}).where(eq(shopItemsTable.id, orderBeforeUpdate[0].shopItemId));
console.log(`[ADMIN] Order #${orderId} status\u2192${orderStatus}: decremented ${qty} stock from item #${orderBeforeUpdate[0].shopItemId}`);
}
}
const updateData = { updatedAt: new Date }; const updateData = { updatedAt: new Date };
if (orderStatus) if (orderStatus)
updateData.status = orderStatus; updateData.status = orderStatus;
@ -35575,7 +35664,7 @@ admin.patch("/orders/:id", async ({ params, body, headers, status: status2 }) =>
updateData.trackingNumber = trackingNumber; updateData.trackingNumber = trackingNumber;
if (isFulfilled !== undefined) if (isFulfilled !== undefined)
updateData.isFulfilled = isFulfilled; updateData.isFulfilled = isFulfilled;
const updated = await db.update(shopOrdersTable).set(updateData).where(eq(shopOrdersTable.id, parseInt(params.id))).returning({ const updated = await db.update(shopOrdersTable).set(updateData).where(eq(shopOrdersTable.id, orderId)).returning({
id: shopOrdersTable.id, id: shopOrdersTable.id,
quantity: shopOrdersTable.quantity, quantity: shopOrdersTable.quantity,
pricePerItem: shopOrdersTable.pricePerItem, pricePerItem: shopOrdersTable.pricePerItem,
@ -35772,8 +35861,9 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
} catch (err) { } catch (err) {
const dbErr = err; const dbErr = err;
const msg = dbErr?.message ?? ""; const msg = dbErr?.message ?? "";
const code = dbErr?.code ?? null; const causeMsg = dbErr?.cause?.message ?? "";
if (msg.includes("does not exist") || code === "42P01") { const code = dbErr?.code ?? dbErr?.cause?.code ?? null;
if (msg.includes("does not exist") || causeMsg.includes("does not exist") || code === "42P01") {
await db.execute(sql` await db.execute(sql`
CREATE TABLE IF NOT EXISTS admin_deleted_orders ( CREATE TABLE IF NOT EXISTS admin_deleted_orders (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
@ -35837,13 +35927,36 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
await tx.delete(refinerySpendingHistoryTable).where(and(eq(refinerySpendingHistoryTable.userId, order[0].userId), eq(refinerySpendingHistoryTable.shopItemId, order[0].shopItemId))); await tx.delete(refinerySpendingHistoryTable).where(and(eq(refinerySpendingHistoryTable.userId, order[0].userId), eq(refinerySpendingHistoryTable.shopItemId, order[0].shopItemId)));
await tx.delete(refineryOrdersTable).where(and(eq(refineryOrdersTable.userId, order[0].userId), eq(refineryOrdersTable.shopItemId, order[0].shopItemId))); await tx.delete(refineryOrdersTable).where(and(eq(refineryOrdersTable.userId, order[0].userId), eq(refineryOrdersTable.shopItemId, order[0].shopItemId)));
await tx.delete(shopRollsTable).where(and(eq(shopRollsTable.userId, order[0].userId), eq(shopRollsTable.shopItemId, order[0].shopItemId))); await tx.delete(shopRollsTable).where(and(eq(shopRollsTable.userId, order[0].userId), eq(shopRollsTable.shopItemId, order[0].shopItemId)));
await tx.delete(shopPenaltiesTable).where(and(eq(shopPenaltiesTable.userId, order[0].userId), eq(shopPenaltiesTable.shopItemId, order[0].shopItemId))); if (order[0].orderType === "luck_win") {
const currentPenalty = await tx.select({
probabilityMultiplier: shopPenaltiesTable.probabilityMultiplier
}).from(shopPenaltiesTable).where(and(eq(shopPenaltiesTable.userId, order[0].userId), eq(shopPenaltiesTable.shopItemId, order[0].shopItemId))).limit(1);
if (currentPenalty.length > 0) {
const restored = Math.min(100, currentPenalty[0].probabilityMultiplier * 2);
if (restored >= 100) {
await tx.delete(shopPenaltiesTable).where(and(eq(shopPenaltiesTable.userId, order[0].userId), eq(shopPenaltiesTable.shopItemId, order[0].shopItemId)));
} else {
await tx.update(shopPenaltiesTable).set({
probabilityMultiplier: restored,
updatedAt: new Date
}).where(and(eq(shopPenaltiesTable.userId, order[0].userId), eq(shopPenaltiesTable.shopItemId, order[0].shopItemId)));
}
console.log(`[ADMIN] Order #${orderId} delete (luck_win): reversed penalty halving ${currentPenalty[0].probabilityMultiplier}\u2192${restored >= 100 ? "deleted" : restored} for user #${order[0].userId} item #${order[0].shopItemId}`);
}
} else {
await tx.delete(shopPenaltiesTable).where(and(eq(shopPenaltiesTable.userId, order[0].userId), eq(shopPenaltiesTable.shopItemId, order[0].shopItemId)));
}
await tx.delete(shopOrdersTable).where(eq(shopOrdersTable.id, orderId)); await tx.delete(shopOrdersTable).where(eq(shopOrdersTable.id, orderId));
if (order[0].orderType === "purchase" || order[0].orderType === "luck_win") { const alreadyInactive = order[0].status === "cancelled" || order[0].status === "deleted";
await tx.update(shopItemsTable).set({ const shouldRestoreStock = !alreadyInactive && (order[0].orderType === "purchase" || order[0].orderType === "luck_win");
count: sql`${shopItemsTable.count} + ${order[0].quantity}`, const restoreQty = order[0].quantity ?? 1;
console.log(`[ADMIN] Order #${orderId} delete: orderType=${order[0].orderType} status=${order[0].status} quantity=${order[0].quantity} shopItemId=${order[0].shopItemId} alreadyInactive=${alreadyInactive} shouldRestoreStock=${shouldRestoreStock} restoreQty=${restoreQty}`);
if (shouldRestoreStock) {
const updated = await tx.update(shopItemsTable).set({
count: sql`${shopItemsTable.count} + ${restoreQty}`,
updatedAt: new Date updatedAt: new Date
}).where(eq(shopItemsTable.id, order[0].shopItemId)); }).where(eq(shopItemsTable.id, order[0].shopItemId)).returning({ id: shopItemsTable.id, count: shopItemsTable.count });
console.log(`[ADMIN] Order #${orderId} stock restored: itemId=${order[0].shopItemId} addedBack=${restoreQty} newCount=${updated[0]?.count ?? "NO_ROW_UPDATED"}`);
} }
}); });
console.log(`[ADMIN] Order #${orderId} archived/deleted by admin #${user2.id}`); console.log(`[ADMIN] Order #${orderId} archived/deleted by admin #${user2.id}`);
@ -35936,20 +36049,47 @@ admin.post("/orders/:id/restore", async ({ params, headers, status: status2 }) =
createdAt: rr.createdAt createdAt: rr.createdAt
}); });
} }
for (const p of shopPenaltiesPayload) { if (orderPayload.orderType === "luck_win") {
await tx.insert(shopPenaltiesTable).values({ const currentPenalty = await tx.select({
userId: p.userId, probabilityMultiplier: shopPenaltiesTable.probabilityMultiplier
shopItemId: p.shopItemId, }).from(shopPenaltiesTable).where(and(eq(shopPenaltiesTable.userId, orderPayload.userId), eq(shopPenaltiesTable.shopItemId, orderPayload.shopItemId))).limit(1);
probabilityMultiplier: p.probabilityMultiplier, if (currentPenalty.length > 0) {
createdAt: p.createdAt, const halved = Math.max(1, Math.floor(currentPenalty[0].probabilityMultiplier / 2));
updatedAt: p.updatedAt await tx.update(shopPenaltiesTable).set({
}); probabilityMultiplier: halved,
updatedAt: new Date
}).where(and(eq(shopPenaltiesTable.userId, orderPayload.userId), eq(shopPenaltiesTable.shopItemId, orderPayload.shopItemId)));
console.log(`[ADMIN] Restore order #${originalOrderId} (luck_win): re-halved penalty ${currentPenalty[0].probabilityMultiplier}\u2192${halved} for user #${orderPayload.userId} item #${orderPayload.shopItemId}`);
} else {
await tx.insert(shopPenaltiesTable).values({
userId: orderPayload.userId,
shopItemId: orderPayload.shopItemId,
probabilityMultiplier: 50
});
console.log(`[ADMIN] Restore order #${originalOrderId} (luck_win): created fresh penalty at 50 for user #${orderPayload.userId} item #${orderPayload.shopItemId}`);
}
} else {
for (const p of shopPenaltiesPayload) {
await tx.insert(shopPenaltiesTable).values({
userId: p.userId,
shopItemId: p.shopItemId,
probabilityMultiplier: p.probabilityMultiplier,
createdAt: p.createdAt,
updatedAt: p.updatedAt
});
}
} }
if (orderPayload.orderType === "purchase" || orderPayload.orderType === "luck_win") { const wasInactiveWhenDeleted = orderPayload.status === "cancelled" || orderPayload.status === "deleted";
const shouldDecrementStock = !wasInactiveWhenDeleted && (orderPayload.orderType === "purchase" || orderPayload.orderType === "luck_win");
if (shouldDecrementStock) {
const qty = orderPayload.quantity ?? 1;
await tx.update(shopItemsTable).set({ await tx.update(shopItemsTable).set({
count: sql`GREATEST(${shopItemsTable.count} - ${orderPayload.quantity ?? 1}, 0)`, count: sql`GREATEST(${shopItemsTable.count} - ${qty}, 0)`,
updatedAt: new Date updatedAt: new Date
}).where(eq(shopItemsTable.id, orderPayload.shopItemId)); }).where(eq(shopItemsTable.id, orderPayload.shopItemId));
console.log(`[ADMIN] Restore order #${originalOrderId}: decremented ${qty} stock from item #${orderPayload.shopItemId}`);
} else if (wasInactiveWhenDeleted) {
console.log(`[ADMIN] Restore order #${originalOrderId}: skipped stock decrement (order was ${orderPayload.status} when archived)`);
} }
await tx.execute(sql`UPDATE admin_deleted_orders SET restored = true, restored_by = ${user2.id}, restored_at = now() WHERE id = ${archived.id}`); await tx.execute(sql`UPDATE admin_deleted_orders SET restored = true, restored_by = ${user2.id}, restored_at = now() WHERE id = ${archived.id}`);
}); });

View file

@ -705,9 +705,7 @@ admin.delete("/bonuses/:id", async ({ params, headers, status }) => {
return status(404, { error: "Bonus not found" }); return status(404, { error: "Bonus not found" });
} }
await db await db.delete(userBonusesTable).where(eq(userBonusesTable.id, bonusId));
.delete(userBonusesTable)
.where(eq(userBonusesTable.id, bonusId));
return { success: true }; return { success: true };
} catch (err) { } catch (err) {
@ -2312,7 +2310,12 @@ admin.patch("/orders/:id", async ({ params, body, headers, status }) => {
notes, notes,
isFulfilled, isFulfilled,
trackingNumber, trackingNumber,
} = body as { status?: string; notes?: string; isFulfilled?: boolean; trackingNumber?: string }; } = body as {
status?: string;
notes?: string;
isFulfilled?: boolean;
trackingNumber?: string;
};
const validStatuses = [ const validStatuses = [
"pending", "pending",
@ -2326,16 +2329,72 @@ admin.patch("/orders/:id", async ({ params, body, headers, status }) => {
return status(400, { error: "Invalid status" }); return status(400, { error: "Invalid status" });
} }
const orderId = parseInt(params.id);
// Fetch the order before updating so we can handle stock changes
const orderBeforeUpdate = await db
.select({
id: shopOrdersTable.id,
status: shopOrdersTable.status,
orderType: shopOrdersTable.orderType,
shopItemId: shopOrdersTable.shopItemId,
quantity: shopOrdersTable.quantity,
})
.from(shopOrdersTable)
.where(eq(shopOrdersTable.id, orderId))
.limit(1);
if (
orderStatus &&
orderBeforeUpdate[0] &&
(orderBeforeUpdate[0].orderType === "purchase" ||
orderBeforeUpdate[0].orderType === "luck_win")
) {
const wasInactive =
orderBeforeUpdate[0].status === "cancelled" ||
orderBeforeUpdate[0].status === "deleted";
const willBeInactive =
orderStatus === "cancelled" || orderStatus === "deleted";
const qty = orderBeforeUpdate[0].quantity ?? 1;
if (!wasInactive && willBeInactive) {
// Cancelling/soft-deleting an active order → restore stock
await db
.update(shopItemsTable)
.set({
count: sql`${shopItemsTable.count} + ${qty}`,
updatedAt: new Date(),
})
.where(eq(shopItemsTable.id, orderBeforeUpdate[0].shopItemId));
console.log(
`[ADMIN] Order #${orderId} status→${orderStatus}: restored ${qty} stock to item #${orderBeforeUpdate[0].shopItemId}`,
);
} else if (wasInactive && !willBeInactive) {
// Re-activating a cancelled/deleted order → decrement stock again
await db
.update(shopItemsTable)
.set({
count: sql`GREATEST(${shopItemsTable.count} - ${qty}, 0)`,
updatedAt: new Date(),
})
.where(eq(shopItemsTable.id, orderBeforeUpdate[0].shopItemId));
console.log(
`[ADMIN] Order #${orderId} status→${orderStatus}: decremented ${qty} stock from item #${orderBeforeUpdate[0].shopItemId}`,
);
}
}
const updateData: Record<string, unknown> = { updatedAt: new Date() }; const updateData: Record<string, unknown> = { updatedAt: new Date() };
if (orderStatus) updateData.status = orderStatus; if (orderStatus) updateData.status = orderStatus;
if (notes !== undefined) updateData.notes = notes; if (notes !== undefined) updateData.notes = notes;
if (trackingNumber !== undefined) updateData.trackingNumber = trackingNumber; if (trackingNumber !== undefined)
updateData.trackingNumber = trackingNumber;
if (isFulfilled !== undefined) updateData.isFulfilled = isFulfilled; if (isFulfilled !== undefined) updateData.isFulfilled = isFulfilled;
const updated = await db const updated = await db
.update(shopOrdersTable) .update(shopOrdersTable)
.set(updateData) .set(updateData)
.where(eq(shopOrdersTable.id, parseInt(params.id))) .where(eq(shopOrdersTable.id, orderId))
.returning({ .returning({
id: shopOrdersTable.id, id: shopOrdersTable.id,
userId: shopOrdersTable.userId, userId: shopOrdersTable.userId,
@ -2652,10 +2711,21 @@ admin.delete("/orders/:id", async ({ params, headers, body, status }) => {
} catch (err) { } catch (err) {
// If the table doesn't exist, create it on-demand (safe: CREATE TABLE IF NOT EXISTS) // If the table doesn't exist, create it on-demand (safe: CREATE TABLE IF NOT EXISTS)
// and retry the select. If it's a different error, rethrow. // and retry the select. If it's a different error, rethrow.
const dbErr = err as { message?: string; code?: string }; // Drizzle wraps PG errors: the top-level message is "Failed query: ..." and the
// actual PG error (with code 42P01 / "does not exist") lives in err.cause.
const dbErr = err as {
message?: string;
code?: string;
cause?: { message?: string; code?: string };
};
const msg = dbErr?.message ?? ""; const msg = dbErr?.message ?? "";
const code = dbErr?.code ?? null; const causeMsg = dbErr?.cause?.message ?? "";
if (msg.includes("does not exist") || code === "42P01") { const code = dbErr?.code ?? dbErr?.cause?.code ?? null;
if (
msg.includes("does not exist") ||
causeMsg.includes("does not exist") ||
code === "42P01"
) {
// Create the table to match the migration shape (minimal safe schema) // Create the table to match the migration shape (minimal safe schema)
await db.execute(sql` await db.execute(sql`
CREATE TABLE IF NOT EXISTS admin_deleted_orders ( CREATE TABLE IF NOT EXISTS admin_deleted_orders (
@ -2785,28 +2855,96 @@ admin.delete("/orders/:id", async ({ params, headers, body, status }) => {
eq(shopRollsTable.shopItemId, order[0].shopItemId), eq(shopRollsTable.shopItemId, order[0].shopItemId),
), ),
); );
await tx // For luck_win orders, reverse the penalty halving that happened on win
.delete(shopPenaltiesTable) // instead of deleting all penalties. The win either halved an existing
.where( // penalty (newMult = floor(old/2)) or created a fresh one at 50.
and( // Reversing: double the current multiplier (capped at 100). If the result
eq(shopPenaltiesTable.userId, order[0].userId), // is 100, that's effectively no penalty so we can delete the row.
eq(shopPenaltiesTable.shopItemId, order[0].shopItemId), if (order[0].orderType === "luck_win") {
), const currentPenalty = await tx
); .select({
probabilityMultiplier: shopPenaltiesTable.probabilityMultiplier,
})
.from(shopPenaltiesTable)
.where(
and(
eq(shopPenaltiesTable.userId, order[0].userId),
eq(shopPenaltiesTable.shopItemId, order[0].shopItemId),
),
)
.limit(1);
if (currentPenalty.length > 0) {
const restored = Math.min(
100,
currentPenalty[0].probabilityMultiplier * 2,
);
if (restored >= 100) {
await tx
.delete(shopPenaltiesTable)
.where(
and(
eq(shopPenaltiesTable.userId, order[0].userId),
eq(shopPenaltiesTable.shopItemId, order[0].shopItemId),
),
);
} else {
await tx
.update(shopPenaltiesTable)
.set({
probabilityMultiplier: restored,
updatedAt: new Date(),
})
.where(
and(
eq(shopPenaltiesTable.userId, order[0].userId),
eq(shopPenaltiesTable.shopItemId, order[0].shopItemId),
),
);
}
console.log(
`[ADMIN] Order #${orderId} delete (luck_win): reversed penalty halving ${currentPenalty[0].probabilityMultiplier}${restored >= 100 ? "deleted" : restored} for user #${order[0].userId} item #${order[0].shopItemId}`,
);
}
} else {
await tx
.delete(shopPenaltiesTable)
.where(
and(
eq(shopPenaltiesTable.userId, order[0].userId),
eq(shopPenaltiesTable.shopItemId, order[0].shopItemId),
),
);
}
await tx.delete(shopOrdersTable).where(eq(shopOrdersTable.id, orderId)); await tx.delete(shopOrdersTable).where(eq(shopOrdersTable.id, orderId));
// Restore item stock for purchase/luck_win orders (count was decremented when the order was placed) // Restore item stock for purchase/luck_win orders (count was decremented when the order was placed)
if ( // Skip if the order was already cancelled/deleted (stock was already restored by the PATCH handler)
order[0].orderType === "purchase" || const alreadyInactive =
order[0].orderType === "luck_win" order[0].status === "cancelled" || order[0].status === "deleted";
) { const shouldRestoreStock =
await tx !alreadyInactive &&
(order[0].orderType === "purchase" ||
order[0].orderType === "luck_win");
const restoreQty = order[0].quantity ?? 1;
console.log(
`[ADMIN] Order #${orderId} delete: orderType=${order[0].orderType} status=${order[0].status} quantity=${order[0].quantity} shopItemId=${order[0].shopItemId} alreadyInactive=${alreadyInactive} shouldRestoreStock=${shouldRestoreStock} restoreQty=${restoreQty}`,
);
if (shouldRestoreStock) {
const updated = await tx
.update(shopItemsTable) .update(shopItemsTable)
.set({ .set({
count: sql`${shopItemsTable.count} + ${order[0].quantity}`, count: sql`${shopItemsTable.count} + ${restoreQty}`,
updatedAt: new Date(), updatedAt: new Date(),
}) })
.where(eq(shopItemsTable.id, order[0].shopItemId)); .where(eq(shopItemsTable.id, order[0].shopItemId))
.returning({ id: shopItemsTable.id, count: shopItemsTable.count });
console.log(
`[ADMIN] Order #${orderId} stock restored: itemId=${order[0].shopItemId} addedBack=${restoreQty} newCount=${updated[0]?.count ?? "NO_ROW_UPDATED"}`,
);
} }
}); });
@ -2941,28 +3079,94 @@ admin.post("/orders/:id/restore", async ({ params, headers, status }) => {
createdAt: rr.createdAt, createdAt: rr.createdAt,
}); });
} }
for (const p of shopPenaltiesPayload) { // For luck_win orders, the delete handler reversed the penalty halving
await tx.insert(shopPenaltiesTable).values({ // instead of deleting penalties. Restoring means re-applying the halving
userId: p.userId, // to the current live penalty (which the delete handler doubled back).
shopItemId: p.shopItemId, if (orderPayload.orderType === "luck_win") {
probabilityMultiplier: p.probabilityMultiplier, const currentPenalty = await tx
createdAt: p.createdAt, .select({
updatedAt: p.updatedAt, probabilityMultiplier: shopPenaltiesTable.probabilityMultiplier,
}); })
.from(shopPenaltiesTable)
.where(
and(
eq(shopPenaltiesTable.userId, orderPayload.userId),
eq(shopPenaltiesTable.shopItemId, orderPayload.shopItemId),
),
)
.limit(1);
if (currentPenalty.length > 0) {
const halved = Math.max(
1,
Math.floor(currentPenalty[0].probabilityMultiplier / 2),
);
await tx
.update(shopPenaltiesTable)
.set({
probabilityMultiplier: halved,
updatedAt: new Date(),
})
.where(
and(
eq(shopPenaltiesTable.userId, orderPayload.userId),
eq(shopPenaltiesTable.shopItemId, orderPayload.shopItemId),
),
);
console.log(
`[ADMIN] Restore order #${originalOrderId} (luck_win): re-halved penalty ${currentPenalty[0].probabilityMultiplier}${halved} for user #${orderPayload.userId} item #${orderPayload.shopItemId}`,
);
} else {
// No existing penalty row — the delete handler must have deleted it
// (multiplier doubled to >=100). Re-create at 50 like the win flow does.
await tx.insert(shopPenaltiesTable).values({
userId: orderPayload.userId,
shopItemId: orderPayload.shopItemId,
probabilityMultiplier: 50,
});
console.log(
`[ADMIN] Restore order #${originalOrderId} (luck_win): created fresh penalty at 50 for user #${orderPayload.userId} item #${orderPayload.shopItemId}`,
);
}
} else {
for (const p of shopPenaltiesPayload) {
await tx.insert(shopPenaltiesTable).values({
userId: p.userId,
shopItemId: p.shopItemId,
probabilityMultiplier: p.probabilityMultiplier,
createdAt: p.createdAt,
updatedAt: p.updatedAt,
});
}
} }
// Decrement stock that was restored during delete (reverses the count+quantity from delete handler) // Decrement stock that was restored during delete (reverses the count+quantity from delete handler)
if ( // Only decrement if the DELETE handler actually restored stock — it skips restoration when the
orderPayload.orderType === "purchase" || // order was already cancelled/deleted (because the PATCH handler already restored stock earlier).
orderPayload.orderType === "luck_win" const wasInactiveWhenDeleted =
) { orderPayload.status === "cancelled" ||
orderPayload.status === "deleted";
const shouldDecrementStock =
!wasInactiveWhenDeleted &&
(orderPayload.orderType === "purchase" ||
orderPayload.orderType === "luck_win");
if (shouldDecrementStock) {
const qty = orderPayload.quantity ?? 1;
await tx await tx
.update(shopItemsTable) .update(shopItemsTable)
.set({ .set({
count: sql`GREATEST(${shopItemsTable.count} - ${orderPayload.quantity ?? 1}, 0)`, count: sql`GREATEST(${shopItemsTable.count} - ${qty}, 0)`,
updatedAt: new Date(), updatedAt: new Date(),
}) })
.where(eq(shopItemsTable.id, orderPayload.shopItemId)); .where(eq(shopItemsTable.id, orderPayload.shopItemId));
console.log(
`[ADMIN] Restore order #${originalOrderId}: decremented ${qty} stock from item #${orderPayload.shopItemId}`,
);
} else if (wasInactiveWhenDeleted) {
console.log(
`[ADMIN] Restore order #${originalOrderId}: skipped stock decrement (order was ${orderPayload.status} when archived)`,
);
} }
await tx.execute( await tx.execute(