mirror of
https://github.com/System-End/scraps.git
synced 2026-04-19 20:55:12 +00:00
Merge branch 'main' of https://github.com/hackclub/scraps
This commit is contained in:
commit
e6473c91ac
2 changed files with 424 additions and 80 deletions
226
backend/dist/index.js
vendored
226
backend/dist/index.js
vendored
|
|
@ -4,25 +4,43 @@ var __getProtoOf = Object.getPrototypeOf;
|
|||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
function __accessProp(key) {
|
||||
return this[key];
|
||||
}
|
||||
var __toESMCache_node;
|
||||
var __toESMCache_esm;
|
||||
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)) : {};
|
||||
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
||||
for (let key of __getOwnPropNames(mod))
|
||||
if (!__hasOwnProp.call(to, key))
|
||||
__defProp(to, key, {
|
||||
get: () => mod[key],
|
||||
get: __accessProp.bind(mod, key),
|
||||
enumerable: true
|
||||
});
|
||||
if (canCache)
|
||||
cache.set(mod, to);
|
||||
return to;
|
||||
};
|
||||
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) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, {
|
||||
get: all[name],
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
set: (newValue) => all[name] = () => newValue
|
||||
set: __exportSetter.bind(all, name)
|
||||
});
|
||||
};
|
||||
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
|
||||
var Type = exports_type3;
|
||||
|
||||
// node_modules/@sinclair/typebox/build/esm/errors/function.mjs
|
||||
function DefaultErrorFunction(error) {
|
||||
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]);
|
||||
return new ValueErrorIterator(iterator);
|
||||
}
|
||||
|
||||
// node_modules/@sinclair/typebox/build/esm/value/assert/assert.mjs
|
||||
var __classPrivateFieldSet = function(receiver, state, value, kind, f) {
|
||||
if (kind === "m")
|
||||
|
|
@ -31159,7 +31177,11 @@ your scraps project *<${projectUrl}|${projectName}>* is currently waiting for a
|
|||
elements: [
|
||||
{
|
||||
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,
|
||||
action_id: "view_project"
|
||||
}
|
||||
|
|
@ -31215,7 +31237,11 @@ keep building and ship again for more scraps! :blobhaj_party:`
|
|||
elements: [
|
||||
{
|
||||
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,
|
||||
action_id: "view_project"
|
||||
}
|
||||
|
|
@ -31223,8 +31249,7 @@ keep building and ship again for more scraps! :blobhaj_party:`
|
|||
}
|
||||
];
|
||||
} 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, ${reviewerMention}: ${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: ${feedbackForAuthor}`;
|
||||
blocks = [
|
||||
{
|
||||
type: "section",
|
||||
|
|
@ -31232,7 +31257,7 @@ keep building and ship again for more scraps! :blobhaj_party:`
|
|||
type: "mrkdwn",
|
||||
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}
|
||||
|
||||
|
|
@ -31244,7 +31269,11 @@ don't worry \u2014 make the requested changes and resubmit! :scraps:`
|
|||
elements: [
|
||||
{
|
||||
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,
|
||||
action_id: "view_project"
|
||||
}
|
||||
|
|
@ -31252,9 +31281,8 @@ don't worry \u2014 make the requested changes and resubmit! :scraps:`
|
|||
}
|
||||
];
|
||||
} else if (action === "permanently_rejected") {
|
||||
const reviewerMention = reviewerSlackId ? `<@${reviewerSlackId}>` : "a reviewer";
|
||||
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 = [
|
||||
{
|
||||
type: "section",
|
||||
|
|
@ -31262,7 +31290,7 @@ don't worry \u2014 make the requested changes and resubmit! :scraps:`
|
|||
type: "mrkdwn",
|
||||
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:*
|
||||
> ${feedbackForAuthor}
|
||||
|
|
@ -31275,7 +31303,11 @@ if you have any questions about this decision, please reach out to one of our ad
|
|||
elements: [
|
||||
{
|
||||
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,
|
||||
action_id: "view_project"
|
||||
}
|
||||
|
|
@ -31968,13 +32000,18 @@ projects.get("/:id", async ({ params, headers }) => {
|
|||
if (!project[0])
|
||||
return { error: "Not found" };
|
||||
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") {
|
||||
return { error: "Not found" };
|
||||
}
|
||||
if (!isOwner) {
|
||||
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 = [];
|
||||
const reviews = await db.select({
|
||||
id: reviewsTable.id,
|
||||
|
|
@ -31986,7 +32023,11 @@ projects.get("/:id", async ({ params, headers }) => {
|
|||
const reviewerIds = reviews.map((r) => r.reviewerId);
|
||||
let reviewers = [];
|
||||
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";
|
||||
for (const r of reviews) {
|
||||
|
|
@ -31997,7 +32038,7 @@ projects.get("/:id", async ({ params, headers }) => {
|
|||
action: r.action,
|
||||
feedbackForAuthor: r.feedbackForAuthor,
|
||||
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({
|
||||
|
|
@ -32104,7 +32145,10 @@ projects.post("/", async ({ body, headers }) => {
|
|||
projectId: newProject[0].id,
|
||||
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 }) => {
|
||||
const user = await getUserFromSession(headers);
|
||||
|
|
@ -32145,7 +32189,10 @@ projects.put("/:id", async ({ params, body, headers }) => {
|
|||
const syncResult = await syncSingleProject(updated[0].id);
|
||||
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 }) => {
|
||||
const user = await getUserFromSession(headers);
|
||||
|
|
@ -32169,7 +32216,9 @@ projects.post("/:id/unsubmit", async ({ params, headers }) => {
|
|||
if (!project[0])
|
||||
return { error: "Not found" };
|
||||
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") {
|
||||
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,
|
||||
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 }) => {
|
||||
const user = await getUserFromSession(headers);
|
||||
|
|
@ -32196,7 +32248,10 @@ projects.post("/:id/submit", async ({ params, headers, body }) => {
|
|||
const { identity } = meResponse;
|
||||
await db.update(usersTable).set({ verificationStatus: identity.verification_status }).where(eq(usersTable.id, user.id));
|
||||
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);
|
||||
}
|
||||
}
|
||||
return { ...updated[0], hackatimeProject: stripHackatimeIds(updated[0].hackatimeProject) };
|
||||
return {
|
||||
...updated[0],
|
||||
hackatimeProject: stripHackatimeIds(updated[0].hackatimeProject)
|
||||
};
|
||||
});
|
||||
projects.get("/:id/reviews", async ({ params, headers }) => {
|
||||
const user = await getUserFromSession(headers);
|
||||
if (!user)
|
||||
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);
|
||||
if (!project[0])
|
||||
return { error: "Not found" };
|
||||
|
|
@ -32259,8 +32318,12 @@ projects.get("/:id/reviews", async ({ params, headers }) => {
|
|||
}).from(reviewsTable).where(eq(reviewsTable.projectId, parseInt(params.id)));
|
||||
const reviewerIds = reviews.map((r) => r.reviewerId);
|
||||
let reviewers = [];
|
||||
if (reviewerIds.length > 0) {
|
||||
reviewers = await db.select({ id: usersTable.id, username: usersTable.username, avatar: usersTable.avatar }).from(usersTable).where(inArray(usersTable.id, reviewerIds));
|
||||
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));
|
||||
}
|
||||
const filteredReviews = project[0].status === "pending_admin_approval" ? reviews.filter((r) => r.action !== "approved") : reviews;
|
||||
return filteredReviews.map((r) => ({
|
||||
|
|
@ -32268,7 +32331,7 @@ projects.get("/:id/reviews", async ({ params, headers }) => {
|
|||
action: r.action,
|
||||
feedbackForAuthor: r.feedbackForAuthor,
|
||||
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;
|
||||
|
|
@ -35566,6 +35629,32 @@ admin.patch("/orders/:id", async ({ params, body, headers, status: status2 }) =>
|
|||
if (orderStatus && !validStatuses.includes(orderStatus)) {
|
||||
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 };
|
||||
if (orderStatus)
|
||||
updateData.status = orderStatus;
|
||||
|
|
@ -35575,7 +35664,7 @@ admin.patch("/orders/:id", async ({ params, body, headers, status: status2 }) =>
|
|||
updateData.trackingNumber = trackingNumber;
|
||||
if (isFulfilled !== undefined)
|
||||
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,
|
||||
quantity: shopOrdersTable.quantity,
|
||||
pricePerItem: shopOrdersTable.pricePerItem,
|
||||
|
|
@ -35772,8 +35861,9 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
|
|||
} catch (err) {
|
||||
const dbErr = err;
|
||||
const msg = dbErr?.message ?? "";
|
||||
const code = dbErr?.code ?? null;
|
||||
if (msg.includes("does not exist") || code === "42P01") {
|
||||
const causeMsg = dbErr?.cause?.message ?? "";
|
||||
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`
|
||||
CREATE TABLE IF NOT EXISTS admin_deleted_orders (
|
||||
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(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(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));
|
||||
if (order[0].orderType === "purchase" || order[0].orderType === "luck_win") {
|
||||
await tx.update(shopItemsTable).set({
|
||||
count: sql`${shopItemsTable.count} + ${order[0].quantity}`,
|
||||
const alreadyInactive = order[0].status === "cancelled" || order[0].status === "deleted";
|
||||
const shouldRestoreStock = !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).set({
|
||||
count: sql`${shopItemsTable.count} + ${restoreQty}`,
|
||||
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}`);
|
||||
|
|
@ -35936,20 +36049,47 @@ admin.post("/orders/:id/restore", async ({ params, headers, status: status2 }) =
|
|||
createdAt: rr.createdAt
|
||||
});
|
||||
}
|
||||
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 === "luck_win") {
|
||||
const currentPenalty = await tx.select({
|
||||
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}\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({
|
||||
count: sql`GREATEST(${shopItemsTable.count} - ${orderPayload.quantity ?? 1}, 0)`,
|
||||
count: sql`GREATEST(${shopItemsTable.count} - ${qty}, 0)`,
|
||||
updatedAt: new Date
|
||||
}).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}`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -705,9 +705,7 @@ admin.delete("/bonuses/:id", async ({ params, headers, status }) => {
|
|||
return status(404, { error: "Bonus not found" });
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(userBonusesTable)
|
||||
.where(eq(userBonusesTable.id, bonusId));
|
||||
await db.delete(userBonusesTable).where(eq(userBonusesTable.id, bonusId));
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
|
|
@ -2332,7 +2330,12 @@ admin.patch("/orders/:id", async ({ params, body, headers, status }) => {
|
|||
notes,
|
||||
isFulfilled,
|
||||
trackingNumber,
|
||||
} = body as { status?: string; notes?: string; isFulfilled?: boolean; trackingNumber?: string };
|
||||
} = body as {
|
||||
status?: string;
|
||||
notes?: string;
|
||||
isFulfilled?: boolean;
|
||||
trackingNumber?: string;
|
||||
};
|
||||
|
||||
const validStatuses = [
|
||||
"pending",
|
||||
|
|
@ -2346,16 +2349,72 @@ admin.patch("/orders/:id", async ({ params, body, headers, 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() };
|
||||
if (orderStatus) updateData.status = orderStatus;
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
if (trackingNumber !== undefined) updateData.trackingNumber = trackingNumber;
|
||||
if (trackingNumber !== undefined)
|
||||
updateData.trackingNumber = trackingNumber;
|
||||
if (isFulfilled !== undefined) updateData.isFulfilled = isFulfilled;
|
||||
|
||||
const updated = await db
|
||||
.update(shopOrdersTable)
|
||||
.set(updateData)
|
||||
.where(eq(shopOrdersTable.id, parseInt(params.id)))
|
||||
.where(eq(shopOrdersTable.id, orderId))
|
||||
.returning({
|
||||
id: shopOrdersTable.id,
|
||||
userId: shopOrdersTable.userId,
|
||||
|
|
@ -2672,10 +2731,21 @@ admin.delete("/orders/:id", async ({ params, headers, body, status }) => {
|
|||
} catch (err) {
|
||||
// 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.
|
||||
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 code = dbErr?.code ?? null;
|
||||
if (msg.includes("does not exist") || code === "42P01") {
|
||||
const causeMsg = dbErr?.cause?.message ?? "";
|
||||
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)
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS admin_deleted_orders (
|
||||
|
|
@ -2805,28 +2875,96 @@ admin.delete("/orders/:id", async ({ params, headers, body, status }) => {
|
|||
eq(shopRollsTable.shopItemId, order[0].shopItemId),
|
||||
),
|
||||
);
|
||||
await tx
|
||||
.delete(shopPenaltiesTable)
|
||||
.where(
|
||||
and(
|
||||
eq(shopPenaltiesTable.userId, order[0].userId),
|
||||
eq(shopPenaltiesTable.shopItemId, order[0].shopItemId),
|
||||
),
|
||||
);
|
||||
// For luck_win orders, reverse the penalty halving that happened on win
|
||||
// instead of deleting all penalties. The win either halved an existing
|
||||
// penalty (newMult = floor(old/2)) or created a fresh one at 50.
|
||||
// Reversing: double the current multiplier (capped at 100). If the result
|
||||
// is 100, that's effectively no penalty so we can delete the row.
|
||||
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));
|
||||
|
||||
// Restore item stock for purchase/luck_win orders (count was decremented when the order was placed)
|
||||
if (
|
||||
order[0].orderType === "purchase" ||
|
||||
order[0].orderType === "luck_win"
|
||||
) {
|
||||
await tx
|
||||
// Skip if the order was already cancelled/deleted (stock was already restored by the PATCH handler)
|
||||
const alreadyInactive =
|
||||
order[0].status === "cancelled" || order[0].status === "deleted";
|
||||
const shouldRestoreStock =
|
||||
!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)
|
||||
.set({
|
||||
count: sql`${shopItemsTable.count} + ${order[0].quantity}`,
|
||||
count: sql`${shopItemsTable.count} + ${restoreQty}`,
|
||||
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"}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -2961,28 +3099,94 @@ admin.post("/orders/:id/restore", async ({ params, headers, status }) => {
|
|||
createdAt: rr.createdAt,
|
||||
});
|
||||
}
|
||||
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,
|
||||
});
|
||||
// For luck_win orders, the delete handler reversed the penalty halving
|
||||
// instead of deleting penalties. Restoring means re-applying the halving
|
||||
// to the current live penalty (which the delete handler doubled back).
|
||||
if (orderPayload.orderType === "luck_win") {
|
||||
const currentPenalty = await tx
|
||||
.select({
|
||||
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)
|
||||
if (
|
||||
orderPayload.orderType === "purchase" ||
|
||||
orderPayload.orderType === "luck_win"
|
||||
) {
|
||||
// Only decrement if the DELETE handler actually restored stock — it skips restoration when the
|
||||
// order was already cancelled/deleted (because the PATCH handler already restored stock earlier).
|
||||
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({
|
||||
count: sql`GREATEST(${shopItemsTable.count} - ${orderPayload.quantity ?? 1}, 0)`,
|
||||
count: sql`GREATEST(${shopItemsTable.count} - ${qty}, 0)`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue