# Changelog

All notable changes to this package will be documented in this file.

## 0.2.0 - 2026-07-24

### Added

- **Runtime-agnostic core**: the service layer now runs on Node.js, Bun, Deno, and Cloudflare Workers using only web-standard primitives. Node-only code paths (filesystem invoice templates, `puppeteer-html-pdf`) are lazy dynamic imports. See `docs/runtime-support.md`.
- **Drizzle database adapter** (`@abshahin/subscriptions/adapters/drizzle`) with support for D1, Turso, and `bun:sqlite`, alongside the existing Prisma adapter.
- **Cache adapters**: `redisCacheAdapter`, `upstashCacheAdapter` (HTTP-based, edge-safe), `kvCacheAdapter` (Cloudflare KV), and `memoryCacheAdapter`. Caching remains optional with a noop fallback.
- **Payment gateway adapters**: `stripeAdapter` (`@abshahin/subscriptions/adapters/stripe`), `paddleAdapter` (`@abshahin/subscriptions/adapters/paddle`), and `lemonSqueezyAdapter` (`@abshahin/subscriptions/adapters/lemonsqueezy`) alongside the existing Moyasar adapter — all built on `fetch` + Web Crypto with no npm dependencies.
- **Framework integrations**: Hono (`@abshahin/subscriptions/integrations/hono`), Next.js (`@abshahin/subscriptions/integrations/next`), and a framework-neutral web-standard handler (`createSubscriptionsHttpHandler`, `@abshahin/subscriptions/integrations/http`) alongside the existing Elysia plugin.
- **Invoice rendering everywhere**: `renderSubscriptionInvoice` accepts a `templateSource` string so invoice HTML renders on any runtime; on Cloudflare Workers, PDFs can be produced with `cloudflarePdfRenderer` (`@abshahin/subscriptions/pdf/cloudflare`) and a Browser Rendering binding.
- **Dual invoice print modes**: invoice endpoints now serve server-generated PDF (`?format=pdf`, via the configured `invoice.pdfRenderer`) or printable HTML for zero-cost client-side printing (`?print=1` toolbar, `?autoPrint=1` auto-opens the browser print dialog) — no puppeteer required. Applies to the framework-neutral HTTP handler (`GET /invoices/:id`) and the Elysia plugin (`GET /invoices/:id/download`); `wrapInvoiceForPrint(html, { autoPrint })` is exported for programmatic use.
- **Events**: `SubscriptionEvent`, the `EventsAdapter` contract, the `withEvents(subs, adapter)` wrapper, and a transactional outbox (`createOutboxEventsAdapter` + `relayOutbox`) for guaranteed, at-least-once delivery. See `docs/events.md`.
- **Dunning**: `options.dunning` with `retryScheduleDays` and a final `action` of `"pause" | "cancel" | "none"`, executed by `subscriptions.processDunning()`. `cancel()` now accepts a `reason`, and `pause()`/`resume()`/`extendTrial()` round out the manual controls. See `docs/dunning.md`.
- **Coupons**: `CouponsService` (`subs.coupons`) with percent/fixed discounts, once/repeating/forever durations, `maxRedemptions`, and `expiresAt`. Validate with `coupons.validate`, preview with `coupons.computeDiscount`, and redeem with `coupons.apply(code, amount)`. See `docs/coupons.md`.
- **Billing features**: sequential invoice numbering (adapter-driven, prefixes configurable via `new InvoicesService(db, { invoiceNumberPrefix, creditNotePrefix })`), per-line-item inclusive/exclusive tax (`taxRate`/`taxInclusive` on invoice line items), credit notes (`invoices.createCreditNote`) and voiding (`invoices.voidInvoice`), multi-currency plan price points (`Plan.prices` + `plans.getPrice`), metered usage reporting (`metered` feature type), and `quantity`/`addOns` data fields on subscriptions. See `docs/billing.md`.
- **Testing utilities** (`@abshahin/subscriptions/testing`): `memoryDatabaseAdapter`, `memoryCacheAdapter`, `fakePaymentGateway`, and `databaseAdapterConformance`/`cacheAdapterConformance` suites for custom adapters. See `docs/testing.md`.
- **Audit logging** (`@abshahin/subscriptions/audit`): `AuditLogAdapter` contract, `createAuditLogger(adapter, { actorId? })` returning an `EventsAdapter` for `withEvents`, and `memoryAuditLogAdapter`. See `docs/audit.md`.

