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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const TEST_PRICE_PRO = "price_test_pro";
const TEST_PRICE_TEAM = "price_test_team";
export const TEST_PRICE_CONCURRENCY = "price_test_concurrency";
const TEST_PRICE_ATOM_GRANT = "price_test_atom_grant";
const TEST_PRICE_USAGE_ALLOWANCE = "price_test_usage_allowance";

const STRIPE_WEBHOOK_SECRET = "whsec_billing_state_test";
const DEFAULT_CREDIT_EXPIRES_MS = 30 * 24 * 60 * 60 * 1000;
Expand Down Expand Up @@ -37,6 +38,19 @@ interface ConcurrencyWebhookLine {
readonly priceId?: string;
}

interface UsageAllowanceWebhookInput extends BillingWebhookFixture {
readonly customerId: string;
readonly subscriptionId: string;
readonly invoiceId?: string;
readonly status?: string;
readonly shortWindowSeconds: number;
readonly shortWindowUnits: number;
readonly weeklyWindowSeconds: number;
readonly weeklyWindowUnits: number;
readonly effectiveAt: Date;
readonly expiresAt: Date;
}

export function createBillingWebhookFixture(): BillingWebhookFixture {
return {
orgId: `org_${randomUUID()}`,
Expand Down Expand Up @@ -414,6 +428,87 @@ export async function postConcurrencyEntitlementsInvoicePaid(
});
}

export async function postUsageAllowanceInvoicePaid(
signal: AbortSignal,
args: UsageAllowanceWebhookInput,
): Promise<void> {
configureBillingWebhookEnv();

const periodStart = seconds(args.effectiveAt);
const periodEnd = seconds(args.expiresAt);
const metadata = {
type: "usage_allowance",
purpose: "usage_allowance",
source: "atom_usage_allowance",
orgId: args.orgId,
shortWindowSeconds: String(args.shortWindowSeconds),
shortWindowUnits: String(args.shortWindowUnits),
weeklyWindowSeconds: String(args.weeklyWindowSeconds),
weeklyWindowUnits: String(args.weeklyWindowUnits),
};

await postStripeEvent(signal, {
type: "invoice.paid",
data: {
object: {
id: args.invoiceId ?? `in_usage_${randomUUID().slice(0, 8)}`,
customer: args.customerId,
metadata,
subtotal: 0,
parent: {
subscription_details: {
subscription: args.subscriptionId,
metadata: {},
},
},
lines: {
data: [
{
id: `il_usage_${randomUUID().slice(0, 8)}`,
price: { id: TEST_PRICE_USAGE_ALLOWANCE },
quantity: 1,
parent: { type: "subscription_item_details" },
period: {
start: periodStart,
end: periodEnd,
},
},
],
},
},
},
});

if (args.status === undefined || args.status === "active") {
return;
}

await postStripeEvent(signal, {
type: "customer.subscription.updated",
data: {
object: {
id: args.subscriptionId,
customer: args.customerId,
status: args.status,
metadata,
cancel_at_period_end: false,
cancel_at: null,
schedule: null,
trial_end: null,
items: {
data: [
{
price: { id: TEST_PRICE_USAGE_ALLOWANCE },
quantity: 1,
current_period_end: periodEnd,
},
],
},
},
},
});
}

export async function postBillingDowngradeCheckoutCompleted(
signal: AbortSignal,
args: SubscriptionWebhookInput & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import {
postCreditPurchaseInvoicePaid,
postOneTimePurchaseCompleted,
postSubscriptionInvoicePaid,
postUsageAllowanceInvoicePaid,
subscriptionCredits,
TEST_PRICE_CONCURRENCY,
type BillingWebhookFixture,
} from "./stripe-billing-webhook";
import { nowDate } from "../../../../lib/time";
import { insertUsageAllowanceWindowsFixture } from "../../../../test-fixtures/usage-allowance";

export interface BillingStatusFixture {
readonly orgId: string;
Expand Down Expand Up @@ -53,12 +56,32 @@ interface ConcurrencyEntitlementSeed {
readonly stripePriceId?: string;
}

interface UsageAllowanceWindowSeed {
readonly kind: "short" | "weekly";
readonly startsAt: Date;
readonly expiresAt: Date;
readonly unitLimit: number;
readonly consumedUnits?: number;
}

interface UsageAllowanceSeed {
readonly status?: string;
readonly shortWindowSeconds: number;
readonly shortWindowUnits: number;
readonly weeklyWindowSeconds?: number;
readonly weeklyWindowUnits: number;
readonly effectiveAt?: Date;
readonly expiresAt?: Date | null;
readonly windows?: readonly UsageAllowanceWindowSeed[];
}

interface BillingStatusSeedValues {
readonly credits?: number;
readonly onboardingPaymentPending?: boolean;
readonly subscription?: SubscriptionSeed;
readonly expiresRecords?: readonly ExpiresRecordSeed[];
readonly concurrencyEntitlements?: readonly ConcurrencyEntitlementSeed[];
readonly usageAllowance?: UsageAllowanceSeed;
readonly extraGrantedCredits?: number;
}

Expand Down Expand Up @@ -233,6 +256,52 @@ async function applyConcurrencySeeds(
}
}

async function insertUsageAllowanceWindows(
orgId: string,
windows: readonly UsageAllowanceWindowSeed[] | undefined,
): Promise<void> {
if (!windows || windows.length === 0) {
return;
}
await insertUsageAllowanceWindowsFixture({
orgId,
windows: windows.map((window) => {
return {
kind: window.kind,
startsAt: window.startsAt,
expiresAt: window.expiresAt,
unitLimit: window.unitLimit,
consumedUnits: window.consumedUnits,
};
}),
});
}

async function applyUsageAllowanceSeed(
signal: AbortSignal,
fixture: BillingWebhookFixture,
customerId: string,
seed: UsageAllowanceSeed | undefined,
): Promise<void> {
if (!seed) {
return;
}

await postUsageAllowanceInvoicePaid(signal, {
...fixture,
customerId,
subscriptionId: generatedStripeSubscriptionId(),
status: seed.status,
shortWindowSeconds: seed.shortWindowSeconds,
shortWindowUnits: seed.shortWindowUnits,
weeklyWindowSeconds: seed.weeklyWindowSeconds ?? 604_800,
weeklyWindowUnits: seed.weeklyWindowUnits,
effectiveAt: seed.effectiveAt ?? nowDate(),
expiresAt: seed.expiresAt ?? new Date("2099-01-01T00:00:00.000Z"),
});
await insertUsageAllowanceWindows(fixture.orgId, seed.windows);
}

export const seedBillingStatusOrg$ = command(
async (
_,
Expand Down Expand Up @@ -266,6 +335,12 @@ export const seedBillingStatusOrg$ = command(
customerId,
values.concurrencyEntitlements,
);
await applyUsageAllowanceSeed(
signal,
fixture,
customerId,
values.usageAllowance,
);

if (values.extraGrantedCredits && values.extraGrantedCredits > 0) {
await postCreditPurchaseInvoicePaid(signal, {
Expand Down
Loading
Loading