mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
d52e2d9fad
* 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: [email protected] <[email protected]> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
65 lines
2.4 KiB
TypeScript
65 lines
2.4 KiB
TypeScript
import logger from "@calcom/lib/logger";
|
|
|
|
import type { ActiveUserBillingService } from "../../active-user/services/ActiveUserBillingService";
|
|
import type { ITeamBillingDataRepository } from "../../repository/teamBillingData/ITeamBillingDataRepository";
|
|
import type { IBillingProviderService } from "../billingProvider/IBillingProviderService";
|
|
import { BaseSeatBillingStrategy } from "./ISeatBillingStrategy";
|
|
import type { SeatChangeContext } from "./ISeatBillingStrategy";
|
|
|
|
const log = logger.getSubLogger({ prefix: ["ActiveUserBillingStrategy"] });
|
|
|
|
export interface IActiveUserBillingStrategyDeps {
|
|
activeUserBillingService: ActiveUserBillingService;
|
|
billingProviderService: IBillingProviderService;
|
|
teamBillingDataRepository: ITeamBillingDataRepository;
|
|
}
|
|
|
|
export class ActiveUserBillingStrategy extends BaseSeatBillingStrategy {
|
|
constructor(private readonly deps: IActiveUserBillingStrategyDeps) {
|
|
super();
|
|
}
|
|
|
|
async onSeatChange(_context: SeatChangeContext): Promise<void> {
|
|
// No-op: active user count is determined at billing time, not on seat changes
|
|
}
|
|
|
|
override async onInvoiceUpcoming(subscriptionId: string): Promise<{ applied: boolean }> {
|
|
const team = await this.deps.teamBillingDataRepository.findBySubscriptionId(subscriptionId);
|
|
if (!team) {
|
|
log.warn(`No team found for subscription ${subscriptionId}`);
|
|
return { applied: false };
|
|
}
|
|
|
|
const subscription = await this.deps.billingProviderService.getSubscription(subscriptionId);
|
|
if (!subscription) {
|
|
log.warn(`No subscription found for ${subscriptionId}`);
|
|
return { applied: false };
|
|
}
|
|
|
|
const periodStart = new Date(subscription.current_period_start * 1000);
|
|
const periodEnd = new Date(subscription.current_period_end * 1000);
|
|
|
|
const activeUserCount = await this.deps.activeUserBillingService.getActiveUserCountForOrg(
|
|
team.id,
|
|
periodStart,
|
|
periodEnd
|
|
);
|
|
|
|
const subscriptionItem = subscription.items[0];
|
|
if (!subscriptionItem) {
|
|
log.warn(`No subscription item found for ${subscriptionId}`);
|
|
return { applied: false };
|
|
}
|
|
|
|
await this.deps.billingProviderService.handleSubscriptionUpdate({
|
|
subscriptionId,
|
|
subscriptionItemId: subscriptionItem.id,
|
|
membershipCount: activeUserCount,
|
|
prorationBehavior: "none",
|
|
});
|
|
|
|
log.info(`Updated subscription ${subscriptionId} to ${activeUserCount} active users for team ${team.id}`);
|
|
return { applied: true };
|
|
}
|
|
}
|