### Changed

- **Build moved from `tsc` to `tsdown`** (Rolldown): all subpath entries and type declarations are emitted in one pass, the invoice template is inlined as a string (no runtime `cp`/filesystem), and `publint` + `attw` run as `bun run check:package`. New subpath exports: `./integrations/{http,hono,next}`, `./adapters/{redis,upstash,cloudflare-kv,drizzle,stripe,paddle,lemonsqueezy}`, `./pdf/{puppeteer,cloudflare}`, `./templates/invoice-utils`, `./testing`, `./audit`, `./core/events`.
- **Web-standard webhook entry**: `subscriptions.handleWebhookRequest(request: Request): Promise<Response>` (configurable `webhookSignatureHeader`/`webhookProvider` options) alongside the existing `handleWebhook`.

## 0.1.4 - 2026-06-21

### Fixed

- **Payments: stopped a double-charge on renewal webhooks.** `handleWebhook` previously called `renew()` (which charges the saved token) while handling a `payment.paid` event, charging the customer a second time for a payment that already succeeded. Renewals triggered from a successful-payment webhook now skip the charge and are recorded as paid externally.
- **Invoices: standardized stored amounts on major/display units everywhere.** The Elysia `/subscribe` and `/change-plan` routes stored amounts in the smallest unit (`× 100`) while the service layer and documented API used major units, and the invoice download endpoint always divided by 100 — so service-created invoices (e.g. cron renewals) rendered as `0.49` instead of `49`. All invoice creation and rendering now use major units consistently.
- **Invoices: eliminated duplicate invoice creation.** The Elysia `/subscribe` and `/change-plan` routes created their own invoices on top of the ones the service layer already creates, producing two invoices per paid subscribe/upgrade. The service is now the single source of truth: the routes pass the gateway payment ID (and paid status) into `create()`/`changePlan()` instead of creating a second invoice.
- **Webhooks: idempotent payment processing.** A successful-payment webhook is now skipped when an invoice for the same gateway payment already exists, preventing duplicate renewals/invoices when both a cron job and a webhook observe the same charge. Backed by an optional `invoices.findByGatewayInvoiceId` adapter method.
- **Webhooks: resolve subscriber from `subscriberId` (falling back to `tenantId`).** Charges issued by the SDK attach `subscriberId`; the handler previously only read `tenantId`, so renewal/cancel branches never fired for SDK-initiated charges.
- **Security: Moyasar webhook verification now fails closed.** When a `webhookSecret` is configured, a missing or empty signature is rejected instead of being silently accepted.
- **Security: Elysia webhook route now reads the `x-moyasar-signature` header**, so Moyasar signatures are actually verified (previously only `stripe-signature` / `x-webhook-signature` were checked, silently skipping verification for Moyasar).
- **Usage: atomic, floor-safe decrement in the Prisma adapter.** Replaced the read-then-write decrement (which lost concurrent updates despite the interface promising atomicity) with a single guarded atomic update that never drops below zero.
- **Usage: deterministic monthly periods in UTC.** Usage period boundaries were computed in the server's local timezone, shifting the billing window by region/DST; they are now computed in UTC.
- **Proration: guarded against divide-by-zero** for zero-length billing periods and centralized the proration math used by `changePlan` (removing three divergent copies).
- **Build: the package now type-checks and builds without the optional `puppeteer-html-pdf` dependency installed**, via an ambient type declaration for the dynamically imported module.

### Added

