ENG-004
A daily task list that resets cleanly
Generate the day's tasks once, track progress against a date key, and pay the completion bonus exactly once — no midnight cron required.
A daily list gives a user a reason to return and a shape to the visit. Implemented badly it needs a cron at midnight, drifts across timezones, and pays the completion bonus twice when someone has two tabs open.
Keying every row on the date removes all three problems. Progress rows carry the day they belong to, so a new day is a new key and yesterday's progress is simply not found. Nothing has to run at midnight.
The completion bonus goes through a unique key on user and date, which makes double-payment impossible rather than unlikely.
/*
CREATE TABLE daily_progress (
user_id INT UNSIGNED NOT NULL,
day DATE NOT NULL,
task_code VARCHAR(40) NOT NULL,
progress INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, day, task_code)
);
CREATE TABLE daily_bonus_paid (
user_id INT UNSIGNED NOT NULL,
day DATE NOT NULL,
PRIMARY KEY (user_id, day) -- one bonus per user per day, enforced
);
*/
function bump_task(PDO $pdo, int $userId, string $taskCode, int $by = 1): void
{
$pdo->prepare(
'INSERT INTO daily_progress (user_id, day, task_code, progress)
VALUES (?, CURDATE(), ?, ?)
ON DUPLICATE KEY UPDATE progress = progress + VALUES(progress)'
)->execute([$userId, $taskCode, $by]);
}
function daily_state(PDO $pdo, int $userId, array $tasks): array
{
$st = $pdo->prepare('SELECT task_code, progress FROM daily_progress WHERE user_id = ? AND day = CURDATE()');
$st->execute([$userId]);
$done = $st->fetchAll(PDO::FETCH_KEY_PAIR);
$out = [];
$complete = true;
foreach ($tasks as $code => $t) {
$have = (int) ($done[$code] ?? 0);
$need = (int) $t['target'];
$out[$code] = ['label' => $t['label'], 'have' => min($have, $need), 'need' => $need, 'done' => $have >= $need];
$complete = $complete && $have >= $need;
}
return ['tasks' => $out, 'complete' => $complete];
}
function pay_daily_bonus(PDO $pdo, int $userId, int $units): bool
{
try {
$pdo->beginTransaction();
$pdo->prepare('INSERT INTO daily_bonus_paid (user_id, day) VALUES (?, CURDATE())')->execute([$userId]);
$pdo->prepare('UPDATE users SET balance = balance + ? WHERE id = ?')->execute([$units, $userId]);
$pdo->prepare('INSERT INTO ledger (user_id, kind, units, created_at) VALUES (?, "daily_bonus", ?, NOW())')
->execute([$userId, $units]);
$pdo->commit();
return true;
} catch (PDOException $e) {
$pdo->rollBack();
return ($e->errorInfo[1] ?? 0) === 1062 ? false : throw $e; // already paid today
}
}
Using it
Render a progress bar per task and one for the day. The bar is what makes the list feel finishable rather than like a list of chores.
Keep the list to three or four items achievable in one visit. A list nobody finishes pays no bonus and motivates nothing.
Prune old progress rows on a schedule; only today's are ever read.
What bites people
CURDATE() is the database server's date. Pick a timezone deliberately and tell users when the day rolls over, or the reset feels arbitrary.
Never award the bonus from a check in PHP alone. Two tabs finishing the last task at once is a completely ordinary occurrence.
Do not carry incomplete progress into the next day. It is a daily list; carrying over makes the reset meaningless.