Skip to content

feat(bot): birthdays schema + /birthday set|clear - #738

Merged
LucasSantana-Dev merged 1 commit into
mainfrom
feature/birthdays-schema-set-command
Apr 20, 2026
Merged

LucasSantana-Dev merged 1 commit into
mainfrom
feature/birthdays-schema-set-command

Conversation

@LucasSantana-Dev

Copy link
Copy Markdown
Owner

Summary

Phase 1 final gap from the competitive scan (Nekotina/Loritta pattern). Two tight subcommands now; scheduler/announcements deferred to a follow-up PR to keep scope S.

Schema — new `MemberBirthday` model

  • `month` (1-12) + `day` (1-31) only — no year, no DOB, no age tracking.
  • `@@unique([guildId, userId])` — one row per guild-member.
  • `@@index([guildId, month, day])` — indexed for the daily-lookup scheduler the follow-up PR will add.

Migration: `prisma/migrations/20260420000000_add_member_birthdays/migration.sql`.

Commands

  • `/birthday set month day` — upserts the record. Validates month bounds + per-month day cap (Feb 30 rejected; Feb 29 accepted as leap-day tolerant).
  • `/birthday clear` — idempotent delete, friendly message when not set.

Test plan

  • Prisma schema validates (client regenerated, types propagate to bot)
  • `npx tsc --noEmit` in `packages/bot` → clean
  • 8 unit tests: DM rejection, month/day validation, Feb 30 rejected, Feb 29 accepted, upsert round-trip, clear (both paths), error handling
  • Deploy + run `prisma migrate deploy` on staging → table exists
  • `/birthday set` in a test guild → row appears; `/birthday clear` → row removed

Follow-up (separate PR)

Daily scheduler that queries `(guildId, month, day)` once per UTC day and posts to a guild-configured channel. Schema is already indexed for that lookup.

Phase 1 final gap from the competitive scan. Two tight subcommands,
deferred scheduler/announcements to a follow-up PR to keep scope S.

Schema (new model MemberBirthday):
- month (1-12) + day (1-31) only — no year, no DOB, no age tracking
- @@unique([guildId, userId]) so one row per guild-member
- @@index([guildId, month, day]) ready for the daily-lookup scheduler

Commands:
- /birthday set month day — upserts, validates month bounds + per-month
  day cap (Feb 30 rejected, Feb 29 accepted as leap-day tolerant)
- /birthday clear — idempotent delete, friendly message when not set

Tests: 8 unit cases cover DM rejection, month/day validation, leap-day
acceptance, upsert round-trip, clear both paths, error handling.

Follow-up (separate PR): daily scheduler that queries the
guildId_month_day index once per UTC day and posts to a guild-configured
channel. Schema is already indexed for that lookup.
@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 Ready Ready Preview, Comment Apr 20, 2026 3:09am

Request Review

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@LucasSantana-Dev has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 29 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 30 minutes and 29 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b27dbd4-8b11-41ab-83be-382f450ee362

📥 Commits

Reviewing files that changed from the base of the PR and between e74bf28 and 4515490.

📒 Files selected for processing (4)
  • packages/bot/src/functions/general/commands/birthday.spec.ts
  • packages/bot/src/functions/general/commands/birthday.ts
  • prisma/migrations/20260420000000_add_member_birthdays/migration.sql
  • prisma/schema.prisma
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/birthdays-schema-set-command

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.

@sonarqubecloud

Copy link
Copy Markdown

@LucasSantana-Dev
LucasSantana-Dev merged commit 87eef2d into main Apr 20, 2026
12 checks passed
LucasSantana-Dev added a commit that referenced this pull request Apr 20, 2026
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.
LucasSantana-Dev added a commit that referenced this pull request Apr 20, 2026
Five commits landed on main since 2.6.147 but Unreleased was empty.
Harvesting them now so the next version bump captures the right notes
and users scanning the changelog see what shipped.

Covered: #726 (CI pnpm→npm), #731 (/social), #733 (/leaderboard),
#738 (birthday schema + /birthday set|clear), #741 (cached-guild log
level).

Queued PRs not yet on main (top.gg webhook stack, Phase 0 docs,
dashboard vote badge, birthday scheduler + channel + list + role) will
be added when they actually merge.
LucasSantana-Dev added a commit that referenced this pull request Apr 20, 2026
Five commits landed on main since 2.6.147 but Unreleased was empty.
Harvesting them now so the next version bump captures the right notes
and users scanning the changelog see what shipped.

Covered: #726 (CI pnpm→npm), #731 (/social), #733 (/leaderboard),
#738 (birthday schema + /birthday set|clear), #741 (cached-guild log
level).

Queued PRs not yet on main (top.gg webhook stack, Phase 0 docs,
dashboard vote badge, birthday scheduler + channel + list + role) will
be added when they actually merge.
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
Phase 1 final gap from the competitive scan. Two tight subcommands,
deferred scheduler/announcements to a follow-up PR to keep scope S.

Schema (new model MemberBirthday):
- month (1-12) + day (1-31) only — no year, no DOB, no age tracking
- @@unique([guildId, userId]) so one row per guild-member
- @@index([guildId, month, day]) ready for the daily-lookup scheduler

Commands:
- /birthday set month day — upserts, validates month bounds + per-month
  day cap (Feb 30 rejected, Feb 29 accepted as leap-day tolerant)
- /birthday clear — idempotent delete, friendly message when not set

Tests: 8 unit cases cover DM rejection, month/day validation, leap-day
acceptance, upsert round-trip, clear both paths, error handling.

Follow-up (separate PR): daily scheduler that queries the
guildId_month_day index once per UTC day and posts to a guild-configured
channel. Schema is already indexed for that lookup.
LucasSantana-Dev added a commit that referenced this pull request May 13, 2026
Five commits landed on main since 2.6.147 but Unreleased was empty.
Harvesting them now so the next version bump captures the right notes
and users scanning the changelog see what shipped.

Covered: #726 (CI pnpm→npm), #731 (/social), #733 (/leaderboard),
#738 (birthday schema + /birthday set|clear), #741 (cached-guild log
level).

Queued PRs not yet on main (top.gg webhook stack, Phase 0 docs,
dashboard vote badge, birthday scheduler + channel + list + role) will
be added when they actually merge.
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.
@LucasSantana-Dev
LucasSantana-Dev deleted the feature/birthdays-schema-set-command branch May 23, 2026 02:21

This branch was successfully deployed

1 active deployment
Preview — 45154909 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