Masterclass students can now claim a "Master" role in the NativePHP
Discord server, unlocking private channels. Follows the existing Ultra
and Early Adopter role pattern.
The entitlement check uses direct product ownership rather than the
team-inheriting hasProductLicense(), so the role tracks course access
exactly: Ultra team members don't get it, since they can't watch the
course either.
Also fixes the banner's visibility gate on the Integrations page, which
previously only admitted Max/Ultra/EAP customers — a course-only
customer saw no Discord section at all.
Requires DISCORD_MASTER_ROLE_ID to be set in the environment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What
Masterclass students can now claim a Master role in the NativePHP Discord server from the Integrations page, unlocking private channels. This follows the existing Ultra / Early Adopter role pattern end to end.
Why the entitlement check is direct-ownership
hasPurchasedMasterclass() deliberately checks the user's own productLicenses rather than reusing hasProductLicense(), which also grants access via the team owner. The course itself is gated on direct ownership (Product::isOwnedBy()), so this keeps the Discord role tracking course access exactly — Ultra team members don't get the Master role, because they can't watch the course either.
Drive-by fix
The Discord banner's visibility gate on the Integrations page previously only admitted Max / Ultra / EAP customers, so someone who had bought only the Masterclass saw no Discord section at all. Masterclass owners now pass the gate.
Deployment note
Requires DISCORD_MASTER_ROLE_ID to be set in the environment. The Master role needs creating in Discord with the relevant private-channel permissions, and the bot's own role must sit above Master in the server's role hierarchy or the assign call will 403.
Not included
Nothing grants the role at purchase time — a buyer who already has Discord connected has to visit Integrations and click the button. That matches how the existing roles behave. If we'd rather it be automatic, the hook point is HandleInvoicePaidJob::createProductLicense(), alongside the existing GitHub repo-access grant (plus products:grant and the Filament comp action).
Testing
28 tests passing across DiscordIntegrationTest and DiscordAccessBannerTest, covering the happy path, non-owners, not-in-guild, disconnect revocation, and page-level gating in both directions. 118 related tests (course content, purchase history, teams, Ultra access) still pass. Pint clean.
🤖 Generated with Claude Code