@startsimpli/billing 0.4.12 → 0.4.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,145 +1,343 @@
1
1
  # @startsimpli/billing
2
2
 
3
- Universal billing integration for StartSimpli Next.js apps. Products and pricing are configured in Django admin — frontend components auto-fetch and render.
3
+ Universal billing UX for StartSimpli apps — auto-enrol on signup, real current-plan card with usage progress, structured limit-reached errors, full subscription-state matrix (trial / past-due / cancelled-in-period / incomplete / non-owner), and a tier-limit nudge that any list page can drop in.
4
4
 
5
- ## Backend Setup
5
+ **Shipping principle:** every component, hook, and adapter contract lives here or in `apps.billing` (Django). Apps add only (a) a slug to `BILLING_AUTO_SUBSCRIBE_PRODUCT_SLUGS` + `BILLING_USAGE_ADAPTERS`, (b) one `apps.<name>.billing.usage.collect(user, team)` callable, (c) `<BillingProvider>` at the dashboard root, and (d) `<SubscriptionManager productId="..." />` on `/settings/billing`. ~40 LOC of app-side glue. Don't reinvent.
6
6
 
7
- The billing backend lives in `start-simpli-api/apps/billing/`. Models, admin, and API are already configured.
7
+ ---
8
8
 
9
- ### Key Endpoints
9
+ ## Quick start (wire a new app in <40 LOC)
10
10
 
11
- | Endpoint | Method | Auth | Description |
12
- |----------|--------|------|-------------|
13
- | `/api/v1/billing/products/` | GET | Public | List public products with offers |
14
- | `/api/v1/billing/products/{slug}/` | GET | Public | Get product by slug |
15
- | `/api/v1/billing/offer-checkout/` | POST | Required | Create checkout session |
16
- | `/api/v1/billing/offer-portal/` | POST | Required | Create portal session |
17
- | `/api/v1/billing/subscription/current/` | GET | Required | Get current user's subscription |
11
+ ### 1. Django side (`start-simpli-api/backend/`)
18
12
 
19
- ### Admin Workflow
13
+ ```python
14
+ # config/settings/base.py
15
+ BILLING_AUTO_SUBSCRIBE_PRODUCT_SLUGS = [
16
+ s.strip() for s in os.environ.get(
17
+ "BILLING_AUTO_SUBSCRIBE_PRODUCT_SLUGS", "vault,my-new-app"
18
+ ).split(",") if s.strip()
19
+ ]
20
20
 
21
- 1. Add a `BillingProviderCredential` (Stripe secret key + webhook secret)
22
- 2. Create a `BillingProduct` (slug = identifier used by frontend)
23
- 3. Add `ProductOffer` inlines (pricing tiers)
24
- 4. Use "Sync to provider" admin action to push to Stripe
21
+ BILLING_USAGE_ADAPTERS = {
22
+ "vault": "apps.vault.billing.usage.collect",
23
+ "my-new-app": "apps.my_new_app.billing.usage.collect",
24
+ }
25
+ ```
25
26
 
26
- ## Frontend Setup
27
+ ```python
28
+ # apps/my_new_app/billing/usage.py
29
+ def collect(user, team) -> dict[str, int]:
30
+ """Return raw counts keyed by ProductOffer.features[].key.
31
+ The billing endpoint merges these with limits from the user's current
32
+ subscription's offer features — apps just report counts."""
33
+ company = team.company
34
+ return {
35
+ "projects": company.projects.count(),
36
+ "members_per_project": company.projects.annotate(n=Count("members")).aggregate(m=Max("n"))["m"] or 0,
37
+ }
38
+ ```
27
39
 
28
- ### Installation
40
+ That's the entire backend wiring. Auto-enrol on signup, the usage endpoint, the `limit_reached` 402 contract — all already in `apps.billing`. See "Limit enforcement" below for how to opt your data models in.
29
41
 
30
- In your Next.js app's `package.json`:
42
+ ### 2. Next.js side (`my-new-app/`)
31
43
 