- `SubscriptionsService.recordPaymentFailure(subscriberId, message)` to record payment failures through the service (keeping caches consistent) instead of writing to the database directly.
- Optional `DatabaseAdapter.invoices.findByGatewayInvoiceId` for idempotent webhook handling.
- `RenewSubscriptionOptions.paidExternally` and `gatewayInvoiceId` so a renewal can be recorded as already paid by an external flow.
- `create()` now accepts `paidExternally` and `gatewayInvoiceId`, and `changePlan()` accepts `gatewayInvoiceId`, so externally collected payments are recorded on the service-created invoice (no duplicate).

## 0.1.2 - 2026-05-06

### Added

- Rebuilt `src/templates/subscription-invoice.hbs` with a simple black-and-white invoice layout.
- Added `tests/generate-test-invoices.ts` for generating sample invoice HTML fixtures.
- Added `test:invoices` script to `package.json` for easy invoice fixture generation.

## 0.1.1 - 2026-04-04

### Added

- Added invoice HTML rendering helpers and Node.js PDF generation support from the package root.
- Added Elysia invoice download support using the bundled Handlebars template and optional `puppeteer-html-pdf` dependency.

### Fixed

- Corrected webhook invoice amount handling to avoid double-division of gateway values before invoice creation.
- Hardened webhook signature validation with a timing-safe verification flow and malformed hex rejection.
- Rehydrated cached subscription and plan `Date` fields so cached reads preserve `Date` behavior.
- Made permission usage increments rollback safely when post-increment validation fails.
- Tightened plan deletion rules so plans with active subscribers or pending downgrades cannot be removed.
- Updated subscription renewal, resume, and lifecycle flows to handle paused subscriptions, expired trials, pending downgrades, and invoice creation consistently.
- Fixed resumed subscriptions so billing periods recalculate correctly after paused subscriptions expire.
- Fixed same-plan previews and preserved generic typing in plan change result APIs.
- Validated numeric feature limits as finite integers and preserved calendar-month billing interval calculations.
- Hardened subscription lifecycle cache invalidation and Redis cron locking so usage state updates cannot be skipped or cause concurrent scheduler execution.

### Changed

- Replaced public `Buffer` webhook payload types with `Uint8Array` to keep the public API runtime-neutral.
- Updated the Moyasar adapter to accept `string | Uint8Array` webhook payloads and decode binary payloads with `TextDecoder`.
- Changed PDF generation helpers to return `Uint8Array` instead of `Buffer`.
- Moved `puppeteer-html-pdf` to an optional peer dependency and documented invoice PDF generation as Node.js-only.
- Added `elysia` as a development dependency so the optional integration typechecks cleanly in this workspace.
- Switched package exports to compiled `dist` output, added `main`/`types` entrypoints, and kept the invoice template bundled in publish output.
- Standardized package internals on explicit `.js` import paths and expanded public entrypoint exports for services, adapters, and invoice helpers.
- Moved invoice template loading to `node:fs/promises`.
- Added cache rehydration for `Date` fields on cached subscriptions and plans so deserialized reads preserve date behavior.
- Expanded Prisma adapter capabilities to detect active subscribers, detect pending downgrades, and query invoices by subscriber.
- Tightened plan deletion rules so plans with active or pending subscribers cannot be removed.
- Updated subscription lifecycle flows to create invoices for new paid subscriptions and upgrades, preserve pending downgrade metadata, and keep same-plan previews stable.
- Changed usage increments to increment first, validate after, and roll back safely when limits are exceeded or validation fails.
- Expanded the database adapter and core types for plan filtering, invoice lookups, tenant-scoped usage tracking, and pending downgrade awareness.
- Updated subscription plan changes to support proration, verified payment tokens, pending 3DS flows, and upgrade invoices.
- Refreshed the README and adapter/integration docs to match the current service and runtime behavior.

### Notes

- Existing Node.js callers that pass `Buffer` continue to work because `Buffer` extends `Uint8Array`.
- Core subscription services remain runtime-neutral, while invoice template and PDF generation continue to require a Node.js runtime.