Files
calendar/packages/features/ee/billing/active-user/services/ActiveUserBillingService.integration-test.ts
T
sean-brydonGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
d52e2d9fad feat: active user billing (#27867)
* factory and statergie

* chore: use correct method of DI

* feat: add onchagne

* add logic to HWM stat

* add webhook resolver methods to each statergy

* move seat tracking + webhooks over to own statergy

* Move to factory base approach

* move logic to correct class

* rename create -> createByTeamId

* fix: remove debug `true ||` overrides from IS_STRIPE_ENABLED and IS_TEAM_BILLING_ENABLED

Remove accidentally committed debug overrides that short-circuited
IS_STRIPE_ENABLED and IS_TEAM_BILLING_ENABLED to always be true,
bypassing Stripe credential checks. This would break self-hosted
instances without Stripe configured.

Identified by cubic (https://cubic.dev)

Co-Authored-By: unknown <>

* feat: active user billing

* add tests

* UI side for users on active billing

* use correct period of stripe sub

* feat: claude feedback

* fix: skip Stripe sync for canceled/expired subscriptions to prevent repeated API calls

Co-Authored-By: unknown <>

* feat: feedback

* feat: only render when active users mode is set

* fix type error

* fix: constants + feature flags

* fix: default to null in tests

* chore: use seats in test

* fix: use node:crypto protocol for Node.js builtin imports

Co-Authored-By: sean@cal.com <Sean@brydon.io>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-12 13:57:32 +00:00

198 lines
5.2 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { prisma } from "@calcom/prisma";
import type { Team, User } from "@calcom/prisma/client";
import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { ActiveUserBillingRepository } from "../repositories/ActiveUserBillingRepository";
import { ActiveUserBillingService } from "./ActiveUserBillingService";
const TEST_PREFIX = `aub-int-${Date.now()}`;
let org: Team;
let users: User[] = [];
let service: ActiveUserBillingService;
const periodStart = new Date("2026-01-01T00:00:00Z");
const periodEnd = new Date("2026-02-01T00:00:00Z");
async function createUser(suffix: string): Promise<User> {
const user = await prisma.user.create({
data: {
email: `${TEST_PREFIX}-${suffix}@example.com`,
username: `${TEST_PREFIX}-${suffix}`,
name: `Test ${suffix}`,
},
});
users.push(user);
return user;
}
async function createBookingForHost(
host: User,
startTime: Date,
endTime: Date,
attendeeEmail?: string
): Promise<number> {
const booking = await prisma.booking.create({
data: {
uid: randomUUID(),
title: `Booking by ${host.name}`,
startTime,
endTime,
userId: host.id,
status: BookingStatus.ACCEPTED,
},
});
if (attendeeEmail) {
await prisma.attendee.create({
data: {
email: attendeeEmail,
name: "Attendee",
timeZone: "UTC",
bookingId: booking.id,
},
});
}
return booking.id;
}
describe("ActiveUserBillingService Integration", () => {
const bookingIds: number[] = [];
beforeAll(async () => {
const repo = new ActiveUserBillingRepository(prisma);
service = new ActiveUserBillingService({
activeUserBillingRepository: repo,
});
org = await prisma.team.create({
data: {
name: `${TEST_PREFIX} Org`,
slug: TEST_PREFIX,
isOrganization: true,
},
});
const alice = await createUser("alice");
const bob = await createUser("bob");
const charlie = await createUser("charlie");
const inactive = await createUser("inactive");
for (const user of [alice, bob, charlie, inactive]) {
await prisma.membership.create({
data: {
userId: user.id,
teamId: org.id,
role: MembershipRole.MEMBER,
accepted: true,
},
});
}
// Alice hosts a booking inside the period
const id1 = await createBookingForHost(
alice,
new Date("2026-01-10T10:00:00Z"),
new Date("2026-01-10T11:00:00Z")
);
bookingIds.push(id1);
// Bob hosts a booking inside the period, with Charlie as attendee
const id2 = await createBookingForHost(
bob,
new Date("2026-01-15T14:00:00Z"),
new Date("2026-01-15T15:00:00Z"),
charlie.email
);
bookingIds.push(id2);
// Inactive user has a booking OUTSIDE the period (before)
const id3 = await createBookingForHost(
inactive,
new Date("2025-12-20T10:00:00Z"),
new Date("2025-12-20T11:00:00Z")
);
bookingIds.push(id3);
});
afterAll(async () => {
await prisma.attendee.deleteMany({
where: { bookingId: { in: bookingIds } },
});
await prisma.booking.deleteMany({ where: { id: { in: bookingIds } } });
await prisma.membership.deleteMany({ where: { teamId: org.id } });
await prisma.team.delete({ where: { id: org.id } });
for (const user of users) {
await prisma.user.delete({ where: { id: user.id } }).catch(() => {});
}
});
it("counts hosts and attendees as active, excludes inactive members", async () => {
const count = await service.getActiveUserCountForOrg(
org.id,
periodStart,
periodEnd
);
// alice: host, bob: host, charlie: attendee of bob's booking
// inactive: booking is outside the period
expect(count).toBe(3);
});
it("returns 0 when period has no bookings", async () => {
const emptyStart = new Date("2027-01-01T00:00:00Z");
const emptyEnd = new Date("2027-02-01T00:00:00Z");
const count = await service.getActiveUserCountForOrg(
org.id,
emptyStart,
emptyEnd
);
expect(count).toBe(0);
});
it("does not double-count a user who is both host and attendee", async () => {
// Alice hosts a booking where Bob is attendee -- Bob is already a host too
const id = await createBookingForHost(
users[0], // alice
new Date("2026-01-20T09:00:00Z"),
new Date("2026-01-20T10:00:00Z"),
users[1].email // bob as attendee
);
bookingIds.push(id);
const count = await service.getActiveUserCountForOrg(
org.id,
periodStart,
periodEnd
);
// Still 3: alice (host), bob (host + attendee but counted once), charlie (attendee)
expect(count).toBe(3);
});
it("includes bookings right at period boundaries", async () => {
// Booking starting exactly at periodStart
const id = await createBookingForHost(
users[3], // inactive user
new Date("2026-01-01T00:00:00Z"),
new Date("2026-01-01T01:00:00Z")
);
bookingIds.push(id);
const count = await service.getActiveUserCountForOrg(
org.id,
periodStart,
periodEnd
);
// Now all 4 are active
expect(count).toBe(4);
});
});