Documentation
¶
Overview ¶
Package payments turns a payment provider's webhook into an ordinary event in the log.
This exists to feed one sentence. The investigator can size a finding in money (internal/investigate/money.go), but only when the metric carries a numeric revenue property — which means only for users who remembered to put an `amount` on their own `track("checkout")` call. Almost nobody does. Meanwhile the real revenue truth is already sitting in Stripe or Polar or Lemon Squeezy, complete and correct, behind a webhook the user can point at us in thirty seconds without asking anyone's permission.
So: verify the signature, translate to event.Event{Name: "payment", Properties: {amount, ...}}, hand it to the same Ingest every other event goes through. Nothing about the append-only log changes, no new storage, no new report — the existing machinery starts pricing findings because the data finally arrived.
PERMISSIONLESS, which is the whole reason these four and not others. Each is a URL the user pastes into their own dashboard. Deliberately NOT the Stripe App or any marketplace listing: those are review-gated, and a channel that needs someone's approval is not a channel.
AMOUNTS ARE MINOR UNITS EVERYWHERE. Every provider here sends integer cents; storing 2900 instead of 29.00 would make every revenue figure a hundred times too large, which is both the easiest mistake to make and the most embarrassing one to ship, so the conversion happens once, here, and is tested per provider.
Index ¶
Constants ¶
const ( Stripe = "stripe" LemonSqueezy = "lemonsqueezy" Polar = "polar" Dodo = "dodo" )
Providers this package speaks, all self-serve.
const Event = "payment"
Event is the name every payment lands under. One name, so a funnel or a finding about revenue means the same thing regardless of which processor produced it — a user who migrates from Lemon Squeezy to Stripe keeps one continuous history rather than two half-series.
Variables ¶
var Supported = []string{Stripe, LemonSqueezy, Polar, Dodo}
Supported lists the providers in the order they appear in the docs.
Functions ¶
func Verify ¶
Verify checks the signature on a raw request body. `headers` is a lookup rather than http.Header so this package stays free of net/http and can be tested directly.
THE BODY MUST BE THE RAW BYTES. Every scheme signs the exact octets sent; decoding to a map and re-encoding reorders keys and changes whitespace, and the signature then never matches — which usually gets "fixed" by disabling verification.
Types ¶
type Payment ¶
type Payment struct {
// ID is the provider's own event id, used verbatim as the event ID so the store's
// already-seen rule deduplicates retries. Every provider on this list retries, several of
// them aggressively, and without this a flaky minute would double someone's revenue.
ID string
// DistinctID ties the payment to a person. See resolveIdentity for how hard it tries.
DistinctID string
// IdentitySource records HOW the identity was resolved, and travels into the event as a
// property. A revenue figure joined by email is worth less than one joined by an id the app
// supplied, and a reader must be able to tell which they are looking at.
IdentitySource string
Amount float64 // major units: dollars, not cents
Currency string
Kind string // the provider's event type, e.g. "checkout.session.completed"
At time.Time
}
Payment is one settled payment, provider-agnostic.
func Parse ¶
Parse turns a verified body into a Payment, or reports why it cannot.
Returns ok=false WITHOUT an error for events that are legitimately not payments — every provider sends subscription updates, customer changes and test pings down the same webhook, and treating those as failures would fill a user's logs with errors about working correctly.