Skip to content

Commit 49dcb18

Browse files
committed
FIX - coins attribution wrong
1 parent e6a68ad commit 49dcb18

7 files changed

Lines changed: 54 additions & 13 deletions

File tree

client/src/components/AdminReviewPage.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ function formatHours(value) {
3939
return Number(value ?? 0).toFixed(2);
4040
}
4141

42+
const BRICKS_PER_APPROVED_HOUR = 20;
43+
4244
function formatDate(value) {
4345
if (!value) return "Shipped (unknown date)";
4446
return `Shipped ${new Date(value).toLocaleDateString()}`;
@@ -231,7 +233,7 @@ function AdminReviewDetail({ projectId }) {
231233
if (!response.ok) throw new Error(data.error || "Failed to load project.");
232234
setProject(data.project);
233235
setJournalEntries(data.journalEntries || []);
234-
setApprovedHours(data.project?.pendingReviewHours ? String(data.project.pendingReviewHours) : "");
236+
setApprovedHours("");
235237
setStatus("");
236238
} catch (err) {
237239
setMessage(err.message);
@@ -355,6 +357,8 @@ function AdminReviewDetail({ projectId }) {
355357
const isSuperadmin = user?.role === "superadmin";
356358
const newHoursMax = Number(project.pendingReviewHours ?? 0);
357359
const newHoursPlaceholder = formatHours(newHoursMax);
360+
const approvedHoursNumber = Number.parseFloat(approvedHours);
361+
const awardPreview = Number.isFinite(approvedHoursNumber) ? approvedHoursNumber * BRICKS_PER_APPROVED_HOUR : 0;
358362

359363
return (
360364
<main className="admin-review-page">
@@ -453,6 +457,7 @@ function AdminReviewDetail({ projectId }) {
453457
disabled={isAlreadyReviewed}
454458
onChange={(event) => setApprovedHours(clampApprovalHours(event.target.value, newHoursMax))}
455459
/>
460+
<small>Only this approved amount awards bricks: {formatHours(awardPreview)} bricks</small>
456461
</div>
457462
<div>
458463
<span>Journal hours</span>

client/src/components/ProjectsPage.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -889,8 +889,9 @@ function ProjectFormModal({
889889
<select value={project.projectType || "software"} onChange={(event) => onChange("projectType", event.target.value)}>
890890
<option value="software">Software</option>
891891
<option value="hardware">Hardware</option>
892-
<option value="art">Art</option>
893-
<option value="other">Other</option>
892+
<option value="art" disabled title="Coming soon...">
893+
Art (Coming soon...)
894+
</option>
894895
</select>
895896
</label>
896897
<label>

client/src/components/ShopPage.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const stackTitle = "https://cdn.hackclub.com/019e3e5a-8745-7bee-a1ab-07b5743f98c
1212
const legoCharacter = "https://cdn.hackclub.com/019e3e5a-6d71-79f0-9633-8668b69f464d/legoChar_1.png";
1313
import "./ShopPage.css";
1414

15+
const BRICKS_PER_USD = 20;
16+
1517
function formatBricks(value) {
1618
const numeric = Number(value);
1719
return Number.isFinite(numeric) ? Math.round(numeric).toString() : "0";
@@ -74,7 +76,7 @@ export function ShopPage() {
7476
const quantity = Math.max(1, Number(purchaseQuantity) || 1);
7577
const shippingUsd = shippingTaxUsd ? Number(shippingTaxUsd) : 0;
7678
const safeShippingUsd = Number.isFinite(shippingUsd) ? shippingUsd : 0;
77-
const shippingBricks = safeShippingUsd ? Math.ceil(safeShippingUsd * 10) : 0;
79+
const shippingBricks = safeShippingUsd ? Math.ceil(safeShippingUsd * BRICKS_PER_USD) : 0;
7880
const totalBricks = baseBricks * quantity + (Number.isFinite(shippingBricks) ? shippingBricks : 0);
7981
const itemUsd = selectedItem?.priceUsd != null ? Number(selectedItem.priceUsd) : baseBricks / 10;
8082
const totalUsd = (Number.isFinite(itemUsd) ? itemUsd : 0) * quantity + safeShippingUsd;

server/adminStats.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export async function getAdminStats() {
126126
`),
127127
pool.query(`SELECT COALESCE(SUM(bricks), 0) AS wallet_bricks FROM users`),
128128
pool.query(`
129-
SELECT COALESCE(SUM(bricks_earned), 0) AS total_earned FROM projects WHERE status = 'approved'
129+
SELECT COALESCE(SUM(bricks_earned), 0) AS total_earned FROM projects WHERE COALESCE(approved_hours, 0) > 0
130130
`),
131131
pool.query(`
132132
SELECT

server/projects.js

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { syncJournalEntryToAirtable } from "./airtableJournals.js";
44
import { deleteProjectSubmissionsFromYsws, ensureYswsProjectSubmissionsTable, submitProjectToYsws } from "./airtableYsws.js";
55
import { fetchHackatimeProjects, sumHackatimeHoursForNames } from "./hackatimeAuth.js";
66

7+
export const BRICKS_PER_APPROVED_HOUR = 20;
8+
79
export async function ensureProjectsTable() {
810
if (!pool) {
911
console.warn("[projects] DATABASE_URL not set; skipping projects table setup.");
@@ -97,6 +99,35 @@ export async function ensureProjectsTable() {
9799
await pool.query(`UPDATE projects SET fraud_flag = FALSE WHERE fraud_flag IS NULL`);
98100
await pool.query(`UPDATE projects SET ship_kind = 'initial' WHERE ship_kind IS NULL`);
99101
await pool.query(`UPDATE projects SET blocked = FALSE WHERE blocked IS NULL`);
102+
await pool.query(`
103+
WITH mismatched AS (
104+
SELECT
105+
id,
106+
user_id,
107+
COALESCE(bricks_earned, 0) AS old_bricks,
108+
ROUND(COALESCE(approved_hours, 0) * $1, 2) AS new_bricks
109+
FROM projects
110+
WHERE COALESCE(bricks_earned, 0) != ROUND(COALESCE(approved_hours, 0) * $1, 2)
111+
),
112+
corrected AS (
113+
UPDATE projects
114+
SET bricks_earned = mismatched.new_bricks,
115+
updated_at = NOW()
116+
FROM mismatched
117+
WHERE projects.id = mismatched.id
118+
RETURNING mismatched.user_id, mismatched.new_bricks - mismatched.old_bricks AS delta
119+
),
120+
deltas AS (
121+
SELECT user_id, SUM(delta) AS delta
122+
FROM corrected
123+
GROUP BY user_id
124+
)
125+
UPDATE users
126+
SET bricks = GREATEST(0, users.bricks + deltas.delta),
127+
updated_at = NOW()
128+
FROM deltas
129+
WHERE users.id = deltas.user_id
130+
`, [BRICKS_PER_APPROVED_HOUR]);
100131
await pool.query(`
101132
UPDATE projects
102133
SET last_shipped_hours = GREATEST(
@@ -436,7 +467,7 @@ export async function approveAdminReviewProject(adminId, projectId, input = {})
436467
throw new Error(`Cannot approve ${newTotalApprovedHours.toFixed(2)} total hours because only ${totalLoggedHours.toFixed(2)} hours are logged.`);
437468
}
438469

439-
const bricksDelta = approvedHours * 20;
470+
const bricksDelta = approvedHours * BRICKS_PER_APPROVED_HOUR;
440471
const reductionHours = Math.max(0, roundedHours(totalLoggedHours - newTotalApprovedHours));
441472
const newCumulativeBricks = Number(project.bricks_earned ?? 0) + bricksDelta;
442473

@@ -506,8 +537,9 @@ export async function rejectAdminReviewProject(adminId, projectId, input = {}) {
506537
await client.query("BEGIN");
507538
await refreshProjectJournalHoursWithClient(client, projectId);
508539

509-
const lookup = await client.query("SELECT ship_kind FROM projects WHERE id = $1 FOR UPDATE", [projectId]);
510-
const isReship = lookup.rows[0]?.ship_kind === "reship";
540+
const lookup = await client.query("SELECT * FROM projects WHERE id = $1 FOR UPDATE", [projectId]);
541+
const project = lookup.rows[0];
542+
const isReship = project?.ship_kind === "reship";
511543

512544
const result = await client.query(
513545
`

server/shopItems.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const REMOVED_SHOP_ITEM_COLUMNS = [
2727
];
2828

2929
const REMOVED_SHOP_ORDER_COLUMNS = ["total_coins"];
30+
const BRICKS_PER_USD = 20;
3031

3132
export async function ensureShopItemsTable() {
3233
if (!pool) {
@@ -60,10 +61,10 @@ export async function ensureShopItemsTable() {
6061

6162
await pool.query(`
6263
UPDATE shop_items
63-
SET price = CEIL(price_usd * 20)
64+
SET price = CEIL(price_usd * $1)
6465
WHERE price_usd IS NOT NULL
65-
AND (price IS NULL OR price != CEIL(price_usd * 20))
66-
`);
66+
AND (price IS NULL OR price != CEIL(price_usd * $1))
67+
`, [BRICKS_PER_USD]);
6768

6869
for (const column of REMOVED_SHOP_ITEM_COLUMNS) {
6970
await pool.query(`ALTER TABLE shop_items DROP COLUMN IF EXISTS ${column}`);
@@ -233,7 +234,7 @@ export async function purchaseShopItemForUser(userId, itemId, input = {}) {
233234
}
234235

235236
const itemBricks = Number(item.price ?? 0);
236-
const shippingBricks = Math.ceil(shippingTaxUsd * 10);
237+
const shippingBricks = Math.ceil(shippingTaxUsd * BRICKS_PER_USD);
237238
const totalBricks = itemBricks * quantity + shippingBricks;
238239

239240
const userResult = await client.query(
@@ -300,7 +301,7 @@ function normalizeShopItemInput(input = {}) {
300301
const price = integerOrNull(input.price);
301302
return {
302303
name: textOrNull(input.name),
303-
price: price ?? (priceUsd === null ? null : Math.ceil(priceUsd * 20)),
304+
price: price ?? (priceUsd === null ? null : Math.ceil(priceUsd * BRICKS_PER_USD)),
304305
itemLink: safeHttpUrl(input.itemLink ?? input.item_link),
305306
imageUrl: safeHttpUrl(input.imageUrl ?? input.image_url),
306307
description: textOrNull(input.description),
63.3 KB
Loading

0 commit comments

Comments
 (0)