From dd50035a47358e540070dc2542684f5799d89448 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 5 Mar 2026 17:24:50 +0000 Subject: [PATCH] Create leads from signal --- api/handler.go | 18 +++++----- api/lead.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++---- api/signal.go | 1 + 3 files changed, 93 insertions(+), 16 deletions(-) diff --git a/api/handler.go b/api/handler.go index ff2623bf..b4e51053 100644 --- a/api/handler.go +++ b/api/handler.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "github.com/Gleipnir-Technology/nidus-sync/auth" @@ -66,21 +67,20 @@ func authenticatedHandlerJSON[T any](f handlerFunctionGet[T]) http.Handler { }) } -type handlerFunctionPost[FormType any, ResponseType any] func(context.Context, *http.Request, *models.Organization, *models.User, FormType) (ResponseType, *nhttp.ErrorWithStatus) +type handlerFunctionPost[ReqType any, ResponseType any] func(context.Context, *http.Request, *models.Organization, *models.User, ReqType) (ResponseType, *nhttp.ErrorWithStatus) -func authenticatedHandlerJSONPost[FormType any, ResponseType any](f handlerFunctionPost[FormType, ResponseType]) http.Handler { +func authenticatedHandlerJSONPost[ReqType any, ResponseType any](f handlerFunctionPost[ReqType, ResponseType]) http.Handler { return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u *models.User) { w.Header().Set("Content-Type", "application/json") - err := r.ParseForm() + var req ReqType + body, err := io.ReadAll(r.Body) if err != nil { - respondError(w, http.StatusBadRequest, "failed to parse form: %w", err) + respondError(w, http.StatusInternalServerError, "Failed to read body: %w", err) return } - - var form FormType - err = decoder.Decode(&form, r.PostForm) + err = json.Unmarshal(body, &req) if err != nil { - respondError(w, http.StatusBadRequest, "Failed to decode form: %w", err) + respondError(w, http.StatusBadRequest, "Failed to decode request: %w", err) return } ctx := r.Context() @@ -89,7 +89,7 @@ func authenticatedHandlerJSONPost[FormType any, ResponseType any](f handlerFunct respondError(w, http.StatusInternalServerError, "Failed to get org: %w", err) return } - response, e := f(ctx, r, org, u, form) + response, e := f(ctx, r, org, u, req) if e != nil { http.Error(w, e.Error(), e.Status) return diff --git a/api/lead.go b/api/lead.go index c9fad0c8..5c056928 100644 --- a/api/lead.go +++ b/api/lead.go @@ -3,17 +3,27 @@ package api import ( "context" "net/http" + "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" - "github.com/rs/zerolog/log" + //"github.com/rs/zerolog/log" + "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" + "github.com/stephenafamo/scan" ) -type formLeads struct { - SignalIDs []int `schema:"signal_ids"` +type createLead struct { + SignalIDs []int `json:"signal_ids"` } type createdLead struct { - ID int `json:"id"` + ID int32 `json:"id"` } type contentListLead struct { Leads []lead `json:"leads"` @@ -27,9 +37,75 @@ func listLead(ctx context.Context, r *http.Request, org *models.Organization, us Leads: make([]lead, 0), }, nil } -func postLeads(ctx context.Context, r *http.Request, org *models.Organization, user *models.User, f formLeads) (*createdLead, *nhttp.ErrorWithStatus) { - log.Info().Ints("signal ids", f.SignalIDs).Msg("fake post leads") +func postLeads(ctx context.Context, r *http.Request, org *models.Organization, user *models.User, req createLead) (*createdLead, *nhttp.ErrorWithStatus) { + if len(req.SignalIDs) == 0 { + return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "can't make a lead with no signals") + } + if len(req.SignalIDs) > 1 { + return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "can't make a lead with multiple signals yet") + } + signal_id := req.SignalIDs[0] + txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil) + defer txn.Rollback(ctx) + + if err != nil { + return nil, nhttp.NewError("start transaction: %w", err) + } + type _Row struct { + ID int32 `db:"site_id"` + Version int32 `db:"site_version"` + } + site, err := bob.One(ctx, db.PGInstance.BobDB, psql.Select( + sm.Columns( + "pool.site_id AS site_id", + "pool.site_version AS site_version", + ), + sm.From("signal_pool"), + sm.InnerJoin("pool").OnEQ( + psql.Quote("signal_pool", "pool_id"), + psql.Quote("pool", "id"), + ), + sm.InnerJoin("site").On( + psql.And( + psql.Quote("pool", "site_id").EQ(psql.Quote("site", "id")), + psql.Quote("pool", "site_version").EQ(psql.Quote("site", "version")), + ), + ), + sm.Where(psql.Quote("signal_pool", "signal_id").EQ(psql.Arg(signal_id))), + sm.Where(psql.Quote("site", "organization_id").EQ(psql.Arg(org.ID))), + ), scan.StructMapper[_Row]()) + if err != nil { + if err.Error() == "sql: no rows in result set" { + return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "Can't make a lead from signal %d: %w", signal_id, err) + } + return nil, nhttp.NewError("failed getting site: %w", err) + } + + lead, err := models.Leads.Insert(&models.LeadSetter{ + Created: omit.From(time.Now()), + Creator: omit.From(user.ID), + // ID + OrganizationID: omit.From(org.ID), + SiteID: omitnull.From(site.ID), + SiteVersion: omitnull.From(site.Version), + Type: omit.From(enums.LeadtypeGreenPool), + }).One(ctx, txn) + if err != nil { + return nil, nhttp.NewError("failed to create lead: %w", err) + } + _, err = psql.Update( + um.Table("signal"), + um.SetCol("addressed").ToArg(time.Now()), + um.SetCol("addressor").ToArg(user.ID), + um.Where(psql.Quote("id").EQ(psql.Arg(signal_id))), + ).Exec(ctx, txn) + if err != nil { + return nil, nhttp.NewError("failed to update signal %d: %w", signal_id, err) + } + + txn.Commit(ctx) + return &createdLead{ - ID: 0, + ID: lead.ID, }, nil } diff --git a/api/signal.go b/api/signal.go index b339658a..5b081446 100644 --- a/api/signal.go +++ b/api/signal.go @@ -96,6 +96,7 @@ func listSignal(ctx context.Context, r *http.Request, org *models.Organization, psql.Quote("address", "id"), ), sm.Where(psql.Quote("signal", "organization_id").EQ(psql.Arg(org.ID))), + sm.Where(psql.Quote("signal", "addressed").IsNull()), ), scan.StructMapper[_Row]()) /*