32
- ```json
33
- {
34
- "dependencies": {
35
- "@startsimpli/billing": "workspace:*"
36
- }
44
+ ```tsx
45
+ // src/app/(dashboard)/layout.tsx — hoist BillingProvider once
46
+ 'use client';
47
+ import { BillingProvider } from '@startsimpli/billing';
48
+ import { authFetch } from '@startsimpli/auth';
49
+
50
+ export default function DashboardLayout({ children }: { children: React.ReactNode }) {
51
+ return (
52
+ <BillingProvider apiBaseUrl="/api" fetcher={authFetch}>
53
+ {children}
54
+ </BillingProvider>
55
+ );
56
+ }
57
+ ```
58
+
59
+ ```tsx
60
+ // src/app/(dashboard)/settings/billing/page.tsx — ~30 LOC
61
+ 'use client';
62
+ import { useEffect, useState } from 'react';
63
+ import { SubscriptionManager, useCheckout } from '@startsimpli/billing';
64
+ import type { ProductOffer } from '@startsimpli/billing';
65
+
66
+ export default function BillingSettingsPage() {
67
+ const { checkout } = useCheckout();
68
+ const [status, setStatus] = useState<'success' | 'cancelled' | null>(null);
69
+
70
+ useEffect(() => {
71
+ const s = new URLSearchParams(window.location.search).get('status');
72
+ if (s === 'success' || s === 'cancelled') setStatus(s);
73
+ }, []);
74
+
75
+ const returnUrl = typeof window !== 'undefined'
76
+ ? `${window.location.origin}/settings/billing` : '/settings/billing';
77
+
78
+ // Free offers → SubscriptionManager calls subscribe-free internally;
79
+ // paid offers route through this Stripe checkout shim.
80
+ const onPaidPlanChange = async (offer: ProductOffer) => {
81
+ const { url } = await checkout({
82
+ offerId: offer.id,
83
+ successUrl: `${returnUrl}?status=success`,
84
+ cancelUrl: `${returnUrl}?status=cancelled`,
85
+ });
86
+ window.location.href = url;
87
+ };
88
+
89
+ return (
90
+ <SubscriptionManager
91
+ productId="my-new-app"
92
+ returnUrl={returnUrl}
93
+ onPlanChange={onPaidPlanChange}
94
+ statusMessage={status}
95
+ />
96
+ );
37
97
  }
