Plan self-hosted team scheduling implementation
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user