mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
fix: billing (#5483)
This commit is contained in:
committed by
GitHub
parent
27da540846
commit
e1bbb0a10f
@@ -3,6 +3,7 @@ import { cache } from "@/lib/cache";
|
||||
import { BILLING_LIMITS, ITEMS_PER_PAGE, PROJECT_FEATURE_KEYS } from "@/lib/constants";
|
||||
import { getProjects } from "@/lib/project/service";
|
||||
import { updateUser } from "@/lib/user/service";
|
||||
import { getBillingPeriodStartDate } from "@/lib/utils/billing";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
@@ -337,19 +338,8 @@ export const getMonthlyOrganizationResponseCount = reactCache(
|
||||
throw new ResourceNotFoundError("Organization", organizationId);
|
||||
}
|
||||
|
||||
// Determine the start date based on the plan type
|
||||
let startDate: Date;
|
||||
if (organization.billing.plan === "free") {
|
||||
// For free plans, use the first day of the current calendar month
|
||||
const now = new Date();
|
||||
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
} else {
|
||||
// For other plans, use the periodStart from billing
|
||||
if (!organization.billing.periodStart) {
|
||||
throw new Error("Organization billing period start is not set");
|
||||
}
|
||||
startDate = organization.billing.periodStart;
|
||||
}
|
||||
// Use the utility function to calculate the start date
|
||||
const startDate = getBillingPeriodStartDate(organization.billing);
|
||||
|
||||
// Get all environment IDs for the organization
|
||||
const projects = await getProjects(organizationId);
|
||||
|
||||
176
apps/web/lib/utils/billing.test.ts
Normal file
176
apps/web/lib/utils/billing.test.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { getBillingPeriodStartDate } from "./billing";
|
||||
|
||||
describe("getBillingPeriodStartDate", () => {
|
||||
let originalDate: DateConstructor;
|
||||
|
||||
beforeEach(() => {
|
||||
// Store the original Date constructor
|
||||
originalDate = global.Date;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore the original Date constructor
|
||||
global.Date = originalDate;
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("returns first day of month for free plans", () => {
|
||||
// Mock the current date to be 2023-03-15
|
||||
vi.setSystemTime(new Date(2023, 2, 15));
|
||||
|
||||
const organization = {
|
||||
billing: {
|
||||
plan: "free",
|
||||
periodStart: new Date("2023-01-15"),
|
||||
period: "monthly",
|
||||
},
|
||||
};
|
||||
|
||||
const result = getBillingPeriodStartDate(organization.billing);
|
||||
|
||||
// For free plans, should return first day of current month
|
||||
expect(result).toEqual(new Date(2023, 2, 1));
|
||||
});
|
||||
|
||||
test("returns correct date for monthly plans", () => {
|
||||
// Mock the current date to be 2023-03-15
|
||||
vi.setSystemTime(new Date(2023, 2, 15));
|
||||
|
||||
const organization = {
|
||||
billing: {
|
||||
plan: "scale",
|
||||
periodStart: new Date("2023-02-10"),
|
||||
period: "monthly",
|
||||
},
|
||||
};
|
||||
|
||||
const result = getBillingPeriodStartDate(organization.billing);
|
||||
|
||||
// For monthly plans, should return periodStart directly
|
||||
expect(result).toEqual(new Date("2023-02-10"));
|
||||
});
|
||||
|
||||
test("returns current month's subscription day for yearly plans when today is after subscription day", () => {
|
||||
// Mock the current date to be March 20, 2023
|
||||
vi.setSystemTime(new Date(2023, 2, 20));
|
||||
|
||||
const organization = {
|
||||
billing: {
|
||||
plan: "scale",
|
||||
periodStart: new Date("2022-05-15"), // Original subscription on 15th
|
||||
period: "yearly",
|
||||
},
|
||||
};
|
||||
|
||||
const result = getBillingPeriodStartDate(organization.billing);
|
||||
|
||||
// Should return March 15, 2023 (same day in current month)
|
||||
expect(result).toEqual(new Date(2023, 2, 15));
|
||||
});
|
||||
|
||||
test("returns previous month's subscription day for yearly plans when today is before subscription day", () => {
|
||||
// Mock the current date to be March 10, 2023
|
||||
vi.setSystemTime(new Date(2023, 2, 10));
|
||||
|
||||
const organization = {
|
||||
billing: {
|
||||
plan: "scale",
|
||||
periodStart: new Date("2022-05-15"), // Original subscription on 15th
|
||||
period: "yearly",
|
||||
},
|
||||
};
|
||||
|
||||
const result = getBillingPeriodStartDate(organization.billing);
|
||||
|
||||
// Should return February 15, 2023 (same day in previous month)
|
||||
expect(result).toEqual(new Date(2023, 1, 15));
|
||||
});
|
||||
|
||||
test("handles subscription day that doesn't exist in current month (February edge case)", () => {
|
||||
// Mock the current date to be February 15, 2023
|
||||
vi.setSystemTime(new Date(2023, 1, 15));
|
||||
|
||||
const organization = {
|
||||
billing: {
|
||||
plan: "scale",
|
||||
periodStart: new Date("2022-01-31"), // Original subscription on 31st
|
||||
period: "yearly",
|
||||
},
|
||||
};
|
||||
|
||||
const result = getBillingPeriodStartDate(organization.billing);
|
||||
|
||||
// Should return January 31, 2023 (previous month's subscription day)
|
||||
// since today (Feb 15) is less than the subscription day (31st)
|
||||
expect(result).toEqual(new Date(2023, 0, 31));
|
||||
});
|
||||
|
||||
test("handles subscription day that doesn't exist in previous month (February to March transition)", () => {
|
||||
// Mock the current date to be March 10, 2023
|
||||
vi.setSystemTime(new Date(2023, 2, 10));
|
||||
|
||||
const organization = {
|
||||
billing: {
|
||||
plan: "scale",
|
||||
periodStart: new Date("2022-01-30"), // Original subscription on 30th
|
||||
period: "yearly",
|
||||
},
|
||||
};
|
||||
|
||||
const result = getBillingPeriodStartDate(organization.billing);
|
||||
|
||||
// Should return February 28, 2023 (last day of February)
|
||||
// since February 2023 doesn't have a 30th day
|
||||
expect(result).toEqual(new Date(2023, 1, 28));
|
||||
});
|
||||
|
||||
test("handles subscription day that doesn't exist in previous month (leap year)", () => {
|
||||
// Mock the current date to be March 10, 2024 (leap year)
|
||||
vi.setSystemTime(new Date(2024, 2, 10));
|
||||
|
||||
const organization = {
|
||||
billing: {
|
||||
plan: "scale",
|
||||
periodStart: new Date("2023-01-30"), // Original subscription on 30th
|
||||
period: "yearly",
|
||||
},
|
||||
};
|
||||
|
||||
const result = getBillingPeriodStartDate(organization.billing);
|
||||
|
||||
// Should return February 29, 2024 (last day of February in leap year)
|
||||
expect(result).toEqual(new Date(2024, 1, 29));
|
||||
});
|
||||
test("handles current month with fewer days than subscription day", () => {
|
||||
// Mock the current date to be April 25, 2023 (April has 30 days)
|
||||
vi.setSystemTime(new Date(2023, 3, 25));
|
||||
|
||||
const organization = {
|
||||
billing: {
|
||||
plan: "scale",
|
||||
periodStart: new Date("2022-01-31"), // Original subscription on 31st
|
||||
period: "yearly",
|
||||
},
|
||||
};
|
||||
|
||||
const result = getBillingPeriodStartDate(organization.billing);
|
||||
|
||||
// Should return March 31, 2023 (since today is before April's adjusted subscription day)
|
||||
expect(result).toEqual(new Date(2023, 2, 31));
|
||||
});
|
||||
|
||||
test("throws error when periodStart is not set for non-free plans", () => {
|
||||
const organization = {
|
||||
billing: {
|
||||
plan: "scale",
|
||||
periodStart: null,
|
||||
period: "monthly",
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
getBillingPeriodStartDate(organization.billing);
|
||||
}).toThrow("billing period start is not set");
|
||||
});
|
||||
});
|
||||
53
apps/web/lib/utils/billing.ts
Normal file
53
apps/web/lib/utils/billing.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
|
||||
// Function to calculate billing period start date based on organization plan and billing period
|
||||
export const getBillingPeriodStartDate = (billing: TOrganizationBilling): Date => {
|
||||
const now = new Date();
|
||||
if (billing.plan === "free") {
|
||||
// For free plans, use the first day of the current calendar month
|
||||
return new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
} else if (billing.period === "yearly" && billing.periodStart) {
|
||||
// For yearly plans, use the same day of the month as the original subscription date
|
||||
const periodStart = new Date(billing.periodStart);
|
||||
const subscriptionDay = periodStart.getDate();
|
||||
|
||||
// Helper function to get the last day of a specific month
|
||||
const getLastDayOfMonth = (year: number, month: number): number => {
|
||||
// Create a date for the first day of the next month, then subtract one day
|
||||
return new Date(year, month + 1, 0).getDate();
|
||||
};
|
||||
|
||||
// Calculate the adjusted day for the current month
|
||||
const lastDayOfCurrentMonth = getLastDayOfMonth(now.getFullYear(), now.getMonth());
|
||||
const adjustedCurrentMonthDay = Math.min(subscriptionDay, lastDayOfCurrentMonth);
|
||||
|
||||
// Calculate the current month's adjusted subscription date
|
||||
const currentMonthSubscriptionDate = new Date(now.getFullYear(), now.getMonth(), adjustedCurrentMonthDay);
|
||||
|
||||
// If today is before the subscription day in the current month (or its adjusted equivalent),
|
||||
// we should use the previous month's subscription day as our start date
|
||||
if (now.getDate() < adjustedCurrentMonthDay) {
|
||||
// Calculate previous month and year
|
||||
const prevMonth = now.getMonth() === 0 ? 11 : now.getMonth() - 1;
|
||||
const prevYear = now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear();
|
||||
|
||||
// Calculate the adjusted day for the previous month
|
||||
const lastDayOfPreviousMonth = getLastDayOfMonth(prevYear, prevMonth);
|
||||
const adjustedPreviousMonthDay = Math.min(subscriptionDay, lastDayOfPreviousMonth);
|
||||
|
||||
// Return the adjusted previous month date
|
||||
return new Date(prevYear, prevMonth, adjustedPreviousMonthDay);
|
||||
} else {
|
||||
return currentMonthSubscriptionDate;
|
||||
}
|
||||
} else if (billing.period === "monthly" && billing.periodStart) {
|
||||
// For monthly plans with a periodStart, use that date
|
||||
return new Date(billing.periodStart);
|
||||
} else {
|
||||
// For other plans, use the periodStart from billing
|
||||
if (!billing.periodStart) {
|
||||
throw new Error("billing period start is not set");
|
||||
}
|
||||
return new Date(billing.periodStart);
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cache } from "@/lib/cache";
|
||||
import { organizationCache } from "@/lib/organization/cache";
|
||||
import { getBillingPeriodStartDate } from "@/lib/utils/billing";
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import { Organization } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
@@ -133,22 +134,7 @@ export const getMonthlyOrganizationResponseCount = reactCache(async (organizatio
|
||||
}
|
||||
|
||||
// Determine the start date based on the plan type
|
||||
let startDate: Date;
|
||||
|
||||
if (billing.data.plan === "free") {
|
||||
// For free plans, use the first day of the current calendar month
|
||||
const now = new Date();
|
||||
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
} else {
|
||||
// For other plans, use the periodStart from billing
|
||||
if (!billing.data.periodStart) {
|
||||
return err({
|
||||
type: "internal_server_error",
|
||||
details: [{ field: "organization", issue: "billing period start is not set" }],
|
||||
});
|
||||
}
|
||||
startDate = billing.data.periodStart;
|
||||
}
|
||||
const startDate = getBillingPeriodStartDate(billing.data);
|
||||
|
||||
// Get all environment IDs for the organization
|
||||
const environmentIdsResult = await getAllEnvironmentsFromOrganizationId(organizationId);
|
||||
|
||||
@@ -74,6 +74,7 @@ export default defineConfig({
|
||||
"modules/analysis/**/*.tsx",
|
||||
"modules/analysis/**/*.ts",
|
||||
"modules/survey/editor/components/end-screen-form.tsx",
|
||||
"lib/utils/billing.ts"
|
||||
],
|
||||
exclude: [
|
||||
"**/.next/**",
|
||||
|
||||
Reference in New Issue
Block a user