38
98
  ```
39
99
 
40
- Add `transpilePackages: ['@startsimpli/billing']` to `next.config.js`.
100
+ You also need 7 thin proxy routes under `src/app/api/billing/*` (products, subscription/current, offer-checkout, offer-portal, subscribe-free, success-sync, usage/[slug]) — copy them verbatim from `vault-web/src/app/api/billing/`. The products + subscription proxies need `fromSnake: true` (Django's public endpoints bypass the camelCase middleware).
41
101
 
42
- ### Usage
102
+ That's everything. The billing page now renders:
103
+ - Auto-enrolled Free plan with live usage progress
104
+ - Per-card CTAs that respect the user's current sub (Pro user sees "Downgrade to Free", not "Get Started")
105
+ - "Upgrade to <NextTier>" CTA on free-tier users
106
+ - Trial countdown, past-due red banner, cancelled-in-period banner with Reactivate
107
+ - Read-only view for non-owner team members
108
+ - Auto-revert to Free when a paid sub expires
109
+
110
+ ---
111
+
112
+ ## Limit enforcement (the `limit_reached` 402 contract)
113
+
114
+ When you want to block creates past a tier's limit, raise `apps.<myapp>.billing.limits.LimitReached` (or use the helper):
115
+
116
+ ```python
117
+ # In your viewset's perform_create — adapter mirror of apps.vault.billing.limits
118
+ from apps.vault.billing.limits import enforce_limit
119
+
120
+ def perform_create(self, serializer):
121
+ team = self.request.user.team_memberships.first().team
122
+ current = Project.objects.filter(company=team.company).count()
123
+ enforce_limit(team, "projects", current, "projects") # 402 if at/over the offer's limit
124
+ serializer.save(...)
125
+ ```
126
+
127
+ The handler at `apps.core.exceptions.custom_exception_handler` preserves your structured `code` (`limit_reached`) and ships `feature_key`, `limit`, `current` at the top level of the response. Silent (allows) on missing-sub / missing-feature / `limit=-1` (unlimited) — billing misconfig must never block a healthy team.
128
+
129
+ Client-side, sniff `ApiException.code === 'limit_reached'` and surface the backend's human message:
43
130
 
44
131
  ```tsx
45
- import { BillingProvider, PricingPage, useCheckout, ManageSubscription } from '@startsimpli/billing'
132
+ import { ApiException } from '@startsimpli/api';
133
+
134
+ try {
135
+ await createProject.mutateAsync(...);
136
+ notifyUsageChanged('my-new-app'); // refresh the LimitNudge bar immediately
137
+ } catch (err) {
138
+ if (err instanceof ApiException && err.code === 'limit_reached') {
139
+ setFormError(err.message); // 'Your Free plan allows 3 projects. Upgrade to add more.'
140
+ }
141
+ }
142
+ ```
46
143
 
47
- // Wrap your app (or a subtree) with BillingProvider
48
- <BillingProvider apiBaseUrl="/api/v1" authToken={accessToken}>
49
- <PricingPage productId="raise-simpli" onSelectOffer={handleSelect} />
50
- </BillingProvider>
144
+ ---
51
145
 
52
- // Checkout hook
53
- const { checkout, loading } = useCheckout()
54
- await checkout({ offerId, successUrl, cancelUrl })
146
+ ## Backend reference
55
147
 
56
- // Subscription management
57
- <ManageSubscription returnUrl="/settings" buttonText="Manage Billing" />
148
+ ### Endpoints
149
+
150
+ | Endpoint | Method | Auth | Description |
151
+ |---|---|---|---|
152
+ | `/api/v1/billing/products/` | GET | Public | List public products with offers |
153
+ | `/api/v1/billing/products/{slug}/` | GET | Public | Get product by slug (raw snake_case) |
154
+ | `/api/v1/billing/subscription/current/` | GET | Required | Current sub incl. `viewer_is_owner` + `owner_email` |
155
+ | `/api/v1/billing/usage/{slug}/` | GET | Required | Per-product `{feature_key: {used, limit}}` |
156
+ | `/api/v1/billing/offer-checkout/` | POST | Required | Create Stripe checkout session |
157
+ | `/api/v1/billing/offer-portal/` | POST | Required | Create Stripe customer portal session |
158
+ | `/api/v1/billing/subscribe-free/` | POST | Required | Subscribe to a `unit_price=0` offer (no Stripe) |
159
+ | `/api/v1/billing/success-sync/` | POST | Required | Pull subscription state from Stripe on checkout return |
160
+
161
+ ### Settings
162
+
163
+ ```python
164
+ # Comma-separated env, defaults to "vault"
165
+ BILLING_AUTO_SUBSCRIBE_PRODUCT_SLUGS = ["vault", "raise-simpli", ...]
166
+
167
+ # Per-product usage adapter callable paths
168
+ BILLING_USAGE_ADAPTERS = {
169
+ "vault": "apps.vault.billing.usage.collect",
170
+ }
58
171
  ```
59
172
 
173
+ ### Signals (auto-magic)
174
+
175
+ - `post_save` on `TeamMember` → `auto_subscribe_team_to_default_free_offers` (startsim-bc8). When a team's OWNER joins, subscribe the team to every listed product's free offer.
176
+ - `post_save` on `Subscription` → terminal-status auto-revert (startsim-7qh). When a PAID sub enters `cancelled` / `incomplete_expired` / `unpaid` / `trial_expired` and no other active sub exists, reactivate the team's pre-existing Free row instead of leaving them on "No active subscription".
177
+
178
+ ### Admin workflow
179
+
180
+ 1. Add a `BillingProviderCredential` (Stripe secret key + webhook secret).
181
+ 2. Create a `BillingProduct` (`slug` = the identifier the frontend uses as `productId`).
182
+ 3. Add `ProductOffer` inlines. `features` JSON shape:
183
+ ```json
184
+ [
185
+ {"key": "projects", "name": "Projects", "value": 3, "limit": 3},
186
+ {"key": "members_per_proj", "name": "Members per project", "value": -1, "limit": -1},
187
+ {"key": "sso", "name": "SSO", "value": true}
188
+ ]
189
+ ```
190
+ - `limit: -1` means unlimited
191
+ - boolean `value: true` renders a checkmark
192
+ - boolean `value: false` is OMITTED from the card (you don't get to brag about what you don't have)
193
+ 4. Use "Sync to provider" admin action to push the offers to Stripe.
194
+
195
+ ---
196
+
197
+ ## Frontend reference
198
+
60
199
  ### Components
61
200
 
62
- | Component | Props | Description |
63
- |-----------|-------|-------------|
64
- | `BillingProvider` | `apiBaseUrl`, `authToken` | Context provider — required wrapper |
65
- | `PricingPage` | `productId`, `onSelectOffer?` | Full pricing display with offer cards |
66
- | `PricingSection` | `productId`, `onSelectOffer?` | Pricing section component |
67
- | `PricingDetailPage` | `productId`, `onSelectOffer?` | Detailed pricing page |
68
- | `UpgradeModal` | `productId`, `open`, `onClose`, `onSelectOffer?` | Modal pricing overlay |
69
- | `ManageSubscription` | `returnUrl`, `buttonText?` | Portal redirect button |
70
- | `SubscriptionManager` | `productId?` | Full subscription management interface |
201
+ | Component | Purpose | Notes |
202
+ |---|---|---|
203
+ | `BillingProvider` | Context provider. Required wrapper. | Hoist to dashboard layout; one provider per app. |
204
+ | `SubscriptionManager` | The whole `/settings/billing` experience. | Renders CurrentPlanCard + per-card grid + state banners + status banners. |
205
+ | `CurrentPlanCard` | The "you are here" widget. | State banners (trial countdown, past-due, cancelled-in-period, incomplete) + feature list with usage progress + upgrade slot. Presentational — testable without provider context. |
206
+ | `LimitNudge` | Usage strip for list-page headers. | `data-state="ok" / "approaching" / "at-cap"`; hidden on `limit=-1` / missing / errors. |
207
+ | `PricingPage` / `PricingSection` / `PricingDetailPage` | Pre-billing display surfaces. | For marketing / landing pages. |
208
+ | `UpgradeModal` | Modal pricing overlay. | For mid-flow upgrade prompts. |
209
+ | `ManageSubscription` | Bare portal redirect button. | Use `SubscriptionManager` instead unless you really just want the button. |
210
+ | `PlanGate` | Feature-gate wrapper. | Show / hide children based on the user's offer features. |
71
211
 
72
212
  ### Hooks
73
213
 
74
- | Hook | Returns | Description |
75
- |------|---------|-------------|
76
- | `useProduct(slug)` | `{ product, loading, error }` | Fetch product + offers |
77
- | `useCheckout()` | `{ checkout, subscribeFree, loading, error }` | Create checkout session or free subscription |
78
- | `usePortal()` | `{ openPortal, loading, error }` | Open customer portal |
79
- | `useSubscription()` | `{ subscription, loading, error, refetch }` | Get current user's subscription |
214
+ | Hook | Returns | Notes |
215
+ |---|---|---|
216
+ | `useProduct(slug)` | `{ product, loading, error, refetch }` | Fetch product + offers |
217
+ | `useSubscription()` | `{ subscription, loading, error, refetch }` | Current sub incl. `viewerIsOwner` + `ownerEmail` |
218
+ | `useUsage(slug)` | `{ usage, loading, error, refetch }` | `{feature_key: {used, limit}}` map; auto-refetches on `visibilitychange` + `billing:usage-changed` event |
219
+ | `useCheckout()` | `{ checkout, subscribeFree, loading, error }` | Stripe session creator + free-subscribe in one |
220
+ | `usePortal()` | `{ openPortal, loading, error }` | Stripe customer portal session |
221
+ | `useSuccessSync()` | `{ sync, data, loading, error }` | Pull subscription state from Stripe on `?status=success` (auto-fired inside SubscriptionManager) |
222
+
223
+ ### Imperative helpers
224
+
225
+ ```tsx
226
+ import { notifyUsageChanged, USAGE_CHANGED_EVENT } from '@startsimpli/billing';
227
+
228
+ // Call after any mutation that changed counts. Every mounted useUsage hook
229
+ // (and therefore every LimitNudge) for the matching slug will refetch.
230
+ notifyUsageChanged('vault');
231
+
232
+ // notifyUsageChanged() with no slug fires for all slugs.
233
+ ```
234
+
235
+ ---
236
+
237
+ ## State matrix
238
+
239
+ `CurrentPlanCard` shows AT MOST ONE banner at the top, with this precedence:
240
+
241
+ 1. **past_due** → red `role="alert"` "Payment failed on `<date>`. Update your card to keep your subscription active." + "Update card" portal CTA
242
+ 2. **cancelled** OR **cancelAtPeriodEnd** → neutral "Your `<Plan>` plan is cancelled. You'll keep access until `<Mon DD, YYYY>`." + "Reactivate" portal CTA
243
+ 3. **incomplete** OR **incomplete_expired** → warning "Your `<Plan>` plan setup is incomplete." + "Finish checkout" portal CTA
244
+ 4. **trialing** → info "Your trial ends in N days (`<Mon DD, YYYY>`)." (warning style when `≤ 3` days, info otherwise) + "Cancel trial" portal CTA. "ends tomorrow" / "ends today" phrasing on the tail.
245
+ 5. none
246
+
247
+ The status badge in the top-right of the card (`data-testid="plan-status"`) carries `role="status"` and shows the human label (Active / Trialing / Past due / Cancelled / Incomplete / Expired / Unpaid). Trialing includes `until <YYYY-MM-DD>` inline.
248
+
249
+ ## Per-card CTA matrix
250
+
251
+ The Change Plan grid shows EVERY offer (including the user's current plan, marked with a `CURRENT PLAN` badge + indigo ring). Each card's CTA branches on its direction relative to the user's current sub:
252
+
253
+ | User on | Card represents | CTA | State |
254
+ |---|---|---|---|
255
+ | (no sub) | any offer | offer.ctaText | enabled |
256
+ | Free | Free | "Current Plan" | **disabled**, with `CURRENT PLAN` badge |
257
+ | Free | Pro | offer.ctaText (e.g. "Start Free Trial") | enabled, trial copy visible |
258
+ | Pro | Free | "Downgrade to Free" | enabled, secondary outline styling |
259
+ | Pro | Pro | "Current Plan" | **disabled**, with `CURRENT PLAN` badge |
260
+ | Pro | Enterprise | offer.ctaText | enabled |
261
+ | Trialing Pro | Pro | "Current Plan" | **disabled** |
262
+ | Non-owner team member | ANY | shown but **disabled** with tooltip "Only the team owner can change the plan" | — |
263
+
264
+ The standalone "Upgrade to `<PaidName>` →" button on the current-plan card picks the NEXT tier above the current sub by `sortOrder` (featured-preferred). Returns null when the user is on the top tier or there's no paid offer at all (free-only apps).
265
+
266
+ ---
80
267
 
81
268
  ## Architecture
82
269
 
83
- - **Provider-agnostic**: `BaseBillingProvider` → `StripeBillingProvider` (extensible to Paddle, etc.)
270
+ - **Provider-agnostic backend**: `BaseBillingProvider` → `StripeBillingProvider` (extensible to Paddle etc.)
84
271
  - **BillingProviderFactory**: Resolves credentials per-team with global fallback
85
272
  - **BillingService**: Orchestrates sync, checkout, and portal operations
86
- - **ProductOffer**: Supports flat, per_seat, tiered, volume, and usage pricing models
87
-
88
- ## Integration with @startsimpli/auth
273
+ - **ProductOffer**: Supports flat / per_seat / tiered / volume / usage pricing models
274
+ - **Apps register usage adapters** (`BILLING_USAGE_ADAPTERS`) rather than embedding usage logic in the billing app — keeps billing slug-agnostic
89
275
 
90
- The billing package integrates seamlessly with `@startsimpli/auth`:
91
-
92
- ```tsx
93
- import { BillingProvider } from '@startsimpli/billing'
94
- import { useAuth } from '@startsimpli/auth'
276
+ ### Error contract
95
277
 
96
- function App() {
97
- const { tokens } = useAuth()
278
+ Backend's `apps.core.exceptions.custom_exception_handler` ships a standardized JSON shape:
98
279
 
99
- return (
100
- <BillingProvider
101
- apiBaseUrl={process.env.NEXT_PUBLIC_API_URL + '/api/v1'}
102
- authToken={tokens?.access}
103
- >
104
- {/* Your app */}
105
- </BillingProvider>
106
- )
280
+ ```json
281
+ {
282
+ "error": "Your Free plan allows 3 environments. Upgrade to add more.",
283
+ "code": "limit_reached",
284
+ "statusCode": 402,
285
+ "feature_key": "environments",
286
+ "limit": 3,
287
+ "current": 3,
288
+ "timestamp": "2026-05-28T18:11:34.655168Z"
107
289
  }
108
290
  ```
