From e20968b0a2463074796099f51a84be3eca09868a Mon Sep 17 00:00:00 2001 From: Stuart Kuentzel Date: Fri, 2 Jan 2026 15:55:21 +0900 Subject: [PATCH] initial for app sumo webhook --- api/handlers/subscription_handlers.go | 9 +++ api/main.go | 1 + controller/subscription_controller.go | 32 +++++++++ db_migrations.go | 9 +++ model/account_redemption_code_model.go | 80 +++++++++++++++++++++ model/account_redemption_code_model_test.go | 52 ++++++++++++++ 6 files changed, 183 insertions(+) create mode 100644 model/account_redemption_code_model.go create mode 100644 model/account_redemption_code_model_test.go diff --git a/api/handlers/subscription_handlers.go b/api/handlers/subscription_handlers.go index 33ceb492..63cfd02a 100644 --- a/api/handlers/subscription_handlers.go +++ b/api/handlers/subscription_handlers.go @@ -71,3 +71,12 @@ func CreateStripePaymentIntent(w http.ResponseWriter, r *http.Request) { func StripeCreateCustomerPortal(w http.ResponseWriter, r *http.Request) { router.WrapWithInputRequireAuth(controller.StripeCreateCustomerPortal, w, r) } + +func AppSumoPurchaseWebhook(w http.ResponseWriter, r *http.Request) { + router.WrapWithInputBodyFormatterNoAuth( + controller.VerifyAppSumoBody, + controller.AppSumoWebhook, + w, + r, + ) +} diff --git a/api/main.go b/api/main.go index 594f543b..7cf3682d 100644 --- a/api/main.go +++ b/api/main.go @@ -126,6 +126,7 @@ Options: router.NewRoute("POST", "/solana/payment-intent", handlers.CreateSolanaPaymentIntent), router.NewRoute("POST", "/stripe/payment-intent", handlers.CreateStripePaymentIntent), router.NewRoute("POST", "/stripe/customer-portal", handlers.StripeCreateCustomerPortal), + router.NewRoute("POST", "/app-sumo/purchase", handlers.AppSumoPurchaseWebhook), router.NewRoute("GET", "/wallet/balance", handlers.WalletBalance), router.NewRoute("POST", "/wallet/validate-address", handlers.WalletValidateAddress), router.NewRoute("POST", "/wallet/circle-init", handlers.WalletCircleInit), diff --git a/controller/subscription_controller.go b/controller/subscription_controller.go index 6d689e14..5d31c3f1 100644 --- a/controller/subscription_controller.go +++ b/controller/subscription_controller.go @@ -2228,3 +2228,35 @@ func CreateSolanaPaymentIntent( model.CreateSolanaPaymentIntent(intent.Reference, clientSession) return &SolanaPaymentIntentResult{}, nil } + +/** + * App Sumo Webhook + */ + +func VerifyAppSumoBody(req *http.Request) (io.Reader, error) { + bodyBytes, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + + // todo - authenticate the request properly + + return bytes.NewReader(bodyBytes), nil +} + +type AppSumoWebhookArgs struct{} + +type AppSumoWebhookResult struct{} + +func AppSumoWebhook( + appSumoWebhook *AppSumoWebhookArgs, + clientSession *session.ClientSession, +) (*AppSumoWebhookResult, error) { + + _, err := model.CreateAccountRedemptionCode(clientSession.Ctx) + if err != nil { + return nil, err + } + + return &AppSumoWebhookResult{}, nil +} diff --git a/db_migrations.go b/db_migrations.go index 6a774e78..7155a59b 100644 --- a/db_migrations.go +++ b/db_migrations.go @@ -2664,4 +2664,13 @@ var migrations = []any{ DROP CONSTRAINT client_connection_reliability_score_pkey, ADD PRIMARY KEY (client_id, lookback_index) `), + + newSqlMigration(` + CREATE TABLE account_redemption_code ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + code varchar(64) NOT NULL UNIQUE, + network_id uuid, + create_time timestamp NOT NULL DEFAULT now() + ) + `), } diff --git a/model/account_redemption_code_model.go b/model/account_redemption_code_model.go new file mode 100644 index 00000000..c456a25a --- /dev/null +++ b/model/account_redemption_code_model.go @@ -0,0 +1,80 @@ +package model + +import ( + "context" + + "github.com/urnetwork/glog" + "github.com/urnetwork/server" + "github.com/urnetwork/server/session" +) + +/** + * Account redemption codes allow users to redeem special offers or credits to their accounts. + * Currently, created in the AppSumo webhook + */ + +func CreateAccountRedemptionCode(ctx context.Context) (redemptionCode string, returnErr error) { + + server.Tx(ctx, func(tx server.PgTx) { + + redemptionCode = generateAlphanumericCode(8) + + _, err := tx.Exec( + ctx, + ` + INSERT INTO account_redemption_code ( + code + ) + VALUES ($1) + `, + redemptionCode, + ) + + if err != nil { + glog.Infof("Error creating account redemption code: %v", err) + returnErr = err + } + + }) + + return + +} + +func ClaimAccountRedemptionCode( + code string, + session *session.ClientSession, +) (claimed bool, err error) { + + server.Tx(session.Ctx, func(tx server.PgTx) { + + result, execErr := tx.Exec( + session.Ctx, + ` + UPDATE account_redemption_code + SET network_id = $2 + WHERE code = $1 AND network_id IS NULL + `, + code, + session.ByJwt.NetworkId, + ) + + if execErr != nil { + err = execErr + return + } + + // check if a row was updated + if result.RowsAffected() == 1 { + claimed = true + } else { + claimed = false // already claimed or code doesn't exist + } + + // todo - apply credits or bonus here + + }) + + return + +} diff --git a/model/account_redemption_code_model_test.go b/model/account_redemption_code_model_test.go new file mode 100644 index 00000000..80524449 --- /dev/null +++ b/model/account_redemption_code_model_test.go @@ -0,0 +1,52 @@ +package model + +import ( + "context" + "testing" + + "github.com/go-playground/assert/v2" + "github.com/urnetwork/server" + "github.com/urnetwork/server/jwt" + "github.com/urnetwork/server/session" +) + +func TestAccountRedemptionCode(t *testing.T) { + server.DefaultTestEnv().Run(func() { + + ctx := context.Background() + networkId := server.NewId() + clientId := server.NewId() + + sessionA := session.Testing_CreateClientSession(ctx, &jwt.ByJwt{ + NetworkId: networkId, + ClientId: &clientId, + }) + + redemptionCode, err := CreateAccountRedemptionCode(ctx) + assert.Equal(t, err, nil) + assert.Equal(t, len(redemptionCode), 8) + + claimed, err := ClaimAccountRedemptionCode(redemptionCode, sessionA) + assert.Equal(t, err, nil) + assert.Equal(t, claimed, true) + + // Try and claim again, should fail + claimed, err = ClaimAccountRedemptionCode(redemptionCode, sessionA) + assert.Equal(t, err, nil) + assert.Equal(t, claimed, false) + + // different account trying to claim, should fail + networkIdB := server.NewId() + clientIdB := server.NewId() + + sessionB := session.Testing_CreateClientSession(ctx, &jwt.ByJwt{ + NetworkId: networkIdB, + ClientId: &clientIdB, + }) + + claimed, err = ClaimAccountRedemptionCode(redemptionCode, sessionB) + assert.Equal(t, err, nil) + assert.Equal(t, claimed, false) + + }) +}