{"_id":"@12-apps/billing","_rev":"4-c72cd22f81bf65ec14db7b9540b8cca9","name":"@12-apps/billing","dist-tags":{"latest":"1.2.0"},"versions":{"1.0.0":{"name":"@12-apps/billing","version":"1.0.0","license":"MIT","_id":"@12-apps/billing@1.0.0","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"homepage":"https://github.com/12-apps/shared-packages#readme","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"dist":{"shasum":"cc0828df42432c5f0999e85528399e192115610d","tarball":"https://registry.npmjs.org/@12-apps/billing/-/billing-1.0.0.tgz","fileCount":16,"integrity":"sha512-zf1svDwhsoGXAy8cfnebjs4KrpxGaOKr9hF0kIm7yCoxYaBpIb+X7c/PJlV21N7BF8XvCNR+sRaTvW9e1/fH/g==","signatures":[{"sig":"MEUCIQD7tm/HtWLI6WXoQZH8ZhNekKxD401mmRoXCod9B+YxyQIgTPwbR116h5TJNVb5Pf8cHGS/mL1PoVqVNY+jtFK6TgQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":72779},"type":"module","engines":{"node":">=22.0.0"},"exports":{".":"./src/index.ts","./server":"./src/server/index.ts","./manifest":"./src/manifest/index.ts","./package.json":"./package.json","./manifest/server":"./src/manifest/server.ts"},"gitHead":"dc9e6aa17ec6dd11045afd8e31fb773b9d869cb1","scripts":{"lint":"eslint src --max-warnings 0","test":"node ../../scripts/vitest-with-teardown.mjs run","clean":"rm -rf node_modules coverage","typecheck":"tsc --noEmit","test:watch":"vitest watch","check-types":"tsc --noEmit"},"_npmUser":{"name":"tigredonorte","email":"tigredonorte3@gmail.com"},"repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/billing"},"_npmVersion":"12.0.2","description":"Subscription billing: the period arithmetic a renewal anchor survives, the lifecycle a read ages against its own dates, the collection retry policy that never re-presents a card the issuer refused for funds, and the card-on-file surface over a provider va","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.1","vitest":"^3.2.4","typescript":"^5.9.2","@types/node":"^22.10.6","@12-apps/wiring":"^1.5.0","@12-apps/eslint-config":"^1.21.0","@12-apps/payments-backend":"^4.14.0","@12-apps/typescript-config":"^1.20.0","eslint-plugin-test-flakiness":"^1.4.0"},"peerDependencies":{"@12-apps/wiring":">=1.3.0","@12-apps/payments-backend":">=4.14.0"},"peerDependenciesMeta":{"@12-apps/wiring":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/billing_1.0.0_1787318498895_0.624030219541696","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@12-apps/billing","version":"1.0.1","license":"MIT","_id":"@12-apps/billing@1.0.1","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"homepage":"https://github.com/12-apps/shared-packages#readme","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"dist":{"shasum":"a81d077171c845118cb750c2ba8ab41baec6be4d","tarball":"https://registry.npmjs.org/@12-apps/billing/-/billing-1.0.1.tgz","fileCount":16,"integrity":"sha512-nyC7nWGta4WT0302jkMyix7HzpMFNmRVSfpAz+xd6S3jMJJhq43xNMDmoWTeKrl7Hec+MLHpJWV/8RN4JyUGew==","signatures":[{"sig":"MEUCIADvTbovl0g3Sqr55TTrm9VURe4EnuylQwkd7cFMPR04AiEA+mZX4+vC4GAg2Aa4mO8U4NIFxeP7pUcAVvysPSmp3Ro=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@12-apps%2fbilling@1.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":72779},"type":"module","engines":{"node":">=22.0.0"},"exports":{".":"./src/index.ts","./server":"./src/server/index.ts","./manifest":"./src/manifest/index.ts","./package.json":"./package.json","./manifest/server":"./src/manifest/server.ts"},"gitHead":"a2984f6ee51ca1733af60fa6b2e2d554313c9e96","scripts":{"lint":"eslint src --max-warnings 0","test":"node ../../scripts/vitest-with-teardown.mjs run","clean":"rm -rf node_modules coverage","typecheck":"tsc --noEmit","test:watch":"vitest watch","check-types":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d6f76a75-2785-41e2-9016-02395b9794e1"}},"repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/billing"},"_npmVersion":"12.0.2","description":"Subscription billing: the period arithmetic a renewal anchor survives, the lifecycle a read ages against its own dates, the collection retry policy that never re-presents a card the issuer refused for funds, and the card-on-file surface over a provider va","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.1","vitest":"^3.2.4","typescript":"^5.9.2","@types/node":"^22.10.6","@12-apps/wiring":"^1.7.0","@12-apps/eslint-config":"^1.21.1","@12-apps/payments-backend":"^4.15.0","@12-apps/typescript-config":"^1.20.1","eslint-plugin-test-flakiness":"^1.4.0"},"peerDependencies":{"@12-apps/wiring":">=1.3.0","@12-apps/payments-backend":">=4.14.0"},"peerDependenciesMeta":{"@12-apps/wiring":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/billing_1.0.1_1787337669726_0.3983365701270918","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@12-apps/billing","version":"1.1.0","license":"MIT","_id":"@12-apps/billing@1.1.0","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"homepage":"https://github.com/12-apps/shared-packages#readme","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"dist":{"shasum":"505651f37e6562ec95f7c2d8b240a6a44e48c98a","tarball":"https://registry.npmjs.org/@12-apps/billing/-/billing-1.1.0.tgz","fileCount":16,"integrity":"sha512-dYKDSYRd8HbW6ygyrI6mYOt0IVGIL4xY2gllQ7U7cnqGAZw/JqNdAASxpraRqjfbxrcFSH5S8F9VCjLcrmH/lg==","signatures":[{"sig":"MEYCIQDYnQTnsZ3aYFp2S8g1GwjpFGMfJi5d3Jm5vxnNuxeHLQIhAJWpNMAc+wfbRDRPZRzFaxO2EQR7IPyYlSxvIrGYLjCW","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@12-apps%2fbilling@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":72804},"type":"module","engines":{"node":">=22.0.0"},"exports":{".":"./src/index.ts","./server":"./src/server/index.ts","./manifest":"./src/manifest/index.ts","./package.json":"./package.json","./manifest/server":"./src/manifest/server.ts"},"gitHead":"4714bd10c901bcc51544843d5d6091cc1509e649","scripts":{"lint":"eslint src --max-warnings 0","test":"node ../../scripts/vitest-with-teardown.mjs run","clean":"rm -rf node_modules coverage","typecheck":"tsc --noEmit","test:watch":"vitest watch","check-types":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d6f76a75-2785-41e2-9016-02395b9794e1"}},"repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/billing"},"_npmVersion":"12.0.2","description":"Subscription billing: the period arithmetic a renewal anchor survives, the lifecycle a read ages against its own dates, the collection retry policy that never re-presents a card the issuer refused for funds, and the card-on-file surface over a provider va","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.1","vitest":"^3.2.4","typescript":"^5.9.2","@types/node":"^22.10.6","@12-apps/wiring":"^1.14.0","@12-apps/eslint-config":"^1.22.0","@12-apps/payments-backend":"^4.21.0","@12-apps/typescript-config":"^1.21.0","eslint-plugin-test-flakiness":"^1.4.0"},"peerDependencies":{"@12-apps/wiring":">=1.3.0","@12-apps/payments-backend":">=4.14.0"},"peerDependenciesMeta":{"@12-apps/wiring":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/billing_1.1.0_1787617823082_0.8852360881323784","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@12-apps/billing","version":"1.2.0","type":"module","sideEffects":false,"description":"Subscription billing: the period arithmetic a renewal anchor survives, the lifecycle a read ages against its own dates, the collection retry policy that never re-presents a card the issuer refused for funds, and the card-on-file surface over a provider va","exports":{".":"./src/index.ts","./server":"./src/server/index.ts","./manifest":"./src/manifest/index.ts","./manifest/server":"./src/manifest/server.ts","./package.json":"./package.json"},"scripts":{"clean":"rm -rf node_modules coverage","test":"node ../../scripts/vitest-with-teardown.mjs run","test:watch":"vitest watch","lint":"eslint src --max-warnings 0","check-types":"tsc --noEmit","typecheck":"tsc --noEmit"},"peerDependencies":{"@12-apps/payments-backend":">=4.14.0","@12-apps/wiring":">=1.3.0"},"peerDependenciesMeta":{"@12-apps/wiring":{"optional":true}},"devDependencies":{"@12-apps/eslint-config":"^1.22.0","@12-apps/payments-backend":"^4.22.0","@12-apps/typescript-config":"^1.21.0","@12-apps/wiring":"^1.14.0","@types/node":"^22.10.6","eslint":"^9.39.1","eslint-plugin-test-flakiness":"^1.4.0","typescript":"^5.9.2","vitest":"^3.2.4"},"engines":{"node":">=22.0.0"},"license":"MIT","publishConfig":{"registry":"https://registry.npmjs.org","access":"public"},"repository":{"type":"git","url":"git+https://github.com/12-apps/shared-packages.git","directory":"packages/billing"},"gitHead":"43e14eaf802e483ef896d8d7d0c17e89efb663bb","_id":"@12-apps/billing@1.2.0","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"homepage":"https://github.com/12-apps/shared-packages#readme","_nodeVersion":"24.19.0","_npmVersion":"12.0.2","dist":{"integrity":"sha512-MtgEB4BsbX9F52pj9qWiJSoO7Am9kDpI5ISWKKg79ZGm4SLoorKjp2AGVXxzHCi+DmaCWqH0Z5laLggXIzEOFQ==","shasum":"8f5a146cab14c1d724dbda784283550778c6203f","tarball":"https://registry.npmjs.org/@12-apps/billing/-/billing-1.2.0.tgz","fileCount":16,"unpackedSize":75172,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@12-apps%2fbilling@1.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDAscRrbWaPFFFgmD6ro8xrUFz95PYWTYshLU7RZT1J4QIgeBq3vs1p/1Cy9Q15ic1nlvm9Cwlr6pZc7gsp5QHEyAI="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d6f76a75-2785-41e2-9016-02395b9794e1"}},"directories":{},"maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/billing_1.2.0_1787679466451_0.6908991727499094"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-21T13:21:38.619Z","modified":"2026-08-25T17:37:47.915Z","1.0.0":"2026-08-21T13:21:39.043Z","1.0.1":"2026-08-21T18:41:09.864Z","1.1.0":"2026-08-25T00:30:23.226Z","1.2.0":"2026-08-25T17:37:46.596Z"},"bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"license":"MIT","homepage":"https://github.com/12-apps/shared-packages#readme","repository":{"type":"git","url":"git+https://github.com/12-apps/shared-packages.git","directory":"packages/billing"},"description":"Subscription billing: the period arithmetic a renewal anchor survives, the lifecycle a read ages against its own dates, the collection retry policy that never re-presents a card the issuer refused for funds, and the card-on-file surface over a provider va","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"readme":"# @12-apps/billing\n\nSubscription billing for a platform that charges its own customers: the period\narithmetic a renewal anchor survives, the lifecycle a read ages against its own\ndates, the retry policy that decides whether a failed collection is worth\nanother attempt, and the card-on-file surface over a provider vault.\n\nEvery number, table and sentence with a commercial opinion in it is **required\nconfig**. The package owns the mechanism and none of the policy — see\n[What stays host-owned](#what-stays-host-owned).\n\n## Two entries, because bundles are physics\n\n| entry | contains | dependencies |\n|---|---|---|\n| `@12-apps/billing` | periods, the lifecycle vocabulary and its ageing, the entitlement seam | **none** |\n| `@12-apps/billing/server` | the retry policy, the cycle collection binding, the card vault, the HTTP surface | `@12-apps/payments-backend` (peer) |\n\nA browser that needs to say \"this account is past due\" imports the root entry\nand pays nothing for the money path. The split is not taste: the payments\npackage is server code, and one shared entry importing it would drag\n`node:crypto` into every SPA that ever read a billing status.\n\n## The isomorphic half\n\n```ts\nimport { createBillingLifecycle, periodEnd, anchorDayOf } from \"@12-apps/billing\";\n\nconst lifecycle = createBillingLifecycle({ graceDays: 7, suspendAfterDays: 30 });\nlifecycle.effectiveStatus(row.status, row, new Date()); // \"past_due\"\n```\n\n**The anchor day is an input.** A customer who subscribes on the 31st has no\n31st in February; clamping is the only sane answer, but clamping *and then\nadvancing from the clamped date* walks their billing day permanently earlier\n(31 Jan → 28 Feb → 28 Mar → …). `anchorDayOf(subscribedAt)` is read once from\nthe subscribe instant and survives every clamp.\n\n**Every read ages the row.** A sweep that stops running must not silently hand\nout free service, so `effectiveStatus` takes the harsher of (stored,\nimplied-by-dates). The sweep then only ever persists what a read would already\nhave concluded — it is a materialization, not the source of truth.\n\n**`createBillingLayer` is the whole translation into a gate.** It turns one\nsubscription row into `{ planKey, plan, status }`, or `null` when billing has\nno opinion at all. Both mapping tables are yours; see below.\n\n## The server half\n\n```ts\nimport { createChargePolicy, createSubscriptionCollection, createCardVault } from \"@12-apps/billing/server\";\n\nconst policy = createChargePolicy({\n  maxAttempts: 4,\n  backoffMs: [30 * 60_000, 2 * 60 * 60_000, 8 * 60 * 60_000],\n  stopWithoutNewCard: [\"INSUFFICIENT_FUNDS\"],\n});\npolicy.decideAfterDecline(snapshot, attempts); // { kind: \"RETRY\", delayMs } | STOP | ALERT | ABORT | DONE\n```\n\n`decideAfterError` and `decideAfterDecline` ask three questions in order — did\nit become a charge, did the provider say no and why, and is another attempt\npossible at all — against `@12-apps/payments-backend`'s taxonomy rather than a\nsecond copy of it, so \"is this safe to retry\" has exactly one answer.\n\nThe vault is two calls and a removal. `begin` opens a provider session for one\nsubscription; `complete` is reached from the browser, so its session id is\nattacker-supplied — what makes it safe is that the `reference` handed to the\nadapter comes from the host's own subscription row through the `findTarget`\nport, never from the request. `forgetAll` detaches **every** pointer the owner\nholds, not the one on the screen: a card can live at yesterday's acquirer as\nwell as today's.\n\nNothing here has a parameter a card could travel in. The number goes from the\ncardholder's keyboard to the provider's SDK to the provider; what crosses these\nseams is an opaque vault id and the display metadata the provider shared.\n\n## Wiring\n\nThe package is a `@12-apps/wiring` producer. It declares `http` and nothing\nelse, and each absence is deliberate:\n\n| capability | declared | why |\n|---|---|---|\n| `http` | ✅ | the card-on-file surface, four endpoints |\n| `observability` | ✅ | namespace `billing` — the money path is the last place a failure may file nowhere |\n| `db` | ❌ | subscriptions, cycles and instruments all carry foreign keys into a host table; a package partial cannot declare a relation into a table it does not own |\n| `permissions` | ❌ | who may put a card on file is a role decision, not a permission id this package could name for every host |\n| `notifications` | ❌ | the one notice this domain sends is entirely host copy |\n| `mcp` | ❌ | a surface that writes a payment instrument stays in a browser, behind a human |\n| `env` | ❌ | the package reads no environment variable; everything arrives as config |\n\n```ts\nimport { billingManifest } from \"@12-apps/billing/manifest\";\nimport { billingServerManifest } from \"@12-apps/billing/manifest/server\";\nimport { createWiringHost } from \"@12-apps/wiring/consumer\";\n\nconst host = createWiringHost({ name: \"web\", kind: \"server\", ports: { loggerFor } });\nhost.adoptServer({\n  manifest: billingManifest,\n  server: billingServerManifest,\n  bindings: { http: { mountPath: \"/api/admin/:tenantSlug/subscription\", config: { /* … */ } } },\n});\nconst wired = host.assemble();\n```\n\n`@12-apps/wiring` is a **type-only** devDependency here; the producer\nassertions run in this package's own suite, so a malformed manifest fails\nbefore any host sees it.\n\n## What stays host-owned\n\nEverything below is a required argument, with no default to fall back to\nsilently:\n\n- **the two lifecycle windows** — how long before a late payer is penalised,\n  and how long before they are suspended. One platform chases for a week and\n  suspends after a month; the next gives a fortnight and never suspends at all.\n- **the retry ladder, the attempt cap, and which declines are chased rather\n  than retried.** The payments package refuses these by name — \"retry ladders,\n  grace windows, when to ask for a different card, is each host's commercial\n  policy\" — and the same boundary holds one layer up.\n- **both gate tables** — which lifecycle each billing state reaches the gate\n  as, and which states keep the tier they froze.\n- **every sentence and every status code** the HTTP surface can answer with. A\n  default in the origin platform's language reads as finished to the next\n  platform right up until it reaches a user.\n- **the guard.** Nothing here authenticates anybody; the host mounts these\n  behind its own resolution and hands the resolved owner in as the actor.\n- **the database**, through the ports in `./server` — the cycle rows, the\n  instrument rows and the subscription lookup.\n\nEach one is checked at construction, not at the renewal that needed it: a\nmis-stated policy throws `BillingConfigError` at boot, where an operator is.\n\n## Adopting\n\nSee [ADOPTING.md](./ADOPTING.md).\n","readmeFilename":"README.md"}