109
291
 
110
- ## Environment Variables
292
+ `@startsimpli/api`'s `ApiException` parses this into `.code` + `.details`. Apps branch on `err.code === 'limit_reached'` to surface the human message verbatim and link to /settings/billing.
293
+
294
+ ---
295
+
296
+ ## Environment variables
111
297
 
112
298
  ```env
113
299
  NEXT_PUBLIC_API_URL=https://api.startsimpli.com
114
300
  ```
115
301
 
116
- Backend requires:
302
+ Backend:
117
303
  ```env
118
- STRIPE_SECRET_KEY=sk_test_...
304
+ STRIPE_SECRET_KEY=sk_test_... # real ~107-char test key from dashboard.stripe.com/test/apikeys
119
305
  STRIPE_WEBHOOK_SECRET=whsec_...
306
+ BILLING_AUTO_SUBSCRIBE_PRODUCT_SLUGS=vault,my-new-app # comma-separated
120
307
  ```
121
308
 
122
- ## Tests
309
+ ---
310
+
311
+ ## Verification
123
312
 
124
313
  ```bash
125
- # Frontend tests (39 tests)
314
+ # Frontend (71 tests across SubscriptionManager, CurrentPlanCard, LimitNudge,
315
+ # useCheckout, useProduct, usePortal, useSubscription, BillingProvider)
126
316
  cd packages/billing
127
- npm test
317
+ pnpm test # or `pnpm vitest run`
318
+ pnpm tsc --noEmit # type-check
128
319
 
129
- # Backend tests (184 tests)
320
+ # Backend (368+ tests in apps/billing/ + apps/vault/)
130
321
  cd start-simpli-api
131
- docker-compose -f docker-compose.local.yml exec -T django pytest apps/billing/tests/ -v
322
+ docker compose -f docker-compose.local.yml exec -T django pytest apps/billing/tests/ -v
323
+
324
+ # systemeval gates (real DB in Docker)
325
+ systemeval test -e billing_auto_subscribe_gate
326
+ systemeval test -e billing_usage_endpoint_gate
132
327
  ```
