Skip to content
Back to skills

Stripe Subscription Billing Test Author

ASecurity

Builds test suites for Stripe recurring-billing flows: trial-to-paid conversion, proration on plan upgrade and downgrade, dunning on failed renewal, cancel and reactivation, and the full subscription webhook event matrix (invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted, invoice.paid). Uses Stripe Billing test clocks (POST /v1/test_helpers/test_clocks) to time-travel through billing cycles without calendar delay. Distinct from stripe-test-cards-and-webhooks...

  • 10 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 12, 2026
testingtypescriptgobashreactnodetestingapi

Works with

  • terminal
  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 12, 2026

npx -y skills add testland/qa --skill stripe-subscription-billing-test-author --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Stripe Subscription Billing Test Author?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Stripe Subscription Billing Test Author
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/testland-stripe-subscription-billing-test-author/badge)](https://www.skillsdirectory.com/skills/testland-stripe-subscription-billing-test-author)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: stripe-subscription-billing-test-author
description: "Builds test suites for Stripe recurring-billing flows: trial-to-paid conversion, proration on plan upgrade and downgrade, dunning on failed renewal, cancel and reactivation, and the full subscription webhook event matrix (invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted, invoice.paid). Uses Stripe Billing test clocks (POST /v1/test_helpers/test_clocks) to time-travel through billing cycles without calendar delay. Distinct from stripe-test-cards-and-webhooks (one-time PaymentIntents) and payment-flow-test-author (idempotency + replay robustness). Does not cover single-event CLI replay or handler idempotency testing (see payment-flow-test-author for those). Use when authoring tests for subscription or recurring-billing integrations."
metadata:
  keywords: "stripe, billing, subscriptions, recurring, test-clocks, dunning, proration, trial"
  version: "1.4.5"
---

# stripe-subscription-billing-test-author

## Overview

Subscription billing is distinct from one-time payments. A customer's card is
charged on a schedule, trials expire, plans change mid-cycle, and payments
fail weeks after signup. Per
[docs.stripe.com/billing/testing](https://docs.stripe.com/billing/testing),
Stripe's sandbox is feature-complete for billing objects: subscriptions,
invoices, trials, and retry schedules all work against test data.

The key tool for deterministic subscription testing is the **Stripe Billing
test clock**, which lets you fast-forward a frozen simulation through days,
weeks, or months without waiting. Per
[docs.stripe.com/api/test_clocks](https://docs.stripe.com/api/test_clocks),
a test clock is created at a `frozen_time` and advanced with
`POST /v1/test_helpers/test_clocks/{id}/advance`, triggering all billing
events that would have fired in the elapsed interval.

This skill walks through five recurring-billing scenarios end to end.

## Lifecycle states to test

Per
[docs.stripe.com/billing/subscriptions/overview](https://docs.stripe.com/billing/subscriptions/overview),
subscriptions move through:

| State | Meaning |
|---|---|
| `trialing` | Trial active; no payment collected yet |
| `active` | Good standing; payment collected |
| `past_due` | Latest invoice unpaid; smart retries running |
| `unpaid` | Retries exhausted; access decision pending |
| `canceled` | Terminal; billing stopped |
| `paused` | Trial ended without payment method attached |

Each transition fires one or more webhook events. Tests must assert both the
subscription status and the correct event sequence.

## Step 1 - Create a test clock

Per
[docs.stripe.com/api/test_clocks](https://docs.stripe.com/api/test_clocks),
create a test clock frozen at a known epoch before creating any billing objects:

```typescript
const clock = await stripe.testHelpers.testClocks.create({
  frozen_time: Math.floor(new Date('2025-01-01T00:00:00Z').getTime() / 1000),
  name: 'billing-suite-run',
});
```

Attach the customer to the clock at creation time:

```typescript
const customer = await stripe.customers.create({
  email: 'tester@example.com',
  test_clock: clock.id,
  payment_method: 'pm_card_visa',  // 4242 4242 4242 4242
  invoice_settings: { default_payment_method: 'pm_card_visa' },
});
```

Advance time with:

```typescript
await stripe.testHelpers.testClocks.advance(clock.id, {
  advances_to: Math.floor(new Date('2025-01-08T00:00:00Z').getTime() / 1000),
});
// Poll until clock.status === 'ready' before asserting
```

Always poll `GET /v1/test_helpers/test_clocks/{id}` until `status` is `ready`
before asserting subscription state; events are asynchronous.

## Step 2 - Trial-to-paid conversion

Per
[docs.stripe.com/billing/testing](https://docs.stripe.com/billing/testing),
the recommended flow:

```typescript
// Freeze clock at Jan 1
const sub = await stripe.subscriptions.create({
  customer: customer.id,
  items: [{ price: PRICE_ID }],
  trial_end: Math.floor(new Date('2025-01-08T00:00:00Z').getTime() / 1000),
});
expect(sub.status).toBe('trialing');

// Advance to Day 5 - trial_will_end fires 3 days before trial ends
await stripe.testHelpers.testClocks.advance(clock.id, {
  advances_to: toEpoch('2025-01-05'),
});
await waitForClockReady(clock.id);
// Assert: customer.subscription.trial_will_end webhook fired

// Advance past trial end (Day 8)
await stripe.testHelpers.testClocks.advance(clock.id, {
  advances_to: toEpoch('2025-01-09'),
});
await waitForClockReady(clock.id);

const updated = await stripe.subscriptions.retrieve(sub.id);
expect(updated.status).toBe('active');
```

Webhook events to assert per
[docs.stripe.com/billing/testing](https://docs.stripe.com/billing/testing):

| Clock position | Event |
|---|---|
| Day 5 | `customer.subscription.trial_will_end` |
| Day 8 | `invoice.created`, `invoice.finalized`, `invoice.paid` |
| Day 8 | `customer.subscription.updated` (status: trialing -> active) |

## Step 3 - Proration on upgrade/downgrade

Per
[docs.stripe.com/billing/subscriptions/prorations](https://docs.stripe.com/billing/subscriptions/prorations),
a mid-cycle plan change generates a credit for unused time on the old price
and a charge for remaining time on the new price. Preview before applying:

```typescript
const prorationDate = Math.floor(Date.now() / 1000);

// Preview the upcoming invoice to verify proration amounts
const preview = await stripe.invoices.createPreview({
  customer: customer.id,
  subscription: sub.id,
  subscription_details: {
    items: [{ id: sub.items.data[0].id, price: NEW_PRICE_ID }],
    proration_date: prorationDate,
  },
});

// Verify credit (negative) and debit (positive) line items exist
const credit = preview.lines.data.find(l => l.amount < 0);
const debit  = preview.lines.data.find(l => l.amount > 0);
expect(credit).toBeTruthy();
expect(debit).toBeTruthy();

// Apply at the same proration_date to lock the amounts
await stripe.subscriptions.update(sub.id, {
  items: [{ id: sub.items.data[0].id, price: NEW_PRICE_ID }],
  proration_date: prorationDate,
  proration_behavior: 'create_prorations',  // default
});
```

Per
[docs.stripe.com/billing/subscriptions/prorations](https://docs.stripe.com/billing/subscriptions/prorations),
`proration_behavior` options are:

| Value | Effect |
|---|---|
| `create_prorations` | Creates proration items; billed on next invoice (default) |
| `always_invoice` | Creates proration items AND immediately invoices |
| `none` | No proration; full new price on next cycle |

Downgrade test: pass a lower-priced plan as `NEW_PRICE_ID` and assert the
credit line item is larger than the debit line item.

## Step 4 - Dunning on failed renewal

Per
[docs.stripe.com/billing/testing](https://docs.stripe.com/billing/testing),
use test card `4000 0000 0000 0341` to trigger payment failure on an active
subscription. Per
[docs.stripe.com/billing/revenue-recovery/smart-retries](https://docs.stripe.com/billing/revenue-recovery/smart-retries),
Smart Retries can be configured for up to 8 attempts within a 2-month window;
`invoice.payment_failed` fires on each failed attempt with `attempt_count`
and `next_payment_attempt` fields.

```typescript
// Attach a card that will decline on recurring collection
const failMethod = await stripe.paymentMethods.attach('pm_card_chargeDeclinedInsufficientFunds', {
  customer: customer.id,
});
await stripe.customers.update(customer.id, {
  invoice_settings: { default_payment_method: failMethod.id },
});

// Advance past the renewal date
await stripe.testHelpers.testClocks.advance(clock.id, {
  advances_to: toEpoch('2025-02-02'),  // One day past renewal
});
await waitForClockReady(clock.id);

const invoice = await getLatestInvoice(sub.id);
expect(invoice.status).toBe('open');

const sub2 = await stripe.subscriptions.retrieve(sub.id);
expect(sub2.status).toBe('past_due');
// Assert invoice.payment_failed was received with attempt_count >= 1
```

Webhook sequence on failed renewal:

| Event | Key field to assert |
|---|---|
| `invoice.payment_failed` | `attempt_count`, `next_payment_attempt` |
| `customer.subscription.updated` | `status: past_due` |

Test recovery: attach a valid payment method, then call
`stripe.invoices.pay(invoice.id)` and assert `invoice.paid` fires and
subscription returns to `active`.

## Step 5 - Cancel and reactivation

```typescript
// Cancel at period end (access persists until billing cycle closes)
await stripe.subscriptions.update(sub.id, { cancel_at_period_end: true });
const canceling = await stripe.subscriptions.retrieve(sub.id);
expect(canceling.cancel_at_period_end).toBe(true);

// Advance past period end
await stripe.testHelpers.testClocks.advance(clock.id, {
  advances_to: toEpoch('2025-02-02'),
});
await waitForClockReady(clock.id);

const canceled = await stripe.subscriptions.retrieve(sub.id);
expect(canceled.status).toBe('canceled');
// Assert: customer.subscription.deleted webhook fired

// Reactivation: create a new subscription (canceled is terminal)
const reactivated = await stripe.subscriptions.create({
  customer: customer.id,
  items: [{ price: PRICE_ID }],
});
expect(reactivated.status).toBe('active');
```

Per
[docs.stripe.com/billing/subscriptions/overview](https://docs.stripe.com/billing/subscriptions/overview),
`canceled` is a terminal state; reactivation means creating a fresh
subscription, not updating the canceled one.

## Step 6 - Webhook event matrix

Per
[docs.stripe.com/billing/testing](https://docs.stripe.com/billing/testing),
the full set of events your handler must process:

| Event | Trigger | Required handler action |
|---|---|---|
| `customer.subscription.created` | New subscription | Record subscription ID + status |
| `customer.subscription.trial_will_end` | 3 days before trial end | Notify customer |
| `customer.subscription.updated` | Any status or plan change | Sync local subscription record |
| `customer.subscription.deleted` | Cancellation takes effect | Revoke access |
| `invoice.created` | Invoice generated at renewal | Finalize within 72 hours |
| `invoice.finalized` | Invoice ready for collection | Log for audit |
| `invoice.paid` | Payment succeeded | Provision or extend access |
| `invoice.payment_failed` | Payment attempt failed | Notify customer; track `attempt_count` |
| `invoice.payment_action_required` | 3DS required on renewal | Prompt re-authentication |

Trigger these in CI using the Stripe CLI:

```bash
stripe trigger invoice.payment_failed
stripe trigger customer.subscription.updated
```

Per
[docs.stripe.com/stripe-cli](https://docs.stripe.com/stripe-cli), forward
webhook events to a local handler during development:

```bash
stripe listen --forward-to http://localhost:3000/webhooks/stripe
```

## Running

```bash
npm test -- --testPathPattern=billing
```

For integration tests that drive test clocks:

```bash
STRIPE_TEST_KEY=sk_test_... npm test -- --testPathPattern=billing/integration
```

## CI integration

```yaml
jobs:
  billing-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v4
      - run: npm ci && npm test -- --testPathPattern=billing
        env:
          STRIPE_TEST_KEY: ${{ secrets.STRIPE_TEST_KEY }}
          STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
```

Test-clock tests do not require `stripe listen` because they drive the
Stripe API directly without local webhook forwarding. Signature verification
tests do need `STRIPE_WEBHOOK_SECRET`.

## Anti-patterns

| Anti-pattern | Why it fails | Fix |
|---|---|---|
| `setTimeout` to wait for trial end | Flaky and slow in CI | Use test clock + `waitForClockReady` |
| Asserting subscription state before clock is `ready` | Race condition; events not yet fired | Poll `GET /v1/test_helpers/test_clocks/{id}` |
| Reusing a test clock customer across test cases | Shared state leaks between scenarios | Create a fresh clock + customer per scenario |
| Canceling and immediately reusing as `active` | `canceled` is terminal | Create a new subscription for reactivation |
| Skipping `invoice.paid` handler | Access provisioned on subscription create, not payment | Provision only after `invoice.paid` |
| Omitting `proration_date` from preview AND update calls | Timing skew makes amounts disagree | Use the same epoch timestamp for both |
| Testing only `invoice.payment_failed` once | Smart Retries fires multiple times with growing `attempt_count` | Test at least two retry cycles |

## Limitations

- Test clocks only work in sandbox (test mode); they are not available in
  live mode.
- Per
  [docs.stripe.com/api/test_clocks](https://docs.stripe.com/api/test_clocks),
  a test clock cannot be advanced backward; frozen time is monotonically
  forward-only.
- Smart Retry timing in test mode compresses retry intervals; exact retry
  schedule differs from production per
  [docs.stripe.com/billing/revenue-recovery/smart-retries](https://docs.stripe.com/billing/revenue-recovery/smart-retries).
- Stripe CLI `trigger` sends synthetic events; test-clock-driven tests
  produce real invoice objects and are preferred for billing accuracy.

## References

- Stripe Billing testing:
  [docs.stripe.com/billing/testing](https://docs.stripe.com/billing/testing).
- Test clocks API:
  [docs.stripe.com/api/test_clocks](https://docs.stripe.com/api/test_clocks).
- Subscription lifecycle:
  [docs.stripe.com/billing/subscriptions/overview](https://docs.stripe.com/billing/subscriptions/overview).
- Prorations:
  [docs.stripe.com/billing/subscriptions/prorations](https://docs.stripe.com/billing/subscriptions/prorations).
- Smart Retries:
  [docs.stripe.com/billing/revenue-recovery/smart-retries](https://docs.stripe.com/billing/revenue-recovery/smart-retries).
- Stripe CLI:
  [docs.stripe.com/stripe-cli](https://docs.stripe.com/stripe-cli).
- Sibling skills:
  `stripe-test-cards-and-webhooks`,
  `payment-flow-test-author`.

Files in this skill

  • .tessl-plugin/plugin.json931 B
  • SKILL.md13.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.