mirror of
https://github.com/System-End/scraps.git
synced 2026-04-19 23:22:54 +00:00
fixes
This commit is contained in:
parent
e6473c91ac
commit
1370abec5d
4 changed files with 301 additions and 127 deletions
175
backend/dist/index.js
vendored
175
backend/dist/index.js
vendored
|
|
@ -25784,7 +25784,8 @@ var config = {
|
|||
airtableBaseId: process.env.AIRTABLE_BASE_ID,
|
||||
airtableProjectsTableId: process.env.AIRTABLE_PROJECTS_TABLE_ID,
|
||||
airtableUsersTableId: process.env.AIRTABLE_USERS_TABLE_ID,
|
||||
fraudToken: process.env.FRAUD_TOKEN
|
||||
fraudToken: process.env.FRAUD_TOKEN,
|
||||
hcbOrgSlug: "ysws-scraps"
|
||||
};
|
||||
|
||||
// node_modules/drizzle-orm/entity.js
|
||||
|
|
@ -31153,6 +31154,29 @@ async function sendSlackDM(slackId, token, text2, blocks) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
async function notifyOrderFulfilled({
|
||||
userSlackId,
|
||||
itemName,
|
||||
trackingNumber,
|
||||
token
|
||||
}) {
|
||||
const trackingLine = trackingNumber ? `
|
||||
|
||||
*tracking number:* \`${trackingNumber}\`` : "";
|
||||
const fallbackText = `:scraps: hey <@${userSlackId}>! your order for *${itemName}* has been fulfilled and is on its way!${trackingNumber ? ` tracking number: ${trackingNumber}` : ""} :blobhaj_party:`;
|
||||
const blocks = [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `:scraps: hey <@${userSlackId}>! :blobhaj_party:
|
||||
|
||||
your order for *${itemName}* has been fulfilled and is on its way!${trackingLine}`
|
||||
}
|
||||
}
|
||||
];
|
||||
return sendSlackDM(userSlackId, token, fallbackText, blocks);
|
||||
}
|
||||
async function notifyProjectSubmitted({
|
||||
userSlackId,
|
||||
projectName,
|
||||
|
|
@ -31196,8 +31220,6 @@ async function notifyProjectReview({
|
|||
projectId,
|
||||
action,
|
||||
feedbackForAuthor,
|
||||
reviewerSlackId,
|
||||
adminSlackIds,
|
||||
scrapsAwarded,
|
||||
frontendUrl,
|
||||
token
|
||||
|
|
@ -31281,37 +31303,16 @@ don't worry \u2014 make the requested changes and resubmit! :scraps:`
|
|||
}
|
||||
];
|
||||
} else if (action === "permanently_rejected") {
|
||||
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. reason: ${feedbackForAuthor}. if you have any questions, please reach out to an admin: ${adminMentions}`;
|
||||
fallbackText = `:scraps: hey <@${userSlackId}>! your scraps project ${projectName} has been unshipped by an admin.`;
|
||||
blocks = [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `:scraps: hey <@${userSlackId}>! :scraps:
|
||||
text: `:scraps: hey <@${userSlackId}>!
|
||||
|
||||
unfortunately, your scraps project *<${projectUrl}|${projectName}>* has been *permanently rejected*.
|
||||
|
||||
*reason:*
|
||||
> ${feedbackForAuthor}
|
||||
|
||||
if you have any questions about this decision, please reach out to one of our admins: ${adminMentions} :scraps:`
|
||||
your scraps project *<${projectUrl}|${projectName}>* has been unshipped by an admin.`
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "button",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: ":scraps: view your project",
|
||||
emoji: true
|
||||
},
|
||||
url: projectUrl,
|
||||
action_id: "view_project"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
|
@ -34313,16 +34314,60 @@ admin.get("/stats", async ({ headers, status: status2 }) => {
|
|||
const costPerHour = roundedTotalHours > 0 ? Math.round(totalScrapsSpent / roundedTotalHours * 100) / 100 : 0;
|
||||
const totalTierCost = tierCostBreakdown.reduce((sum, t2) => sum + t2.totalCost, 0);
|
||||
const avgCostPerHour = roundedTotalHours > 0 ? Math.round(totalTierCost / roundedTotalHours * 100) / 100 : 0;
|
||||
const [luckWinOrders, consolationCount] = await Promise.all([
|
||||
const activeOrderStatuses = ["cancelled", "deleted"];
|
||||
const [luckWinOrders, purchaseOrders, consolationCount] = await Promise.all([
|
||||
db.select({
|
||||
itemPrice: shopItemsTable.price
|
||||
}).from(shopOrdersTable).innerJoin(shopItemsTable, eq(shopOrdersTable.shopItemId, shopItemsTable.id)).where(eq(shopOrdersTable.orderType, "luck_win")),
|
||||
db.select({ count: sql`count(*)` }).from(shopOrdersTable).where(eq(shopOrdersTable.orderType, "consolation"))
|
||||
}).from(shopOrdersTable).innerJoin(shopItemsTable, eq(shopOrdersTable.shopItemId, shopItemsTable.id)).where(and(eq(shopOrdersTable.orderType, "luck_win"), notInArray(shopOrdersTable.status, activeOrderStatuses))),
|
||||
db.select({
|
||||
itemPrice: shopItemsTable.price,
|
||||
quantity: shopOrdersTable.quantity
|
||||
}).from(shopOrdersTable).innerJoin(shopItemsTable, eq(shopOrdersTable.shopItemId, shopItemsTable.id)).where(and(eq(shopOrdersTable.orderType, "purchase"), notInArray(shopOrdersTable.status, activeOrderStatuses))),
|
||||
db.select({ count: sql`count(*)` }).from(shopOrdersTable).where(and(eq(shopOrdersTable.orderType, "consolation"), notInArray(shopOrdersTable.status, activeOrderStatuses)))
|
||||
]);
|
||||
const luckWinDollarCost = luckWinOrders.reduce((sum, o) => sum + o.itemPrice / SCRAPS_PER_DOLLAR, 0);
|
||||
const purchaseDollarCost = purchaseOrders.reduce((sum, o) => sum + o.itemPrice * o.quantity / SCRAPS_PER_DOLLAR, 0);
|
||||
const consolationDollarCost = Number(consolationCount[0]?.count || 0) * 2;
|
||||
const totalRealCost = luckWinDollarCost + consolationDollarCost;
|
||||
const totalRealCost = luckWinDollarCost + purchaseDollarCost + consolationDollarCost;
|
||||
const realCostPerHour = roundedTotalHours > 0 ? Math.round(totalRealCost / roundedTotalHours * 100) / 100 : 0;
|
||||
const [
|
||||
fulfilledLuckWinOrders,
|
||||
fulfilledPurchaseOrders,
|
||||
fulfilledConsolationCount,
|
||||
fulfilledUpgrades
|
||||
] = await Promise.all([
|
||||
db.select({
|
||||
itemPrice: shopItemsTable.price
|
||||
}).from(shopOrdersTable).innerJoin(shopItemsTable, eq(shopOrdersTable.shopItemId, shopItemsTable.id)).where(and(eq(shopOrdersTable.orderType, "luck_win"), eq(shopOrdersTable.isFulfilled, true))),
|
||||
db.select({
|
||||
itemPrice: shopItemsTable.price,
|
||||
quantity: shopOrdersTable.quantity
|
||||
}).from(shopOrdersTable).innerJoin(shopItemsTable, eq(shopOrdersTable.shopItemId, shopItemsTable.id)).where(and(eq(shopOrdersTable.orderType, "purchase"), eq(shopOrdersTable.isFulfilled, true))),
|
||||
db.select({ count: sql`count(*)` }).from(shopOrdersTable).where(and(eq(shopOrdersTable.orderType, "consolation"), eq(shopOrdersTable.isFulfilled, true))),
|
||||
db.execute(sql`SELECT COALESCE(SUM(rsh.cost), 0) AS total_cost
|
||||
FROM refinery_spending_history rsh
|
||||
WHERE (rsh.user_id, rsh.shop_item_id) IN (
|
||||
SELECT DISTINCT so.user_id, so.shop_item_id
|
||||
FROM shop_orders so
|
||||
WHERE so.order_type = 'luck_win' AND so.is_fulfilled = true
|
||||
)`)
|
||||
]);
|
||||
const fulfilledLuckWinDollarCost = fulfilledLuckWinOrders.reduce((sum, o) => sum + o.itemPrice / SCRAPS_PER_DOLLAR, 0);
|
||||
const fulfilledPurchaseDollarCost = fulfilledPurchaseOrders.reduce((sum, o) => sum + o.itemPrice * o.quantity / SCRAPS_PER_DOLLAR, 0);
|
||||
const fulfilledConsolationDollarCost = Number(fulfilledConsolationCount[0]?.count || 0) * 2;
|
||||
const fulfilledUpgradeDollarCost = Number(fulfilledUpgrades.rows[0]?.total_cost || 0) / SCRAPS_PER_DOLLAR;
|
||||
const totalActualCost = fulfilledLuckWinDollarCost + fulfilledPurchaseDollarCost + fulfilledConsolationDollarCost + fulfilledUpgradeDollarCost;
|
||||
const actualCostPerHour = roundedTotalHours > 0 ? Math.round(totalActualCost / roundedTotalHours * 100) / 100 : 0;
|
||||
let hcbBalanceCents = 0;
|
||||
if (config.hcbOrgSlug) {
|
||||
try {
|
||||
const hcbRes = await fetch(`https://hcb.hackclub.com/api/v3/organizations/${config.hcbOrgSlug}`, { headers: { Accept: "application/json" } });
|
||||
if (hcbRes.ok) {
|
||||
const hcbData = await hcbRes.json();
|
||||
hcbBalanceCents = hcbData.balances?.balance_cents ?? 0;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return {
|
||||
totalUsers,
|
||||
totalProjects,
|
||||
|
|
@ -34346,10 +34391,26 @@ admin.get("/stats", async ({ headers, status: status2 }) => {
|
|||
shopRealCost: {
|
||||
luckWinItemsCost: Math.round(luckWinDollarCost * 100) / 100,
|
||||
luckWinCount: luckWinOrders.length,
|
||||
purchaseItemsCost: Math.round(purchaseDollarCost * 100) / 100,
|
||||
purchaseCount: purchaseOrders.length,
|
||||
consolationShippingCost: Math.round(consolationDollarCost * 100) / 100,
|
||||
consolationCount: Number(consolationCount[0]?.count || 0),
|
||||
totalRealCost: Math.round(totalRealCost * 100) / 100,
|
||||
realCostPerHour
|
||||
},
|
||||
shopActualCost: {
|
||||
hcbBalanceCents,
|
||||
hcbBalance: Math.round(hcbBalanceCents / 100 * 100) / 100,
|
||||
fulfilledLuckWinCost: Math.round(fulfilledLuckWinDollarCost * 100) / 100,
|
||||
fulfilledLuckWinCount: fulfilledLuckWinOrders.length,
|
||||
fulfilledPurchaseCost: Math.round(fulfilledPurchaseDollarCost * 100) / 100,
|
||||
fulfilledPurchaseCount: fulfilledPurchaseOrders.length,
|
||||
fulfilledConsolationCost: Math.round(fulfilledConsolationDollarCost * 100) / 100,
|
||||
fulfilledConsolationCount: Number(fulfilledConsolationCount[0]?.count || 0),
|
||||
fulfilledUpgradeCost: Math.round(fulfilledUpgradeDollarCost * 100) / 100,
|
||||
totalActualCost: Math.round(totalActualCost * 100) / 100,
|
||||
actualCostPerHour,
|
||||
remainingBudget: Math.round((hcbBalanceCents / 100 - totalActualCost) * 100) / 100
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
@ -34862,20 +34923,12 @@ admin.post("/reviews/:id", async ({ params, body, headers }) => {
|
|||
try {
|
||||
const projectAuthor = await db.select({ slackId: usersTable.slackId }).from(usersTable).where(eq(usersTable.id, project[0].userId)).limit(1);
|
||||
if (projectAuthor[0]?.slackId) {
|
||||
let adminSlackIds = [];
|
||||
if (action === "permanently_rejected") {
|
||||
const admins = await db.select({ slackId: usersTable.slackId }).from(usersTable).where(eq(usersTable.role, "admin"));
|
||||
adminSlackIds = admins.map((a) => a.slackId).filter((id) => !!id);
|
||||
}
|
||||
const reviewerSlackId = user2.slackId ?? null;
|
||||
await notifyProjectReview({
|
||||
userSlackId: projectAuthor[0].slackId,
|
||||
projectName: project[0].name,
|
||||
projectId,
|
||||
action,
|
||||
feedbackForAuthor,
|
||||
reviewerSlackId,
|
||||
adminSlackIds,
|
||||
scrapsAwarded,
|
||||
frontendUrl: config.frontendUrl,
|
||||
token: config.slackBotToken
|
||||
|
|
@ -35098,8 +35151,6 @@ admin.post("/second-pass/:id", async ({ params, body, headers }) => {
|
|||
projectId,
|
||||
action: "approved",
|
||||
feedbackForAuthor: "Your project has been approved and shipped!",
|
||||
reviewerSlackId: user2.slackId ?? null,
|
||||
adminSlackIds: [],
|
||||
scrapsAwarded,
|
||||
frontendUrl: config.frontendUrl,
|
||||
token: config.slackBotToken
|
||||
|
|
@ -35130,8 +35181,6 @@ admin.post("/second-pass/:id", async ({ params, body, headers }) => {
|
|||
projectId,
|
||||
action: "denied",
|
||||
feedbackForAuthor: feedbackForAuthor || "The admin has rejected the initial approval. Please make improvements and resubmit.",
|
||||
reviewerSlackId: user2.slackId ?? null,
|
||||
adminSlackIds: [],
|
||||
scrapsAwarded: 0,
|
||||
frontendUrl: config.frontendUrl,
|
||||
token: config.slackBotToken
|
||||
|
|
@ -35595,12 +35644,29 @@ admin.get("/orders", async ({ headers, query, status: status2 }) => {
|
|||
itemImage: shopItemsTable.image,
|
||||
userId: usersTable.id,
|
||||
username: usersTable.username,
|
||||
slackId: usersTable.slackId
|
||||
slackId: usersTable.slackId,
|
||||
userEmail: usersTable.email
|
||||
}).from(shopOrdersTable).innerJoin(shopItemsTable, eq(shopOrdersTable.shopItemId, shopItemsTable.id)).innerJoin(usersTable, eq(shopOrdersTable.userId, usersTable.id)).orderBy(desc(shopOrdersTable.createdAt));
|
||||
if (orderStatus) {
|
||||
ordersQuery = ordersQuery.where(eq(shopOrdersTable.status, orderStatus));
|
||||
}
|
||||
return await ordersQuery;
|
||||
const rows = await ordersQuery;
|
||||
const uniqueEmails = [
|
||||
...new Set(rows.map((r) => r.userEmail).filter(Boolean))
|
||||
];
|
||||
const banMap = new Map;
|
||||
await Promise.all(uniqueEmails.map(async (email) => {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(email);
|
||||
banMap.set(email, htUser?.banned ?? false);
|
||||
} catch {
|
||||
banMap.set(email, false);
|
||||
}
|
||||
}));
|
||||
return rows.map(({ userEmail, ...row }) => ({
|
||||
...row,
|
||||
hackatimeBanned: banMap.get(userEmail ?? "") ?? false
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return status2(500, { error: "Failed to fetch orders" });
|
||||
|
|
@ -35635,7 +35701,8 @@ admin.patch("/orders/:id", async ({ params, body, headers, status: status2 }) =>
|
|||
status: shopOrdersTable.status,
|
||||
orderType: shopOrdersTable.orderType,
|
||||
shopItemId: shopOrdersTable.shopItemId,
|
||||
quantity: shopOrdersTable.quantity
|
||||
quantity: shopOrdersTable.quantity,
|
||||
isFulfilled: shopOrdersTable.isFulfilled
|
||||
}).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";
|
||||
|
|
@ -35666,6 +35733,7 @@ admin.patch("/orders/:id", async ({ params, body, headers, status: status2 }) =>
|
|||
updateData.isFulfilled = isFulfilled;
|
||||
const updated = await db.update(shopOrdersTable).set(updateData).where(eq(shopOrdersTable.id, orderId)).returning({
|
||||
id: shopOrdersTable.id,
|
||||
userId: shopOrdersTable.userId,
|
||||
quantity: shopOrdersTable.quantity,
|
||||
pricePerItem: shopOrdersTable.pricePerItem,
|
||||
totalPrice: shopOrdersTable.totalPrice,
|
||||
|
|
@ -35680,7 +35748,20 @@ admin.patch("/orders/:id", async ({ params, body, headers, status: status2 }) =>
|
|||
if (!updated[0]) {
|
||||
return status2(404, { error: "Not found" });
|
||||
}
|
||||
return updated[0];
|
||||
if (isFulfilled === true && orderBeforeUpdate[0] && !orderBeforeUpdate[0].isFulfilled && config.slackBotToken) {
|
||||
const [orderUser] = await db.select({ slackId: usersTable.slackId }).from(usersTable).where(eq(usersTable.id, updated[0].userId)).limit(1);
|
||||
const [orderItem] = await db.select({ name: shopItemsTable.name }).from(shopItemsTable).where(eq(shopItemsTable.id, orderBeforeUpdate[0].shopItemId)).limit(1);
|
||||
if (orderUser?.slackId && orderItem?.name) {
|
||||
notifyOrderFulfilled({
|
||||
userSlackId: orderUser.slackId,
|
||||
itemName: orderItem.name,
|
||||
trackingNumber: updated[0].trackingNumber,
|
||||
token: config.slackBotToken
|
||||
}).catch((err) => console.error("Failed to send fulfillment DM:", err));
|
||||
}
|
||||
}
|
||||
const { userId: _userId, ...returnedOrder } = updated[0];
|
||||
return returnedOrder;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return status2(500, { error: "Failed to update order" });
|
||||
|
|
|
|||
|
|
@ -142,15 +142,15 @@ export async function notifyOrderFulfilled({
|
|||
}): Promise<boolean> {
|
||||
const trackingLine = trackingNumber
|
||||
? `\n\n*tracking number:* \`${trackingNumber}\``
|
||||
: '';
|
||||
: "";
|
||||
|
||||
const fallbackText = `:scraps: hey <@${userSlackId}>! your order for *${itemName}* has been fulfilled and is on its way!${trackingNumber ? ` tracking number: ${trackingNumber}` : ''} :blobhaj_party:`;
|
||||
const fallbackText = `:scraps: hey <@${userSlackId}>! your order for *${itemName}* has been fulfilled and is on its way!${trackingNumber ? ` tracking number: ${trackingNumber}` : ""} :blobhaj_party:`;
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
type: 'section',
|
||||
type: "section",
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
type: "mrkdwn",
|
||||
text: `:scraps: hey <@${userSlackId}>! :blobhaj_party:\n\nyour order for *${itemName}* has been fulfilled and is on its way!${trackingLine}`,
|
||||
},
|
||||
},
|
||||
|
|
@ -216,8 +216,6 @@ export async function notifyProjectReview({
|
|||
projectId,
|
||||
action,
|
||||
feedbackForAuthor,
|
||||
reviewerSlackId,
|
||||
adminSlackIds,
|
||||
scrapsAwarded,
|
||||
frontendUrl,
|
||||
token,
|
||||
|
|
@ -227,8 +225,6 @@ export async function notifyProjectReview({
|
|||
projectId: number;
|
||||
action: "approved" | "denied" | "permanently_rejected";
|
||||
feedbackForAuthor: string;
|
||||
reviewerSlackId?: string | null;
|
||||
adminSlackIds: string[];
|
||||
scrapsAwarded?: number;
|
||||
frontendUrl: string;
|
||||
token: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
import { Elysia } from "elysia";
|
||||
import { eq, ne, and, inArray, sql, desc, asc, or, isNull } from "drizzle-orm";
|
||||
import {
|
||||
eq,
|
||||
ne,
|
||||
and,
|
||||
inArray,
|
||||
notInArray,
|
||||
sql,
|
||||
desc,
|
||||
asc,
|
||||
or,
|
||||
isNull,
|
||||
} from "drizzle-orm";
|
||||
import { db } from "../db";
|
||||
import { usersTable, userBonusesTable } from "../schemas/users";
|
||||
import { projectsTable } from "../schemas/projects";
|
||||
|
|
@ -194,8 +205,9 @@ admin.get("/stats", async ({ headers, status }) => {
|
|||
? Math.round((totalTierCost / roundedTotalHours) * 100) / 100
|
||||
: 0;
|
||||
|
||||
// Max dollar cost from shop fulfillment (all luck_win orders regardless of fulfillment)
|
||||
const [luckWinOrders, consolationCount] = await Promise.all([
|
||||
// Max dollar cost from shop fulfillment (all non-cancelled/deleted orders)
|
||||
const activeOrderStatuses = ["cancelled", "deleted"];
|
||||
const [luckWinOrders, purchaseOrders, consolationCount] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
itemPrice: shopItemsTable.price,
|
||||
|
|
@ -205,73 +217,130 @@ admin.get("/stats", async ({ headers, status }) => {
|
|||
shopItemsTable,
|
||||
eq(shopOrdersTable.shopItemId, shopItemsTable.id),
|
||||
)
|
||||
.where(eq(shopOrdersTable.orderType, "luck_win")),
|
||||
.where(
|
||||
and(
|
||||
eq(shopOrdersTable.orderType, "luck_win"),
|
||||
notInArray(shopOrdersTable.status, activeOrderStatuses),
|
||||
),
|
||||
),
|
||||
db
|
||||
.select({
|
||||
itemPrice: shopItemsTable.price,
|
||||
quantity: shopOrdersTable.quantity,
|
||||
})
|
||||
.from(shopOrdersTable)
|
||||
.innerJoin(
|
||||
shopItemsTable,
|
||||
eq(shopOrdersTable.shopItemId, shopItemsTable.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(shopOrdersTable.orderType, "purchase"),
|
||||
notInArray(shopOrdersTable.status, activeOrderStatuses),
|
||||
),
|
||||
),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(shopOrdersTable)
|
||||
.where(eq(shopOrdersTable.orderType, "consolation")),
|
||||
.where(
|
||||
and(
|
||||
eq(shopOrdersTable.orderType, "consolation"),
|
||||
notInArray(shopOrdersTable.status, activeOrderStatuses),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
const luckWinDollarCost = luckWinOrders.reduce(
|
||||
(sum, o) => sum + o.itemPrice / SCRAPS_PER_DOLLAR,
|
||||
0,
|
||||
);
|
||||
const purchaseDollarCost = purchaseOrders.reduce(
|
||||
(sum, o) => sum + (o.itemPrice * o.quantity) / SCRAPS_PER_DOLLAR,
|
||||
0,
|
||||
);
|
||||
const consolationDollarCost = Number(consolationCount[0]?.count || 0) * 2;
|
||||
const totalRealCost = luckWinDollarCost + consolationDollarCost;
|
||||
const totalRealCost =
|
||||
luckWinDollarCost + purchaseDollarCost + consolationDollarCost;
|
||||
const realCostPerHour =
|
||||
roundedTotalHours > 0
|
||||
? Math.round((totalRealCost / roundedTotalHours) * 100) / 100
|
||||
: 0;
|
||||
|
||||
// Actual fulfillment cost (only fulfilled orders + upgrades consumed on fulfilled wins)
|
||||
const [fulfilledLuckWinOrders, fulfilledConsolationCount, fulfilledUpgrades] =
|
||||
await Promise.all([
|
||||
db
|
||||
.select({
|
||||
itemPrice: shopItemsTable.price,
|
||||
})
|
||||
.from(shopOrdersTable)
|
||||
.innerJoin(
|
||||
shopItemsTable,
|
||||
eq(shopOrdersTable.shopItemId, shopItemsTable.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(shopOrdersTable.orderType, "luck_win"),
|
||||
eq(shopOrdersTable.isFulfilled, true),
|
||||
),
|
||||
const [
|
||||
fulfilledLuckWinOrders,
|
||||
fulfilledPurchaseOrders,
|
||||
fulfilledConsolationCount,
|
||||
fulfilledUpgrades,
|
||||
] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
itemPrice: shopItemsTable.price,
|
||||
})
|
||||
.from(shopOrdersTable)
|
||||
.innerJoin(
|
||||
shopItemsTable,
|
||||
eq(shopOrdersTable.shopItemId, shopItemsTable.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(shopOrdersTable.orderType, "luck_win"),
|
||||
eq(shopOrdersTable.isFulfilled, true),
|
||||
),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(shopOrdersTable)
|
||||
.where(
|
||||
and(
|
||||
eq(shopOrdersTable.orderType, "consolation"),
|
||||
eq(shopOrdersTable.isFulfilled, true),
|
||||
),
|
||||
),
|
||||
db.execute(
|
||||
sql`SELECT COALESCE(SUM(rsh.cost), 0) AS total_cost
|
||||
FROM refinery_spending_history rsh
|
||||
WHERE (rsh.user_id, rsh.shop_item_id) IN (
|
||||
SELECT DISTINCT so.user_id, so.shop_item_id
|
||||
FROM shop_orders so
|
||||
WHERE so.order_type = 'luck_win' AND so.is_fulfilled = true
|
||||
)`,
|
||||
),
|
||||
]);
|
||||
db
|
||||
.select({
|
||||
itemPrice: shopItemsTable.price,
|
||||
quantity: shopOrdersTable.quantity,
|
||||
})
|
||||
.from(shopOrdersTable)
|
||||
.innerJoin(
|
||||
shopItemsTable,
|
||||
eq(shopOrdersTable.shopItemId, shopItemsTable.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(shopOrdersTable.orderType, "purchase"),
|
||||
eq(shopOrdersTable.isFulfilled, true),
|
||||
),
|
||||
),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(shopOrdersTable)
|
||||
.where(
|
||||
and(
|
||||
eq(shopOrdersTable.orderType, "consolation"),
|
||||
eq(shopOrdersTable.isFulfilled, true),
|
||||
),
|
||||
),
|
||||
db.execute(
|
||||
sql`SELECT COALESCE(SUM(rsh.cost), 0) AS total_cost
|
||||
FROM refinery_spending_history rsh
|
||||
WHERE (rsh.user_id, rsh.shop_item_id) IN (
|
||||
SELECT DISTINCT so.user_id, so.shop_item_id
|
||||
FROM shop_orders so
|
||||
WHERE so.order_type = 'luck_win' AND so.is_fulfilled = true
|
||||
)`,
|
||||
),
|
||||
]);
|
||||
|
||||
const fulfilledLuckWinDollarCost = fulfilledLuckWinOrders.reduce(
|
||||
(sum, o) => sum + o.itemPrice / SCRAPS_PER_DOLLAR,
|
||||
0,
|
||||
);
|
||||
const fulfilledPurchaseDollarCost = fulfilledPurchaseOrders.reduce(
|
||||
(sum, o) => sum + (o.itemPrice * o.quantity) / SCRAPS_PER_DOLLAR,
|
||||
0,
|
||||
);
|
||||
const fulfilledConsolationDollarCost =
|
||||
Number(fulfilledConsolationCount[0]?.count || 0) * 2;
|
||||
const fulfilledUpgradeDollarCost =
|
||||
Number((fulfilledUpgrades.rows[0] as { total_cost: string })?.total_cost || 0) /
|
||||
SCRAPS_PER_DOLLAR;
|
||||
Number(
|
||||
(fulfilledUpgrades.rows[0] as { total_cost: string })?.total_cost || 0,
|
||||
) / SCRAPS_PER_DOLLAR;
|
||||
const totalActualCost =
|
||||
fulfilledLuckWinDollarCost +
|
||||
fulfilledPurchaseDollarCost +
|
||||
fulfilledConsolationDollarCost +
|
||||
fulfilledUpgradeDollarCost;
|
||||
const actualCostPerHour =
|
||||
|
|
@ -288,7 +357,9 @@ admin.get("/stats", async ({ headers, status }) => {
|
|||
{ headers: { Accept: "application/json" } },
|
||||
);
|
||||
if (hcbRes.ok) {
|
||||
const hcbData = await hcbRes.json() as { balances?: { balance_cents?: number } };
|
||||
const hcbData = (await hcbRes.json()) as {
|
||||
balances?: { balance_cents?: number };
|
||||
};
|
||||
hcbBalanceCents = hcbData.balances?.balance_cents ?? 0;
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -319,6 +390,8 @@ admin.get("/stats", async ({ headers, status }) => {
|
|||
shopRealCost: {
|
||||
luckWinItemsCost: Math.round(luckWinDollarCost * 100) / 100,
|
||||
luckWinCount: luckWinOrders.length,
|
||||
purchaseItemsCost: Math.round(purchaseDollarCost * 100) / 100,
|
||||
purchaseCount: purchaseOrders.length,
|
||||
consolationShippingCost: Math.round(consolationDollarCost * 100) / 100,
|
||||
consolationCount: Number(consolationCount[0]?.count || 0),
|
||||
totalRealCost: Math.round(totalRealCost * 100) / 100,
|
||||
|
|
@ -329,6 +402,9 @@ admin.get("/stats", async ({ headers, status }) => {
|
|||
hcbBalance: Math.round((hcbBalanceCents / 100) * 100) / 100,
|
||||
fulfilledLuckWinCost: Math.round(fulfilledLuckWinDollarCost * 100) / 100,
|
||||
fulfilledLuckWinCount: fulfilledLuckWinOrders.length,
|
||||
fulfilledPurchaseCost:
|
||||
Math.round(fulfilledPurchaseDollarCost * 100) / 100,
|
||||
fulfilledPurchaseCount: fulfilledPurchaseOrders.length,
|
||||
fulfilledConsolationCost:
|
||||
Math.round(fulfilledConsolationDollarCost * 100) / 100,
|
||||
fulfilledConsolationCount: Number(
|
||||
|
|
@ -1121,30 +1197,12 @@ admin.post("/reviews/:id", async ({ params, body, headers }) => {
|
|||
.limit(1);
|
||||
|
||||
if (projectAuthor[0]?.slackId) {
|
||||
// Get admin Slack IDs for permanently rejected projects
|
||||
let adminSlackIds: string[] = [];
|
||||
if (action === "permanently_rejected") {
|
||||
const admins = await db
|
||||
.select({ slackId: usersTable.slackId })
|
||||
.from(usersTable)
|
||||
.where(eq(usersTable.role, "admin"));
|
||||
|
||||
adminSlackIds = admins
|
||||
.map((a) => a.slackId)
|
||||
.filter((id): id is string => !!id);
|
||||
}
|
||||
|
||||
// Get the reviewer's Slack ID
|
||||
const reviewerSlackId = user.slackId ?? null;
|
||||
|
||||
await notifyProjectReview({
|
||||
userSlackId: projectAuthor[0].slackId,
|
||||
projectName: project[0].name,
|
||||
projectId,
|
||||
action,
|
||||
feedbackForAuthor,
|
||||
reviewerSlackId,
|
||||
adminSlackIds,
|
||||
scrapsAwarded,
|
||||
frontendUrl: config.frontendUrl,
|
||||
token: config.slackBotToken,
|
||||
|
|
@ -1479,8 +1537,6 @@ admin.post("/second-pass/:id", async ({ params, body, headers }) => {
|
|||
projectId,
|
||||
action: "approved",
|
||||
feedbackForAuthor: "Your project has been approved and shipped!",
|
||||
reviewerSlackId: user.slackId ?? null,
|
||||
adminSlackIds: [],
|
||||
scrapsAwarded,
|
||||
frontendUrl: config.frontendUrl,
|
||||
token: config.slackBotToken,
|
||||
|
|
@ -1539,8 +1595,6 @@ admin.post("/second-pass/:id", async ({ params, body, headers }) => {
|
|||
feedbackForAuthor:
|
||||
feedbackForAuthor ||
|
||||
"The admin has rejected the initial approval. Please make improvements and resubmit.",
|
||||
reviewerSlackId: user.slackId ?? null,
|
||||
adminSlackIds: [],
|
||||
scrapsAwarded: 0,
|
||||
frontendUrl: config.frontendUrl,
|
||||
token: config.slackBotToken,
|
||||
|
|
@ -2295,7 +2349,9 @@ admin.get("/orders", async ({ headers, query, status }) => {
|
|||
const rows = await ordersQuery;
|
||||
|
||||
// Batch-check Hackatime ban status for unique user emails
|
||||
const uniqueEmails = [...new Set(rows.map((r) => r.userEmail).filter(Boolean))] as string[];
|
||||
const uniqueEmails = [
|
||||
...new Set(rows.map((r) => r.userEmail).filter(Boolean)),
|
||||
] as string[];
|
||||
const banMap = new Map<string, boolean>();
|
||||
await Promise.all(
|
||||
uniqueEmails.map(async (email) => {
|
||||
|
|
@ -2359,6 +2415,7 @@ admin.patch("/orders/:id", async ({ params, body, headers, status }) => {
|
|||
orderType: shopOrdersTable.orderType,
|
||||
shopItemId: shopOrdersTable.shopItemId,
|
||||
quantity: shopOrdersTable.quantity,
|
||||
isFulfilled: shopOrdersTable.isFulfilled,
|
||||
})
|
||||
.from(shopOrdersTable)
|
||||
.where(eq(shopOrdersTable.id, orderId))
|
||||
|
|
@ -2434,7 +2491,12 @@ admin.patch("/orders/:id", async ({ params, body, headers, status }) => {
|
|||
return status(404, { error: "Not found" });
|
||||
}
|
||||
|
||||
if (isFulfilled === true && config.slackBotToken) {
|
||||
if (
|
||||
isFulfilled === true &&
|
||||
orderBeforeUpdate[0] &&
|
||||
!orderBeforeUpdate[0].isFulfilled &&
|
||||
config.slackBotToken
|
||||
) {
|
||||
const [orderUser] = await db
|
||||
.select({ slackId: usersTable.slackId })
|
||||
.from(usersTable)
|
||||
|
|
@ -2444,8 +2506,7 @@ admin.patch("/orders/:id", async ({ params, body, headers, status }) => {
|
|||
const [orderItem] = await db
|
||||
.select({ name: shopItemsTable.name })
|
||||
.from(shopItemsTable)
|
||||
.innerJoin(shopOrdersTable, eq(shopOrdersTable.shopItemId, shopItemsTable.id))
|
||||
.where(eq(shopOrdersTable.id, parseInt(params.id)))
|
||||
.where(eq(shopItemsTable.id, orderBeforeUpdate[0].shopItemId))
|
||||
.limit(1);
|
||||
|
||||
if (orderUser?.slackId && orderItem?.name) {
|
||||
|
|
@ -2454,7 +2515,7 @@ admin.patch("/orders/:id", async ({ params, body, headers, status }) => {
|
|||
itemName: orderItem.name,
|
||||
trackingNumber: updated[0].trackingNumber,
|
||||
token: config.slackBotToken,
|
||||
}).catch((err) => console.error('Failed to send fulfillment DM:', err));
|
||||
}).catch((err) => console.error("Failed to send fulfillment DM:", err));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@
|
|||
interface ShopRealCost {
|
||||
luckWinItemsCost: number;
|
||||
luckWinCount: number;
|
||||
purchaseItemsCost: number;
|
||||
purchaseCount: number;
|
||||
consolationShippingCost: number;
|
||||
consolationCount: number;
|
||||
totalRealCost: number;
|
||||
|
|
@ -57,6 +59,8 @@
|
|||
hcbBalance: number;
|
||||
fulfilledLuckWinCost: number;
|
||||
fulfilledLuckWinCount: number;
|
||||
fulfilledPurchaseCost: number;
|
||||
fulfilledPurchaseCount: number;
|
||||
fulfilledConsolationCost: number;
|
||||
fulfilledConsolationCount: number;
|
||||
fulfilledUpgradeCost: number;
|
||||
|
|
@ -575,7 +579,7 @@
|
|||
<p class="text-4xl font-bold text-red-700">
|
||||
${stats.shopRealCost.totalRealCost.toFixed(2)}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">items won + consolation shipping</p>
|
||||
<p class="text-xs text-gray-400">items won + purchases + consolation shipping</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -590,6 +594,21 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 rounded-2xl border-4 border-black p-6">
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-full bg-black text-white">
|
||||
<Coins size={32} />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-bold text-gray-500">direct purchases</p>
|
||||
<p class="text-4xl font-bold">
|
||||
${stats.shopRealCost.purchaseItemsCost.toFixed(2)}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">
|
||||
{stats.shopRealCost.purchaseCount} items purchased
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 rounded-2xl border-4 border-black p-6">
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-full bg-black text-white">
|
||||
<Coins size={32} />
|
||||
|
|
@ -678,7 +697,9 @@
|
|||
<p class="text-4xl font-bold text-red-700">
|
||||
${stats.shopActualCost.totalActualCost.toFixed(2)}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">fulfilled items + consolations + upgrades</p>
|
||||
<p class="text-xs text-gray-400">
|
||||
fulfilled wins + purchases + consolations + upgrades
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -697,6 +718,21 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 rounded-2xl border-4 border-black p-6">
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-full bg-black text-white">
|
||||
<Coins size={32} />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-bold text-gray-500">fulfilled purchases</p>
|
||||
<p class="text-4xl font-bold">
|
||||
${stats.shopActualCost.fulfilledPurchaseCost.toFixed(2)}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">
|
||||
{stats.shopActualCost.fulfilledPurchaseCount} purchases fulfilled
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 rounded-2xl border-4 border-black p-6">
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-full bg-black text-white">
|
||||
<Coins size={32} />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue