diff --git a/docs/superpowers/plans/2026-06-07-self-hosted-team-scheduling.md b/docs/superpowers/plans/2026-06-07-self-hosted-team-scheduling.md new file mode 100644 index 0000000000..9535c3a518 --- /dev/null +++ b/docs/superpowers/plans/2026-06-07-self-hosted-team-scheduling.md @@ -0,0 +1,879 @@ +# Self-Hosted Team Scheduling Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Restore secure, Stripe-free team creation, membership management, collective scheduling, and round-robin scheduling for existing Authentik users. + +**Architecture:** Add a focused `TeamService` that owns standalone-team authorization and membership invariants, expose it through a new viewer teams router, and build App Router pages around those APIs. Reconnect Cal.diy's existing host, availability, event-type, and round-robin booking paths while replacing permissive authorization stubs with concrete membership checks. Remove Stripe from all active application and deployment paths after team scheduling works. + +**Tech Stack:** Next.js App Router, React, TRPC, Prisma/Postgres, NextAuth, Zod, Vitest, Playwright, Docker Compose. + +--- + +## File Structure + +### New team domain files + +- `packages/features/teams/services/TeamService.ts`: team CRUD, membership mutations, role checks, and final-owner invariants. +- `packages/features/teams/services/TeamService.test.ts`: unit tests using mocked Prisma transactions. +- `packages/features/teams/repositories/TeamRepository.ts`: focused Prisma reads and writes used by `TeamService`. +- `packages/trpc/server/routers/viewer/teams/_router.ts`: authenticated team procedures. +- `packages/trpc/server/routers/viewer/teams/*.schema.ts`: Zod procedure inputs. +- `packages/trpc/server/routers/viewer/teams/*.handler.ts`: thin handlers calling `TeamService`. + +### New team UI files + +- `apps/web/app/(use-page-wrapper)/(main-nav)/teams/page.tsx`: list memberships and create-team entry point. +- `apps/web/app/(use-page-wrapper)/teams/new/page.tsx`: create-team page. +- `apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[teamId]/profile/page.tsx`: team profile settings. +- `apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[teamId]/members/page.tsx`: member management. +- `apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[teamId]/settings/page.tsx`: deletion and destructive settings. +- `apps/web/modules/teams/TeamList.tsx`: team listing. +- `apps/web/modules/teams/TeamForm.tsx`: create/edit form. +- `apps/web/modules/teams/TeamMembers.tsx`: existing-user search and membership role controls. +- `apps/web/modules/event-types/components/tabs/team/EventTeamAssignmentTab.tsx`: team host selection, + fixed-host, priority, weight, group, and schedule controls. + +### Existing files restored or secured + +- `packages/trpc/server/procedures/pbacProcedures.ts`: replace always-true team permission stub. +- `packages/trpc/server/routers/viewer/eventTypes/util.ts`: replace always-true event permission stub. +- `packages/trpc/server/routers/viewer/eventTypes/heavy/create.handler.ts`: validate team membership and host selection. +- `apps/web/modules/event-types/components/CreateEventTypeDialog.tsx`: render team creation form. +- `apps/web/modules/event-types/components/EventTypeWebWrapper.tsx`: restore team assignment tab. +- `packages/trpc/server/routers/viewer/bookings/get.handler.ts`: enforce role-aware team booking visibility. +- `apps/web/app/(booking-page-wrapper)/team/[teamSlug]/page.tsx`: public team profile. +- `apps/web/app/(booking-page-wrapper)/team/[teamSlug]/[type]/page.tsx`: public team event page. + +### Stripe removal files + +- `packages/lib/constants.ts`: make Stripe and team billing unavailable. +- `packages/app-store/apps.metadata.generated.ts`: remove Stripe from exposed app metadata. +- `packages/app-store/apps.browser.generated.tsx`: remove Stripe UI imports. +- `packages/app-store/apps.schemas.generated.ts`: remove Stripe schemas. +- `packages/app-store/apps.keys-schemas.generated.ts`: remove Stripe keys. +- `packages/app-store/payment.services.generated.ts`: remove Stripe payment service. +- `packages/features/users/lib/deleteUser.ts`: stop calling Stripe on deletion. +- `packages/trpc/server/routers/viewer/me/deleteMeWithoutPassword.handler.ts`: stop calling Stripe on deletion. +- `packages/trpc/server/routers/loggedInViewer/_router.tsx`: remove Stripe customer procedure. +- `apps/web/modules/bookings/hooks/useBookings.ts`: remove payment-link behavior. +- `apps/web/modules/signup-view.tsx`: remove Stripe checkout redirect code. +- `docker-compose.yml`, `portainer.env.example`, `.env.example`: remove Stripe deployment variables. + +## Task 1: Implement Concrete Team Authorization + +**Files:** +- Modify: `packages/features/membership/services/membershipService.ts` +- Modify: `packages/trpc/server/procedures/pbacProcedures.ts` +- Modify: `packages/trpc/server/routers/viewer/eventTypes/util.ts` +- Create: `packages/features/membership/services/membershipService.test.ts` +- Create: `packages/trpc/server/procedures/pbacProcedures.test.ts` + +- [ ] **Step 1: Write failing membership permission tests** + +Add tests proving accepted membership and role intersection are required: + +```ts +it("allows an accepted admin when ADMIN is permitted", async () => { + repository.findUniqueByUserIdAndTeamId.mockResolvedValue({ + accepted: true, + role: MembershipRole.ADMIN, + }); + + await expect(service.hasAnyRole(10, 20, [MembershipRole.ADMIN, MembershipRole.OWNER])).resolves.toBe(true); +}); + +it("rejects an unaccepted owner", async () => { + repository.findUniqueByUserIdAndTeamId.mockResolvedValue({ + accepted: false, + role: MembershipRole.OWNER, + }); + + await expect(service.hasAnyRole(10, 20, [MembershipRole.OWNER])).resolves.toBe(false); +}); +``` + +- [ ] **Step 2: Run tests and verify they fail** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run packages/features/membership/services/membershipService.test.ts +``` + +Expected: FAIL because `hasAnyRole` does not exist. + +- [ ] **Step 3: Implement the membership role helper** + +Add to `MembershipService`: + +```ts +async hasAnyRole(teamId: number, userId: number, allowedRoles: MembershipRole[]): Promise { + const membership = await this.membershipRepository.findUniqueByUserIdAndTeamId({ teamId, userId }); + return !!membership?.accepted && allowedRoles.includes(membership.role); +} +``` + +- [ ] **Step 4: Replace team PBAC placeholder checks** + +In `pbacProcedures.ts`, replace `PermissionCheckService` with `MembershipService` and enforce +`fallbackRoles`: + +```ts +const membershipService = new MembershipService(); +const hasPermission = await membershipService.hasAnyRole(input.teamId, ctx.user.id, fallbackRoles); +``` + +In `eventTypes/util.ts`, use the same membership service for team events. Preserve the existing +personal-event ownership checks. + +- [ ] **Step 5: Add procedure authorization tests** + +Cover: + +```ts +expect(memberCallingAdminProcedure).rejects.toMatchObject({ code: "FORBIDDEN" }); +expect(adminCallingAdminProcedure).resolves.toBeDefined(); +expect(memberReadingTeamEvent).resolves.toBeDefined(); +``` + +- [ ] **Step 6: Run focused tests** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run \ + packages/features/membership/services/membershipService.test.ts \ + packages/trpc/server/procedures/pbacProcedures.test.ts \ + packages/trpc/server/routers/viewer/eventTypes/__tests__/util.test.ts +``` + +Expected: PASS. + +- [ ] **Step 7: Commit authorization foundation** + +```bash +git add packages/features/membership/services packages/trpc/server/procedures/pbacProcedures.ts packages/trpc/server/routers/viewer/eventTypes/util.ts packages/trpc/server/routers/viewer/eventTypes/__tests__/util.test.ts +git commit -m "Secure team membership authorization" +``` + +## Task 2: Add Team Service and Membership Invariants + +**Files:** +- Create: `packages/features/teams/repositories/TeamRepository.ts` +- Create: `packages/features/teams/services/TeamService.ts` +- Create: `packages/features/teams/services/TeamService.test.ts` + +- [ ] **Step 1: Write failing service tests** + +Cover team creation, multi-team membership, duplicate membership, and final-owner protection: + +```ts +it("creates a standalone team and accepted owner membership in one transaction", async () => { + await service.create({ creatorId: 1, name: "Support", slug: "support" }); + expect(repository.createWithOwner).toHaveBeenCalledWith({ + creatorId: 1, + name: "Support", + slug: "support", + }); +}); + +it("allows the same user to join different teams", async () => { + await service.addMember({ actorId: 1, teamId: 10, userId: 2, role: MembershipRole.MEMBER }); + await service.addMember({ actorId: 3, teamId: 20, userId: 2, role: MembershipRole.ADMIN }); + expect(repository.addMembership).toHaveBeenCalledTimes(2); +}); + +it("rejects demoting the final owner", async () => { + repository.countAcceptedOwners.mockResolvedValue(1); + await expect( + service.changeRole({ actorId: 1, teamId: 10, userId: 1, role: MembershipRole.ADMIN }) + ).rejects.toMatchObject({ code: "CONFLICT" }); +}); +``` + +- [ ] **Step 2: Run tests and verify they fail** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run packages/features/teams/services/TeamService.test.ts +``` + +Expected: FAIL because the service does not exist. + +- [ ] **Step 3: Implement repository transaction methods** + +Implement focused methods including: + +```ts +createWithOwner(input: { creatorId: number; name: string; slug: string }) { + return this.prisma.$transaction(async (tx) => { + const team = await tx.team.create({ + data: { name: input.name, slug: input.slug, isOrganization: false, pendingPayment: false }, + }); + await tx.membership.create({ + data: { teamId: team.id, userId: input.creatorId, accepted: true, role: MembershipRole.OWNER }, + }); + return team; + }); +} +``` + +Also implement `listForUser`, `findAccessible`, `addMembership`, `removeMembership`, `changeRole`, +`countAcceptedOwners`, `update`, `delete`, and paginated `searchExistingUsers`. + +- [ ] **Step 4: Implement TeamService role checks and invariants** + +Use `MembershipService.hasAnyRole` for actor checks. Reject organization teams and reject any +membership operation that would leave zero accepted owners. + +- [ ] **Step 5: Run service tests** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run packages/features/teams/services/TeamService.test.ts +``` + +Expected: PASS. + +- [ ] **Step 6: Commit team domain** + +```bash +git add packages/features/teams +git commit -m "Add standalone team service" +``` + +## Task 3: Expose Team CRUD and Membership APIs + +**Files:** +- Create: `packages/trpc/server/routers/viewer/teams/_router.ts` +- Create: `packages/trpc/server/routers/viewer/teams/team.schema.ts` +- Create: `packages/trpc/server/routers/viewer/teams/team.handler.ts` +- Create: `packages/trpc/server/routers/viewer/teams/team.handler.test.ts` +- Modify: `packages/trpc/server/routers/viewer/_router.ts` + +- [ ] **Step 1: Write failing router handler tests** + +Test these procedures: + +```ts +list() +create({ name, slug }) +get({ teamId }) +update({ teamId, name, slug }) +delete({ teamId }) +searchUsers({ teamId, query, cursor }) +addMember({ teamId, userId, role }) +changeRole({ teamId, userId, role }) +removeMember({ teamId, userId }) +``` + +Verify members cannot call admin mutations and search returns only existing Cal users. + +- [ ] **Step 2: Run tests and verify they fail** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run packages/trpc/server/routers/viewer/teams/team.handler.test.ts +``` + +Expected: FAIL because the teams router does not exist. + +- [ ] **Step 3: Implement Zod schemas** + +Use explicit standalone team roles: + +```ts +export const ZTeamRole = z.enum(["OWNER", "ADMIN", "MEMBER"]); +export const ZTeamId = z.object({ teamId: z.number().int().positive() }); +export const ZCreateTeam = z.object({ + name: z.string().trim().min(1).max(80), + slug: z.string().trim().min(1).max(80).regex(/^[a-z0-9-]+$/), +}); +``` + +- [ ] **Step 4: Implement handlers and router** + +Keep handlers thin: + +```ts +export const createTeamHandler = ({ ctx, input }: Options) => + new TeamService(new TeamRepository(ctx.prisma)).create({ + creatorId: ctx.user.id, + name: input.name, + slug: input.slug, + }); +``` + +Register `teams: teamsRouter` in `viewerRouter`. + +- [ ] **Step 5: Run handler and changed-file checks** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run packages/trpc/server/routers/viewer/teams/team.handler.test.ts +node .yarn/releases/yarn-4.12.0.cjs workspace @calcom/web check-changed-files +``` + +Expected: tests PASS and no new changed-file type errors. + +- [ ] **Step 6: Commit APIs** + +```bash +git add packages/trpc/server/routers/viewer/teams packages/trpc/server/routers/viewer/_router.ts +git commit -m "Add team management API" +``` + +## Task 4: Build Team Management Pages + +**Files:** +- Create: `apps/web/modules/teams/TeamList.tsx` +- Create: `apps/web/modules/teams/TeamForm.tsx` +- Create: `apps/web/modules/teams/TeamMembers.tsx` +- Create: `apps/web/app/(use-page-wrapper)/(main-nav)/teams/page.tsx` +- Create: `apps/web/app/(use-page-wrapper)/teams/new/page.tsx` +- Create: `apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[teamId]/profile/page.tsx` +- Create: `apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[teamId]/members/page.tsx` +- Create: `apps/web/app/(use-page-wrapper)/settings/(settings-layout)/teams/[teamId]/settings/page.tsx` +- Modify: `apps/web/modules/shell/Kbar.tsx` +- Test: `apps/web/playwright/teams.e2e.ts` + +- [ ] **Step 1: Write failing Playwright test for team and multi-team membership** + +Add a test that: + +```ts +await owner.goto("/teams"); +await owner.createTeam("Support", "support"); +await owner.addExistingUser("bob@example.com", "MEMBER"); +await owner.createTeam("Sales", "sales"); +await owner.addExistingUser("bob@example.com", "ADMIN"); +await bob.expectTeams(["Support", "Sales"]); +``` + +- [ ] **Step 2: Run the Playwright test and verify it fails** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs playwright test apps/web/playwright/teams.e2e.ts +``` + +Expected: FAIL because `/teams` is missing. + +- [ ] **Step 3: Implement team list and create form** + +Use `trpc.viewer.teams.list` and `trpc.viewer.teams.create`. The create page redirects to +`/settings/teams/{id}/members` after success. + +- [ ] **Step 4: Implement profile, members, and settings pages** + +`TeamMembers` must: + +- debounce authenticated user search; +- show current role; +- disable final-owner demotion/removal controls; +- permit admin/owner mutations only; +- never display invitation-by-email controls. + +- [ ] **Step 5: Add Teams navigation** + +Update `Kbar.tsx` and the main navigation configuration so `/teams` is reachable without a manual URL. + +- [ ] **Step 6: Run UI test** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs playwright test apps/web/playwright/teams.e2e.ts +``` + +Expected: PASS. + +- [ ] **Step 7: Commit team UI** + +```bash +git add apps/web/modules/teams apps/web/app/'(use-page-wrapper)' apps/web/modules/shell/Kbar.tsx apps/web/playwright/teams.e2e.ts +git commit -m "Add team management pages" +``` + +## Task 5: Restore Team Event Creation and Host Selection + +**Files:** +- Modify: `apps/web/modules/event-types/components/CreateEventTypeDialog.tsx` +- Modify: `apps/web/modules/event-types/components/EventTypeWebWrapper.tsx` +- Create: `apps/web/modules/event-types/components/tabs/team/EventTeamAssignmentTab.tsx` +- Modify: `packages/trpc/server/routers/viewer/eventTypes/heavy/create.handler.ts` +- Modify: `packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts` +- Create: `packages/trpc/server/routers/viewer/eventTypes/heavy/create.handler.test.ts` +- Test: `apps/web/playwright/team-event-types.e2e.ts` + +- [ ] **Step 1: Write failing handler tests** + +Cover: + +```ts +it("creates a round robin event for an admin with accepted member hosts") +it("creates a collective event for an owner") +it("rejects a member creating a team event") +it("rejects a host who is not an accepted team member") +``` + +- [ ] **Step 2: Run tests and verify they fail** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run packages/trpc/server/routers/viewer/eventTypes/heavy/create.handler.test.ts +``` + +Expected: at least the unauthorized-host test FAILS. + +- [ ] **Step 3: Secure team event creation** + +Replace the local permissive `PermissionCheckService` in `create.handler.ts` with +`MembershipService.hasAnyRole(teamId, userId, [ADMIN, OWNER])`. Before persisting hosts, query +accepted memberships and reject any user ID outside that set. + +- [ ] **Step 4: Restore the team creation form** + +In `CreateEventTypeDialog.tsx`, replace: + +```tsx +{teamId ? null : } +``` + +with a form that always renders and, for team profiles, exposes `ROUND_ROBIN` and `COLLECTIVE` +scheduling choices while passing `teamId`. + +- [ ] **Step 5: Build host assignment UI** + +Create `EventTeamAssignmentTab.tsx` using the existing TRPC host APIs: + +```tsx +const members = trpc.viewer.eventTypes.searchTeamMembers.useInfiniteQuery({ + teamId: team.id, + limit: 50, + search, +}); + +const selectedHosts = form.watch("hosts"); +``` + +Render accepted members with checkboxes and controls bound to the existing host form shape: + +```ts +{ + userId: number; + isFixed: boolean; + priority: number; + weight: number; + scheduleId: number | null; + groupId: string | null; +} +``` + +Disable fixed-host, priority, weight, and group controls for collective events where they do not +apply. Replace the null dynamic import in `EventTypeWebWrapper.tsx` with this component. + +- [ ] **Step 6: Add Playwright event-type test** + +Create a team event, select two hosts, set round robin, save, reload, and verify both hosts and the +scheduling type remain selected. + +- [ ] **Step 7: Run focused tests** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run packages/trpc/server/routers/viewer/eventTypes/heavy/create.handler.test.ts +node .yarn/releases/yarn-4.12.0.cjs playwright test apps/web/playwright/team-event-types.e2e.ts +``` + +Expected: PASS. + +- [ ] **Step 8: Commit team event restoration** + +```bash +git add apps/web/modules/event-types packages/trpc/server/routers/viewer/eventTypes apps/web/playwright/team-event-types.e2e.ts +git commit -m "Restore team event creation" +``` + +## Task 6: Restore Public Team Profiles and Booking Routes + +**Files:** +- Create: `apps/web/app/(booking-page-wrapper)/team/[teamSlug]/page.tsx` +- Create: `apps/web/app/(booking-page-wrapper)/team/[teamSlug]/[type]/page.tsx` +- Create: `packages/features/teams/repositories/PublicTeamRepository.ts` +- Modify: `apps/web/next.config.ts` +- Test: `apps/web/playwright/team-booking-pages.e2e.ts` + +- [ ] **Step 1: Write failing public-route tests** + +Test: + +```ts +await page.goto("/team/support"); +await expect(page.getByText("Support")).toBeVisible(); +await page.goto("/team/support/demo"); +await expect(page.getByTestId("booker")).toBeVisible(); +``` + +Also verify a nonexistent/private team returns not found and private membership data is absent. + +- [ ] **Step 2: Run tests and verify they fail** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs playwright test apps/web/playwright/team-booking-pages.e2e.ts +``` + +Expected: FAIL because team routes were removed. + +- [ ] **Step 3: Implement public team repository** + +Return only: + +```ts +{ + id, + name, + slug, + logoUrl, + bio, + eventTypes: { id, title, slug, length, schedulingType } +} +``` + +Do not return email addresses, membership roles, schedules, or calendar credentials. + +- [ ] **Step 4: Implement public pages using existing booker** + +Resolve the team event by `team.slug` plus `eventType.slug`, then pass the event to the existing +booking-page wrapper and Booker components. + +- [ ] **Step 5: Remove conflicting rewrites** + +Update `apps/web/next.config.ts` so `/team/:slug` and `/team/:slug/:type` reach the new App Router +pages instead of being treated as personal booking routes. + +- [ ] **Step 6: Run public-route tests** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs playwright test apps/web/playwright/team-booking-pages.e2e.ts +``` + +Expected: PASS. + +- [ ] **Step 7: Commit public routes** + +```bash +git add apps/web/app/'(booking-page-wrapper)'/team packages/features/teams/repositories/PublicTeamRepository.ts apps/web/next.config.ts apps/web/playwright/team-booking-pages.e2e.ts +git commit -m "Restore public team booking pages" +``` + +## Task 7: Verify Round-Robin and Collective Scheduling End to End + +**Files:** +- Modify: `packages/features/availability/lib/getAggregatedAvailability/getAggregatedAvailability.test.ts` +- Modify: `packages/features/bookings/lib/handleNewBooking/test/complex-schedules.test.ts` +- Create: `apps/web/playwright/team-round-robin.e2e.ts` + +- [ ] **Step 1: Add availability aggregation tests** + +Add cases proving: + +- round robin exposes a slot when at least one non-fixed host is available; +- collective exposes a slot only when every host is available; +- fixed round-robin hosts must be available; +- connected-calendar busy periods remove a host from eligibility. + +- [ ] **Step 2: Add assignment tests** + +Add booking tests proving priority, weight, recent booking load, and fixed hosts affect the selected +host without rewriting the existing algorithm. + +- [ ] **Step 3: Run focused scheduling tests** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run \ + packages/features/availability/lib/getAggregatedAvailability/getAggregatedAvailability.test.ts \ + packages/features/bookings/lib/handleNewBooking/test/complex-schedules.test.ts +``` + +Expected: PASS because these engine paths remain in Cal.diy. A failure blocks later tasks and must be +debugged against `getAggregatedAvailability.ts` or `RegularBookingService.ts` before continuing. + +- [ ] **Step 4: Add round-robin Playwright workflow** + +Set distinct availability for two members, book a slot available to only one member, and verify the +booking is assigned to that eligible member. + +- [ ] **Step 5: Run end-to-end scheduling test** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs playwright test apps/web/playwright/team-round-robin.e2e.ts +``` + +Expected: PASS. + +- [ ] **Step 6: Commit scheduling verification/restoration** + +```bash +git add packages/features/availability packages/features/bookings apps/web/playwright/team-round-robin.e2e.ts +git commit -m "Verify team round robin scheduling" +``` + +## Task 8: Enforce Team Booking Visibility + +**Files:** +- Modify: `packages/trpc/server/routers/viewer/bookings/get.handler.ts` +- Modify: `packages/trpc/server/routers/viewer/bookings/get.handler.integration-test.ts` +- Modify: `apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx` + +- [ ] **Step 1: Write failing booking visibility tests** + +Cover: + +```ts +it("shows all team bookings to accepted owners and admins") +it("shows an assigned team booking to the assigned member") +it("hides an unassigned team booking from a member") +it("does not leak bookings across teams") +``` + +- [ ] **Step 2: Run tests and verify they fail** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run packages/trpc/server/routers/viewer/bookings/get.handler.integration-test.ts +``` + +Expected: FAIL because Cal.diy currently treats team-wide read access as removed. + +- [ ] **Step 3: Implement role-aware query conditions** + +Build the team booking condition from accepted memberships: + +```ts +const adminTeamIds = memberships + .filter((membership) => ["OWNER", "ADMIN"].includes(membership.role)) + .map((membership) => membership.teamId); +``` + +Include all bookings where `eventType.teamId` is in `adminTeamIds`; for member teams, include only +bookings assigned to the current user. Do not rely on permissive PBAC stubs. + +- [ ] **Step 4: Restore booking page capability flag** + +Replace the Cal.diy hardcoded “no teams” behavior in the bookings page with the role-aware result +from the server query. + +- [ ] **Step 5: Run visibility tests** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run packages/trpc/server/routers/viewer/bookings/get.handler.integration-test.ts +``` + +Expected: PASS. + +- [ ] **Step 6: Commit booking visibility** + +```bash +git add packages/trpc/server/routers/viewer/bookings/get.handler.ts packages/trpc/server/routers/viewer/bookings/get.handler.integration-test.ts apps/web/app/'(use-page-wrapper)'/'(main-nav)'/bookings +git commit -m "Enforce team booking visibility" +``` + +## Task 9: Remove Stripe and Paid Booking Behavior + +**Files:** +- Modify: `packages/lib/constants.ts` +- Modify: `packages/app-store/apps.metadata.generated.ts` +- Modify: `packages/app-store/apps.browser.generated.tsx` +- Modify: `packages/app-store/apps.schemas.generated.ts` +- Modify: `packages/app-store/apps.keys-schemas.generated.ts` +- Modify: `packages/app-store/payment.services.generated.ts` +- Modify: `packages/app-store-cli/src/build.ts` +- Modify: `packages/features/users/lib/deleteUser.ts` +- Modify: `packages/trpc/server/routers/viewer/me/deleteMeWithoutPassword.handler.ts` +- Modify: `packages/trpc/server/routers/loggedInViewer/_router.tsx` +- Modify: `apps/web/modules/bookings/hooks/useBookings.ts` +- Modify: `apps/web/modules/signup-view.tsx` +- Modify: `docker-compose.yml` +- Modify: `portainer.env.example` +- Modify: `.env.example` +- Create: `packages/lib/constants.test.ts` +- Test: `packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts` + +- [ ] **Step 1: Write failing Stripe-disabled tests** + +Verify: + +```ts +expect(IS_STRIPE_ENABLED).toBe(false); +expect(IS_TEAM_BILLING_ENABLED).toBe(false); +expect(appStoreMetadata).not.toHaveProperty("stripepayment"); +``` + +Add a booking test where legacy Stripe metadata is present and confirm booking completes without a +payment flow. + +- [ ] **Step 2: Run tests and verify they fail** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run packages/lib/constants.test.ts packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +``` + +Expected: FAIL while Stripe remains exposed. + +- [ ] **Step 3: Disable Stripe constants and app-store exposure** + +Set: + +```ts +export const IS_STRIPE_ENABLED = false; +export const IS_TEAM_BILLING_ENABLED = false; +export const IS_TEAM_BILLING_ENABLED_CLIENT = false; +``` + +Remove Stripe imports/entries from generated app registries. In `packages/app-store-cli/src/build.ts`, +exclude the Stripe directory before generated outputs are assembled: + +```ts +const DISABLED_APPS = new Set(["stripepayment"]); + +function forEachAppDir(callback: (arg: App) => void, filter: (arg: App) => boolean = () => true) { + for (let i = 0; i < appDirs.length; i++) { + if (DISABLED_APPS.has(appDirs[i].name)) continue; + // existing metadata parsing and callback logic + } +} +``` + +- [ ] **Step 4: Remove active Stripe runtime calls** + +Remove Stripe customer deletion, Stripe customer query routing, signup checkout redirects, payment +link creation, and paid-booking branching. Legacy Stripe event metadata must be ignored. + +- [ ] **Step 5: Remove Stripe deployment variables** + +Remove all Stripe keys and billing flags from Compose and environment examples. + +- [ ] **Step 6: Run Stripe-disabled tests and registry generation** + +Run: + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run packages/lib/constants.test.ts packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +node .yarn/releases/yarn-4.12.0.cjs workspace @calcom/app-store-cli build +rg -n "stripepayment|STRIPE_" docker-compose.yml portainer.env.example .env.example packages/app-store/apps.metadata.generated.ts +``` + +Expected: tests PASS; generator succeeds; `rg` returns no active deployment or exposed registry +matches. + +- [ ] **Step 7: Commit Stripe removal** + +```bash +git add packages/lib packages/app-store packages/features/users packages/features/bookings packages/trpc/server/routers/loggedInViewer packages/trpc/server/routers/viewer/me apps/web/modules docker-compose.yml portainer.env.example .env.example +git commit -m "Remove Stripe and paid booking flows" +``` + +## Task 10: Final Verification, Portainer Validation, and Push + +**Files:** +- Modify only files required by verification failures. + +- [ ] **Step 1: Run focused team and authorization tests** + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run \ + packages/features/membership/services/membershipService.test.ts \ + packages/features/teams/services/TeamService.test.ts \ + packages/trpc/server/routers/viewer/teams/team.handler.test.ts \ + packages/trpc/server/routers/viewer/bookings/get.handler.integration-test.ts \ + packages/features/availability/lib/getAggregatedAvailability/getAggregatedAvailability.test.ts +``` + +Expected: PASS. + +- [ ] **Step 2: Run existing Authentik tests** + +```bash +node .yarn/releases/yarn-4.12.0.cjs vitest run packages/features/auth/lib/next-auth-options.test.ts +``` + +Expected: PASS. + +- [ ] **Step 3: Run changed-file type checks** + +```bash +node .yarn/releases/yarn-4.12.0.cjs workspace @calcom/web check-changed-files +``` + +Expected: no new type errors. + +- [ ] **Step 4: Validate Portainer Compose** + +```bash +docker compose --env-file portainer.env.example config --quiet +``` + +Expected: exit code 0. + +- [ ] **Step 5: Run complete team Playwright suite** + +```bash +node .yarn/releases/yarn-4.12.0.cjs playwright test \ + apps/web/playwright/teams.e2e.ts \ + apps/web/playwright/team-event-types.e2e.ts \ + apps/web/playwright/team-booking-pages.e2e.ts \ + apps/web/playwright/team-round-robin.e2e.ts +``` + +Expected: PASS. + +- [ ] **Step 6: Verify no tracked symlinks and no Stripe deployment surface** + +```bash +git ls-files -s | awk '$1 ~ /^120000/ {print}' +rg -n "STRIPE_|stripepayment" docker-compose.yml portainer.env.example .env.example +git diff --check +git status --short +``` + +Expected: no symlink output, no Stripe environment matches, no whitespace errors, and only intended +changes. + +- [ ] **Step 7: Commit final verification fixes** + +```bash +git add -A +git commit -m "Complete self-hosted team scheduling" +``` + +- [ ] **Step 8: Create and force-push compact snapshot** + +```bash +snapshot=$(git commit-tree HEAD^{tree} -m "Import Cal.diy with Authentik and self-hosted team scheduling") +git push internal "${snapshot}:refs/heads/main" --force +git ls-remote internal refs/heads/main +``` + +Expected: push succeeds and `ls-remote` returns the new snapshot SHA.