@ssheleg/sheleg-dev 0.4.3 → 0.5.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,63 @@ All notable changes to this project are documented here.
4
4
  Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
  Versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.5.0 — 2026-08-14
8
+
9
+ Two gaps found by reading a web2app funnel guide against the pack. Both are about
10
+ the same seam: the payment layer is where a funnel's money and its measurement
11
+ actually live, and both skills were treating it as somebody else's concern.
12
+
13
+ ### Added
14
+
15
+ - **`stripe-billing` → `references/provider-concentration.md`.** The skill covered
16
+ dunning *inside* Stripe and never dependence *on* Stripe. The new reference is
17
+ about the seam rather than about adding a second provider: the entitlement keyed
18
+ to your own user id with provider ids as fields, the customer identity resolved
19
+ before checkout, and the reconciliation job as the only number in the system
20
+ that is not your own telemetry. `SKILL.md` → **Reconciliation** already excluded
21
+ "rows that were never Stripe's", which is that seam half-built and was never
22
+ named as one.
23
+
24
+ It also declines the obvious answer: a generic `PaymentProvider` interface
25
+ written against one provider encodes that provider's model and gets rewritten
26
+ for the second one anyway. The seam is in the data, not in the code shape.
27
+
28
+ **Automatic card updates are documented with their two limits**, both taken from
29
+ Stripe's own text rather than from recall: coverage varies by country, and Stripe
30
+ states it is **not possible to identify which cards support it** — so any plan
31
+ that assumes a reissue will be caught has an unmeasurable branch. Handle
32
+ `payment_method.automatically_updated` and `payment_method.updated`, write the
33
+ new expiry and last four to your own records, and review anything keyed on
34
+ `fingerprint`, which moves when the card number does.
35
+
36
+ And the distinction the pack had no field for: a subscription that ended because
37
+ someone chose to leave, versus one that ended because a bank reissued a number.
38
+ A single `canceled` status for both is how a recoverable failure gets a farewell
39
+ email.
40
+
41
+ ### Changed
42
+
43
+ - **`ad-tracking` now says where a purchase event comes from.** Deduplication by
44
+ `event_id` was already covered; what was missing is the reason the server side
45
+ is not optional. Every other event on the list is something a person did in a
46
+ tab. A purchase is the outcome of a charge that succeeded inside a payment
47
+ system, and the only thing that knows it succeeded is the provider's webhook —
48
+ so firing it from the thank-you page fires it from the one place with no idea
49
+ whether the charge cleared, which is why a browser-only purchase count has an
50
+ event for every session that reached the page and a refund for none of them.
51
+
52
+ The ordering that follows: the webhook handler is the source of truth and sends
53
+ the server-side conversion; the browser event stays for the signals only it
54
+ carries and is subordinate on conflict; and everything the browser loses, it
55
+ loses closest to the money, so the bias is downward by an amount that cannot be
56
+ measured from inside the browser. Reconcile against the provider's count of
57
+ succeeded charges, which is the only external check on this event that is not
58
+ itself telemetry.
59
+
60
+ Both `SKILL.md` bodies stay inside the 500-line progressive-disclosure budget:
61
+ `stripe-billing` went to 502 lines when this was written inline, which is what
62
+ moved the full treatment into a reference and left a pointer behind.
63
+
7
64
  ## v0.4.3 — 2026-08-14
8
65
 
9
66
  A red `validate` could not stop a publish, and this repository proved it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssheleg/sheleg-dev",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Six integration skills for the layer a product reaches once it has users: Stripe subscription billing, crypto payments, ad and conversion tracking under Consent Mode v2, Google sign-in and the wider Google auth surface, and Core Web Vitals. This package is the installer CLI.",
5
5
  "bin": {
6
6
  "sheleg-dev": "bin/sheleg-dev.js"
@@ -41,5 +41,8 @@
41
41
  "claude-code",
42
42
  "cursor",
43
43
  "skill"
44
- ]
44
+ ],
45
+ "scripts": {
46
+ "test": "python3 test/validate.py"
47
+ }
45
48
  }
