fixed payout locks & time sensitive todos & minor ui updates on chores view
This commit is contained in:
@@ -286,6 +286,8 @@ async function main() {
|
||||
number("value", true),
|
||||
select("rewardType", ["cash", "prize", "points"], true),
|
||||
select("status", ["unclaimed", "requested", "claimed"], true),
|
||||
select("claimable", ["immediate", "payday"], true),
|
||||
text("settleDate"),
|
||||
date("claimedAt"),
|
||||
date("requestedAt"),
|
||||
text("date"),
|
||||
|
||||
+57
-2
@@ -9,6 +9,7 @@ import {
|
||||
todayInTz,
|
||||
wallClockToUtc,
|
||||
resolveTz,
|
||||
nextPaydayAfter as nextPaydayAfterTz,
|
||||
} from "../../timezone.ts";
|
||||
|
||||
const app = new Hono();
|
||||
@@ -69,6 +70,35 @@ function periodEnd(period: string, start: string): string {
|
||||
return start;
|
||||
}
|
||||
|
||||
function nextPaydayAfter(dateStr: string, payday: number, tz?: string): string {
|
||||
return nextPaydayAfterTz(dateStr, payday, resolveServerTz(tz));
|
||||
}
|
||||
|
||||
// Rewards from weekly/monthly bonus configs are claimable only on payday.
|
||||
// Stamp the reward with a settleDate = the next payday after the period ends.
|
||||
function claimableStamp(cfg: any, payday: number, tz?: string) {
|
||||
if (cfg.period !== "weekly" && cfg.period !== "monthly") {
|
||||
return { claimable: "immediate", settleDate: "" };
|
||||
}
|
||||
const tzR = resolveServerTz(tz);
|
||||
const now = todayInTz(tzR);
|
||||
const start =
|
||||
cfg.period === "monthly" ? `${now.slice(0, 7)}-01` : weekStart(payday, tzR);
|
||||
const end = periodEnd(cfg.period, start);
|
||||
return { claimable: "payday", settleDate: nextPaydayAfter(end, payday, tzR) };
|
||||
}
|
||||
|
||||
// Payday-gated rewards can't be claimed until their settleDate (the payday).
|
||||
function assertPaydayUnlocked(reward: any, tz?: string) {
|
||||
if (!reward || reward.claimable !== "payday" || !reward.settleDate) return;
|
||||
const today = todayInTz(resolveServerTz(tz));
|
||||
if (today < reward.settleDate) {
|
||||
throw new Error(
|
||||
`This bonus pays out on payday (${reward.settleDate}) — hang tight!`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function getFamPayday(famId: string): Promise<number> {
|
||||
try {
|
||||
const fam = await pb.getList("fams", `id = '${famId}'`);
|
||||
@@ -809,11 +839,11 @@ app.get("/api/admin/:famId/debug/eow-preview", requireAdmin, async (c) => {
|
||||
const [pw, cw] = await Promise.all([
|
||||
pb.getList(
|
||||
"rewards",
|
||||
`famId = '${famId}' && rewardType = 'points' && status = 'claimed'`,
|
||||
`famId = '${famId}' && rewardType = 'points' && status = 'claimed' && date >= '${ws}'`,
|
||||
),
|
||||
pb.getList(
|
||||
"rewards",
|
||||
`famId = '${famId}' && rewardType = 'cash' && status = 'claimed'`,
|
||||
`famId = '${famId}' && rewardType = 'cash' && status = 'claimed' && date >= '${ws}'`,
|
||||
),
|
||||
]);
|
||||
rewardPointsList = pw.items;
|
||||
@@ -1327,6 +1357,7 @@ async function evaluateFam(famId: string): Promise<void> {
|
||||
status: cfg.rewardType === "points" ? "claimed" : "unclaimed",
|
||||
claimedAt: cfg.rewardType === "points" ? now : null,
|
||||
date: now.slice(0, 10),
|
||||
...claimableStamp(cfg, paydayEval, tzEval),
|
||||
});
|
||||
createdReward = true;
|
||||
}
|
||||
@@ -1377,6 +1408,7 @@ async function evaluateFam(famId: string): Promise<void> {
|
||||
status: cfg.rewardType === "points" ? "claimed" : "unclaimed",
|
||||
claimedAt: cfg.rewardType === "points" ? now : null,
|
||||
date: now.slice(0, 10),
|
||||
...claimableStamp(cfg, paydayEval, tzEval),
|
||||
});
|
||||
createdReward = true;
|
||||
}
|
||||
@@ -1434,6 +1466,7 @@ async function evaluateFam(famId: string): Promise<void> {
|
||||
status: cfg.rewardType === "points" ? "claimed" : "unclaimed",
|
||||
claimedAt: cfg.rewardType === "points" ? now : null,
|
||||
date: now.slice(0, 10),
|
||||
...claimableStamp(cfg, paydayEval, tzEval),
|
||||
});
|
||||
createdReward = true;
|
||||
}
|
||||
@@ -1576,6 +1609,8 @@ app.post(
|
||||
status: cfg.rewardType === "points" ? "claimed" : "unclaimed",
|
||||
claimedAt: cfg.rewardType === "points" ? now : null,
|
||||
date: now.slice(0, 10),
|
||||
claimable: "immediate",
|
||||
settleDate: "",
|
||||
});
|
||||
created.push(record);
|
||||
}
|
||||
@@ -1690,6 +1725,7 @@ app.post("/api/members/my-chores", requireDeviceToken, async (c) => {
|
||||
const payday = await getFamPayday(famId);
|
||||
const paydayTime = await getFamPaydayTime(famId);
|
||||
const timezone = await getFamTimezone(famId);
|
||||
const settings = await getFamSettings(famId);
|
||||
const rewardsList = rewards.items;
|
||||
const configsList = bonusConfigs.items;
|
||||
return c.json({
|
||||
@@ -1701,6 +1737,7 @@ app.post("/api/members/my-chores", requireDeviceToken, async (c) => {
|
||||
payday,
|
||||
paydayTime,
|
||||
timezone,
|
||||
simulateEow: !!settings.simulateEow,
|
||||
});
|
||||
} catch (err) {
|
||||
return handleError(c, err);
|
||||
@@ -1921,6 +1958,18 @@ app.post("/api/members/rewards/:id/claim", requireDeviceToken, async (c) => {
|
||||
const famId = c.get("famId");
|
||||
const memberId = c.get("memberId");
|
||||
const now = new Date().toISOString();
|
||||
const tz = await getFamTimezone(famId);
|
||||
const found = await pb.getList("rewards", `famId = '${famId}' && id = '${id}'`);
|
||||
const reward = found.items?.[0];
|
||||
if (!reward) return c.json({ error: "Reward not found" }, 404);
|
||||
try {
|
||||
assertPaydayUnlocked(reward, tz);
|
||||
} catch (e) {
|
||||
return c.json(
|
||||
{ error: e instanceof Error ? e.message : "Not claimable yet" },
|
||||
400,
|
||||
);
|
||||
}
|
||||
const record = await pb.update("rewards", id, {
|
||||
status: "requested",
|
||||
requestedAt: now,
|
||||
@@ -1951,6 +2000,7 @@ app.post("/api/members/rewards/request-all", requireDeviceToken, async (c) => {
|
||||
const famId = c.get("famId");
|
||||
const memberId = c.get("memberId");
|
||||
const now = new Date().toISOString();
|
||||
const tz = await getFamTimezone(famId);
|
||||
// Find all unclaimed rewards for this member
|
||||
const rewards = await pb.getList(
|
||||
"rewards",
|
||||
@@ -1958,6 +2008,11 @@ app.post("/api/members/rewards/request-all", requireDeviceToken, async (c) => {
|
||||
);
|
||||
let count = 0;
|
||||
for (const r of rewards.items) {
|
||||
try {
|
||||
assertPaydayUnlocked(r, tz);
|
||||
} catch {
|
||||
continue; // payday-gated reward not yet settled — leave for payday
|
||||
}
|
||||
await pb.update("rewards", r.id, {
|
||||
status: "requested",
|
||||
requestedAt: now,
|
||||
|
||||
@@ -284,6 +284,42 @@ export async function migrate(): Promise<void> {
|
||||
console.log(` ↳ rewards collection not found (will be created by seed)`);
|
||||
}
|
||||
|
||||
// Ensure rewards.claimable + rewards.settleDate exist (payday-gated bonuses)
|
||||
const rewardsCol2 = await getCollection("rewards");
|
||||
if (rewardsCol2) {
|
||||
const fieldNames = rewardsCol2.fields.map((f: any) => f.name);
|
||||
const missing: any[] = [];
|
||||
if (!fieldNames.includes("claimable")) {
|
||||
missing.push({
|
||||
name: "claimable",
|
||||
type: "select",
|
||||
required: true,
|
||||
values: ["immediate", "payday"],
|
||||
maxSelect: 1,
|
||||
});
|
||||
}
|
||||
if (!fieldNames.includes("settleDate")) {
|
||||
missing.push({ name: "settleDate", type: "text", required: false });
|
||||
}
|
||||
if (missing.length) {
|
||||
console.log("[migrate] Adding rewards.claimable/settleDate (payday gating)...");
|
||||
rewardsCol2.fields.push(...missing);
|
||||
await updateCollection(rewardsCol2.id, {
|
||||
name: "rewards",
|
||||
type: "base",
|
||||
listRule: rewardsCol2.listRule,
|
||||
viewRule: rewardsCol2.viewRule,
|
||||
createRule: rewardsCol2.createRule,
|
||||
updateRule: rewardsCol2.updateRule,
|
||||
deleteRule: rewardsCol2.deleteRule,
|
||||
fields: rewardsCol2.fields,
|
||||
});
|
||||
console.log(" ✓ rewards.claimable/settleDate added");
|
||||
} else {
|
||||
console.log(` ↳ rewards.claimable/settleDate already exist`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Update settings collection (drop old fields) ──
|
||||
const settingsCol = await getCollection("settings");
|
||||
if (settingsCol) {
|
||||
|
||||
Reference in New Issue
Block a user