133
328
 
134
- ## Development
329
+ ### Live verification policy
135
330
 
136
- ```bash
137
- # Type checking
138
- npm run type-check
331
+ Every UI change here must be driven through a real browser via the debugg-ai MCP (`mcp__debugg-ai__check_app_in_browser`) against a running localhost dev server. Type-checks + vitest are NOT proof. See the [MCP-default rule in CLAUDE.md](/CLAUDE.md). The canonical pattern for state-driven UI is to flip the test user's `Subscription.status` via Django shell, then drive the billing page — see `startsim-c6p` close-reasons for examples of every state.
139
332
 
140
- # Watch mode for tests
141
- npm run test:watch
333
+ ---
142
334
 
143
- # Coverage report
144
- npm run test:coverage
145
- ```
335
+ ## Real-world reference
336
+
337
+ `vault-web` is the canonical integration:
338
+ - Backend slug: `vault` in `BILLING_AUTO_SUBSCRIBE_PRODUCT_SLUGS` + `BILLING_USAGE_ADAPTERS`
339
+ - Usage adapter: `apps.vault.billing.usage.collect` (reports `environments` + `secrets_per_env`)
340
+ - Limit enforcement: `apps.vault.billing.limits.enforce_limit` in `EnvironmentViewSet` + `SecretViewSet`
341
+ - Frontend: `<BillingProvider>` in `vault-web/src/app/(dashboard)/layout.tsx` (one provider for the whole dashboard); `<SubscriptionManager productId="vault" />` in `/settings/billing` (34 LOC including the Stripe URL shim); `<LimitNudge>` in `/environments` (env count) and `/environments/[slug]` (per-env secret count); `notifyUsageChanged('vault')` after every env/secret mutation.
342
+
343
+ Copy that pattern. If you find yourself writing app-local billing UI logic, stop — extend the shared layer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/billing",
3
- "version": "0.4.12",
3
+ "version": "0.4.15",
4
4
  "description": "Universal billing integration for StartSimpli Next.js apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -8,7 +8,8 @@
