Skip to content

feat(bot): birthday role auto-grant + /birthday role - #743

Merged
LucasSantana-Dev merged 1 commit into
feature/birthday-schedulerfrom
feature/birthday-role
Apr 20, 2026
Merged

LucasSantana-Dev merged 1 commit into
feature/birthday-schedulerfrom
feature/birthday-role

Conversation

@LucasSantana-Dev

Copy link
Copy Markdown
Owner

Summary

Completes the birthday feature with the remaining Nekotina-style perk. Stacked on #740.

Schema

  • `GuildSettings.birthdayRoleId` (`String?`) — new migration `20260420020000_add_birthday_role_to_guild_settings`. Null by default (no side-effects for existing guilds).

Command

  • `/birthday role ` — Manage-Guild gated. Upserts `GuildSettings`. Empty role disables the grant.

Scheduler (auto-grant/revoke)

  • On each tick, for every guild with matches today: fetch role + grant to celebrators who don't already have it.
  • For guilds with `birthdayRoleId` set but no matches today: iterate the role's current members and revoke. This gives 24-hour auto-expiry without a tracking table — clean rollover when the UTC day changes.
  • Scoped API calls — uses `guild.roles.fetch(roleId).members` rather than a full member sweep. No pagination, no rate-limit scaling risk.

Failure modes

  • Missing role → logged, skipped, no crash
  • `roles.add`/`roles.remove` failures → logged per-member, loop continues
  • DB errors in tick → swallowed at the top-level try/catch (consistent with announcement path)

Test plan

  • Prisma schema validates + client regenerated
  • `npx tsc --noEmit` in `packages/bot` → clean
  • 9 unit tests pass on the scheduler (6 existing + 3 new: grant path, revoke stale holders, no-op on already-granted)
  • Deploy + run `prisma migrate deploy` adds the column
  • `/birthday role @Birthday` + set a birthday for today in a test guild → role appears within 1 tick
  • Change the date to tomorrow → next tick revokes the role

Stacking

