Documentation
¶
Overview ¶
Package billing validates App Store and Google Play subscription receipts and parses the two stores' server notifications, mapping both into one normalized model. It is deliberately self-contained: no database, no proto, no server wiring. Callers hand it project store credentials / receipts and get back a NormalizedSubscription or NormalizedNotification; the caller owns persistence and entitlement derivation.
The store is the source of truth; moth is a validating mirror. Apple StoreKit 2 signed transactions and App Store Server Notifications V2 are verified locally against Apple's root CA via their x5c certificate chain (NOT a JWKS — see jws.go), and authoritative renewal state comes from the App Store Server API. Google Play state comes from the Play Developer API (purchases.subscriptionsv2.get); RTDN pushes are nudges to re-read, never trusted for state. Every outbound client takes an injectable HTTP Doer, overridable base/token URLs, and an injectable clock, so the whole engine is testable against httptest doubles with no network — mirroring internal/oidc.
Index ¶
- Constants
- Variables
- func AppleRoots() *x509.CertPool
- func AuthenticatePushToken(got, want string) bool
- func ParseP8(data []byte) (*ecdsa.PrivateKey, error)
- func StripeMicrosForUnitAmount(unitAmount int64, currency string) int64
- func StripeRecurringForPeriod(period string) (interval string, count int, err error)
- func StripeTrialDays(trial string) (int64, error)
- func StripeUnitAmountForMicros(micros int64, currency string) (int64, error)
- func VerifyStripeSignature(payload []byte, header, secret string, now time.Time, tolerance time.Duration) error
- type AppleClient
- type AppleVerifier
- type DeveloperNotification
- type Doer
- type GoogleClient
- type GoogleServiceAccount
- type GoogleTokenSource
- type JWSRenewalInfo
- type JWSTransaction
- type NormalizedNotification
- type NormalizedSubscription
- type StripeAPIError
- type StripeCheckoutParams
- type StripeCheckoutSession
- type StripeClient
- func (c *StripeClient) CreateBillingPortalSession(ctx context.Context, customerID, returnURL string) (StripePortalSession, error)
- func (c *StripeClient) CreateCheckoutSession(ctx context.Context, params StripeCheckoutParams) (StripeCheckoutSession, error)
- func (c *StripeClient) CreateCustomer(ctx context.Context, email string, metadata map[string]string) (StripeCustomer, error)
- func (c *StripeClient) CreatePrice(ctx context.Context, params StripePriceParams) (StripePrice, error)
- func (c *StripeClient) CreateProduct(ctx context.Context, name string, metadata map[string]string) (StripeProduct, error)
- func (c *StripeClient) CreateWebhookEndpoint(ctx context.Context, endpointURL string, events []string) (StripeWebhookEndpoint, error)
- func (c *StripeClient) GetPrice(ctx context.Context, id string) (StripePrice, error)
- func (c *StripeClient) GetProduct(ctx context.Context, id string) (StripeProduct, error)
- func (c *StripeClient) GetSubscription(ctx context.Context, id string) (NormalizedSubscription, StripeSubscription, error)
- func (c *StripeClient) ListWebhookEndpoints(ctx context.Context) ([]StripeWebhookEndpoint, error)
- func (c *StripeClient) UpdateProduct(ctx context.Context, id, name string) (StripeProduct, error)
- func (c *StripeClient) UpdateWebhookEndpoint(ctx context.Context, id string, events []string) (StripeWebhookEndpoint, error)
- type StripeCustomer
- type StripeEvent
- type StripePortalSession
- type StripePrice
- type StripePriceParams
- type StripeProduct
- type StripeSubscription
- type StripeSubscriptionItem
- type StripeWebhookEndpoint
- type SubscriptionPurchaseV2
Constants ¶
const ( AppleStoreKitProdURL = "https://api.storekit.itunes.apple.com" AppleStoreKitSandboxURL = "https://api.storekit-sandbox.itunes.apple.com" )
App Store Server API hosts. The API mirrors data across environments: a production transaction is unknown to the sandbox host and vice-versa, so a production lookup that 404s is retried against sandbox (Apple's documented fallback rule).
const ( StoreApple = "apple" StoreGoogle = "google" StoreStripe = "stripe" )
Store identifies which store a subscription originates from.
const ( EnvSandbox = "sandbox" EnvProduction = "production" )
Environment distinguishes sandbox/test receipts from live ones. Stored so a project never conflates a tester's sandbox subscription with production.
const ( StatusActive = "active" StatusTrialing = "trialing" StatusInGracePeriod = "in_grace_period" StatusInBillingRetry = "in_billing_retry" // Google "on hold" StatusPaused = "paused" StatusExpired = "expired" StatusRevoked = "revoked" )
Subscription status enum, minimal and mapped identically from both stores (plan/11 §Model). These string constants are the contract the server agent maps onto its subscription_status column and entitlement-derivation matrix:
active, trialing, in_grace_period, in_billing_retry -> entitlement GRANTED
(grace/retry keep
access per store
policy)
paused, expired, revoked -> NOT granted
const ( GooglePlayBaseURL = "https://androidpublisher.googleapis.com" GoogleTokenURL = "https://oauth2.googleapis.com/token" )
Google Play Developer API defaults, overridable for tests.
const ( GoogleNotifRecovered = 1 GoogleNotifRenewed = 2 GoogleNotifCanceled = 3 GoogleNotifPurchased = 4 GoogleNotifOnHold = 5 GoogleNotifInGracePeriod = 6 GoogleNotifRestarted = 7 GoogleNotifPriceChangeConfirmed = 8 GoogleNotifDeferred = 9 GoogleNotifPaused = 10 GoogleNotifPauseScheduleChanged = 11 GoogleNotifRevoked = 12 GoogleNotifExpired = 13 )
Google RTDN subscriptionNotification.notificationType codes.
const StripeAPIBaseURL = "https://api.stripe.com"
StripeAPIBaseURL is the production Stripe REST API host, overridable for tests via StripeClient.BaseURL. Stripe has no separate sandbox host — test mode is selected by the secret key (sk_test_...) and reported back as livemode=false.
Variables ¶
var ( ErrMalformed = errors.New("billing: malformed token") ErrInvalidSignature = errors.New("billing: invalid signature") ErrUntrustedChain = errors.New("billing: certificate chain not trusted") ErrBundleMismatch = errors.New("billing: bundle id mismatch") ErrNotFound = errors.New("billing: not found") )
Validation / verification errors. Callers map ErrMalformed and the verify failures to connect.CodeInvalidArgument, and a store 404 to connect.CodeNotFound (see ErrNotFound below).
Functions ¶
func AppleRoots ¶
AppleRoots returns the CertPool moth verifies Apple JWS chains against — the embedded Apple Root CA - G3. Tests inject their own pool instead.
func AuthenticatePushToken ¶
AuthenticatePushToken verifies a shared-secret path token guarding the RTDN webhook, in constant time. moth generates a random token per project and registers the Pub/Sub push endpoint as /billing/google/rtdn/{slug}?token=SECRET (or as a path segment); Google replays it on every push, and a request without the exact secret is dropped.
This is the simple, self-contained option. The alternative — verifying the OIDC identity token Google signs the push with (aud = the endpoint, iss = accounts.google.com, email = the push service account) — needs the Google JWKS verifier (internal/oidc) and the configured service-account email; it is preferable when the endpoint is public and the operator cannot keep a URL secret. moth ships the shared-secret path and documents the OIDC path here.
func ParseP8 ¶
func ParseP8(data []byte) (*ecdsa.PrivateKey, error)
ParseP8 re-exports oidc.ParseP8: an Apple .p8 In-App-Purchase key is the same PEM-wrapped PKCS#8 EC P-256 as a Sign-in-with-Apple key.
func StripeMicrosForUnitAmount ¶
StripeMicrosForUnitAmount converts Stripe's unit_amount back to moth micros for the currency.
func StripeRecurringForPeriod ¶
StripeRecurringForPeriod maps a moth product billing_period onto Stripe's recurring[interval]/[interval_count]. It accepts the same free-form vocabulary as the Apple/Google catalog sync (setup.parseBillingPeriod): words ("weekly", "monthly", "two_month", "quarterly", "half_year", "yearly" and their synonyms) and the ISO-8601 forms P1W/P1M/P2M/P3M/P6M/P1Y.
func StripeTrialDays ¶
StripeTrialDays maps a moth product trial_period onto Stripe's subscription_data[trial_period_days]. It accepts the same vocabulary as StripeRecurringForPeriod plus arbitrary ISO-8601 durations of a single unit (P3D, P2W, ...); calendar units use the usual 30-day month / 365-day year approximation since Stripe trials are day-denominated. Empty means no trial (0, nil).
func StripeUnitAmountForMicros ¶
StripeUnitAmountForMicros converts a moth price (micros of whole currency units) to Stripe's unit_amount for the currency. Amounts that are not representable in the currency's minor unit are rejected rather than silently rounded.
func VerifyStripeSignature ¶
func VerifyStripeSignature(payload []byte, header, secret string, now time.Time, tolerance time.Duration) error
VerifyStripeSignature verifies a Stripe-Signature header (t=<unix>,v1=<hex>[,v1=<hex>...]) against the endpoint's signing secret: HMAC-SHA256 over "<t>.<payload>", constant-time compare against every v1 (Stripe sends several during secret rolls), and a ±tolerance window on t (skipped when tolerance <= 0). A malformed header wraps ErrMalformed; every other failure — empty secret included — wraps ErrInvalidSignature.
Types ¶
type AppleClient ¶
type AppleClient struct {
// BaseURL is the primary host (defaults to production).
BaseURL string
// SandboxURL is the fallback host tried on a production 404 (defaults to
// the sandbox host; set "" to disable fallback, e.g. in tests).
SandboxURL string
IssuerID string // ASC issuer id (iss)
KeyID string // In-App-Purchase key id (JWS kid)
BundleID string // bid claim + verifier bundle check
Key *ecdsa.PrivateKey
HTTPC Doer
Now func() time.Time
// Verifier verifies the signed transaction/renewal JWS in responses.
// Defaults to a verifier over Apple's real roots bound to BundleID.
Verifier *AppleVerifier
}
AppleClient calls the App Store Server API, authenticating each request with a short-lived ES256 JWT minted from the project's In-App-Purchase .p8 key (same mechanism as App Store Connect, distinct key type and audience). Every signed blob it receives is verified through Verifier before use.
func (*AppleClient) GetAllSubscriptionStatuses ¶
func (c *AppleClient) GetAllSubscriptionStatuses(ctx context.Context, originalTransactionID string) (NormalizedSubscription, error)
GetAllSubscriptionStatuses resolves an originalTransactionId to authoritative state: it fetches the subscription-group statuses, verifies the signed transaction + renewal info of the matching last transaction, and returns a NormalizedSubscription. On a production 404 it retries sandbox (the documented fallback), so a sandbox transaction resolves without the caller guessing the environment.
func (*AppleClient) GetTransactionInfo ¶
func (c *AppleClient) GetTransactionInfo(ctx context.Context, transactionID string) (*JWSTransaction, error)
GetTransactionInfo fetches and verifies a single transaction by transactionId, returning its decoded payload. Production 404 falls back to sandbox.
func (*AppleClient) Token ¶
func (c *AppleClient) Token() (string, error)
Token mints the ES256 request JWT: header alg/kid/typ; claims iss/iat/exp/aud/bid (Apple's "Generating JSON Web Tokens for API requests").
type AppleVerifier ¶
type AppleVerifier struct {
// contains filtered or unexported fields
}
AppleVerifier verifies Apple JWS blobs (transactions, renewal info, notifications) against a trust anchor. Zero value is not usable; use NewAppleVerifier.
func NewAppleVerifier ¶
func NewAppleVerifier(roots *x509.CertPool, expectBundle string, now func() time.Time) *AppleVerifier
NewAppleVerifier returns a verifier over roots (defaults to AppleRoots when nil — tests pass a test CA pool). now defaults to time.Now. expectBundle, when non-empty, is required to match every verified transaction's bundleId, rejecting a receipt minted for another app.
func (*AppleVerifier) VerifyNotification ¶
func (v *AppleVerifier) VerifyNotification(signedPayload string) (*NormalizedNotification, error)
VerifyNotification verifies an App Store Server Notification V2 signedPayload (same x5c chain as a transaction), then verifies the nested transaction / renewal-info JWS it carries, and returns a NormalizedNotification. The notification's Subscription is best-effort: the caller must still re-read authoritative state via the App Store Server API before granting.
func (*AppleVerifier) VerifyRenewalInfo ¶
func (v *AppleVerifier) VerifyRenewalInfo(jws string) (*JWSRenewalInfo, error)
VerifyRenewalInfo verifies a signed renewal-info JWS and returns its decoded payload.
func (*AppleVerifier) VerifyTransaction ¶
func (v *AppleVerifier) VerifyTransaction(jws string) (*JWSTransaction, error)
VerifyTransaction verifies a signed transaction JWS and returns its decoded payload. It rejects a tampered signature, an untrusted or expired chain, and (when expectBundle is set) a mismatched bundle id.
type DeveloperNotification ¶
type DeveloperNotification struct {
Version string `json:"version"`
PackageName string `json:"packageName"`
EventTimeMillis string `json:"eventTimeMillis"`
SubscriptionNotification *struct {
Version string `json:"version"`
NotificationType int `json:"notificationType"`
PurchaseToken string `json:"purchaseToken"`
SubscriptionID string `json:"subscriptionId"`
} `json:"subscriptionNotification"`
TestNotification *struct {
Version string `json:"version"`
} `json:"testNotification"`
}
DeveloperNotification is the Play RTDN payload carried inside a Pub/Sub push.
func ParsePubSubPush ¶
func ParsePubSubPush(body []byte) (notif *DeveloperNotification, messageID string, err error)
ParsePubSubPush parses a Pub/Sub push envelope and decodes the embedded DeveloperNotification. The RTDN is a nudge: the caller re-reads state via GetSubscriptionV2 using the returned purchaseToken. MessageID is the Pub/Sub message id, usable as the idempotency key.
type Doer ¶
Doer is the subset of *http.Client the package needs; injectable so tests and the server package can point it at doubles. Mirrors oidc.Doer.
type GoogleClient ¶
type GoogleClient struct {
// BaseURL defaults to GooglePlayBaseURL; tests point it at a double.
BaseURL string
PackageName string
Tokens *GoogleTokenSource
HTTPC Doer
}
GoogleClient calls the Google Play Developer API for one project's package.
func (*GoogleClient) AcknowledgeSubscription ¶
func (c *GoogleClient) AcknowledgeSubscription(ctx context.Context, subscriptionID, purchaseToken string) error
AcknowledgeSubscription acknowledges a purchase (purchases.subscriptions. acknowledge). Google auto-refunds a subscription that is not acknowledged within three days, so the caller acknowledges once acknowledgementState is "acknowledgementStatePending".
func (*GoogleClient) GetSubscriptionV2 ¶
func (c *GoogleClient) GetSubscriptionV2(ctx context.Context, purchaseToken string) (NormalizedSubscription, SubscriptionPurchaseV2, error)
GetSubscriptionV2 resolves a purchaseToken to authoritative subscription state and returns a NormalizedSubscription. A 404 (token for another package/project, or an unknown token) surfaces as ErrNotFound.
type GoogleServiceAccount ¶
type GoogleServiceAccount struct {
ClientEmail string `json:"client_email"`
PrivateKeyID string `json:"private_key_id"`
PrivateKeyPEM string `json:"private_key"`
TokenURI string `json:"token_uri"`
// contains filtered or unexported fields
}
GoogleServiceAccount is the subset of a Google service-account JSON key moth needs to mint an androidpublisher access token.
func ParseServiceAccount ¶
func ParseServiceAccount(data []byte) (*GoogleServiceAccount, error)
ParseServiceAccount parses a service-account JSON key and its PEM private key. The parsed key is stored encrypted under the master key by the caller; this only turns bytes into a usable signer.
type GoogleTokenSource ¶
type GoogleTokenSource struct {
// contains filtered or unexported fields
}
GoogleTokenSource exchanges a service-account assertion for an androidpublisher OAuth2 access token (RFC 7523 JWT-bearer grant) and caches it until shortly before expiry. Safe for concurrent use.
func NewGoogleTokenSource ¶
func NewGoogleTokenSource(sa *GoogleServiceAccount, tokenURL string, httpc Doer, now func() time.Time) *GoogleTokenSource
NewGoogleTokenSource returns a cached token source. tokenURL defaults to the service account's token_uri (or GoogleTokenURL); httpc and now default to a timeout-bounded client and time.Now.
type JWSRenewalInfo ¶
type JWSRenewalInfo struct {
OriginalTransactionID string `json:"originalTransactionId"`
ProductID string `json:"productId"`
AutoRenewProductID string `json:"autoRenewProductId"`
AutoRenewStatus int `json:"autoRenewStatus"` // 1 on, 0 off
ExpirationIntent int `json:"expirationIntent"`
GracePeriodExpiresDate int64 `json:"gracePeriodExpiresDate"`
Environment string `json:"environment"`
}
JWSRenewalInfo is the JWSRenewalInfoDecodedPayload subset moth reads from a signed renewal-info blob.
type JWSTransaction ¶
type JWSTransaction struct {
TransactionID string `json:"transactionId"`
OriginalTransactionID string `json:"originalTransactionId"`
BundleID string `json:"bundleId"`
ProductID string `json:"productId"`
SubscriptionGroupIdentifier string `json:"subscriptionGroupIdentifier"`
PurchaseDate int64 `json:"purchaseDate"`
ExpiresDate int64 `json:"expiresDate"`
// Type is e.g. "Auto-Renewable Subscription".
Type string `json:"type"`
// Environment is "Sandbox" or "Production".
Environment string `json:"environment"`
// OfferType is non-zero for introductory / promotional / trial offers;
// used to distinguish trialing from active.
OfferType int `json:"offerType"`
// RevocationDate is set when Apple revoked the purchase (refund, family
// removal). Non-zero => revoked.
RevocationDate int64 `json:"revocationDate"`
// Price is the store-reported price in milliunits (thousandths) of Currency,
// as the buyer's storefront charged it. Currency is the ISO-4217 code. Both
// are present on StoreKit 2 transactions since 2023; zero/empty on older
// receipts, in which case the caller falls back to the catalog price.
Price int64 `json:"price"`
Currency string `json:"currency"`
}
JWSTransaction is the JWSTransactionDecodedPayload subset moth reads from a StoreKit 2 signed transaction. Dates are Apple's milliseconds-since-epoch.
type NormalizedNotification ¶
type NormalizedNotification struct {
Store string
Type string
Subtype string
NotificationID string
Subscription NormalizedSubscription
Raw json.RawMessage
}
NormalizedNotification is the store-agnostic shape of a store notification. Type/Subtype carry the store's own notification vocabulary (for the audit trail); NotificationID is the store's unique id used to dedupe replays. The embedded Subscription is best-effort from the notification body — the caller MUST re-read authoritative state from the store API before trusting it for entitlement changes (plan/11: "the notification is a nudge, not a payload to trust").
type NormalizedSubscription ¶
type NormalizedSubscription struct {
// Store is StoreApple or StoreGoogle.
Store string
// ProductID is the store product identifier (Apple productId / Google
// line-item productId). Maps to a moth product tier downstream.
ProductID string
// StoreTransactionID is the stable store identity moth keys the
// subscription on: originalTransactionId (Apple) or purchaseToken
// (Google).
StoreTransactionID string
// SubscriptionID is the Google base-plan/subscription id, or the Apple
// subscriptionGroupIdentifier. Empty when the store does not supply one.
SubscriptionID string
// Status is one of the status constants above.
Status string
// CurrentPeriodEnd is when the paid period ends (renewal or expiry).
// Zero when the store did not supply an expiry.
CurrentPeriodEnd time.Time
// AutoRenew reflects the store's renewal flag at read time.
AutoRenew bool
// Environment is EnvSandbox or EnvProduction.
Environment string
// PriceAmountMicros is the store-reported transaction price in micros
// (millionths) of Currency, when the store supplies it (Apple StoreKit 2
// transactions carry price + currency; Google's subscriptionsv2 does not).
// Zero when the store did not report a price — the caller then falls back to
// the moth catalog price. This is the storefront-localized amount the buyer
// actually paid, so per-currency revenue reflects real charges rather than a
// single catalog list price.
PriceAmountMicros int64
// Currency is the ISO-4217 code of PriceAmountMicros, "" when the store
// reported none.
Currency string
// RawState is the verified store JSON (decoded transaction / purchase),
// persisted verbatim for audit and reconciliation.
RawState json.RawMessage
}
NormalizedSubscription is the store-agnostic subscription state both stores map into. The server agent persists it to the subscriptions table and derives entitlements from Status. It is intentionally flat and free of any store SDK type.
type StripeAPIError ¶
StripeAPIError is a non-2xx (non-404) Stripe API response. Callers use errors.As to branch on the stable Code (e.g. "resource_missing" when a stored customer or price id no longer exists in the account/mode) instead of string-matching the message.
func (*StripeAPIError) Error ¶
func (e *StripeAPIError) Error() string
type StripeCheckoutParams ¶
type StripeCheckoutParams struct {
// PriceID is the recurring price the session subscribes to.
PriceID string
// CustomerID binds the session to an existing Stripe customer.
CustomerID string
SuccessURL string
CancelURL string
// ClientReferenceID carries the moth user id back on
// checkout.session.completed.
ClientReferenceID string
// Metadata is attached to BOTH the session and (via subscription_data)
// the resulting subscription, so the subscription itself carries the moth
// project/user ids.
Metadata map[string]string
// TrialPeriodDays > 0 starts the subscription with a free trial.
TrialPeriodDays int64
}
StripeCheckoutParams shapes a subscription-mode Checkout Session.
type StripeCheckoutSession ¶
type StripeCheckoutSession struct {
ID string `json:"id"`
URL string `json:"url"`
Subscription string `json:"subscription"`
Customer string `json:"customer"`
ClientReferenceID string `json:"client_reference_id"`
Metadata map[string]string `json:"metadata"`
}
StripeCheckoutSession is the subset of a Checkout Session moth reads: on create the hosted URL to redirect to; on checkout.session.completed the created subscription/customer and the moth identity echoes.
type StripeClient ¶
type StripeClient struct {
// BaseURL defaults to StripeAPIBaseURL; tests point it at a double.
BaseURL string
// SecretKey is the project's sk_/rk_ secret key, sent as a Bearer token.
SecretKey string
HTTPC Doer
Now func() time.Time
}
StripeClient calls the Stripe REST API directly (no Stripe SDK) with a project's restricted/secret key. Like the Apple and Google clients it is a struct literal with zero values meaning production defaults, so the whole engine stays testable against httptest doubles.
func (*StripeClient) CreateBillingPortalSession ¶
func (c *StripeClient) CreateBillingPortalSession(ctx context.Context, customerID, returnURL string) (StripePortalSession, error)
CreateBillingPortalSession creates a Billing Portal session (POST /v1/billing_portal/sessions) for a customer.
func (*StripeClient) CreateCheckoutSession ¶
func (c *StripeClient) CreateCheckoutSession(ctx context.Context, params StripeCheckoutParams) (StripeCheckoutSession, error)
CreateCheckoutSession creates a subscription-mode hosted Checkout Session (POST /v1/checkout/sessions). Checkout stays Stripe-hosted: moth never renders a card field.
func (*StripeClient) CreateCustomer ¶
func (c *StripeClient) CreateCustomer(ctx context.Context, email string, metadata map[string]string) (StripeCustomer, error)
CreateCustomer creates a Stripe customer (POST /v1/customers). moth creates at most one per (project, user), lazily on first checkout, carrying the moth ids in metadata.
func (*StripeClient) CreatePrice ¶
func (c *StripeClient) CreatePrice(ctx context.Context, params StripePriceParams) (StripePrice, error)
CreatePrice creates a recurring price (POST /v1/prices). Stripe prices are immutable: a price change creates a new price and re-points the tier.
func (*StripeClient) CreateProduct ¶
func (c *StripeClient) CreateProduct(ctx context.Context, name string, metadata map[string]string) (StripeProduct, error)
CreateProduct creates a Stripe product (POST /v1/products) during catalog provisioning, carrying the moth tier identity in metadata.
func (*StripeClient) CreateWebhookEndpoint ¶
func (c *StripeClient) CreateWebhookEndpoint(ctx context.Context, endpointURL string, events []string) (StripeWebhookEndpoint, error)
CreateWebhookEndpoint creates a webhook endpoint (POST /v1/webhook_endpoints) subscribed to events. The response carries the signing Secret exactly once — the caller stores it encrypted.
func (*StripeClient) GetPrice ¶
func (c *StripeClient) GetPrice(ctx context.Context, id string) (StripePrice, error)
GetPrice fetches a price by id (GET /v1/prices/{id}) so provisioning can detect drift between the moth catalog and the live Stripe price. An unknown id surfaces as ErrNotFound.
func (*StripeClient) GetProduct ¶
func (c *StripeClient) GetProduct(ctx context.Context, id string) (StripeProduct, error)
GetProduct fetches a product by id (GET /v1/products/{id}) so provisioning can detect display-name drift between the moth catalog and the live Stripe product. An unknown id surfaces as ErrNotFound.
func (*StripeClient) GetSubscription ¶
func (c *StripeClient) GetSubscription(ctx context.Context, id string) (NormalizedSubscription, StripeSubscription, error)
GetSubscription resolves a Stripe subscription id (sub_...) to authoritative state (GET /v1/subscriptions/{id}) and returns it normalized alongside the partial raw decode. Webhooks are nudges: every event triggers this re-read. An unknown id surfaces as ErrNotFound.
func (*StripeClient) ListWebhookEndpoints ¶
func (c *StripeClient) ListWebhookEndpoints(ctx context.Context) ([]StripeWebhookEndpoint, error)
ListWebhookEndpoints lists ALL of the account's webhook endpoints (GET /v1/webhook_endpoints) so setup can detect an already-provisioned endpoint. Stripe pages the list (default 10) — a moth instance hosting a portfolio of projects easily exceeds that, and a missed endpoint would make setup create duplicates and rotate the stored signing secret — so this follows has_more/starting_after to exhaustion.
func (*StripeClient) UpdateProduct ¶
func (c *StripeClient) UpdateProduct(ctx context.Context, id, name string) (StripeProduct, error)
UpdateProduct renames a product in place (POST /v1/products/{id}). Unlike prices, Stripe product names are mutable, so a moth display-name change is a real update rather than a recreate.
func (*StripeClient) UpdateWebhookEndpoint ¶
func (c *StripeClient) UpdateWebhookEndpoint(ctx context.Context, id string, events []string) (StripeWebhookEndpoint, error)
UpdateWebhookEndpoint re-points an existing endpoint's event subscription and re-enables it if Stripe disabled it (POST /v1/webhook_endpoints/{id}). The signing secret is NOT returned on update — only create reveals it.
type StripeCustomer ¶
StripeCustomer is the subset of a Stripe customer moth reads.
type StripeEvent ¶
type StripeEvent struct {
ID string `json:"id"`
Type string `json:"type"`
LiveMode bool `json:"livemode"`
Data struct {
Object json.RawMessage `json:"object"`
} `json:"data"`
}
StripeEvent is the webhook event envelope. Data.Object is the raw embedded object, decoded on demand via CheckoutSession/SubscriptionObject — and only ever treated as a nudge: state is re-read from the API before applying.
func ParseStripeEvent ¶
func ParseStripeEvent(payload []byte) (StripeEvent, error)
ParseStripeEvent parses a verified webhook body into the event envelope. The event id (evt_...) is the dedupe key. Malformed JSON or a missing id/type wraps ErrMalformed.
func (StripeEvent) CheckoutSession ¶
func (e StripeEvent) CheckoutSession() (StripeCheckoutSession, error)
CheckoutSession decodes Data.Object as a Checkout Session (checkout.session.completed events).
func (StripeEvent) SubscriptionObject ¶
func (e StripeEvent) SubscriptionObject() (StripeSubscription, error)
SubscriptionObject decodes Data.Object as a subscription (customer.subscription.* events), keeping the verbatim object as Raw.
type StripePortalSession ¶
StripePortalSession is a Billing Portal session: the URL is where the user manages payment methods, invoices and cancellation, Stripe-hosted.
type StripePrice ¶
type StripePrice struct {
ID string
ProductID string
UnitAmount int64
Currency string
Interval string
IntervalCount int
Active bool
}
StripePrice is the subset of a Stripe price moth reads, flattened. Currency is upper-cased ISO-4217 to compare directly against the moth catalog.
type StripePriceParams ¶
type StripePriceParams struct {
ProductID string
// Currency is ISO-4217 in any case; Stripe is sent the lowercase form.
Currency string
// UnitAmount is in the currency's minor units (cents).
UnitAmount int64
// Interval is day|week|month|year (see StripeRecurringForPeriod).
Interval string
IntervalCount int
Metadata map[string]string
}
StripePriceParams shapes a recurring price for a provisioned product.
type StripeProduct ¶
StripeProduct is the subset of a Stripe product moth reads.
type StripeSubscription ¶
type StripeSubscription struct {
ID string `json:"id"`
Status string `json:"status"`
CancelAtPeriodEnd bool `json:"cancel_at_period_end"`
// CurrentPeriodEnd (unix seconds) lives on the subscription in older
// Stripe API versions; current versions expose it per item instead —
// normalization reads both and prefers whichever is set.
CurrentPeriodEnd int64 `json:"current_period_end"`
Customer string `json:"customer"`
LiveMode bool `json:"livemode"`
TrialEnd int64 `json:"trial_end"`
// PauseCollection is non-nil while payment collection is paused.
PauseCollection *struct {
Behavior string `json:"behavior"`
} `json:"pause_collection"`
Metadata map[string]string `json:"metadata"`
Items struct {
Data []StripeSubscriptionItem `json:"data"`
} `json:"items"`
// Raw is the verbatim JSON this struct was decoded from (the API response
// body or a webhook data.object), carried into RawState for audit.
Raw json.RawMessage `json:"-"`
}
StripeSubscription is the subset of a Stripe subscription moth reads.
type StripeSubscriptionItem ¶
type StripeSubscriptionItem struct {
// CurrentPeriodEnd (unix seconds) is where current Stripe API versions
// report the period end.
CurrentPeriodEnd int64 `json:"current_period_end"`
Price struct {
ID string `json:"id"`
Product string `json:"product"`
UnitAmount int64 `json:"unit_amount"`
Currency string `json:"currency"`
Recurring struct {
Interval string `json:"interval"`
IntervalCount int `json:"interval_count"`
} `json:"recurring"`
} `json:"price"`
}
StripeSubscriptionItem is the subset of a subscription item moth reads.
type StripeWebhookEndpoint ¶
type StripeWebhookEndpoint struct {
ID string `json:"id"`
URL string `json:"url"`
Status string `json:"status"`
EnabledEvents []string `json:"enabled_events"`
Secret string `json:"secret"`
}
StripeWebhookEndpoint is the subset of a Stripe webhook endpoint moth reads. Secret (whsec_...) is returned by Stripe only on create.
type SubscriptionPurchaseV2 ¶
type SubscriptionPurchaseV2 struct {
SubscriptionState string `json:"subscriptionState"`
LineItems []struct {
ProductID string `json:"productId"`
ExpiryTime string `json:"expiryTime"` // RFC3339
AutoRenewingPlan *struct {
AutoRenewEnabled bool `json:"autoRenewEnabled"`
} `json:"autoRenewingPlan"`
// OfferDetails is present while the line item is on a base-plan offer
// (a free trial or an introductory/promotional price). A non-empty
// OfferID means the current period is discounted/free; once it renews
// onto the plain base plan the field is absent. moth uses this to
// classify the trialing period, mirroring Apple's offerType.
OfferDetails *struct {
OfferID string `json:"offerId"`
BasePlanID string `json:"basePlanId"`
} `json:"offerDetails"`
} `json:"lineItems"`
// TestPurchase is present only for license-tester / sandbox purchases.
TestPurchase *struct{} `json:"testPurchase"`
AcknowledgementState string `json:"acknowledgementState"`
}
SubscriptionPurchaseV2 is the subset of the purchases.subscriptionsv2.get response moth reads.