feat(admin): Paddle webhook ingress — verify, idempotent claim, dispatch
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
// Package billing turns verified Paddle webhooks into licence actions. It never
|
||||
// verifies signatures (that is paddle.VerifySignature at the edge) and never
|
||||
// signs (that is licensing.Issue); it decides what a subscription's current
|
||||
// state means and calls the issuer.
|
||||
package billing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Event is the decoded Paddle webhook envelope. Data is left raw so each handler
|
||||
// decodes only the shape it needs.
|
||||
type Event struct {
|
||||
EventID string `json:"event_id"`
|
||||
EventType string `json:"event_type"`
|
||||
OccurredAt time.Time `json:"occurred_at"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// Dispatch routes one event to its handler. Unknown event types are a no-op
|
||||
// success: Paddle sends many we do not care about, and 200 stops it retrying.
|
||||
func Dispatch(ctx context.Context, ev Event) error {
|
||||
switch ev.EventType {
|
||||
case "subscription.created", "subscription.updated", "subscription.activated":
|
||||
return handleSubscription(ctx, ev)
|
||||
case "subscription.canceled":
|
||||
return handleCanceled(ctx, ev)
|
||||
case "subscription.past_due":
|
||||
return handlePastDue(ctx, ev)
|
||||
case "transaction.completed":
|
||||
return handleTransactionCompleted(ctx, ev)
|
||||
case "transaction.payment_failed":
|
||||
return handlePaymentFailed(ctx, ev)
|
||||
case "customer.updated":
|
||||
return handleCustomerUpdated(ctx, ev)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// decode is a small helper so every handler decodes Data the same way.
|
||||
func decode[T any](ev Event) (T, error) {
|
||||
var v T
|
||||
if err := json.Unmarshal(ev.Data, &v); err != nil {
|
||||
return v, fmt.Errorf("decode %s: %w", ev.EventType, err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Stubs replaced in tasks 3 and 4.
|
||||
func handleSubscription(ctx context.Context, ev Event) error { return nil }
|
||||
func handleCanceled(ctx context.Context, ev Event) error { return nil }
|
||||
func handlePastDue(ctx context.Context, ev Event) error { return nil }
|
||||
func handleTransactionCompleted(ctx context.Context, ev Event) error { return nil }
|
||||
func handlePaymentFailed(ctx context.Context, ev Event) error { return nil }
|
||||
func handleCustomerUpdated(ctx context.Context, ev Event) error { return nil }
|
||||
Reference in New Issue
Block a user