diff --git a/stripesync/coupons.go b/stripesync/coupons.go index 533526f..9c22f2b 100644 --- a/stripesync/coupons.go +++ b/stripesync/coupons.go @@ -3,7 +3,6 @@ package stripesync import ( "context" "database/sql" - "errors" "fmt" "github.com/friendlycaptcha/friendly-stripe-sync/internal/db/postgres" @@ -63,9 +62,8 @@ func (o *StripeSync) ensureCouponLoaded(c context.Context, couponID string) (boo // HandleCouponUpdated will fetch the coupon from Stripe because AppliesTo isn't set err = o.handleCouponUpdated(c, &stripe.Coupon{ID: couponID}) if err != nil { - var stripeErr *stripe.Error - if errors.As(err, &stripeErr) && stripeErr.Code == stripe.ErrorCodeResourceMissing { - log.Info().Str("coupon_id", couponID).Msg("Skipping coupon that no longer exists in Stripe") + if IsMissingResourceError(err) { + log.Info().Err(err).Str("coupon_id", couponID).Msg("Skipping coupon that no longer exists in Stripe") return false, nil } return false, fmt.Errorf("failed to upsert coupon: %w", err) diff --git a/stripesync/logger.go b/stripesync/logger.go new file mode 100644 index 0000000..0dc86fc --- /dev/null +++ b/stripesync/logger.go @@ -0,0 +1,56 @@ +package stripesync + +import ( + "errors" + + "github.com/rs/zerolog/log" + "github.com/stripe/stripe-go/v74" +) + +// IsMissingResourceError reports whether err is a Stripe error for a resource that does not exist. +func IsMissingResourceError(err error) bool { + var stripeErr *stripe.Error + return errors.As(err, &stripeErr) && stripeErr.Code == stripe.ErrorCodeResourceMissing +} + +type stripeLogger struct { + downgrade func(error) bool +} + +func (stripeLogger) Debugf(format string, v ...interface{}) { + stripe.DefaultLeveledLogger.Debugf(format, v...) +} + +func (stripeLogger) Infof(format string, v ...interface{}) { + stripe.DefaultLeveledLogger.Infof(format, v...) +} + +func (stripeLogger) Warnf(format string, v ...interface{}) { + stripe.DefaultLeveledLogger.Warnf(format, v...) +} + +func (l stripeLogger) Errorf(format string, v ...interface{}) { + if l.downgrade != nil { + for _, a := range v { + if err, ok := a.(error); ok && l.downgrade(err) { + log.Warn().Msgf(format, v...) + return + } + } + } + + log.Error().Msgf(format, v...) +} + +func stripeBackends(downgrade func(error) bool) *stripe.Backends { + logger := stripeLogger{downgrade: downgrade} + newBackend := func(t stripe.SupportedBackend) stripe.Backend { + return stripe.GetBackendWithConfig(t, &stripe.BackendConfig{LeveledLogger: logger}) + } + + return &stripe.Backends{ + API: newBackend(stripe.APIBackend), + Connect: newBackend(stripe.ConnectBackend), + Uploads: newBackend(stripe.UploadsBackend), + } +} diff --git a/stripesync/logger_test.go b/stripesync/logger_test.go new file mode 100644 index 0000000..75a6799 --- /dev/null +++ b/stripesync/logger_test.go @@ -0,0 +1,50 @@ +package stripesync + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" + "github.com/stripe/stripe-go/v74" + "github.com/stripe/stripe-go/v74/coupon" +) + +func TestStripeLoggerErrorLevel(t *testing.T) { + tests := []struct { + name string + body string + downgrade func(error) bool + want string + }{ + {"missing resource without downgrade", `{"error":{"code":"resource_missing","type":"invalid_request_error"}}`, nil, `"level":"error"`}, + {"missing resource with downgrade", `{"error":{"code":"resource_missing","type":"invalid_request_error"}}`, IsMissingResourceError, `"level":"warn"`}, + {"other error with downgrade", `{"error":{"code":"api_key_expired","type":"invalid_request_error"}}`, IsMissingResourceError, `"level":"error"`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(tt.body)) + })) + defer srv.Close() + + var buf bytes.Buffer + prev := log.Logger + log.Logger = zerolog.New(&buf) + defer func() { log.Logger = prev }() + + b := stripe.GetBackendWithConfig(stripe.APIBackend, &stripe.BackendConfig{ + LeveledLogger: stripeLogger{downgrade: tt.downgrade}, + URL: stripe.String(srv.URL), + }) + client := coupon.Client{B: b, Key: "sk_test_dummy"} + _, _ = client.Get("co_missing", nil) + + assert.Contains(t, buf.String(), tt.want) + }) + } +} diff --git a/stripesync/stripesync.go b/stripesync/stripesync.go index d315b4f..2bfb96d 100644 --- a/stripesync/stripesync.go +++ b/stripesync/stripesync.go @@ -22,6 +22,9 @@ type Config struct { // ExcludedFields is a list of fields that should be excluded from the sync. ExcludedFields []string + // DowngradeStripeError decides which Stripe SDK errors to log at warn instead of error. + DowngradeStripeError func(error) bool + Postgres PostgresConfig } @@ -50,7 +53,7 @@ func (pc PostgresConfig) StoreConfig() postgres.Config { // New creates a new StripeSync handle. func New(cfg Config) (*StripeSync, error) { stripeClient := &client.API{} - stripeClient.Init(cfg.StripeAPIKey, nil) + stripeClient.Init(cfg.StripeAPIKey, stripeBackends(cfg.DowngradeStripeError)) db, err := postgres.NewPostgresStore(cfg.Postgres.StoreConfig()) if err != nil {