Base: `feature/birthday-scheduler` (#740). Will auto-retarget to `main` when #740 lands.

Completes the birthday feature with the remaining Nekotina-style perk.

Schema:
- GuildSettings.birthdayRoleId (String?) — new migration. Null by default.

Command:
- /birthday role <role> — ManageGuild-gated, upserts GuildSettings.
  Empty role disables the grant entirely.

Scheduler:
- On each tick, for every guild with matches today: fetch role + grant
  to celebrators who don't already have it.
- For guilds with birthdayRoleId set but NO matches today, iterate the
  role's current members and revoke. This means:
  - 24-hour auto-expiry happens naturally (next UTC day tick finds no
    match for the previous day's celebrators, revokes cleanly).
  - No separate tracking table / timer state needed.
  - Admin reconfiguration is idempotent (stale roleIds simply get logged
    and skipped).

Scoped API calls — uses guild.roles.fetch(roleId).members iteration
rather than fetching every guild member. No pagination, no rate-limit
scaling risk at Lucky's current scale.

Tests: 9 total (3 new role cases on top of the 6 announcement cases):
- Grants the role to today's celebrators
- Revokes from stale holders when no matches today
- Skips re-grant when member already has the role
@vercel

vercel Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lucky Building Building Preview, Comment Apr 20, 2026 3:35am

Request Review

@LucasSantana-Dev
LucasSantana-Dev merged commit 8b55ba2 into feature/birthday-scheduler Apr 20, 2026
6 of 8 checks passed
@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d13e835-5aff-449e-8bb9-ca24f73488c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/birthday-role

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@LucasSantana-Dev
LucasSantana-Dev deleted the feature/birthday-role branch April 20, 2026 03:35
LucasSantana-Dev added a commit that referenced this pull request Apr 21, 2026
Promotes ~/.claude/plans/lucky-next-phases-ultraplan.md into a committed
spec under docs/specs/2026-04-20-lucky-next-phases/ via the spec-new
skill. Includes a rewritten tasks.md with Phase 0/1 marked shipped
(PRs #728-#743), Phase 2 status (PRs #746/#747/#749/#750 shipped, real
Stripe SDK + PR 4 dashboard remaining), and an audit inventory of
premium gates across packages/*/src to guide PR 4 wiring.

Key audit findings:
- Only PremiumService + replenisher.ts reference isPremium today
- Collaborative playlist service already exists — natural P2 gate
- embedColor palette is the cheapest premium UX gate
- Leaderboard row-limit is frontend-paginated, not backend-enforced
LucasSantana-Dev added a commit that referenced this pull request Apr 22, 2026
* docs(spec): promote lucky-next-phases ultraplan to docs/specs

Promotes ~/.claude/plans/lucky-next-phases-ultraplan.md into a committed
spec under docs/specs/2026-04-20-lucky-next-phases/ via the spec-new
skill. Includes a rewritten tasks.md with Phase 0/1 marked shipped
(PRs #728-#743), Phase 2 status (PRs #746/#747/#749/#750 shipped, real
Stripe SDK + PR 4 dashboard remaining), and an audit inventory of
premium gates across packages/*/src to guide PR 4 wiring.

Key audit findings:
- Only PremiumService + replenisher.ts reference isPremium today
- Collaborative playlist service already exists — natural P2 gate
- embedColor palette is the cheapest premium UX gate
- Leaderboard row-limit is frontend-paginated, not backend-enforced

* docs(spec): mark Phase 2 (Stripe/premium billing) as deferred

Per 2026-04-20 decision: removing Stripe features for now. The existing
premiumService.isPremium() check serves as the feature toggle (returns
false without subscription rows, so all premium-adjacent paths behave
as if off).

Closed PRs: #745 (spec), #747 (env), #749 (routes)
Kept: #746 (schema, harmless no-op on main), #750 (autoplay gate =
the live toggle)
LucasSantana-Dev added a commit that referenced this pull request Apr 22, 2026
* feat(bot): birthday scheduler + /birthday channel

Completes the birthdays feature on top of the schema + /birthday set|clear
shipped in #738.

Schema change:
- GuildSettings.birthdayChannelId (String?) — null by default so the
  scheduler safely skips guilds that haven't opted in. New migration.

Command:
- /birthday channel <channel> — ManageGuild-gated, upserts GuildSettings.
  Empty channel disables announcements (same field set to null).

Scheduler (packages/bot/src/utils/general/birthdayScheduler.ts):
- Runs once at startup + every hour (BIRTHDAY_TICK_INTERVAL_MS override)
- Queries MemberBirthday by today's UTC (month, day) via the existing
  @@index([guildId, month, day])
- Groups by guild, reads GuildSettings.birthdayChannelId, posts a
  celebration embed mentioning each user
- Idempotent per UTC day (in-memory lastAnnouncedPerGuild map) so hourly
  reruns are no-ops; posts again when the date rolls over
- Wired into clientHandler/service.ts alongside modDigestScheduler
- Failures are logged and swallowed (won't crash the bot)

Tests: 14 total across birthday.ts and birthdayScheduler.ts — covering
empty results, multi-user guild, no-config skip, day-rollover idempotency,
and db-error resilience.

* feat(bot): birthday role auto-grant + /birthday role (#743)

Completes the birthday feature with the remaining Nekotina-style perk.

Schema:
- GuildSettings.birthdayRoleId (String?) — new migration. Null by default.

Command:
- /birthday role <role> — ManageGuild-gated, upserts GuildSettings.
  Empty role disables the grant entirely.

Scheduler:
- On each tick, for every guild with matches today: fetch role + grant
  to celebrators who don't already have it.
- For guilds with birthdayRoleId set but NO matches today, iterate the
  role's current members and revoke. This means:
  - 24-hour auto-expiry happens naturally (next UTC day tick finds no
    match for the previous day's celebrators, revokes cleanly).
  - No separate tracking table / timer state needed.
  - Admin reconfiguration is idempotent (stale roleIds simply get logged
    and skipped).

Scoped API calls — uses guild.roles.fetch(roleId).members iteration
rather than fetching every guild member. No pagination, no rate-limit
scaling risk at Lucky's current scale.

Tests: 9 total (3 new role cases on top of the 6 announcement cases):
- Grants the role to today's celebrators
- Revokes from stale holders when no matches today
- Skips re-grant when member already has the role

* test(bot): cover birthday scheduler + /birthday channel/role commands (unblocks #740 Sonar)

* fix(bot): remove stray merge conflict marker in birthday.ts

Leftover <<<<<<< HEAD from the earlier merge resolution — no corresponding
======= / >>>>>>> pair, so the merged content is correct, just had a
stray opening marker on line 203 that broke TypeScript compile.
LucasSantana-Dev added a commit that referenced this pull request May 13, 2026
* docs(spec): promote lucky-next-phases ultraplan to docs/specs

Promotes ~/.claude/plans/lucky-next-phases-ultraplan.md into a committed
spec under docs/specs/2026-04-20-lucky-next-phases/ via the spec-new
skill. Includes a rewritten tasks.md with Phase 0/1 marked shipped
(PRs #728-#743), Phase 2 status (PRs #746/#747/#749/#750 shipped, real
Stripe SDK + PR 4 dashboard remaining), and an audit inventory of
premium gates across packages/*/src to guide PR 4 wiring.

Key audit findings:
- Only PremiumService + replenisher.ts reference isPremium today
- Collaborative playlist service already exists — natural P2 gate
- embedColor palette is the cheapest premium UX gate
- Leaderboard row-limit is frontend-paginated, not backend-enforced

* docs(spec): mark Phase 2 (Stripe/premium billing) as deferred

Per 2026-04-20 decision: removing Stripe features for now. The existing
premiumService.isPremium() check serves as the feature toggle (returns
false without subscription rows, so all premium-adjacent paths behave
as if off).

Closed PRs: #745 (spec), #747 (env), #749 (routes)
Kept: #746 (schema, harmless no-op on main), #750 (autoplay gate =
the live toggle)
LucasSantana-Dev added a commit that referenced this pull request May 13, 2026
* feat(bot): birthday scheduler + /birthday channel

Completes the birthdays feature on top of the schema + /birthday set|clear
shipped in #738.

Schema change:
- GuildSettings.birthdayChannelId (String?) — null by default so the
  scheduler safely skips guilds that haven't opted in. New migration.

Command:
- /birthday channel <channel> — ManageGuild-gated, upserts GuildSettings.
  Empty channel disables announcements (same field set to null).

Scheduler (packages/bot/src/utils/general/birthdayScheduler.ts):
- Runs once at startup + every hour (BIRTHDAY_TICK_INTERVAL_MS override)
- Queries MemberBirthday by today's UTC (month, day) via the existing
  @@index([guildId, month, day])
- Groups by guild, reads GuildSettings.birthdayChannelId, posts a
  celebration embed mentioning each user
- Idempotent per UTC day (in-memory lastAnnouncedPerGuild map) so hourly
  reruns are no-ops; posts again when the date rolls over
- Wired into clientHandler/service.ts alongside modDigestScheduler
- Failures are logged and swallowed (won't crash the bot)

Tests: 14 total across birthday.ts and birthdayScheduler.ts — covering
empty results, multi-user guild, no-config skip, day-rollover idempotency,
and db-error resilience.

* feat(bot): birthday role auto-grant + /birthday role (#743)

Completes the birthday feature with the remaining Nekotina-style perk.

Schema:
- GuildSettings.birthdayRoleId (String?) — new migration. Null by default.

Command:
- /birthday role <role> — ManageGuild-gated, upserts GuildSettings.
  Empty role disables the grant entirely.

Scheduler:
- On each tick, for every guild with matches today: fetch role + grant
  to celebrators who don't already have it.
- For guilds with birthdayRoleId set but NO matches today, iterate the
  role's current members and revoke. This means:
  - 24-hour auto-expiry happens naturally (next UTC day tick finds no
    match for the previous day's celebrators, revokes cleanly).
  - No separate tracking table / timer state needed.
  - Admin reconfiguration is idempotent (stale roleIds simply get logged
    and skipped).

Scoped API calls — uses guild.roles.fetch(roleId).members iteration
rather than fetching every guild member. No pagination, no rate-limit
scaling risk at Lucky's current scale.

Tests: 9 total (3 new role cases on top of the 6 announcement cases):
- Grants the role to today's celebrators
- Revokes from stale holders when no matches today
- Skips re-grant when member already has the role

* test(bot): cover birthday scheduler + /birthday channel/role commands (unblocks #740 Sonar)

* fix(bot): remove stray merge conflict marker in birthday.ts

Leftover <<<<<<< HEAD from the earlier merge resolution — no corresponding
======= / >>>>>>> pair, so the merged content is correct, just had a
stray opening marker on line 203 that broke TypeScript compile.

This branch was successfully deployed

1 active deployment
Preview — fc76665d Deployed Apr 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant