mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
test(billing): cover the Stripe field locations this change depends on
Every subscription fixture in the suite carries current_period_end at the top level, which is the location the pinned API version no longer uses. So the item level read, the reason this code exists, had no test at all and every other case passed through the legacy fallback instead. Covers both locations for the period and for the invoice's subscription link, the null case the webhook relies on to leave a one-off invoice alone, and the retry-window bound through the payload shape production actually sends.
This commit is contained in:
@@ -15,6 +15,9 @@ import {
|
||||
getOrCreateStripeCustomerId,
|
||||
getStorageCleanupEligibleAt,
|
||||
getStripeCheckoutState,
|
||||
getInvoiceSubscriptionId,
|
||||
getSubscriptionPeriodEnd,
|
||||
getSubscriptionPeriodStart,
|
||||
getTrialNotice,
|
||||
getWorkspaceCreationEligibility,
|
||||
hasActiveSubscription,
|
||||
@@ -868,6 +871,88 @@ function updateData(): Record<string, unknown> {
|
||||
return dbMock.user.update.mock.calls[0][0].data as Record<string, unknown>;
|
||||
}
|
||||
|
||||
// The shape Stripe actually sends on the pinned API version: the period lives on the
|
||||
// subscription's items, not on the subscription. `stripeSub` above still uses the older
|
||||
// top-level shape, so without these the whole reason this code exists goes untested and
|
||||
// every other test in this file passes through the legacy fallback instead.
|
||||
describe('Stripe field locations', () => {
|
||||
const periodStart = 1_800_000_000;
|
||||
const periodEnd = periodStart + 30 * 86_400;
|
||||
|
||||
function itemPeriodSub(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 'sub_1',
|
||||
customer: 'cus_1',
|
||||
status: 'past_due',
|
||||
items: {
|
||||
data: [
|
||||
{
|
||||
price: { id: ENTITLED_PRICE },
|
||||
current_period_start: periodStart,
|
||||
current_period_end: periodEnd,
|
||||
},
|
||||
],
|
||||
},
|
||||
...overrides,
|
||||
} as unknown as Stripe.Subscription;
|
||||
}
|
||||
|
||||
it('reads the period off the subscription items', () => {
|
||||
expect(getSubscriptionPeriodEnd(itemPeriodSub())).toBe(periodEnd);
|
||||
expect(getSubscriptionPeriodStart(itemPeriodSub())).toBe(periodStart);
|
||||
});
|
||||
|
||||
// A webhook body can still be rendered at the version that was current when the
|
||||
// endpoint was created, so the old location has to keep working.
|
||||
it('falls back to the legacy top-level period', () => {
|
||||
const legacy = {
|
||||
items: { data: [{ price: { id: ENTITLED_PRICE } }] },
|
||||
current_period_start: periodStart,
|
||||
current_period_end: periodEnd,
|
||||
} as unknown as Stripe.Subscription;
|
||||
|
||||
expect(getSubscriptionPeriodEnd(legacy)).toBe(periodEnd);
|
||||
expect(getSubscriptionPeriodStart(legacy)).toBe(periodStart);
|
||||
});
|
||||
|
||||
it('returns null when neither location carries a period', () => {
|
||||
const bare = {
|
||||
items: { data: [{ price: { id: ENTITLED_PRICE } }] },
|
||||
} as unknown as Stripe.Subscription;
|
||||
|
||||
expect(getSubscriptionPeriodEnd(bare)).toBeNull();
|
||||
expect(getSubscriptionPeriodStart(bare)).toBeNull();
|
||||
});
|
||||
|
||||
it('reads the invoice subscription off parent.subscription_details', () => {
|
||||
const invoice = {
|
||||
parent: { subscription_details: { subscription: 'sub_9' } },
|
||||
} as unknown as Stripe.Invoice;
|
||||
|
||||
expect(getInvoiceSubscriptionId(invoice)).toBe('sub_9');
|
||||
});
|
||||
|
||||
it('accepts an expanded subscription object on the invoice parent', () => {
|
||||
const invoice = {
|
||||
parent: { subscription_details: { subscription: { id: 'sub_9' } } },
|
||||
} as unknown as Stripe.Invoice;
|
||||
|
||||
expect(getInvoiceSubscriptionId(invoice)).toBe('sub_9');
|
||||
});
|
||||
|
||||
it('falls back to the legacy top-level invoice subscription', () => {
|
||||
expect(getInvoiceSubscriptionId({ subscription: 'sub_9' } as unknown as Stripe.Invoice)).toBe(
|
||||
'sub_9'
|
||||
);
|
||||
});
|
||||
|
||||
// A one-off invoice belongs to no subscription, and the webhook relies on this to leave
|
||||
// the account alone rather than marking it canceled.
|
||||
it('returns null for an invoice with no subscription', () => {
|
||||
expect(getInvoiceSubscriptionId({} as unknown as Stripe.Invoice)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('database backed billing helpers', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
@@ -1555,6 +1640,35 @@ describe('database backed billing helpers', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// The same thing through the payload shape production actually sends, where the period
|
||||
// sits on the items rather than on the subscription. Every other fixture in this file
|
||||
// uses the older top-level shape and so never exercises the read this change is for.
|
||||
it('bounds a past_due subscription whose period is on its items', async () => {
|
||||
dbMock.user.findUnique.mockResolvedValue({ id: 'u1', billingTrialConsumedAt: null });
|
||||
const periodStart = Math.floor(NOW.getTime() / 1000);
|
||||
const periodEnd = periodStart + 30 * 24 * 60 * 60;
|
||||
|
||||
await syncStripeSubscriptionToUser({
|
||||
id: 'sub_1',
|
||||
customer: 'cus_1',
|
||||
status: 'past_due',
|
||||
items: {
|
||||
data: [
|
||||
{
|
||||
price: { id: ENTITLED_PRICE },
|
||||
current_period_start: periodStart,
|
||||
current_period_end: periodEnd,
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as Stripe.Subscription);
|
||||
|
||||
expect((updateData().stripeCurrentPeriodEnd as Date).getTime()).toBe(periodEnd * 1000);
|
||||
expect((updateData().billingAccessEndedAt as Date).getTime()).toBe(
|
||||
(periodStart + 14 * 24 * 60 * 60) * 1000
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to ended_at when there is no period end', async () => {
|
||||
dbMock.user.findUnique.mockResolvedValue({ id: 'u1', billingTrialConsumedAt: null });
|
||||
const endedAt = Math.floor(NOW.getTime() / 1000) - 7200;
|
||||
|
||||
Reference in New Issue
Block a user