8
8
  ".": "./src/index.ts",
9
9
  "./client": "./src/index.ts",
10
10
  "./server": "./src/server/index.ts",
11
- "./types": "./src/types/index.ts"
11
+ "./types": "./src/types/index.ts",
12
+ "./mock": "./src/mock/index.ts"
12
13
  },
13
14
  "files": [
14
15
  "src",
@@ -17,15 +18,6 @@
17
18
  "publishConfig": {
18
19
  "access": "public"
19
20
  },
20
- "scripts": {
21
- "build": "tsup",
22
- "dev": "tsup --watch",
23
- "type-check": "tsc --noEmit",
24
- "test": "vitest run",
25
- "test:watch": "vitest",
26
- "test:coverage": "vitest run --coverage",
27
- "clean": "rm -rf dist"
28
- },
29
21
  "peerDependencies": {
30
22
  "next": ">=14.0.0",
31
23
  "react": "^18.0.0 || ^19.0.0",
@@ -53,5 +45,14 @@
53
45
  "nextjs",
54
46
  "django",
55
47
  "startsimpli"
56
- ]
57
- }
48
+ ],
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "dev": "tsup --watch",
52
+ "type-check": "tsc --noEmit",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "test:coverage": "vitest run --coverage",
56
+ "clean": "rm -rf dist"
57
+ }
58
+ }