Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 4 additions & 6 deletions packages/backend/convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import type * as billing_checkoutIntent from "../billing/checkoutIntent.js";
import type * as billing_polar from "../billing/polar.js";
import type * as billingModel from "../billingModel.js";
import type * as billingRetention from "../billingRetention.js";
import type * as billingWebhooks from "../billingWebhooks.js";
import type * as billing_webhook_model from "../billing_webhook_model.js";
import type * as billing_webhooks from "../billing_webhooks.js";
import type * as crons from "../crons.js";
import type * as deploymentPreflight from "../deploymentPreflight.js";
import type * as draftRestore from "../draftRestore.js";
Expand All @@ -36,7 +37,6 @@ import type * as libraries from "../libraries.js";
import type * as model_aiCredits from "../model/aiCredits.js";
import type * as model_aiWorkspaceBounds from "../model/aiWorkspaceBounds.js";
import type * as model_aiWorkspaceFingerprint from "../model/aiWorkspaceFingerprint.js";
import type * as model_billingEventOrdering from "../model/billingEventOrdering.js";
import type * as model_billingRetention from "../model/billingRetention.js";
import type * as model_contentObjects from "../model/contentObjects.js";
import type * as model_draft from "../model/draft.js";
Expand All @@ -47,7 +47,6 @@ import type * as model_libraryAccess from "../model/libraryAccess.js";
import type * as model_pageDeletion from "../model/pageDeletion.js";
import type * as model_pageDocuments from "../model/pageDocuments.js";
import type * as model_pageHierarchy from "../model/pageHierarchy.js";
import type * as model_polarOrderAmounts from "../model/polarOrderAmounts.js";
import type * as model_publishedRelease from "../model/publishedRelease.js";
import type * as model_releaseChangeDetails from "../model/releaseChangeDetails.js";
import type * as model_releaseChanges from "../model/releaseChanges.js";
Expand Down Expand Up @@ -104,7 +103,8 @@ declare const fullApi: ApiFromModules<{
"billing/polar": typeof billing_polar;
billingModel: typeof billingModel;
billingRetention: typeof billingRetention;
billingWebhooks: typeof billingWebhooks;
billing_webhook_model: typeof billing_webhook_model;
billing_webhooks: typeof billing_webhooks;
crons: typeof crons;
deploymentPreflight: typeof deploymentPreflight;
draftRestore: typeof draftRestore;
Expand All @@ -124,7 +124,6 @@ declare const fullApi: ApiFromModules<{
"model/aiCredits": typeof model_aiCredits;
"model/aiWorkspaceBounds": typeof model_aiWorkspaceBounds;
"model/aiWorkspaceFingerprint": typeof model_aiWorkspaceFingerprint;
"model/billingEventOrdering": typeof model_billingEventOrdering;
"model/billingRetention": typeof model_billingRetention;
"model/contentObjects": typeof model_contentObjects;
"model/draft": typeof model_draft;
Expand All @@ -135,7 +134,6 @@ declare const fullApi: ApiFromModules<{
"model/pageDeletion": typeof model_pageDeletion;
"model/pageDocuments": typeof model_pageDocuments;
"model/pageHierarchy": typeof model_pageHierarchy;
"model/polarOrderAmounts": typeof model_polarOrderAmounts;
"model/publishedRelease": typeof model_publishedRelease;
"model/releaseChangeDetails": typeof model_releaseChangeDetails;
"model/releaseChanges": typeof model_releaseChanges;
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/convex/_generated/server.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import type { DataModel } from "./dataModel.js";
* Typesafe environment variables declared in `convex.config.ts`.
*/
type Env = {
readonly BASEBLOCKS_BILLING_ENVIRONMENT: "sandbox" | "production";
readonly BASEBLOCKS_PAST_DUE_GRACE_DAYS: string | undefined;
readonly INTEGRATIONS_ENABLED: "true" | "false";
readonly POLAR_ACCESS_TOKEN: string;
readonly POLAR_ALLOW_PRODUCTION: "true" | "false";
readonly POLAR_WEBHOOK_SECRET: string;
};

/**
Expand Down
227 changes: 227 additions & 0 deletions packages/backend/convex/billing-webhook-model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { describe, expect, test } from "bun:test";
import { convexTest } from "convex-test";
import type { QueryCtx } from "./_generated/server";
import {
applyPolarBillingEvent,
type PolarBillingEventCommand,
} from "./billing_webhook_model";
import schema from "./schema";

const organizationId = "organization-alpha";
const modules = { "./_generated/api.ts": async () => ({}) };
const paidAt = Date.parse("2026-08-12T21:22:19.594Z");

function paidCreditOrder(
overrides: Partial<PolarBillingEventCommand> = {},
): PolarBillingEventCommand {
return {
providerEnvironment: "production",
deliveryId: "delivery-order-paid-1",
eventType: "order.paid",
eventOccurredAt: paidAt,
event: {
kind: "order",
organizationId,
providerOrderId: "polar-order-1",
providerCheckoutId: "polar-checkout-1",
providerCustomerId: "polar-customer-1",
providerProductId: "polar-credit-product",
state: "paid",
subtotalAmountMinor: 500n,
discountAmountMinor: 0n,
taxAmountMinor: 83n,
grossAmountMinor: 500n,
netAmountMinor: 417n,
refundedGrossAmountMinor: 0n,
currency: "usd",
billingReason: "purchase",
providerModifiedAt: paidAt,
},
...overrides,
};
}

async function seedWorkspace(
t: ReturnType<typeof convexTest>,
kind: "credits" | "plus" = "credits",
) {
await t.mutation(async (ctx) => {
const now = Date.now();
await ctx.db.insert("workspaceProfiles", {
organizationId,
intent: kind === "plus" ? "work" : "personal",
source: "onboarding",
schemaVersion: 1,
createdAt: now,
updatedAt: now,
});
await ctx.db.insert("billingCatalogItems", {
providerEnvironment: "production",
sku: kind === "plus" ? "plus_monthly" : "ai_credit_top_up",
kind: kind === "plus" ? "plus" : "aiCreditPack",
providerProductId:
kind === "plus" ? "polar-plus-product" : "polar-credit-product",
providerPriceId: kind === "plus" ? "polar-plus-price" : undefined,
planKey: kind === "plus" ? "plus" : undefined,
recurringInterval: kind === "plus" ? "month" : undefined,
priceAmountMinor: kind === "plus" ? 1_000n : 500n,
currency: "usd",
creditUnits: kind === "plus" ? 2_000_000n : undefined,
configurationVersion:
kind === "plus" ? "polar-plus-v1" : "polar-credit-v1",
active: true,
createdAt: now,
updatedAt: now,
});
});
}

async function readState(ctx: QueryCtx) {
const [account, orders, entitlement] = await Promise.all([
ctx.db
.query("aiCreditAccounts")
.withIndex("by_organization", (query) =>
query.eq("organizationId", organizationId),
)
.unique(),
ctx.db
.query("billingOrders")
.withIndex("by_organization_created", (query) =>
query.eq("organizationId", organizationId),
)
.collect(),
ctx.db
.query("workspaceEntitlements")
.withIndex("by_organization", (query) =>
query.eq("organizationId", organizationId),
)
.unique(),
]);
return { account, orders, entitlement };
}

describe("Polar billing event interface", () => {
test("grants a paid order exactly once across both forms of redelivery", async () => {
const t = convexTest(schema, modules);
await seedWorkspace(t);

const applied = await t.mutation(async (ctx) =>
applyPolarBillingEvent(ctx, paidCreditOrder()),
);
const repeatedDelivery = await t.mutation(async (ctx) =>
applyPolarBillingEvent(ctx, paidCreditOrder()),
);
await t.mutation(async (ctx) =>
applyPolarBillingEvent(
ctx,
paidCreditOrder({ deliveryId: "delivery-provider-redelivery" }),
),
);
const state = await t.query(readState);

expect(applied.outcome).toBe("applied");
expect(repeatedDelivery.outcome).toBe("duplicate");
expect(state.account?.availablePrepaidUnits).toBe(5_000_000n);
expect(state.orders).toHaveLength(1);
});

test("refunds monotonically and rejects an older paid state", async () => {
const t = convexTest(schema, modules);
await seedWorkspace(t);
await t.mutation(async (ctx) =>
applyPolarBillingEvent(ctx, paidCreditOrder()),
);
const refundedAt = Date.parse("2026-08-14T00:00:00Z");
const order = paidCreditOrder().event;
if (order.kind !== "order") throw new Error("Expected an order fixture");
await t.mutation(async (ctx) =>
applyPolarBillingEvent(
ctx,
paidCreditOrder({
deliveryId: "delivery-order-refunded",
eventType: "order.refunded",
eventOccurredAt: refundedAt,
event: {
...order,
state: "refunded",
refundedGrossAmountMinor: 500n,
providerModifiedAt: refundedAt,
},
}),
),
);
const stale = await t.mutation(async (ctx) =>
applyPolarBillingEvent(
ctx,
paidCreditOrder({ deliveryId: "delivery-stale-paid" }),
),
);
const state = await t.query(readState);

expect(stale.outcome).toBe("ignored");
expect(state.account?.availablePrepaidUnits).toBe(0n);
expect(state.orders).toHaveLength(1);
});

test("reconciles a newer authoritative order amount upward", async () => {
const t = convexTest(schema, modules);
await seedWorkspace(t);
await t.mutation(async (ctx) =>
applyPolarBillingEvent(ctx, paidCreditOrder()),
);
const order = paidCreditOrder().event;
if (order.kind !== "order") throw new Error("Expected an order fixture");
const updatedAt = Date.parse("2026-08-13T00:00:00Z");
await t.mutation(async (ctx) =>
applyPolarBillingEvent(
ctx,
paidCreditOrder({
deliveryId: "delivery-order-updated-amount",
eventType: "order.updated",
eventOccurredAt: updatedAt,
event: {
...order,
grossAmountMinor: 700n,
providerModifiedAt: updatedAt,
},
}),
),
);

const state = await t.query(readState);
expect(state.account?.availablePrepaidUnits).toBe(7_000_000n);
expect(state.orders).toHaveLength(1);
});

test("atomically enables an active subscription entitlement", async () => {
const t = convexTest(schema, modules);
await seedWorkspace(t, "plus");

const result = await t.mutation(async (ctx) =>
applyPolarBillingEvent(ctx, {
providerEnvironment: "production",
deliveryId: "delivery-subscription-active-1",
eventType: "subscription.active",
eventOccurredAt: paidAt,
event: {
kind: "subscription",
organizationId,
providerSubscriptionId: "polar-subscription-1",
providerCustomerId: "polar-customer-1",
providerProductId: "polar-plus-product",
providerStatus: "active",
seatQuantity: 2,
cancelAtPeriodEnd: false,
currentPeriodStart: Date.parse("2026-08-01T00:00:00.000Z"),
currentPeriodEnd: Date.parse("2026-09-01T00:00:00.000Z"),
providerModifiedAt: paidAt,
},
}),
);
const state = await t.query(readState);

expect(result.outcome).toBe("applied");
expect(state.entitlement?.plusEnabled).toBe(true);
expect(state.entitlement?.paidSeatCapacity).toBe(2);
});
});
Loading