feat(admin): Paddle webhook ingress — verify, idempotent claim, dispatch

This commit is contained in:
2026-07-27 10:37:53 +01:00
parent fbd93d0ea5
commit 6832bfd7bb
3 changed files with 127 additions and 0 deletions
+59
View File
@@ -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 }