From 4545fe018e1b5fb89d6a71b18950af3b86d5ffca Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:24:16 +0100 Subject: [PATCH] feat: add role via system to rejoining visionaries (#1198) Co-authored-by: Hampus --- fluxer_api/src/api/Config.ts | 1 + fluxer_api/src/api/config/APIConfig.ts | 1 + .../stripe/services/StripePremiumService.ts | 25 +++++++++++++++++-- .../stripe/tests/StripePremiumService.test.ts | 9 ++++++- .../tests/StripeWebhookEdgeCases.test.ts | 9 ++++++- packages/config/src/MasterConfig.ts | 1 + .../src/config_loader/EnvironmentOverrides.ts | 1 + 7 files changed, 43 insertions(+), 4 deletions(-) diff --git a/fluxer_api/src/api/Config.ts b/fluxer_api/src/api/Config.ts index fbda7feb5..15c911039 100644 --- a/fluxer_api/src/api/Config.ts +++ b/fluxer_api/src/api/Config.ts @@ -370,6 +370,7 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig { selfHosted: master.instance.self_hosted, autoJoinInviteCode: master.instance.auto_join_invite_code, visionariesGuildId: master.instance.visionaries_guild_id, + visionariesGuildVisionaryRoleId: master.instance.visionaries_guild_visionary_role_id, branding: { productName: master.instance.branding.product_name, iconUrl: master.instance.branding.icon_url, diff --git a/fluxer_api/src/api/config/APIConfig.ts b/fluxer_api/src/api/config/APIConfig.ts index 6bea76a23..f85a080ab 100644 --- a/fluxer_api/src/api/config/APIConfig.ts +++ b/fluxer_api/src/api/config/APIConfig.ts @@ -281,6 +281,7 @@ export interface APIConfig { selfHosted: boolean; autoJoinInviteCode?: string; visionariesGuildId?: string; + visionariesGuildVisionaryRoleId?: string; branding: { productName: string; iconUrl?: string; diff --git a/fluxer_api/src/api/stripe/services/StripePremiumService.ts b/fluxer_api/src/api/stripe/services/StripePremiumService.ts index b214baa60..8a65c4956 100644 --- a/fluxer_api/src/api/stripe/services/StripePremiumService.ts +++ b/fluxer_api/src/api/stripe/services/StripePremiumService.ts @@ -4,8 +4,9 @@ import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; import {MissingAccessError} from '@fluxer/errors/src/domains/core/MissingAccessError'; import {StripeError} from '@fluxer/errors/src/domains/payment/StripeError'; import type {UserID} from '../../BrandedTypes'; -import {createGuildID} from '../../BrandedTypes'; +import {createGuildID, createRoleID} from '../../BrandedTypes'; import {Config} from '../../Config'; +import {SYSTEM_USER_ID} from '../../constants/Core'; import type {GiftCodeDurationType} from '../../database/types/PaymentTypes'; import type {UserRow} from '../../database/types/UserTypes'; import type {IGuildRepositoryAggregate} from '../../guild/repositories/IGuildRepositoryAggregate'; @@ -286,7 +287,12 @@ export class StripePremiumService { if (!Config.instance.visionariesGuildId) { throw new StripeError('Visionaries guild id not configured'); } + if (!Config.instance.visionariesGuildVisionaryRoleId) { + throw new StripeError('Visionaries guild visionary role id not configured'); + } const visionariesGuildId = createGuildID(BigInt(Config.instance.visionariesGuildId)); + const visionaryRoleId = createRoleID(BigInt(Config.instance.visionariesGuildVisionaryRoleId)); + const requestCache = createRequestCache(); const existingMember = await this.guildRepository.getMember(visionariesGuildId, userId); if (!existingMember) { await this.guildService.members.addUserToGuild({ @@ -294,10 +300,25 @@ export class StripePremiumService { guildId: visionariesGuildId, sendJoinMessage: true, skipBanCheck: true, - requestCache: createRequestCache(), + requestCache, }); Logger.debug({userId, guildId: visionariesGuildId}, 'Added visionary user to visionaries guild'); } + + try { + await this.guildService.members.systemAddMemberRole({ + targetId: userId, + guildId: visionariesGuildId, + roleId: visionaryRoleId, + initiatorId: SYSTEM_USER_ID, + requestCache, + }); + } catch (error) { + Logger.error( + {userId, guildId: visionariesGuildId, roleId: visionaryRoleId, error}, + 'Failed to add visionary role to a rejoining visionary.', + ); + } } private async dispatchUser(user: User): Promise { diff --git a/fluxer_api/src/api/stripe/tests/StripePremiumService.test.ts b/fluxer_api/src/api/stripe/tests/StripePremiumService.test.ts index 496a381e5..994e5a9c3 100644 --- a/fluxer_api/src/api/stripe/tests/StripePremiumService.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripePremiumService.test.ts @@ -5,26 +5,31 @@ import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; import {afterAll, beforeAll, beforeEach, describe, expect, test} from 'vitest'; import {createTestAccount} from '../../auth/tests/AuthTestUtils'; import {Config} from '../../Config'; -import {createGuild} from '../../guild/tests/GuildTestUtils'; +import {createGuild, createRole, getMember} from '../../guild/tests/GuildTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness'; import {createBuilder} from '../../test/TestRequestBuilder'; describe('StripePremiumService', () => { let harness: ApiTestHarness; let originalVisionariesGuildId: string | undefined; + let originalVisionariesGuildVisionaryRoleId: string | undefined; beforeAll(async () => { harness = await createApiTestHarness(); originalVisionariesGuildId = Config.instance.visionariesGuildId ?? undefined; + originalVisionariesGuildVisionaryRoleId = Config.instance.visionariesGuildVisionaryRoleId ?? undefined; }); afterAll(async () => { await harness.shutdown(); Config.instance.visionariesGuildId = originalVisionariesGuildId; + Config.instance.visionariesGuildVisionaryRoleId = originalVisionariesGuildVisionaryRoleId; }); beforeEach(async () => { await harness.resetData(); const owner = await createTestAccount(harness); const visionariesGuild = await createGuild(harness, owner.token, 'Visionaries Test Guild'); + const visionaryRole = await createRole(harness, owner.token, visionariesGuild.id, {name: 'Visionary'}); Config.instance.visionariesGuildId = visionariesGuild.id; + Config.instance.visionariesGuildVisionaryRoleId = visionaryRole.id; }); describe('POST /premium/visionary/rejoin', () => { test('allows visionary users to rejoin guild', async () => { @@ -37,6 +42,8 @@ describe('StripePremiumService', () => { }) .execute(); await createBuilder(harness, account.token).post('/premium/visionary/rejoin').expect(204).execute(); + const member = await getMember(harness, account.token, Config.instance.visionariesGuildId!, account.userId); + expect(member.roles).toContain(Config.instance.visionariesGuildVisionaryRoleId); }); test('rejects users without visionary access', async () => { const account = await createTestAccount(harness); diff --git a/fluxer_api/src/api/stripe/tests/StripeWebhookEdgeCases.test.ts b/fluxer_api/src/api/stripe/tests/StripeWebhookEdgeCases.test.ts index 593c1b383..020c5c4c2 100644 --- a/fluxer_api/src/api/stripe/tests/StripeWebhookEdgeCases.test.ts +++ b/fluxer_api/src/api/stripe/tests/StripeWebhookEdgeCases.test.ts @@ -5,26 +5,31 @@ import {UserPremiumTypes} from '@fluxer/constants/src/UserConstants'; import {afterAll, beforeAll, beforeEach, describe, expect, test} from 'vitest'; import {createTestAccount} from '../../auth/tests/AuthTestUtils'; import {Config} from '../../Config'; -import {createGuild} from '../../guild/tests/GuildTestUtils'; +import {createGuild, createRole, getMember} from '../../guild/tests/GuildTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness'; import {createBuilder} from '../../test/TestRequestBuilder'; describe('Stripe Webhook Edge Cases', () => { let harness: ApiTestHarness; let originalVisionariesGuildId: string | undefined; + let originalVisionariesGuildVisionaryRoleId: string | undefined; beforeAll(async () => { harness = await createApiTestHarness(); originalVisionariesGuildId = Config.instance.visionariesGuildId ?? undefined; + originalVisionariesGuildVisionaryRoleId = Config.instance.visionariesGuildVisionaryRoleId ?? undefined; }); afterAll(async () => { await harness.shutdown(); Config.instance.visionariesGuildId = originalVisionariesGuildId; + Config.instance.visionariesGuildVisionaryRoleId = originalVisionariesGuildVisionaryRoleId; }); beforeEach(async () => { await harness.resetData(); const owner = await createTestAccount(harness); const visionariesGuild = await createGuild(harness, owner.token, 'Visionaries Webhook Test Guild'); + const visionaryRole = await createRole(harness, owner.token, visionariesGuild.id, {name: 'Visionary'}); Config.instance.visionariesGuildId = visionariesGuild.id; + Config.instance.visionariesGuildVisionaryRoleId = visionaryRole.id; }); describe('Premium stacking - consecutive grants extending duration', () => { test('stacks multiple monthly subscriptions end-to-end', async () => { @@ -366,6 +371,8 @@ describe('Stripe Webhook Edge Cases', () => { .execute(); expect(receiverAfter.premium_type).toBe(UserPremiumTypes.LIFETIME); expect(receiverAfter.premium_lifetime_sequence).toBe(0); + const member = await getMember(harness, receiver.token, Config.instance.visionariesGuildId!, receiver.userId); + expect(member.roles).toContain(Config.instance.visionariesGuildVisionaryRoleId); }); test('redeeming 1-month gift code grants subscription premium', async () => { const gifter = await createTestAccount(harness); diff --git a/packages/config/src/MasterConfig.ts b/packages/config/src/MasterConfig.ts index 3774662c1..1e6b6ce77 100644 --- a/packages/config/src/MasterConfig.ts +++ b/packages/config/src/MasterConfig.ts @@ -316,6 +316,7 @@ export interface MasterConfig { self_hosted: boolean; auto_join_invite_code?: string; visionaries_guild_id?: string; + visionaries_guild_visionary_role_id?: string; branding: InstanceBrandingConfig; setup: { configured: boolean; diff --git a/packages/config/src/config_loader/EnvironmentOverrides.ts b/packages/config/src/config_loader/EnvironmentOverrides.ts index 07ab25859..686024ab2 100644 --- a/packages/config/src/config_loader/EnvironmentOverrides.ts +++ b/packages/config/src/config_loader/EnvironmentOverrides.ts @@ -321,6 +321,7 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record = { FLUXER_SELF_HOSTED: {path: ['instance', 'self_hosted'], parse: parseEnvValue}, FLUXER_AUTO_JOIN_INVITE_CODE: {path: ['instance', 'auto_join_invite_code']}, FLUXER_VISIONARIES_GUILD_ID: {path: ['instance', 'visionaries_guild_id']}, + FLUXER_VISIONARIES_GUILD_VISIONARY_ROLE_ID: {path: ['instance', 'visionaries_guild_visionary_role_id']}, FLUXER_APP_PRODUCT_NAME: {path: ['instance', 'branding', 'product_name']}, FLUXER_APP_ICON_URL: {path: ['instance', 'branding', 'icon_url']}, FLUXER_APP_SYMBOL_URL: {path: ['instance', 'branding', 'symbol_url']},