@@ -2,7 +2,7 @@
2
2
  "name": "sheleg-dev",
3
3
  "displayName": "Sheleg Dev",
4
4
  "description": "The integration layer a product needs once it has users: Stripe subscription billing reconciled into your own database, crypto payments that survive over-payment and duplicate webhooks, ad and conversion tracking under Consent Mode v2, Google sign-in with the account-linking guards, the wider Google auth surface, and Core Web Vitals work that moves the score.",
5
- "version": "0.4.3",
5
+ "version": "0.5.0",
6
6
  "author": {
7
7
  "name": "ssheleg",
8
8
  "url": "https://x.com/sshlg93"
@@ -319,6 +319,36 @@ from both the browser and the server (Conversions API, server-side GA4) must
319
319
  carry the SAME `event_id` / `transaction_id`, or both are counted. Test it by
320
320
  reloading the thank-you page: a purchase that increments twice is a
321
321
  deduplication bug, not a tracking success.
322
+
323
+ **A purchase is not a browser event, and that decides where it comes from.**
324
+ Every other event on the list is something a person did in a tab, so the browser
325
+ is its natural source. A purchase is the outcome of a charge that succeeded
326
+ inside a payment system, and the only thing that knows it succeeded is the
327
+ provider's webhook. Firing it from the thank-you page means firing it from the
328
+ one place that has no idea whether the charge cleared — which is why a purchase
329
+ event exists for every session that reached the page and a refund exists for
330
+ none of them.
331
+
332
+ The consequence is an ordering, not a preference:
333
+
334
+ 1. **The webhook handler is the source of truth for the purchase.** It writes the
335
+ entitlement, and the same handler sends the server-side conversion. If the
336
+ handler is where the money is recorded, it is where the event belongs.
337
+ 2. **The browser event stays, and it stays subordinate.** Keep it for the
338
+ attribution signals only the browser carries (click ids, the consent state,
339
+ the session), give it the `event_id` the server will reuse, and treat the
340
+ server as authoritative when they disagree.
341
+ 3. **Everything the browser loses, it loses closest to the money.** Blockers, a
342
+ mobile browser terminating a tab on redirect, a payment provider's hosted page
343
+ ending the session before the return leg — each of them removes the *last*
344
+ step preferentially, so the browser-only purchase count is biased low by an
345
+ amount you cannot measure from inside the browser.
346
+
347
+ Reconcile against the provider rather than the analytics dashboard: the count of
348
+ succeeded charges in a period is a number the payment system will give you
349
+ exactly, and it is the only external check on this event that is not itself
350
+ telemetry. See `stripe-billing` → **Depending on one provider** for what else
351
+ that reconciliation surfaces.
322
352
  ## Content Security Policy
323
353
 
324
354
  Read `references/performance-security.md` → **Content Security Policy** for the
@@ -43,6 +43,7 @@ Deep material, loaded on demand:
43
43
  | [`references/subscription-lifecycle.md`](references/subscription-lifecycle.md) | implementing checkout, verify, renewal, seats, plan change, trials, cancellation, clawback |
44
44
  | [`references/price-integrity.md`](references/price-integrity.md) | pricing lives in more than one file, or an advertised price must be proved against Stripe |
45
45
  | [`references/testing-and-local-dev.md`](references/testing-and-local-dev.md) | local webhooks, fixtures, mocks, and a suite that would actually catch a money defect |
46
+ | [`references/provider-concentration.md`](references/provider-concentration.md) | growing revenue, opening a second market, separating involuntary churn, or asking what happens if the payment account is limited |
46
47
 
47
48
  ---
48
49
 
@@ -395,6 +396,29 @@ units: convert once, at the boundary, and compare in integer cents —
395
396
 
396
397
  ---
397
398
 
399
+ ## Depending on one provider
400
+
401
+ Everything above assumes Stripe is the payment system. That is a reasonable
402
+ default and it is also a concentration: the account that runs the charges can be
403
+ limited or closed, and there is no second route unless one was built before it
404
+ was needed. Three things make adding a second provider cheap later and are
405
+ nearly free now — the entitlement keyed to **your** user id with provider ids as
406
+ fields, the customer identity resolved before checkout, and the reconciliation
407
+ job above, which is the only number in the system that is not your own telemetry.
408
+
409
+ At volume, three leaks that do not look like payment problems: approval rates
410
+ differing by country, renewals failing on reissued cards, and a `canceled`
411
+ status that cannot tell a decision from a bank reissue. Stripe's **automatic
412
+ card updates** cover some of the second one, with two limits worth knowing —
413
+ coverage varies by country and Stripe states you **cannot identify which cards
414
+ it covers**, so handle `payment_method.automatically_updated` and
415
+ `payment_method.updated` rather than assuming the save.
416
+
417
+ Full treatment, including the involuntary-versus-voluntary table:
418
+ [`references/provider-concentration.md`](references/provider-concentration.md).
419
+
420
+ ---
421
+
398
422
  ## Local development
399
423
 
400
424
  **Skip Stripe.** A `SKIP_BILLING=true` branch that creates an obviously fake
@@ -0,0 +1,120 @@
1
+ # Depending on one provider
2
+
3
+ **Load this when** revenue is growing, when a second market is being opened, when
4
+ involuntary churn needs separating from voluntary, or when someone asks what
5
+ happens if the payment account is limited.
6
+
7
+ Everything in `SKILL.md` assumes Stripe is the payment system. That is a
8
+ reasonable default and it is also a concentration: the account that runs the
9
+ charges can be limited or closed, and when it is, there is no second route unless
10
+ one was built before it was needed. The seam that makes a second route possible
11
+ costs almost nothing while there is one provider, and becomes a migration once
12
+ there are two.
13
+
14
+ ## Contents
15
+
16
+ - [Keep the seam, not the second provider](#keep-the-seam-not-the-second-provider)
17
+ - [Three ways this costs money at volume](#three-ways-this-costs-money-at-volume)
18
+ - [Automatic card updates, and what they do not tell you](#automatic-card-updates-and-what-they-do-not-tell-you)
19
+ - [Involuntary is not voluntary](#involuntary-is-not-voluntary)
20
+ - [Checklist](#checklist)
21
+
22
+ ## Keep the seam, not the second provider
23
+
24
+ Running two providers from day one is two webhook surfaces, two reconciliations,
25
+ two test matrices and two sets of tax obligations, for a problem most products
26
+ never have. Adding the second one later is cheap **if** three things were true
27
+ from the start, and expensive if they were not.
28
+
29
+ - **The entitlement is yours and the charge is theirs.** Your own row, keyed to
30
+ your own user id, with the provider's ids as fields on it rather than as its
31
+ primary key. `SKILL.md` → **Reconciliation** already assumes this: its loop
32
+ excludes "rows that were never Stripe's" — comped, manual and other-provider
33
+ plans with synthetic ids. That exclusion is this seam, half-built.
34
+ - **The customer identity is yours too.** Email or user id, resolved before
35
+ checkout, so a second provider can be added without re-identifying the base.
36
+ A provider's customer id as your only handle on a person is the migration
37
+ nobody budgets for.
38
+ - **The reconciliation job is the external check.** It is the one number in the
39
+ system that is not telemetry: charges that succeeded, reported by the party
40
+ that processed them. Everything else you know about revenue is inference from
41
+ your own instrumentation.
42
+
43
+ What this is **not**: an argument for an abstraction layer over payments. A
44
+ generic `PaymentProvider` interface written against one provider encodes that
45
+ provider's model and has to be rewritten for the second one anyway. The seam is
46
+ in the data — whose id owns what — not in the code shape.
47
+
48
+ ## Three ways this costs money at volume
49
+
50
+ None of these looks like a payments problem from inside the app.
51
+
52
+ | What happens | Why it is invisible | Where to look |
53
+ |---|---|---|
54
+ | Approval rates differ by country, and some markets are not served at all | An unserved market is obvious. A market with a poor approval rate is not: a declined charge and a customer who changed their mind produce the same empty result in the funnel | Decline codes grouped by country, measured against **attempts** rather than against successes |
55
+ | Renewals fail on cards that expired, were reissued, or were blocked | The subscription ends and nobody decided to end it, so it lands in churn beside voluntary cancellations | `invoice.payment_failed` and the dunning path, kept separate from the `customer.subscription.deleted` that came from your own UI |
56
+ | A replaced card silently keeps working, or silently does not | See the next section: the mechanism exists, its coverage varies by country, and Stripe states you cannot tell which cards it covers | `payment_method.automatically_updated` and `payment_method.updated` |
57
+
58
+ The measurement that makes the first row actionable is the denominator. Approval
59
+ rate is successes over *attempts*, and a dashboard that reports successes over
60
+ successes reports 100% in every market.
61
+
62
+ ## Automatic card updates, and what they do not tell you
63
+
64
+ Stripe works with the card networks and **automatically** attempts to update
65
+ saved card details when a customer's card is replaced, so a saved payment method
66
+ can keep working after an expiry or a reissue. There is nothing to switch on.
67
+
68
+ Two limits, both stated by Stripe and both load-bearing:
69
+
70
+ - Support is **wide in the United States** across American Express, Visa,
71
+ Mastercard and Discover issued there, and **varies by country** elsewhere,
72
+ because it needs the issuer to participate.
73
+ - It is **not possible to identify which cards support it**. So you cannot
74
+ compute how much of your subscription base is protected, and any plan that
75
+ assumes the mechanism will catch a reissue is a plan with an unmeasurable
76
+ branch.
77
+
78
+ What to do rather than assume:
79
+
80
+ - Listen for **`payment_method.automatically_updated`** for network-driven
81
+ changes and **`payment_method.updated`** for API-driven ones.
82
+ - Both carry the card's new expiration and last four. Write them to your own
83
+ records from the event, or the billing page keeps showing digits the customer
84
+ no longer has, and a support ticket follows a change that actually succeeded.
85
+ - A card update that includes a new **number** changes the payment method's
86
+ `fingerprint`. If anything in your system keys off the fingerprint — dedup of
87
+ saved cards, fraud rules, "you already used this card" — that identity moves
88
+ under it.
89
+
90
+ ## Involuntary is not voluntary
91
+
92
+ Stripe owns the retry schedule and the terminal state (`SKILL.md` →
93
+ **Renewal**, **Cancellation**). What it cannot own is the difference between a
94
+ subscription that ended because someone chose to leave and one that ended because
95
+ a bank reissued a number.
96
+
97
+ Keep them apart in your own data, because everything downstream reads it:
98
+
99
+ | | Voluntary | Involuntary |
100
+ |---|---|---|
101
+ | Arrives as | your own cancel route, or `cancel_at_period_end` | `invoice.payment_failed` → dunning → terminal |
102
+ | The customer's intent | stated | never expressed |
103
+ | The right message | confirm, and say what happens to their data | tell them a payment failed and how to fix it |
104
+ | The wrong message | — | a farewell email, which spends the relationship on a recoverable failure |
105
+ | Counts as churn | yes | yes, and reporting it in the same number hides a fixable revenue leak |
106
+
107
+ A single `canceled` status for both is the shape this failure takes. Record the
108
+ cause, not just the outcome.
109
+
110
+ ## Checklist
111
+
112
+ - [ ] Entitlement rows keyed to your user id, provider ids as fields
113
+ - [ ] Customer identity (email or user id) resolved before checkout
114
+ - [ ] Reconciliation job running, and its result compared against your own numbers
115
+ - [ ] Decline codes grouped by country, measured against attempts
116
+ - [ ] `payment_method.automatically_updated` and `payment_method.updated` handled,
117
+ and the stored last-four and expiry written from them
118
+ - [ ] Anything keyed on `fingerprint` reviewed for the case where it changes
119
+ - [ ] Involuntary and voluntary cancellation distinguishable in your own data
120
+ - [ ] No farewell email on the involuntary path