Files
Checkmate/server/tests/services/system/BillingService.test.js
T

183 lines
8.0 KiB
JavaScript

import BillingService from "@/services/system/BillingService.js";
import ApiError from "@/utils/ApiError.js";
// Mock Org model
jest.mock("@/db/models/index.js", () => ({
Org: { findById: jest.fn(), findOne: jest.fn() },
}));
import { Org } from "@/db/models/index.js";
// Capture a shared mock Stripe client instance
jest.mock("stripe", () => {
const mockClient = {
prices: { list: jest.fn() },
customers: { create: jest.fn() },
checkout: { sessions: { create: jest.fn() } },
billingPortal: { sessions: { create: jest.fn() } },
subscriptions: { retrieve: jest.fn() },
};
const MockStripe = jest.fn(() => mockClient);
MockStripe.__mockClient = mockClient;
return { __esModule: true, default: MockStripe };
});
import Stripe from "stripe";
describe("BillingService", () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe("listPlans", () => {
it("lists plans in order and maps prices by lookup_key", async () => {
(Stripe.__mockClient).prices.list.mockResolvedValue({
data: [
{ lookup_key: "free", unit_amount: 0 },
{ lookup_key: "pro", unit_amount: 1000 },
{ lookup_key: "business", unit_amount: 2500 },
{ lookup_key: "enterprise", unit_amount: 9999 },
],
});
const svc = new BillingService();
const plans = await svc.listPlans();
expect(plans.map((p) => p.plan)).toEqual([
"free",
"pro",
"business",
"enterprise",
]);
expect(plans.find((p) => p.plan === "pro")?.price).toBe(1000);
expect(plans.find((p) => p.plan === "enterprise")?.price).toBe(9999);
});
});
describe("subscribePlan", () => {
it("throws when planKey missing", async () => {
const svc = new BillingService();
await expect(svc.subscribePlan("e@x.com", "org1", undefined)).rejects.toMatchObject({
message: "planKey is required",
status: 400,
});
});
it("throws when planKey invalid", async () => {
const svc = new BillingService();
await expect(svc.subscribePlan("e@x.com", "org1", "bogus"))
.rejects.toMatchObject({ message: "Invalid planKey", status: 400 });
});
it("throws when org not found", async () => {
Org.findById.mockResolvedValue(null);
const svc = new BillingService();
await expect(svc.subscribePlan("e@x.com", "org1", "pro"))
.rejects.toMatchObject({ message: "Organization not found", status: 404 });
});
it("creates customer and new subscription checkout when none exists", async () => {
const org = { _id: "org1", save: jest.fn(), billingCustomerId: null, subscriptionId: null };
Org.findById.mockResolvedValue(org);
(Stripe.__mockClient).customers.create.mockResolvedValue({ id: "cus_123" });
(Stripe.__mockClient).prices.list.mockResolvedValue({ data: [{ id: "price_123", lookup_key: "pro" }] });
(Stripe.__mockClient).checkout.sessions.create.mockResolvedValue({ url: "https://checkout" });
const svc = new BillingService();
const url = await svc.subscribePlan("e@x.com", "org1", "pro");
expect(org.billingCustomerId).toBe("cus_123");
expect(org.save).toHaveBeenCalled();
expect(Stripe.__mockClient.checkout.sessions.create).toHaveBeenCalled();
expect(url).toBe("https://checkout");
});
it("updates existing subscription via billing portal when subscriptionId exists", async () => {
const org = { _id: "org1", save: jest.fn(), billingCustomerId: "cus_123", subscriptionId: "sub_1" };
Org.findById.mockResolvedValue(org);
(Stripe.__mockClient).prices.list.mockResolvedValue({ data: [{ id: "price_123", lookup_key: "pro" }] });
(Stripe.__mockClient).billingPortal.sessions.create.mockResolvedValue({ url: "https://portal" });
const svc = new BillingService();
const url = await svc.subscribePlan("e@x.com", "org1", "pro");
expect(Stripe.__mockClient.billingPortal.sessions.create).toHaveBeenCalledWith(
expect.objectContaining({ customer: "cus_123" })
);
expect(url).toBe("https://portal");
});
it("throws when no price found for plan", async () => {
const org = { _id: "org1", save: jest.fn(), billingCustomerId: "cus_123", subscriptionId: null };
Org.findById.mockResolvedValue(org);
(Stripe.__mockClient).prices.list.mockResolvedValue({ data: [] });
const svc = new BillingService();
await expect(svc.subscribePlan("e@x.com", "org1", "pro"))
.rejects.toMatchObject({ message: "Price not found for the selected plan", status: 404 });
});
});
describe("cancelSubscription", () => {
it("throws when org not found", async () => {
Org.findById.mockResolvedValue(null);
const svc = new BillingService();
await expect(svc.cancelSubscription("org1"))
.rejects.toMatchObject({ message: "Organization not found", status: 404 });
});
it("throws when no active subscription found", async () => {
Org.findById.mockResolvedValue({ _id: "org1", subscriptionId: null });
const svc = new BillingService();
await expect(svc.cancelSubscription("org1"))
.rejects.toMatchObject({ message: "No active subscription found", status: 400 });
});
it("returns billing portal url on success", async () => {
Org.findById.mockResolvedValue({ _id: "org1", billingCustomerId: "cus_1", subscriptionId: "sub_1" });
(Stripe.__mockClient).billingPortal.sessions.create.mockResolvedValue({ url: "https://cancel" });
const svc = new BillingService();
const url = await svc.cancelSubscription("org1");
expect(url).toBe("https://cancel");
});
it("throws when billing portal session lacks url", async () => {
Org.findById.mockResolvedValue({ _id: "org1", billingCustomerId: "cus_1", subscriptionId: "sub_1" });
(Stripe.__mockClient).billingPortal.sessions.create.mockResolvedValue({ url: null });
const svc = new BillingService();
await expect(svc.cancelSubscription("org1"))
.rejects.toMatchObject({ message: "Failed to create billing portal session", status: 500 });
});
});
describe("confirmPlan", () => {
it("throws when org not found", async () => {
Org.findOne.mockResolvedValue(null);
const svc = new BillingService();
await expect(svc.confirmPlan("org1", "pro"))
.rejects.toMatchObject({ message: "Organization not found", status: 404 });
});
it("returns true only when expected is free if no subscriptionId", async () => {
Org.findOne.mockResolvedValue({ _id: "org1", subscriptionId: null });
const svc = new BillingService();
await expect(svc.confirmPlan("org1", "free")).resolves.toBe(true);
await expect(svc.confirmPlan("org1", "pro")).resolves.toBe(false);
});
it("returns true when active plan equals org.planKey", async () => {
Org.findOne.mockResolvedValue({ _id: "org1", subscriptionId: "sub1", planKey: "business" });
(Stripe.__mockClient).subscriptions.retrieve.mockResolvedValue({ items: { data: [{ price: { lookup_key: "business" } }] } });
const svc = new BillingService();
await expect(svc.confirmPlan("org1", "pro")).resolves.toBe(true);
});
it("returns true when active plan equals expectedPlanKey", async () => {
Org.findOne.mockResolvedValue({ _id: "org1", subscriptionId: "sub1", planKey: "pro" });
(Stripe.__mockClient).subscriptions.retrieve.mockResolvedValue({ items: { data: [{ price: { lookup_key: "enterprise" } }] } });
const svc = new BillingService();
await expect(svc.confirmPlan("org1", "enterprise")).resolves.toBe(true);
});
it("returns false when active plan differs from expected", async () => {
Org.findOne.mockResolvedValue({ _id: "org1", subscriptionId: "sub1", planKey: "pro" });
(Stripe.__mockClient).subscriptions.retrieve.mockResolvedValue({ items: { data: [{ price: { lookup_key: "business" } }] } });
const svc = new BillingService();
await expect(svc.confirmPlan("org1", "enterprise")).resolves.toBe(false);
});
});
});