Plan self-hosted team scheduling implementation

This commit is contained in:
Zachariah K. Sharma
2026-06-07 01:38:28 -06:00
parent 084c0d6ee0
commit f9ef20ce74
@@ -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<boolean> {
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 : <CreateEventTypeForm ... />}
```
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.