Files
anonymous_github/test/billing-integration.test.js
T
Thomas DurieuxandClaude Opus 5.5 511dcd7067 feat(admin): billing tab to monitor subscriptions and sales (#952)
* feat(admin): add a Billing tab for subscriptions and sales

The admin can now follow subscriptions and sales without opening Stripe:
- Summary tiles: active subscriptions (monthly/annual), MRR with annual
  plans counted as 1/12, sales and revenue for 7d/30d/90d/12m/all time,
  all-time sales, subscriptions cancelling at period end, and refunds
  and disputes.
- A stacked chart of sales per day, week or month by plan family.
- A by-plan table: active grants, period and all-time sales and revenue.
- A paginated list of grants with plan, Stripe status, active/expired/
  revoked and user filters, linked to the admin user, repository or
  conference.
- One row of webhook health: pending, failed, oldest pending event, last
  error and unreviewed payment reversals.

A sale is one checkout or subscription, so a paper pack's three grants
count once. Revenue is each sale's Stripe list price, resolved once per
price and cached in memory. Currencies are never added together. Without
Stripe the page shows counts and marks revenue as unavailable.

GET /api/admin/billing/summary and /grants sit behind the admin check.
Grants get a { purchasedAt, _id } index for the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: preserve payment history in admin billing totals

* fix: scope billing history and preserve original sale prices

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 14:37:29 +01:00

650 lines
48 KiB
JavaScript

const { URL } = require("node:url");
const { setTimeout } = require("node:timers");
const { expect } = require("chai");
require("ts-node/register/transpile-only");
const mongoose = require("mongoose");
const config = require("../src/config").default;
const { BillingPrice, BillingAccount, BillingGrant, BillingQuota, BillingLock, BillingEvent, BillingRevocation, BillingDraft } = require("../src/core/billing/models");
const Repo = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default;
const Conference = require("../src/core/model/conference/conferences.model").default;
const { reserveRepository, allowance, assertConferenceAdmission } = require("../src/core/billing/quotas");
const { fulfillCheckout, syncSubscription, createCheckout, processBillingEvent } = require("../src/core/billing/stripe");
const uri = process.env.BILLING_TEST_MONGODB_URI;
const owner = "507f1f77bcf86cd799439011";
const future = () => new Date(Date.now() + 86400000);
async function rejects(promise, message) {
try { await promise; throw new Error("Expected rejection"); } catch (error) { expect(error.message).to.equal(message); }
}
(uri ? describe : describe.skip)("billing with MongoDB", function () {
this.timeout(15000);
const original = { ...config };
before(async () => {
if (!/^billing_test_/.test(new URL(uri).pathname.slice(1))) throw new Error("Use a disposable billing_test_ database");
await mongoose.connect(uri);
await Promise.all([BillingPrice, BillingAccount, BillingGrant, BillingQuota, BillingLock, BillingEvent, BillingRevocation, BillingDraft, Repo, Conference].map(m => m.init()));
});
beforeEach(async () => {
Object.assign(config, { BILLING_ENABLED: true, FREE_AUTHOR_REPOSITORIES: 3, FREE_AUTHOR_STORAGE: 100,
PAID_AUTHOR_STORAGE: 1000, PAID_AUTHOR_REPOSITORIES: 20, STRIPE_AUTHOR_MONTHLY_PRICE: "price_monthly", STRIPE_AUTHOR_PASS_PRICE: "price_pass", STRIPE_CONFERENCE_PRICE: "price_conf", STRIPE_CONFERENCE_SMALL_PRICE: "price_conf_small", STRIPE_CONFERENCE_LARGE_PRICE: "price_conf_large", PAID_CONFERENCE_SMALL_REPOSITORIES: 25, STRIPE_REPO_EXTEND_PRICE: "", STRIPE_REPO_NEVER_PRICE: "", STRIPE_REPO_SLOT_PRICE: "", STRIPE_REPO_PACK_PRICE: "" });
Object.assign(config, { STRIPE_AUTHOR_ANNUAL_PRICE: "price_annual", STRIPE_LEGACY_PRICES: "" });
await Promise.all([BillingPrice, BillingAccount, BillingGrant, BillingQuota, BillingLock, BillingEvent, BillingRevocation, BillingDraft, Repo, Conference].map(m => m.deleteMany({})));
await BillingAccount.create({ _id: owner, customerId: "cus_owner" });
});
after(async () => { Object.assign(config, original); await mongoose.disconnect(); });
it("indexes terminal quota lookups by repository ID rather than scanning removed repositories", async () => {
await Repo.insertMany(Array.from({ length: 1000 }, (_, i) => ({ repoId: `removed-${i}`, owner, status: "removed" })));
await BillingQuota.create({ _id: `user:${owner}`, allocations: [{ repoId: "removed-999", reservationId: "old", bytes: 90 }] });
const find = Repo.find; let terminalQuery;
Repo.find = function (...args) {
const query = find.apply(this, args);
if (args[0]?.repoId?.$in) terminalQuery = query;
return query;
};
try {
await reserveRepository(owner, "new", 100);
expect((await BillingQuota.findById(`user:${owner}`)).allocations.map(a => a.repoId)).to.deep.equal(["new"]);
const plan = await terminalQuery.clone().explain("executionStats");
expect(plan.executionStats.totalDocsExamined).to.equal(1);
expect(plan.executionStats.totalKeysExamined).to.equal(1);
} finally { Repo.find = find; }
});
it("atomically admits only three concurrent repositories", async () => {
const results = await Promise.allSettled(Array.from({ length: 12 }, (_, i) => reserveRepository(owner, `repo-${i}`, 10)));
expect(results.filter(r => r.status === "fulfilled")).to.have.length(3);
expect((await BillingQuota.findById(`user:${owner}`)).allocations).to.have.length(3);
});
it("atomically caps storage and never shrinks a concurrent reservation", async () => {
const results = await Promise.allSettled([reserveRepository(owner, "a", 60), reserveRepository(owner, "b", 60)]);
expect(results.filter(r => r.status === "fulfilled")).to.have.length(1);
const ledger = await BillingQuota.findById(`user:${owner}`);
const id = ledger.allocations[0].repoId;
await Promise.all([reserveRepository(owner, id, 90), reserveRepository(owner, id, 1)]);
expect((await BillingQuota.findById(`user:${owner}`)).allocations[0].bytes).to.equal(90);
});
it("does not seed personal capacity with conference-funded or pass-funded repositories", async () => {
await BillingGrant.create([
{ _id: "conf-grant", owner, scope: "conference:funded", status: "paid", until: future(), planId: "conference" },
{ _id: "pass-grant", owner, scope: "repo:pass", status: "paid", until: future(), planId: "author_pass" },
]);
await Repo.create([...["a", "b", "c"].map(repoId => ({ repoId, owner, conference: "funded", status: "ready", size: { storage: 80 } })),
{ repoId: "pass", owner, status: "ready", size: { storage: 80 } }]);
await reserveRepository(owner, "personal", 10);
const ledger = await BillingQuota.findById(`user:${owner}`);
expect(ledger.allocations.map(a => a.repoId)).to.deep.equal(["personal"]);
});
it("accounts for concurrent resolved-file growth without double charging retries", async () => {
const { reserveFileExpansion } = require("../src/core/billing/quotas");
await Repo.create({ repoId: "lfs", owner, billingManaged: true, status: "ready", size: { storage: 10 } });
await reserveRepository(owner, "lfs", 10);
await Promise.all(Array.from({ length: 8 }, () => reserveFileExpansion("lfs", "file-a", 40)));
expect((await BillingQuota.findById(`user:${owner}`)).allocations[0].bytes).to.equal(50);
await reserveFileExpansion("lfs", "file-b", 40);
await rejects(reserveFileExpansion("lfs", "file-c", 20), "quota_exceeded");
await reserveRepository(owner, "lfs", 15);
expect((await BillingQuota.findById(`user:${owner}`)).allocations[0].bytes).to.equal(95);
await reserveFileExpansion("lfs", "file-a", 1);
expect((await BillingQuota.findById(`user:${owner}`)).allocations[0].bytes).to.equal(95);
});
it("shows only personally funded repositories before a user ledger exists", async () => {
const utils = require("../src/server/routes/route-utils");
const originalUser = utils.getUser;
await BillingGrant.create({ _id: "conf-usage", owner, scope: "conference:funded", status: "paid", until: future(), planId: "conference" });
utils.getUser = async () => ({ id: owner, quotaRepositories: async () => [
{ repoId: "conference-work", conference: "funded", size: { storage: 90, file: 1 } },
{ repoId: "personal-work", size: { storage: 10, file: 1 } },
] });
try {
const router = require("../src/server/routes/user").default;
const handler = router.stack.find(layer => layer.route?.path === "/quota").route.stack[0].handle;
let body;
await handler({}, { json: value => { body = value; } });
expect(body.repository.used).to.equal(1);
expect(body.storage.used).to.equal(10);
expect(body.file.used).to.equal(1);
} finally { utils.getUser = originalUser; }
});
for (const replacement of [true, false]) {
it(`retains a reservation during retirement and ${replacement ? "replacement" : "releases it after permanent removal"}`, async () => {
const Repository = require("../src/core/Repository").default;
const db = require("../src/server/database");
const wasConnected = db.isConnected;
config.FREE_AUTHOR_REPOSITORIES = 1;
const model = await Repo.create({ repoId: "published", owner, billingManaged: true, status: "ready", size: { storage: 10 } });
await reserveRepository(owner, "published", 20);
const repo = new Repository(model);
repo.resetSate = async () => {
expect((await Repo.findOne({ repoId: "published" })).status).to.equal("removing");
await rejects(reserveRepository(owner, "competitor", 1), "quota_exceeded");
};
db.isConnected = true;
try { await repo.remove({ replacement }); }
finally { db.isConnected = wasConnected; }
expect((await Repo.findOne({ repoId: "published" })).status).to.equal(replacement ? "preparing" : "removed");
if (replacement) {
await rejects(reserveRepository(owner, "competitor", 1), "quota_exceeded");
expect((await BillingQuota.findById(`user:${owner}`)).allocations[0].bytes).to.equal(20);
} else await reserveRepository(owner, "competitor", 1);
});
}
for (const fails of [false, true]) {
it(`claims removed restoration before admission and ${fails ? "recovers a failed admission" : "protects it from a competitor"}`, async () => {
const Repository = require("../src/core/Repository").default;
const utils = require("../src/server/routes/route-utils");
const quotas = require("../src/core/billing/quotas");
const queue = require("../src/queue");
const saved = { getRepo: utils.getRepo, getUser: utils.getUser, handleError: utils.handleError, reserve: quotas.reserveRepository, queue: queue.downloadQueue };
config.FREE_AUTHOR_REPOSITORIES = 1;
const model = await Repo.create({ repoId: "restore", owner, billingManaged: true, status: "removed", statusDate: new Date(0),
source: { type: "GitHubStream", repositoryName: "owner/repo", commit: "abc123", branch: "main" }, size: { storage: 10 } });
utils.getRepo = async () => new Repository(model);
utils.getUser = async () => ({ isAdmin: true });
utils.handleError = error => { throw error; };
let queued = false;
queue.downloadQueue = { add: async () => { queued = true; } };
quotas.reserveRepository = async (...args) => {
expect((await Repo.findById(model._id)).status).to.equal("preparing");
if (fails) throw Error("quota_exceeded");
await saved.reserve(...args);
await rejects(saved.reserve(owner, "competitor", 1), "quota_exceeded");
};
try {
const router = require("../src/server/routes/repository-private").default;
const handler = router.stack.find(layer => layer.route?.path === "/:repoId/" && layer.route.methods.post).route.stack[0].handle;
const work = handler({ body: { repoId: "restore", fullName: "owner/repo", source: { commit: "abc123", branch: "main" }, terms: [],
options: { expirationMode: "remove", expirationDate: future() } } }, { json() {} });
if (fails) await rejects(work, "quota_exceeded"); else await work;
expect(queued).to.equal(!fails);
expect((await Repo.findById(model._id)).status).to.equal(fails ? "removed" : "preparing");
} finally {
utils.getRepo = saved.getRepo; utils.getUser = saved.getUser; utils.handleError = saved.handleError;
quotas.reserveRepository = saved.reserve; queue.downloadQueue = saved.queue;
}
});
}
it("does not let stale removal cleanup delete a fresh reservation", async () => {
config.FREE_AUTHOR_REPOSITORIES = 1;
await Repo.create({ repoId: "restoring", owner, status: "ready", size: { storage: 10 } });
await reserveRepository(owner, "restoring", 10);
await Repo.updateOne({ repoId: "restoring" }, { $set: { status: "removed" } });
const update = BillingQuota.updateOne;
let interleaved = false;
BillingQuota.updateOne = async function (...args) {
if (args[1].$pull && !interleaved) {
interleaved = true;
await Repo.updateOne({ repoId: "restoring" }, { $set: { status: "preparing" } });
await reserveRepository(owner, "restoring", 20);
}
return update.apply(this, args);
};
try { await rejects(reserveRepository(owner, "competitor", 1), "quota_exceeded"); }
finally { BillingQuota.updateOne = update; }
expect(interleaved).to.equal(true);
expect((await BillingQuota.findById(`user:${owner}`)).allocations[0].bytes).to.equal(20);
});
it("counts legacy repositories and releases removed repositories", async () => {
await Repo.create(["one", "two", "three"].map(repoId => ({ repoId, owner, status: "ready", size: { storage: 10 } })));
await rejects(reserveRepository(owner, "four", 1), "quota_exceeded");
await Repo.updateOne({ repoId: "one" }, { $set: { status: "removed" } });
await reserveRepository(owner, "four", 1);
expect((await BillingQuota.findById(`user:${owner}`)).allocations.map(a => a.repoId)).not.to.include("one");
});
it("charges shared repositories only to their owner, regardless of coauthor subscriptions", async () => {
const collaborator = "507f1f77bcf86cd799439012";
await Repo.create(["one", "two", "three"].map(repoId => ({
repoId, owner, status: "ready", size: { storage: 10 },
coauthors: [{ username: "collaborator", githubId: "42" }],
})));
// A collaborator can create their own repository even when the owner is full.
await reserveRepository(collaborator, "own-work", 10);
expect((await BillingQuota.findById(`user:${collaborator}`)).allocations.map(a => a.repoId)).to.deep.equal(["own-work"]);
await BillingGrant.create({ _id: "coauthor-sub", owner: collaborator,
scope: `user:${collaborator}`, status: "active", until: future(), planId: "author_monthly" });
expect((await allowance(owner, "one")).plan).to.equal("free");
await rejects(reserveRepository(owner, "four", 1), "quota_exceeded");
// Editing shared work at the owner's count cap is allowed if its bytes fit.
await reserveRepository(owner, "one", 20);
await rejects(reserveRepository(owner, "one", 90), "quota_exceeded");
});
it("uses a conference allowance before an author's subscription", async () => {
await BillingGrant.create([{ _id: "sub", owner, scope: `user:${owner}`, status: "active", until: future(), planId: "author_monthly" },
{ _id: "conf", owner, scope: "conference:CONF", status: "paid", until: future(), planId: "conference" }]);
expect((await allowance(owner, "repo", "CONF")).scope).to.equal("conference:CONF");
await BillingGrant.updateOne({ _id: "conf" }, { $set: { revoked: true } });
expect((await allowance(owner, "repo", "CONF")).scope).to.equal(`user:${owner}`);
});
it("requires the owner's recurring subscription for never expiration", async () => {
const { neverExpirationDeadline, NEVER_EXPIRATION_GRACE_MS } = require("../src/core/billing/expiration");
await rejects(neverExpirationDeadline(owner), "subscription_required_for_never_expiration");
await BillingGrant.create({ _id: "pass-only", owner, scope: "repo:repo", status: "paid", until: future(), planId: "author_pass" });
await rejects(neverExpirationDeadline(owner), "subscription_required_for_never_expiration");
const until = future();
await BillingGrant.create({ _id: "author", owner, scope: `user:${owner}`, status: "active", until, planId: "author_annual" });
expect((await neverExpirationDeadline(owner)).getTime()).to.equal(until.getTime() + NEVER_EXPIRATION_GRACE_MS);
});
it("renews never expiration, honors 90 days of grace and leaves legacy links alone", async () => {
const { reconcileNeverExpiration, NEVER_EXPIRATION_GRACE_MS } = require("../src/core/billing/expiration");
const until = future();
const oldDeadline = new Date(Date.now() + 1000);
await Repo.create([{ repoId: "paid-never", owner, status: "ready", options: { expirationMode: "never" }, billingNeverExpiresUntil: oldDeadline },
{ repoId: "legacy-never", owner, status: "ready", options: { expirationMode: "never" } }]);
await BillingGrant.create({ _id: "author", owner, scope: `user:${owner}`, status: "active", until, planId: "author_monthly" });
await reconcileNeverExpiration();
const deadline = new Date(until.getTime() + NEVER_EXPIRATION_GRACE_MS);
expect((await Repo.findOne({ repoId: "paid-never" })).billingNeverExpiresUntil.getTime()).to.equal(deadline.getTime());
await reconcileNeverExpiration(new Date(deadline.getTime() - 1));
expect((await Repo.findOne({ repoId: "paid-never" })).options.expirationMode).to.equal("never");
await reconcileNeverExpiration(deadline, async () => {});
const expired = await Repo.findOne({ repoId: "paid-never" });
expect(expired.options.expirationMode).to.equal("remove");
expect(expired.options.expirationDate.getTime()).to.equal(deadline.getTime());
expect((await Repo.findOne({ repoId: "legacy-never" })).options.expirationMode).to.equal("never");
});
function session(status = "paid") {
return { id: "cs_pass", metadata: { application: "anonymous-github", owner, planId: "author_pass", target: "repo" },
customer: "cus_owner", status: "complete", payment_status: status, created: Math.floor(Date.now() / 1000),
line_items: { data: [{ price: { id: "price_pass" }, quantity: 1 }] }, payment_intent: { id: "pi_pass", latest_charge: { amount_refunded: 0, disputed: false } } };
}
function addonSession(id, planId, created = Math.floor(Date.now() / 1000)) {
return { id, metadata: { application: "anonymous-github", owner, planId, target: "repo" },
customer: "cus_owner", status: "complete", payment_status: "paid", created,
line_items: { data: [{ price: { id: `price_${planId}` }, quantity: 1 }] }, payment_intent: { id: `pi_${id}`, latest_charge: { amount_refunded: 0, disputed: false } } };
}
function sessionsStripe(sessions) {
return { checkout: { sessions: { retrieve: async id => sessions.find(s => s.id === id) } } };
}
it("stacks extended expiration purchases and records never expire once", async () => {
Object.assign(config, { STRIPE_REPO_EXTEND_PRICE: "price_repo_extend", STRIPE_REPO_NEVER_PRICE: "price_repo_never" });
const year = 365 * 86400000;
const first = addonSession("cs_extend_1", "repo_extend");
const second = addonSession("cs_extend_2", "repo_extend", first.created + 60);
const never = addonSession("cs_never", "repo_never");
const stripe = sessionsStripe([first, second, never]);
await fulfillCheckout(first.id, stripe);
await fulfillCheckout(second.id, stripe);
// A duplicate delivery must not add another year.
await fulfillCheckout(second.id, stripe);
expect((await BillingGrant.findById(first.id)).until.getTime()).to.equal(first.created * 1000 + year);
expect((await BillingGrant.findById(second.id)).until.getTime()).to.equal(first.created * 1000 + 2 * year);
await fulfillCheckout(never.id, stripe);
expect((await BillingGrant.findById(never.id)).until.toISOString()).to.equal("9999-12-31T00:00:00.000Z");
});
it("hosts an extra repository outside the free allowance for its buyer only", async () => {
config.STRIPE_REPO_SLOT_PRICE = "price_repo_slot";
await Promise.all(["a", "b", "repo"].map(id => reserveRepository(owner, id, 10)));
await rejects(reserveRepository(owner, "four", 10), "quota_exceeded");
// Buying a slot for a counted repository frees its place in the free ledger.
const slot = addonSession("cs_slot", "repo_slot");
await fulfillCheckout(slot.id, sessionsStripe([slot]));
expect((await BillingGrant.findById(slot.id)).until.toISOString()).to.equal("9999-12-31T00:00:00.000Z");
expect((await BillingQuota.findById(`user:${owner}`)).allocations.map(a => a.repoId)).to.not.include("repo");
expect((await allowance(owner, "repo")).plan).to.equal("repo_slot");
await reserveRepository(owner, "repo", 10);
await reserveRepository(owner, "four", 10);
// Another account cannot use a slot bought for the same repository ID.
expect((await allowance("507f1f77bcf86cd799439099", "repo")).plan).to.equal("free");
config.STRIPE_REPO_SLOT_PRICE = "";
});
it("fulfills a paper pack as three add-ons that a refund revokes together", async () => {
config.STRIPE_REPO_PACK_PRICE = "price_repo_pack";
const pack = addonSession("cs_pack", "repo_pack");
const stripe = sessionsStripe([pack]);
await fulfillCheckout(pack.id, stripe);
await fulfillCheckout(pack.id, stripe);
const grants = await BillingGrant.find({ paymentIntentId: "pi_cs_pack" }).lean();
expect(grants.map(g => g.planId).sort()).to.deep.equal(["author_pass", "repo_extend", "repo_slot"]);
expect(grants.find(g => g.planId === "repo_extend").until.getTime()).to.equal(pack.created * 1000 + 365 * 86400000);
// Live updates win over the manual-refresh slot while both fund the repository.
expect((await allowance(owner, "repo")).plan).to.equal("author_pass");
await processBillingEvent({ type: "charge.refunded", data: { object: { payment_intent: "pi_cs_pack", refunded: true } } },
{ ...stripe, invoicePayments: { list: async () => ({ data: [] }) } });
expect(await BillingGrant.countDocuments({ paymentIntentId: "pi_cs_pack", revoked: true })).to.equal(3);
expect((await allowance(owner, "repo")).plan).to.equal("free");
config.STRIPE_REPO_PACK_PRICE = "";
});
it("does not let expiration add-ons replace the storage allowance", async () => {
await BillingGrant.create([{ _id: "extend", owner, scope: "repo:repo", status: "paid", until: future(), planId: "repo_extend" },
{ _id: "never", owner, scope: "repo:repo", status: "paid", until: future(), planId: "repo_never" }]);
expect((await allowance(owner, "repo")).plan).to.equal("free");
await BillingGrant.create({ _id: "pass", owner, scope: "repo:repo", status: "paid", until: future(), planId: "author_pass" });
expect((await allowance(owner, "repo")).plan).to.equal("author_pass");
});
it("limits dated expiration by free window, add-on, subscription and conference", async () => {
const { maximumExpiration } = require("../src/core/billing/expiration");
const now = new Date("2026-01-10T12:00:00Z");
const months = (date, n) => { const d = new Date(date); d.setMonth(d.getMonth() + n); d.setHours(23, 59, 59, 999); return d.getTime(); };
expect((await maximumExpiration(owner, "repo", undefined, now)).getTime()).to.equal(months(now, 5));
const extended = new Date("2027-06-01T00:00:00Z");
await BillingGrant.create({ _id: "extend", owner, scope: "repo:repo", status: "paid", until: extended, planId: "repo_extend" });
const withAddon = await maximumExpiration(owner, "repo", undefined, now);
expect(withAddon.getTime()).to.be.greaterThan(extended.getTime() - 86400000);
expect((await maximumExpiration(owner, "other", undefined, now)).getTime()).to.equal(months(now, 5));
await BillingGrant.updateOne({ _id: "extend" }, { $set: { revoked: true } });
expect((await maximumExpiration(owner, "repo", undefined, now)).getTime()).to.equal(months(now, 5));
await Conference.create({ conferenceID: "LONG", owners: [owner], status: "ready", startDate: now, endDate: new Date("2026-09-01T00:00:00Z"), plan: { planID: "conference" } });
expect((await maximumExpiration(owner, "repo", "LONG", now)).toISOString()).to.equal("2026-09-01T00:00:00.000Z");
await BillingGrant.create({ _id: "sub", owner, scope: `user:${owner}`, status: "active", until: new Date("2026-02-10T00:00:00Z"), planId: "author_monthly" });
expect((await maximumExpiration(owner, "repo", undefined, now)).getTime()).to.equal(months(now, 12));
});
it("lets a Never expire add-on keep one repository online until it is revoked", async () => {
const { neverExpirationDeadline, reconcileNeverExpiration, NEVER_EXPIRATION_GRACE_MS } = require("../src/core/billing/expiration");
await rejects(neverExpirationDeadline(owner, undefined, "repo"), "subscription_required_for_never_expiration");
await BillingGrant.create({ _id: "never", owner, scope: "repo:repo", status: "paid", until: new Date("9999-12-31T00:00:00Z"), planId: "repo_never" });
const before = Date.now();
const deadline = await neverExpirationDeadline(owner, undefined, "repo");
expect(deadline.getTime()).to.be.at.least(before + NEVER_EXPIRATION_GRACE_MS);
await rejects(neverExpirationDeadline(owner, undefined, "other"), "subscription_required_for_never_expiration");
await Repo.create({ repoId: "repo", owner, status: "ready", options: { expirationMode: "never" }, billingNeverExpiresUntil: new Date(Date.now() + 86400000) });
// A valid add-on rolls a close deadline forward to the full grace period.
await reconcileNeverExpiration();
const rolled = (await Repo.findOne({ repoId: "repo" })).billingNeverExpiresUntil.getTime();
expect(rolled).to.be.at.least(before + NEVER_EXPIRATION_GRACE_MS);
// A recent deadline is not rewritten on every run.
await reconcileNeverExpiration(new Date(Date.now() + 86400000));
expect((await Repo.findOne({ repoId: "repo" })).billingNeverExpiresUntil.getTime()).to.equal(rolled);
// After a refund the deadline stops moving and the repository expires.
await BillingGrant.updateOne({ _id: "never" }, { $set: { revoked: true } });
await reconcileNeverExpiration(new Date(rolled), async () => {});
expect((await Repo.findOne({ repoId: "repo" })).options.expirationMode).to.equal("remove");
});
it("ignores an unfinished recurring Checkout before Stripe assigns a subscription", async () => {
const stripe = { checkout: { sessions: { retrieve: async () => ({
id: "cs_open", metadata: { application: "anonymous-github", owner, planId: "author_monthly" },
customer: "cus_owner", status: "open", payment_status: "unpaid", subscription: null,
}) } } };
await fulfillCheckout("cs_open", stripe);
expect(await BillingGrant.countDocuments()).to.equal(0);
});
it("ignores unpaid checkout and fulfills duplicate deliveries exactly once", async () => {
let value = session("unpaid");
const stripe = { checkout: { sessions: { retrieve: async () => value } } };
await fulfillCheckout("cs_pass", stripe);
expect(await BillingGrant.countDocuments()).to.equal(0);
value = session();
await Promise.all([fulfillCheckout("cs_pass", stripe), fulfillCheckout("cs_pass", stripe)]);
const grant = await BillingGrant.findById("cs_pass");
expect(await BillingGrant.countDocuments()).to.equal(1);
expect(grant.scope).to.equal("repo:repo");
expect(grant.until.getTime()).to.equal(value.created * 1000 + 180 * 86400000);
});
it("does not reactivate a refunded pass on a checkout replay", async () => {
const value = session();
const stripe = { checkout: { sessions: { retrieve: async () => value } } };
await fulfillCheckout("cs_pass", stripe);
await processBillingEvent({ type: "charge.refunded", data: { object: { payment_intent: "pi_pass" } } }, { ...stripe, invoicePayments: { list: async () => ({ data: [] }) } });
await fulfillCheckout("cs_pass", stripe);
expect((await BillingGrant.findById("cs_pass")).revoked).to.equal(true);
});
it("reconciles retired one-time prices and continues refund recovery", async () => {
const { reconcileBillingAccount } = require("../src/core/billing/reconciliation");
const iterable = values => ({ async *[Symbol.asyncIterator]() { yield* values; } });
const value = session();
const stripe = { checkout: { sessions: { retrieve: async () => value, list: () => iterable([value]) } },
subscriptions: { list: () => iterable([]) }, charges: { list: () => iterable([{ payment_intent: "pi_pass", refunded: true }]) },
invoicePayments: { list: async () => ({ data: [] }) } };
await fulfillCheckout(value.id, stripe);
config.STRIPE_AUTHOR_PASS_PRICE = "";
await BillingGrant.deleteMany({});
await reconcileBillingAccount(owner, stripe);
expect((await BillingGrant.findById(value.id)).revoked).to.equal(true);
expect((await BillingAccount.findById(owner)).reconciledAt).to.be.instanceOf(Date);
value.line_items.data[0].price.id = "price_unrecognized";
await rejects(fulfillCheckout(value.id, stripe), "billing_invalid_purchase");
});
it("preserves previously accepted grants without historical price fields", async () => {
const value = session();
await BillingGrant.create({ _id: value.id, owner, customerId: "cus_owner", scope: "repo:repo", planId: "author_pass", status: "paid" });
config.STRIPE_AUTHOR_PASS_PRICE = "";
await fulfillCheckout(value.id, { checkout: { sessions: { retrieve: async () => value } } });
expect((await BillingPrice.findById("price_pass")).planId).to.equal("author_pass");
});
it("rejects a checkout for a different Stripe customer", async () => {
const value = session(); value.customer = "cus_other";
await rejects(fulfillCheckout("cs_pass", { checkout: { sessions: { retrieve: async () => value } } }), "billing_customer_mismatch");
});
function subscription(overrides = {}) {
return { id: "sub_owner", metadata: { application: "anonymous-github", owner }, customer: "cus_owner", status: "active",
latest_invoice: { status: "paid" }, items: { data: [{ price: { id: "price_monthly" }, current_period_end: Math.floor(future().getTime() / 1000) }] }, cancel_at_period_end: false, ...overrides };
}
it("snapshots the original paid invoice price once across plan changes", async () => {
let value = subscription({ start_date: 1700000000,
latest_invoice: { id: "in_initial", status: "paid", billing_reason: "subscription_create",
lines: { data: [{ parent: { subscription_item_details: { subscription_item: "si_original", proration: false } },
pricing: { price_details: { price: "price_monthly" } } }] } },
items: { data: [{ id: "si_original", price: { id: "price_monthly" }, current_period_end: Math.floor(future().getTime() / 1000) }] },
});
const stripe = { subscriptions: { retrieve: async () => value } };
await syncSubscription("sub_owner", stripe);
expect(await BillingGrant.findById("sub_owner").lean()).to.include({ salePriceId: "price_monthly", salePlanId: "author_monthly" });
value = subscription({ latest_invoice: { status: "paid", billing_reason: "subscription_cycle" },
items: { data: [{ id: "si_original", price: { id: "price_annual" }, current_period_end: Math.floor(future().getTime() / 1000) }] } });
await syncSubscription("sub_owner", stripe);
expect(await BillingGrant.findById("sub_owner").lean()).to.include({ priceId: "price_annual", planId: "author_annual", salePriceId: "price_monthly", salePlanId: "author_monthly" });
});
it("recognizes portal cancellations scheduled with cancel_at", async () => {
await syncSubscription("sub_owner", { subscriptions: { retrieve: async () => subscription({
cancel_at_period_end: false, cancel_at: Math.floor(future().getTime() / 1000),
}) } });
expect((await BillingGrant.findById("sub_owner")).cancelAtPeriodEnd).to.equal(true);
expect((await allowance(owner)).plan).to.equal("author_monthly");
});
it("honors paid subscription periods, cancellation and renewal failure", async () => {
let value = subscription();
const stripe = { subscriptions: { retrieve: async () => value } };
await syncSubscription("sub_owner", stripe);
expect((await allowance(owner)).plan).to.equal("author_monthly");
const paidThrough = (await BillingGrant.findById("sub_owner")).until;
value = subscription({ status: "past_due", latest_invoice: { status: "open" } });
value.items.data[0].current_period_end += 86400 * 30;
await syncSubscription("sub_owner", stripe);
expect((await BillingGrant.findById("sub_owner")).until).to.deep.equal(paidThrough);
value = subscription({ cancel_at_period_end: true });
await syncSubscription("sub_owner", stripe);
expect((await allowance(owner)).plan).to.equal("author_monthly");
value = subscription({ status: "canceled" });
await syncSubscription("sub_owner", stripe);
expect((await allowance(owner)).plan).to.equal("free");
});
it("protects a newer subscription update from an older in-flight fetch", async () => {
await BillingGrant.create({ _id: "sub_owner", owner, scope: `user:${owner}`, planId: "author_monthly", status: "active", until: future() });
let release; const gate = new Promise(resolve => { release = resolve; }); let started;
const firstRead = new Promise(resolve => { started = resolve; }); let calls = 0;
const stripe = { subscriptions: { retrieve: async () => {
if (++calls === 1) { started(); await gate; return subscription(); }
return subscription({ status: "canceled" });
} } };
const old = syncSubscription("sub_owner", stripe);
await firstRead;
await syncSubscription("sub_owner", stripe);
release(); await old;
expect((await BillingGrant.findById("sub_owner")).status).to.equal("canceled");
});
it("keeps conference admission closed until verified payment", async () => {
await Conference.create({ conferenceID: "CONF", owners: [owner], status: "pending_payment", startDate: new Date(Date.now() - 1000), endDate: future(), plan: { planID: "conference" } });
await rejects(assertConferenceAdmission(owner, "repo", "CONF"), "conf_not_activated");
const value = session(); value.id = "cs_conf"; value.metadata.planId = "conference"; value.metadata.target = "CONF"; value.line_items.data[0].price.id = "price_conf";
await fulfillCheckout("cs_conf", { checkout: { sessions: { retrieve: async () => value } } });
expect((await Conference.findOne({ conferenceID: "CONF" })).status).to.equal("ready");
await assertConferenceAdmission(owner, "repo", "CONF");
await BillingGrant.updateOne({ _id: "cs_conf" }, { $set: { revoked: true } });
await rejects(assertConferenceAdmission(owner, "repo", "CONF"), "conference_payment_required");
});
it("activates a smaller conference package with its own capacity and only for its tier", async () => {
await Conference.create({ conferenceID: "WS", owners: [owner], status: "pending_payment", startDate: new Date(Date.now() - 1000), endDate: future(), plan: { planID: "conference_workshop" } });
await rejects(createCheckout(owner, "conference_large", "WS", {}), "billing_invalid_conference");
const value = session(); value.id = "cs_ws"; value.metadata.planId = "conference_workshop"; value.metadata.target = "WS"; value.line_items.data[0].price.id = "price_conf_small";
await fulfillCheckout("cs_ws", { checkout: { sessions: { retrieve: async () => value } } });
expect((await Conference.findOne({ conferenceID: "WS" })).status).to.equal("ready");
const limits = await allowance(owner, "repo", "WS");
expect(limits.scope).to.equal("conference:WS");
expect(limits.plan).to.equal("conference_workshop");
expect(limits.repositories).to.equal(25);
await assertConferenceAdmission(owner, "repo", "WS");
});
it("keeps legacy and new annual subscriptions after the annual price is replaced", async () => {
const { rememberConfiguredPrices } = require("../src/core/billing/stripe");
// Deployed before the switch: the old annual price is recorded at startup.
await rememberConfiguredPrices();
const annual = (id, price) => subscription({ id, items: { data: [{ price: { id: price }, current_period_end: Math.floor(future().getTime() / 1000) }] } });
let legacy = annual("sub_owner", "price_annual");
const stripe = { subscriptions: { retrieve: async id => id === "sub_owner" ? legacy : annual(id, "price_annual_new") } };
config.STRIPE_AUTHOR_ANNUAL_PRICE = "price_annual_new";
await syncSubscription("sub_owner", stripe);
let grant = await BillingGrant.findById("sub_owner");
expect(grant.planId).to.equal("author_annual");
expect(grant.priceId).to.equal("price_annual");
expect((await allowance(owner)).plan).to.equal("author_annual");
// Webhook updates (renewal, cancellation) keep resolving the legacy price.
legacy = { ...legacy, cancel_at_period_end: true };
await processBillingEvent({ type: "customer.subscription.updated", data: { object: { id: "sub_owner" } } }, stripe);
grant = await BillingGrant.findById("sub_owner");
expect(grant.planId).to.equal("author_annual");
expect(grant.cancelAtPeriodEnd).to.equal(true);
// New subscriptions use the new price.
await processBillingEvent({ type: "customer.subscription.created", data: { object: { id: "sub_new" } } }, stripe);
expect((await BillingGrant.findById("sub_new")).planId).to.equal("author_annual");
});
it("recognizes a declared legacy price without a recorded one", async () => {
config.STRIPE_AUTHOR_ANNUAL_PRICE = "price_annual_new";
const stripe = { subscriptions: { retrieve: async () => subscription({ items: { data: [{ price: { id: "price_annual_old" }, current_period_end: Math.floor(future().getTime() / 1000) }] } }) } };
await syncSubscription("sub_owner", stripe);
expect((await BillingGrant.findById("sub_owner")).planId).to.equal("unknown");
config.STRIPE_LEGACY_PRICES = "price_annual_old:author_annual";
await syncSubscription("sub_owner", stripe);
expect((await BillingGrant.findById("sub_owner")).planId).to.equal("author_annual");
expect((await allowance(owner)).plan).to.equal("author_annual");
});
it("keeps historical one-time purchases after their price is replaced", async () => {
const value = session(); value.id = "cs_old_pass";
await fulfillCheckout("cs_old_pass", { checkout: { sessions: { retrieve: async () => value } } });
expect((await allowance(owner, "repo")).plan).to.equal("author_pass");
config.STRIPE_AUTHOR_PASS_PRICE = "price_pass_new";
// A redelivered webhook for the old sale still fulfills and keeps the grant.
await fulfillCheckout("cs_old_pass", { checkout: { sessions: { retrieve: async () => value } } });
expect((await BillingGrant.findById("cs_old_pass")).revoked).to.not.equal(true);
expect((await allowance(owner, "repo")).plan).to.equal("author_pass");
// A sale at an old price never recorded is accepted once it is declared.
const other = session(); other.id = "cs_older"; other.metadata.target = "other"; other.line_items.data[0].price.id = "price_pass_older";
await rejects(fulfillCheckout("cs_older", { checkout: { sessions: { retrieve: async () => other } } }), "billing_invalid_purchase");
config.STRIPE_LEGACY_PRICES = "price_pass_older:author_pass";
await fulfillCheckout("cs_older", { checkout: { sessions: { retrieve: async () => other } } });
expect((await allowance(owner, "other")).plan).to.equal("author_pass");
});
it("keeps a Never expire link online when reconciliation recovers its purchase", async () => {
const { reconcileNeverExpiration, NEVER_EXPIRATION_GRACE_MS } = require("../src/core/billing/expiration");
const now = new Date();
await Repo.create({ repoId: "recovered", owner, status: "ready", options: { expirationMode: "never" }, billingNeverExpiresUntil: new Date(now.getTime() - 1000) });
await reconcileNeverExpiration(now, async () => {
await BillingGrant.create({ _id: "never-recovered", owner, scope: "repo:recovered", status: "paid", until: new Date("9999-12-31T00:00:00Z"), planId: "repo_never" });
});
const repo = await Repo.findOne({ repoId: "recovered" });
expect(repo.options.expirationMode).to.equal("never");
expect(repo.billingNeverExpiresUntil.getTime()).to.equal(now.getTime() + NEVER_EXPIRATION_GRACE_MS);
});
it("keeps automatic updates for a paid-slot repository while its owner subscribes", async () => {
const { hasAutomaticUpdates } = require("../src/core/billing/expiration");
await BillingGrant.create({ _id: "slot", owner, scope: "repo:slotted", status: "paid", until: new Date("9999-12-31T00:00:00Z"), planId: "repo_slot" });
expect(await hasAutomaticUpdates(owner, "slotted")).to.equal(false);
await BillingGrant.create({ _id: "author", owner, scope: `user:${owner}`, status: "active", until: future(), planId: "author_monthly" });
expect(await hasAutomaticUpdates(owner, "slotted")).to.equal(true);
});
it("only reports conference hosting while its package payment stands", async () => {
const utils = require("../src/server/routes/route-utils");
const router = require("../src/server/routes/conference").default;
const get = router.stack.find(layer => layer.route?.path === "/:conferenceID" && layer.route.methods.get).route.stack[0].handle;
const originalUser = utils.getUser;
utils.getUser = async () => ({ model: { id: "someone-else" }, isAdmin: false });
try {
await Conference.create({ conferenceID: "PAID", owners: [owner], status: "ready", startDate: new Date(Date.now() - 1000), endDate: future(), plan: { planID: "conference" } });
await BillingGrant.create({ _id: "conf-paid", owner, scope: "conference:PAID", status: "paid", until: future(), planId: "conference" });
const hosting = async () => { let body; await get({ params: { conferenceID: "PAID" } }, { json: value => { body = value; } }); return body.hosting; };
expect(await hosting()).to.equal(true);
await BillingGrant.updateOne({ _id: "conf-paid" }, { $set: { revoked: true } });
expect(await hosting()).to.equal(false);
} finally { utils.getUser = originalUser; }
});
it("prevents two simultaneous checkout sessions and reuses an open session", async () => {
let creates = 0;
const stripe = { prices: { retrieve: async () => ({ active: true, currency: "eur", unit_amount: 500, recurring: { interval: "month", interval_count: 1 } }) }, subscriptions: { list: async () => ({ data: [] }) }, checkout: { sessions: {
create: async () => { creates++; await new Promise(r => setTimeout(r, 30)); return { id: "cs_new", url: "https://checkout.stripe.com/test" }; },
retrieve: async () => ({ id: "cs_new", status: "open", url: "https://checkout.stripe.com/test" }),
} } };
const results = await Promise.allSettled([createCheckout(owner, "author_monthly", "", stripe), createCheckout(owner, "author_monthly", "", stripe)]);
expect(results.filter(r => r.status === "fulfilled")).to.have.length(1);
await createCheckout(owner, "author_monthly", "", stripe);
expect(creates).to.equal(1);
});
it("revokes only the refunded subscription, including refunds arriving before subscription sync", async () => {
await BillingGrant.create({ _id: "other-sub", owner, customerId: "cus_owner", subscriptionId: "other-sub", status: "active", until: future() });
const stripe = { invoicePayments: { list: async () => ({ data: [{ invoice: "in_refunded" }] }) },
invoices: { retrieve: async () => ({ parent: { subscription_details: { subscription: "sub_refunded" } } }) } };
await processBillingEvent({ type: "charge.refunded", data: { object: { customer: "cus_owner", payment_intent: "pi_refunded" } } }, stripe);
expect((await BillingGrant.findById("sub_refunded")).revoked).to.equal(true);
expect((await BillingGrant.findById("other-sub")).revoked).to.equal(false);
await syncSubscription("sub_refunded", { subscriptions: { retrieve: async () => subscription({ id: "sub_refunded" }) } });
expect((await BillingGrant.findById("sub_refunded")).revoked).to.equal(true);
});
it("keeps reviewer visits from refreshing snapshots after paid access expires", async () => {
const Repository = require("../src/core/Repository").default;
const model = new Repo({ repoId: "frozen", owner, billingManaged: true, status: "ready", lastView: new Date(0), options: { update: true, expirationMode: "never" }, source: { type: "GitHubStream", repositoryName: "owner/repo" } });
const repo = new Repository(model);
repo.getToken = () => { throw new Error("Unexpected upstream refresh"); };
await BillingGrant.create({ _id: "expired-sub", owner, scope: `user:${owner}`, status: "active", until: new Date(0), planId: "author_monthly" });
await repo.updateIfNeeded();
expect(repo.status).to.equal("ready");
});
it("authenticates billing, checks CSRF, and verifies raw webhook signatures", async () => {
const express = require("express");
const sessionMiddleware = require("express-session");
const Stripe = require("stripe");
const Users = require("../src/core/model/users/users.model").default;
const routes = require("../src/server/routes/billing");
await Users.updateOne({ _id: owner }, { $set: { username: "billing-test-author", status: "active" } }, { upsert: true });
Object.assign(config, { STRIPE_SECRET_KEY: "sk_test_example", STRIPE_WEBHOOK_SECRET: "whsec_test_example", STRIPE_AUTHOR_MONTHLY_PRICE: "", STRIPE_AUTHOR_ANNUAL_PRICE: "", STRIPE_AUTHOR_PASS_PRICE: "", STRIPE_CONFERENCE_PRICE: "", STRIPE_CONFERENCE_SMALL_PRICE: "", STRIPE_CONFERENCE_LARGE_PRICE: "" });
const app = express();
app.use("/webhook", routes.billingWebhook);
app.use(express.json());
app.use(sessionMiddleware({ secret: "billing-test-session-secret", resave: false, saveUninitialized: false }));
app.use((req, _res, next) => { req.isAuthenticated = () => req.headers["x-test-auth"] === "yes"; req.user = { user: { _id: owner } }; next(); });
app.use("/billing", routes.default);
const server = await new Promise(resolve => { const listening = app.listen(0, "127.0.0.1", () => resolve(listening)); });
const base = `http://127.0.0.1:${server.address().port}`;
try {
expect((await globalThis.fetch(`${base}/billing`)).status).to.equal(401);
const get = await globalThis.fetch(`${base}/billing`, { headers: { "x-test-auth": "yes" } });
const data = await get.json();
expect(data.csrf).to.be.a("string");
expect(JSON.stringify(data)).not.to.include("cus_owner");
const headers = { "x-test-auth": "yes", cookie: get.headers.get("set-cookie").split(";")[0], "content-type": "application/json" };
expect((await globalThis.fetch(`${base}/billing/checkout`, { method: "POST", headers, body: JSON.stringify({ planId: "arbitrary" }) })).status).to.equal(403);
expect((await globalThis.fetch(`${base}/billing/draft`, { method: "POST", headers, body: JSON.stringify({ path: "/anonymize", values: { terms: "Private Author" } }) })).status).to.equal(403);
headers["x-csrf-token"] = data.csrf;
const saved = await globalThis.fetch(`${base}/billing/draft`, { method: "POST", headers, body: JSON.stringify({ path: "/anonymize", values: { terms: "Private Author" } }) });
expect(saved.status).to.equal(200);
const draft = await saved.json();
expect((await globalThis.fetch(`${base}/billing/draft/${draft.id}`)).status).to.equal(401);
const restored = await globalThis.fetch(`${base}/billing/draft/${draft.id}`, { headers });
expect(restored.headers.get("cache-control")).to.equal("no-store");
expect((await restored.json()).values.terms).to.equal("Private Author");
await BillingDraft.updateOne({ _id: draft.id }, { owner: "another-owner" });
expect((await globalThis.fetch(`${base}/billing/draft/${draft.id}`, { headers })).status).to.equal(404);
const rejected = await globalThis.fetch(`${base}/billing/checkout`, { method: "POST", headers, body: JSON.stringify({ planId: "arbitrary" }) });
expect((await rejected.json()).error).to.equal("billing_plan_unavailable");
const payload = JSON.stringify({ id: "evt_test", type: "customer.created", data: { object: { id: "cus_unused" } } });
const stripe = new Stripe("sk_test_example");
const signature = stripe.webhooks.generateTestHeaderString({ payload, secret: config.STRIPE_WEBHOOK_SECRET });
expect((await globalThis.fetch(`${base}/webhook`, { method: "POST", headers: { "content-type": "application/json", "stripe-signature": signature }, body: payload })).status).to.equal(200);
expect((await globalThis.fetch(`${base}/webhook`, { method: "POST", headers: { "content-type": "application/json", "stripe-signature": signature }, body: payload + " " })).status).to.equal(400);
expect((await BillingEvent.findById("test:platform:evt_test")).state).to.equal("pending");
const originalUpdate = BillingEvent.updateOne;
BillingEvent.updateOne = async () => { throw new Error("simulated database outage"); };
try {
expect((await globalThis.fetch(`${base}/webhook`, { method: "POST", headers: { "content-type": "application/json", "stripe-signature": signature }, body: payload })).status).to.equal(500);
} finally { BillingEvent.updateOne = originalUpdate; }
} finally { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); await Users.deleteOne({ _id: owner }); }
});
});