## Problem When the `EntitlementGuard` refuses a request with **402** (team is over its free allowance / spending cap, or has no subscription to bill), the handler never runs — so it must not charge. But it did: the guard (order **1100**) ran *after* the charge interceptor (**1000**), so `openProcess` had already written the charge before the 402, and `afterCompletion` then billed it as "customer paid for the attempt" — a ledger debit, and a **Stripe meter for a subscribed-over-cap team**. ## Fix **Run the guard first** (order **900**, before the charge interceptor at 1000). Spring runs interceptors in ascending order on the way in and **skips a later interceptor's `preHandle` (and `afterCompletion`) entirely once an earlier one returns `false`** — so a refused request short-circuits with its 402 *before* the charge interceptor runs at all. A blocked request never opens a process, materialises inputs, or writes a charge. This replaces the earlier attribute-flag + `afterCompletion`-refund approach with a simpler reorder (per review): the no-charge-on-block guarantee is now **structural**, and it also avoids the wasted open-then-refund churn (no temp-file write, no debit/refund pair) for refused requests. ### Why the reorder is safe - `EntitlementGuard` reads no `PaygChargeInterceptor` state and has **no `afterCompletion`** (only `preHandle`), so reverse-order teardown is a non-issue. - The legacy `UnifiedCreditInterceptor` (default order **0**, and only registered under the `legacy-credits` profile) still runs first, so any legacy rejection wins. - For *admitted* requests both interceptors still run (guard then charge) — behaviour is unchanged; only refused requests now short-circuit before the charge. ## Tests `PaygWebMvcConfigTest` locks the `ENTITLEMENT_GUARD_ORDER < INTERCEPTOR_ORDER` invariant (if it's ever reversed, refused requests would bill again — this fails first). Existing `EntitlementGuardTest` already proves the guard returns 402 on a degraded/billable request. `:saas:test` + spotless green. ## Related (separate, in progress) The **fail-cleanly + fire-the-modal** half (suppress the error toast, trigger the subscribe/raise-cap modal via the existing `subscribed`/`category` signal — catching the 402 centrally so direct API usage is handled, and propagating the entitlement reason through the async policy-run status) lands **with Ethan's modal** so we don't remove the toast before there's a popup to replace it.
Stirling PDF - The Open-Source PDF Platform
Stirling PDF is a powerful, open-source PDF editing platform. Run it as a personal desktop app, in the browser, or deploy it on your own servers with a private API. Edit, sign, redact, convert, and automate PDFs without sending documents to external services.
Key Capabilities
- Everywhere you work - Desktop client, browser UI, and self-hosted server with a private API.
- 50+ PDF tools - Edit, merge, split, sign, redact, convert, OCR, compress, and more.
- Automation & workflows - No-code pipelines direct in UI with APIs to process millions of PDFs.
- Enterprise‑grade - SSO, auditing, and flexible on‑prem deployments.
- Developer platform - REST APIs available for nearly all tools to integrate into your existing systems.
- Global UI - Interface available in 40+ languages.
For a full feature list, see the docs: https://docs.stirlingpdf.com
Quick Start
docker run -p 8080:8080 docker.stirlingpdf.com/stirlingtools/stirling-pdf
Then open: http://localhost:8080
For full installation options (including desktop and Kubernetes), see our Documentation Guide.
Resources
Support
- Community Discord
- Bug Reports: Github issues
Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
This project uses Task as a unified command runner for all build, dev, and test commands. Run task install to get started, or see the Developer Guide for full details.
For adding translations, see the Translation Guide.
License
Stirling PDF is open-core. See LICENSE for details.

