From adc99e8871e992981145ce43e9624259f66dba41 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 26 Jan 2026 16:10:30 +0000 Subject: [PATCH] Add ability to delay text message sending --- background/text.go | 2 +- comms/text/db.go | 33 ++ comms/text/initial.go | 31 + comms/text/report-subscription.go | 19 +- comms/text/text.go | 16 +- config/config.go | 4 +- db/dberrors/comms.text_job.bob.go | 17 + db/dbinfo/comms.text_job.bob.go | 147 +++++ db/dbinfo/comms.text_log.bob.go | 14 +- db/enums/enums.bob.go | 70 +++ db/factory/bobfactory_context.bob.go | 5 + db/factory/bobfactory_main.bob.go | 46 ++ db/factory/bobfactory_random.bob.go | 10 + db/factory/comms.phone.bob.go | 94 ++- db/factory/comms.text_job.bob.go | 499 ++++++++++++++++ db/factory/comms.text_log.bob.go | 44 ++ db/migrations/00041_text_log_overhaul.sql | 16 + db/models/bob_joins.bob.go | 2 + db/models/bob_loaders.bob.go | 4 + db/models/bob_where.bob.go | 3 + db/models/comms.phone.bob.go | 254 ++++++++ db/models/comms.text_job.bob.go | 679 ++++++++++++++++++++++ db/models/comms.text_log.bob.go | 41 +- main.go | 6 + 24 files changed, 2028 insertions(+), 28 deletions(-) create mode 100644 comms/text/initial.go create mode 100644 db/dberrors/comms.text_job.bob.go create mode 100644 db/dbinfo/comms.text_job.bob.go create mode 100644 db/factory/comms.text_job.bob.go create mode 100644 db/models/comms.text_job.bob.go diff --git a/background/text.go b/background/text.go index 80de3b2b..91308769 100644 --- a/background/text.go +++ b/background/text.go @@ -14,7 +14,7 @@ func ReportSubscriptionConfirmationText(destination text.E164, report_id string) enqueueJobText(text.NewJobReportSubscriptionConfirmation( destination, report_id, - config.RMOPhoneNumber, + config.PhoneNumberReport, )) } diff --git a/comms/text/db.go b/comms/text/db.go index 5b609df9..1795a061 100644 --- a/comms/text/db.go +++ b/comms/text/db.go @@ -10,14 +10,21 @@ import ( "strings" "time" + "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" + "github.com/nyaruka/phonenumbers" "github.com/rs/zerolog/log" "github.com/stephenafamo/bob/types/pgtypes" ) +func StoreSources() error { + ctx := context.TODO() + src := phonenumbers.Format(&config.PhoneNumberReport, phonenumbers.E164) + return ensureInDB(ctx, src) +} func convertToPGData(data map[string]string) pgtypes.HStore { result := pgtypes.HStore{} for k, v := range data { @@ -26,6 +33,21 @@ func convertToPGData(data map[string]string) pgtypes.HStore { return result } +func delayMessage(ctx context.Context, source string, destination string, content string, type_ enums.CommsTextjobtype) error { + job, err := models.CommsTextJobs.Insert(&models.CommsTextJobSetter{ + Content: omit.From(content), + Created: omit.From(time.Now()), + Destination: omit.From(destination), + //ID: + Type: omit.From(type_), + }).One(ctx, db.PGInstance.BobDB) + if err != nil { + return fmt.Errorf("Failed to add delayed text job: %w", err) + } + log.Info().Int32("id", job.ID).Msg("Created delayed text job") + return nil +} + func ensureInDB(ctx context.Context, destination string) (err error) { _, err = models.FindCommsPhone(ctx, db.PGInstance.BobDB, destination) if err != nil { @@ -58,6 +80,17 @@ func insertTextLog(ctx context.Context, content string, destination string, sour return err } +func isSubscribed(ctx context.Context, destination string) (bool, error) { + phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, destination) + if err != nil { + if err.Error() == "sql: no rows in result set" { + return false, nil + } + return false, fmt.Errorf("Failed to find phone number %s: %w", destination, err) + } + return phone.IsSubscribed, nil +} + func generatePublicId(t enums.CommsMessagetypeemail, m map[string]string) string { if m == nil || len(m) == 0 { // Return hash of empty string for empty maps diff --git a/comms/text/initial.go b/comms/text/initial.go new file mode 100644 index 00000000..d2bf35a5 --- /dev/null +++ b/comms/text/initial.go @@ -0,0 +1,31 @@ +package text + +import ( + "context" + "fmt" + + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/enums" + "github.com/Gleipnir-Technology/nidus-sync/db/models" +) + +func ensureInitialText(ctx context.Context, src string, dst string) error { + // + origin := enums.CommsTextoriginWebsiteAction + rows, err := models.CommsTextLogs.Query( + models.SelectWhere.CommsTextLogs.Destination.EQ(dst), + models.SelectWhere.CommsTextLogs.IsWelcome.EQ(true), + ).All(ctx, db.PGInstance.BobDB) + if err != nil { + return fmt.Errorf("Failed to query text logs: %w", err) + } + if len(rows) > 0 { + return nil + } + content := "Welcome to Report Mosquitoes Online. We received your request and want to confirm text updates. Reply YES to continue. Reply STOP at any time to unsubscribe" + err = sendText(ctx, src, dst, content, origin) + if err != nil { + return fmt.Errorf("Failed to send initial confirmation: %w", err) + } + return nil +} diff --git a/comms/text/report-subscription.go b/comms/text/report-subscription.go index 1dd1f193..092b9ee2 100644 --- a/comms/text/report-subscription.go +++ b/comms/text/report-subscription.go @@ -48,9 +48,24 @@ func sendReportSubscription(ctx context.Context, job Job) error { return fmt.Errorf("job is not for report subscription confirmation") } - err := sendText(ctx, j.src, j.dst, j.content(), enums.CommsTextoriginWebsiteAction) + sub, err := isSubscribed(ctx, job.destination()) if err != nil { - return fmt.Errorf("Failed to send report subscription confirmation: %w", err) + return fmt.Errorf("Failed to check if subscribed: %w", err) + } + if !sub { + err = sendText(ctx, j.source(), j.destination(), j.content(), enums.CommsTextoriginWebsiteAction) + if err != nil { + return fmt.Errorf("Failed to send report subscription confirmation: %w", err) + } + } else { + err = delayMessage(ctx, j.source(), j.destination(), j.content(), enums.CommsTextjobtypeReportConfirmation) + if err != nil { + return fmt.Errorf("Failed to delay report subscription message: %w", err) + } + err := ensureInitialText(ctx, j.source(), j.destination()) + if err != nil { + return fmt.Errorf("Failed to ensure initial text has been sent: %w", err) + } } return nil } diff --git a/comms/text/text.go b/comms/text/text.go index e0846d2b..415a301d 100644 --- a/comms/text/text.go +++ b/comms/text/text.go @@ -19,14 +19,12 @@ func ParsePhoneNumber(input string) (*E164, error) { return phonenumbers.Parse(input, "US") } -func sendText(ctx context.Context, source E164, destination E164, message string, origin enums.CommsTextorigin) error { - src := phonenumbers.Format(&source, phonenumbers.E164) - dest := phonenumbers.Format(&destination, phonenumbers.E164) - err := ensureInDB(ctx, dest) +func sendText(ctx context.Context, source string, destination string, message string, origin enums.CommsTextorigin) error { + err := ensureInDB(ctx, destination) if err != nil { return fmt.Errorf("Failed to ensure text message destination is in the DB: %w", err) } - err = insertTextLog(ctx, message, dest, src, origin) + err = insertTextLog(ctx, message, destination, source, origin) if err != nil { return fmt.Errorf("Failed to insert text message in the DB: %w", err) } @@ -36,16 +34,16 @@ func sendText(ctx context.Context, source E164, destination E164, message string params.SetMessagingServiceSid(config.TwilioMessagingServiceSID) params.SetBody(message) - params.SetTo(dest) + params.SetTo(destination) resp, err := client.Api.CreateMessage(params) if err != nil { - return fmt.Errorf("Failed to create message to %s: %w", dest, err) + return fmt.Errorf("Failed to create message to %s: %w", destination, err) } else { if resp.Body != nil { - log.Info().Str("dest", dest).Str("body", *resp.Body).Msg("Text message response") + log.Info().Str("dest", destination).Str("body", *resp.Body).Msg("Text message response") } else { - log.Info().Str("dest", dest).Msg("Text message response is nil") + log.Info().Str("dest", destination).Msg("Text message response is nil") } } return nil diff --git a/config/config.go b/config/config.go index 584e5f0c..09b356f2 100644 --- a/config/config.go +++ b/config/config.go @@ -26,7 +26,7 @@ var ( ForwardEmailReportUsername string MapboxToken string PGDSN string - RMOPhoneNumber phonenumbers.PhoneNumber + PhoneNumberReport phonenumbers.PhoneNumber TwilioAuthToken string TwilioAccountSID string TwilioMessagingServiceSID string @@ -135,7 +135,7 @@ func Parse() (err error) { if err != nil { return fmt.Errorf("Failed to parse '%s' as a valid phone number: %w", rmo_phone_number, err) } - RMOPhoneNumber = *p + PhoneNumberReport = *p TwilioAccountSID = os.Getenv("TWILIO_ACCOUNT_SID") if TwilioAccountSID == "" { diff --git a/db/dberrors/comms.text_job.bob.go b/db/dberrors/comms.text_job.bob.go new file mode 100644 index 00000000..cedcca8e --- /dev/null +++ b/db/dberrors/comms.text_job.bob.go @@ -0,0 +1,17 @@ +// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package dberrors + +var CommsTextJobErrors = &commsTextJobErrors{ + ErrUniqueTextJobPkey: &UniqueConstraintError{ + schema: "comms", + table: "text_job", + columns: []string{"id"}, + s: "text_job_pkey", + }, +} + +type commsTextJobErrors struct { + ErrUniqueTextJobPkey *UniqueConstraintError +} diff --git a/db/dbinfo/comms.text_job.bob.go b/db/dbinfo/comms.text_job.bob.go new file mode 100644 index 00000000..aa87ea6f --- /dev/null +++ b/db/dbinfo/comms.text_job.bob.go @@ -0,0 +1,147 @@ +// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package dbinfo + +import "github.com/aarondl/opt/null" + +var CommsTextJobs = Table[ + commsTextJobColumns, + commsTextJobIndexes, + commsTextJobForeignKeys, + commsTextJobUniques, + commsTextJobChecks, +]{ + Schema: "comms", + Name: "text_job", + Columns: commsTextJobColumns{ + Content: column{ + Name: "content", + DBType: "text", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + Created: column{ + Name: "created", + DBType: "timestamp without time zone", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + Destination: column{ + Name: "destination", + DBType: "text", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + ID: column{ + Name: "id", + DBType: "integer", + Default: "nextval('comms.text_job_id_seq'::regclass)", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + Type: column{ + Name: "type_", + DBType: "comms.textjobtype", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + }, + Indexes: commsTextJobIndexes{ + TextJobPkey: index{ + Type: "btree", + Name: "text_job_pkey", + Columns: []indexColumn{ + { + Name: "id", + Desc: null.FromCond(false, true), + IsExpression: false, + }, + }, + Unique: true, + Comment: "", + NullsFirst: []bool{false}, + NullsDistinct: false, + Where: "", + Include: []string{}, + }, + }, + PrimaryKey: &constraint{ + Name: "text_job_pkey", + Columns: []string{"id"}, + Comment: "", + }, + ForeignKeys: commsTextJobForeignKeys{ + CommsTextJobTextJobDestinationFkey: foreignKey{ + constraint: constraint{ + Name: "comms.text_job.text_job_destination_fkey", + Columns: []string{"destination"}, + Comment: "", + }, + ForeignTable: "comms.phone", + ForeignColumns: []string{"e164"}, + }, + }, + + Comment: "Used to track text messages that should be sent later", +} + +type commsTextJobColumns struct { + Content column + Created column + Destination column + ID column + Type column +} + +func (c commsTextJobColumns) AsSlice() []column { + return []column{ + c.Content, c.Created, c.Destination, c.ID, c.Type, + } +} + +type commsTextJobIndexes struct { + TextJobPkey index +} + +func (i commsTextJobIndexes) AsSlice() []index { + return []index{ + i.TextJobPkey, + } +} + +type commsTextJobForeignKeys struct { + CommsTextJobTextJobDestinationFkey foreignKey +} + +func (f commsTextJobForeignKeys) AsSlice() []foreignKey { + return []foreignKey{ + f.CommsTextJobTextJobDestinationFkey, + } +} + +type commsTextJobUniques struct{} + +func (u commsTextJobUniques) AsSlice() []constraint { + return []constraint{} +} + +type commsTextJobChecks struct{} + +func (c commsTextJobChecks) AsSlice() []check { + return []check{} +} diff --git a/db/dbinfo/comms.text_log.bob.go b/db/dbinfo/comms.text_log.bob.go index 7447d67d..0e4a1329 100644 --- a/db/dbinfo/comms.text_log.bob.go +++ b/db/dbinfo/comms.text_log.bob.go @@ -51,6 +51,15 @@ var CommsTextLogs = Table[ Generated: false, AutoIncr: false, }, + IsWelcome: column{ + Name: "is_welcome", + DBType: "boolean", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, Origin: column{ Name: "origin", DBType: "comms.textorigin", @@ -115,7 +124,7 @@ var CommsTextLogs = Table[ }, }, - Comment: "", + Comment: "Used to track text messages that were sent.", } type commsTextLogColumns struct { @@ -123,13 +132,14 @@ type commsTextLogColumns struct { Created column Destination column ID column + IsWelcome column Origin column Source column } func (c commsTextLogColumns) AsSlice() []column { return []column{ - c.Content, c.Created, c.Destination, c.ID, c.Origin, c.Source, + c.Content, c.Created, c.Destination, c.ID, c.IsWelcome, c.Origin, c.Source, } } diff --git a/db/enums/enums.bob.go b/db/enums/enums.bob.go index 2cccb414..81f030ec 100644 --- a/db/enums/enums.bob.go +++ b/db/enums/enums.bob.go @@ -272,6 +272,76 @@ func (e *CommsMessagetypeemail) Scan(value any) error { return nil } +// Enum values for CommsTextjobtype +const ( + CommsTextjobtypeReportConfirmation CommsTextjobtype = "report-confirmation" +) + +func AllCommsTextjobtype() []CommsTextjobtype { + return []CommsTextjobtype{ + CommsTextjobtypeReportConfirmation, + } +} + +type CommsTextjobtype string + +func (e CommsTextjobtype) String() string { + return string(e) +} + +func (e CommsTextjobtype) Valid() bool { + switch e { + case CommsTextjobtypeReportConfirmation: + return true + default: + return false + } +} + +// useful when testing in other packages +func (e CommsTextjobtype) All() []CommsTextjobtype { + return AllCommsTextjobtype() +} + +func (e CommsTextjobtype) MarshalText() ([]byte, error) { + return []byte(e), nil +} + +func (e *CommsTextjobtype) UnmarshalText(text []byte) error { + return e.Scan(text) +} + +func (e CommsTextjobtype) MarshalBinary() ([]byte, error) { + return []byte(e), nil +} + +func (e *CommsTextjobtype) UnmarshalBinary(data []byte) error { + return e.Scan(data) +} + +func (e CommsTextjobtype) Value() (driver.Value, error) { + return string(e), nil +} + +func (e *CommsTextjobtype) Scan(value any) error { + switch x := value.(type) { + case string: + *e = CommsTextjobtype(x) + case []byte: + *e = CommsTextjobtype(x) + case nil: + return fmt.Errorf("cannot nil into CommsTextjobtype") + default: + return fmt.Errorf("cannot scan type %T: %v", value, value) + } + + if !e.Valid() { + return fmt.Errorf("invalid CommsTextjobtype value: %s", *e) + } + + return nil +} + // Enum values for CommsTextorigin const ( CommsTextoriginDistrict CommsTextorigin = "district" diff --git a/db/factory/bobfactory_context.bob.go b/db/factory/bobfactory_context.bob.go index f105c35c..4258ed4f 100644 --- a/db/factory/bobfactory_context.bob.go +++ b/db/factory/bobfactory_context.bob.go @@ -32,9 +32,14 @@ var ( // Relationship Contexts for comms.phone commsPhoneWithParentsCascadingCtx = newContextual[bool]("commsPhoneWithParentsCascading") + commsPhoneRelDestinationTextJobsCtx = newContextual[bool]("comms.phone.comms.text_job.comms.text_job.text_job_destination_fkey") commsPhoneRelDestinationTextLogsCtx = newContextual[bool]("comms.phone.comms.text_log.comms.text_log.text_log_destination_fkey") commsPhoneRelSourceTextLogsCtx = newContextual[bool]("comms.phone.comms.text_log.comms.text_log.text_log_source_fkey") + // Relationship Contexts for comms.text_job + commsTextJobWithParentsCascadingCtx = newContextual[bool]("commsTextJobWithParentsCascading") + commsTextJobRelDestinationPhoneCtx = newContextual[bool]("comms.phone.comms.text_job.comms.text_job.text_job_destination_fkey") + // Relationship Contexts for comms.text_log commsTextLogWithParentsCascadingCtx = newContextual[bool]("commsTextLogWithParentsCascading") commsTextLogRelDestinationPhoneCtx = newContextual[bool]("comms.phone.comms.text_log.comms.text_log.text_log_destination_fkey") diff --git a/db/factory/bobfactory_main.bob.go b/db/factory/bobfactory_main.bob.go index ab527cba..6e8d4391 100644 --- a/db/factory/bobfactory_main.bob.go +++ b/db/factory/bobfactory_main.bob.go @@ -25,6 +25,7 @@ type Factory struct { baseCommsEmailLogMods CommsEmailLogModSlice baseCommsEmailTemplateMods CommsEmailTemplateModSlice baseCommsPhoneMods CommsPhoneModSlice + baseCommsTextJobMods CommsTextJobModSlice baseCommsTextLogMods CommsTextLogModSlice baseFieldseekerContainerrelateMods FieldseekerContainerrelateModSlice baseFieldseekerFieldscoutinglogMods FieldseekerFieldscoutinglogModSlice @@ -295,6 +296,9 @@ func (f *Factory) FromExistingCommsPhone(m *models.CommsPhone) *CommsPhoneTempla o.IsSubscribed = func() bool { return m.IsSubscribed } ctx := context.Background() + if len(m.R.DestinationTextJobs) > 0 { + CommsPhoneMods.AddExistingDestinationTextJobs(m.R.DestinationTextJobs...).Apply(ctx, o) + } if len(m.R.DestinationTextLogs) > 0 { CommsPhoneMods.AddExistingDestinationTextLogs(m.R.DestinationTextLogs...).Apply(ctx, o) } @@ -305,6 +309,39 @@ func (f *Factory) FromExistingCommsPhone(m *models.CommsPhone) *CommsPhoneTempla return o } +func (f *Factory) NewCommsTextJob(mods ...CommsTextJobMod) *CommsTextJobTemplate { + return f.NewCommsTextJobWithContext(context.Background(), mods...) +} + +func (f *Factory) NewCommsTextJobWithContext(ctx context.Context, mods ...CommsTextJobMod) *CommsTextJobTemplate { + o := &CommsTextJobTemplate{f: f} + + if f != nil { + f.baseCommsTextJobMods.Apply(ctx, o) + } + + CommsTextJobModSlice(mods).Apply(ctx, o) + + return o +} + +func (f *Factory) FromExistingCommsTextJob(m *models.CommsTextJob) *CommsTextJobTemplate { + o := &CommsTextJobTemplate{f: f, alreadyPersisted: true} + + o.Content = func() string { return m.Content } + o.Created = func() time.Time { return m.Created } + o.Destination = func() string { return m.Destination } + o.ID = func() int32 { return m.ID } + o.Type = func() enums.CommsTextjobtype { return m.Type } + + ctx := context.Background() + if m.R.DestinationPhone != nil { + CommsTextJobMods.WithExistingDestinationPhone(m.R.DestinationPhone).Apply(ctx, o) + } + + return o +} + func (f *Factory) NewCommsTextLog(mods ...CommsTextLogMod) *CommsTextLogTemplate { return f.NewCommsTextLogWithContext(context.Background(), mods...) } @@ -328,6 +365,7 @@ func (f *Factory) FromExistingCommsTextLog(m *models.CommsTextLog) *CommsTextLog o.Created = func() time.Time { return m.Created } o.Destination = func() string { return m.Destination } o.ID = func() int32 { return m.ID } + o.IsWelcome = func() bool { return m.IsWelcome } o.Origin = func() enums.CommsTextorigin { return m.Origin } o.Source = func() string { return m.Source } @@ -3276,6 +3314,14 @@ func (f *Factory) AddBaseCommsPhoneMod(mods ...CommsPhoneMod) { f.baseCommsPhoneMods = append(f.baseCommsPhoneMods, mods...) } +func (f *Factory) ClearBaseCommsTextJobMods() { + f.baseCommsTextJobMods = nil +} + +func (f *Factory) AddBaseCommsTextJobMod(mods ...CommsTextJobMod) { + f.baseCommsTextJobMods = append(f.baseCommsTextJobMods, mods...) +} + func (f *Factory) ClearBaseCommsTextLogMods() { f.baseCommsTextLogMods = nil } diff --git a/db/factory/bobfactory_random.bob.go b/db/factory/bobfactory_random.bob.go index 23a5b12f..28ceac86 100644 --- a/db/factory/bobfactory_random.bob.go +++ b/db/factory/bobfactory_random.bob.go @@ -101,6 +101,16 @@ func random_enums_CommsMessagetypeemail(f *faker.Faker, limits ...string) enums. return all[f.IntBetween(0, len(all)-1)] } +func random_enums_CommsTextjobtype(f *faker.Faker, limits ...string) enums.CommsTextjobtype { + if f == nil { + f = &defaultFaker + } + + var e enums.CommsTextjobtype + all := e.All() + return all[f.IntBetween(0, len(all)-1)] +} + func random_enums_CommsTextorigin(f *faker.Faker, limits ...string) enums.CommsTextorigin { if f == nil { f = &defaultFaker diff --git a/db/factory/comms.phone.bob.go b/db/factory/comms.phone.bob.go index 415c6957..cefbb124 100644 --- a/db/factory/comms.phone.bob.go +++ b/db/factory/comms.phone.bob.go @@ -44,10 +44,15 @@ type CommsPhoneTemplate struct { } type commsPhoneR struct { + DestinationTextJobs []*commsPhoneRDestinationTextJobsR DestinationTextLogs []*commsPhoneRDestinationTextLogsR SourceTextLogs []*commsPhoneRSourceTextLogsR } +type commsPhoneRDestinationTextJobsR struct { + number int + o *CommsTextJobTemplate +} type commsPhoneRDestinationTextLogsR struct { number int o *CommsTextLogTemplate @@ -67,6 +72,19 @@ func (o *CommsPhoneTemplate) Apply(ctx context.Context, mods ...CommsPhoneMod) { // setModelRels creates and sets the relationships on *models.CommsPhone // according to the relationships in the template. Nothing is inserted into the db func (t CommsPhoneTemplate) setModelRels(o *models.CommsPhone) { + if t.r.DestinationTextJobs != nil { + rel := models.CommsTextJobSlice{} + for _, r := range t.r.DestinationTextJobs { + related := r.o.BuildMany(r.number) + for _, rel := range related { + rel.Destination = o.E164 // h2 + rel.R.DestinationPhone = o + } + rel = append(rel, related...) + } + o.R.DestinationTextJobs = rel + } + if t.r.DestinationTextLogs != nil { rel := models.CommsTextLogSlice{} for _, r := range t.r.DestinationTextLogs { @@ -171,6 +189,26 @@ func ensureCreatableCommsPhone(m *models.CommsPhoneSetter) { func (o *CommsPhoneTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m *models.CommsPhone) error { var err error + isDestinationTextJobsDone, _ := commsPhoneRelDestinationTextJobsCtx.Value(ctx) + if !isDestinationTextJobsDone && o.r.DestinationTextJobs != nil { + ctx = commsPhoneRelDestinationTextJobsCtx.WithValue(ctx, true) + for _, r := range o.r.DestinationTextJobs { + if r.o.alreadyPersisted { + m.R.DestinationTextJobs = append(m.R.DestinationTextJobs, r.o.Build()) + } else { + rel0, err := r.o.CreateMany(ctx, exec, r.number) + if err != nil { + return err + } + + err = m.AttachDestinationTextJobs(ctx, exec, rel0...) + if err != nil { + return err + } + } + } + } + isDestinationTextLogsDone, _ := commsPhoneRelDestinationTextLogsCtx.Value(ctx) if !isDestinationTextLogsDone && o.r.DestinationTextLogs != nil { ctx = commsPhoneRelDestinationTextLogsCtx.WithValue(ctx, true) @@ -178,12 +216,12 @@ func (o *CommsPhoneTemplate) insertOptRels(ctx context.Context, exec bob.Executo if r.o.alreadyPersisted { m.R.DestinationTextLogs = append(m.R.DestinationTextLogs, r.o.Build()) } else { - rel0, err := r.o.CreateMany(ctx, exec, r.number) + rel1, err := r.o.CreateMany(ctx, exec, r.number) if err != nil { return err } - err = m.AttachDestinationTextLogs(ctx, exec, rel0...) + err = m.AttachDestinationTextLogs(ctx, exec, rel1...) if err != nil { return err } @@ -198,12 +236,12 @@ func (o *CommsPhoneTemplate) insertOptRels(ctx context.Context, exec bob.Executo if r.o.alreadyPersisted { m.R.SourceTextLogs = append(m.R.SourceTextLogs, r.o.Build()) } else { - rel1, err := r.o.CreateMany(ctx, exec, r.number) + rel2, err := r.o.CreateMany(ctx, exec, r.number) if err != nil { return err } - err = m.AttachSourceTextLogs(ctx, exec, rel1...) + err = m.AttachSourceTextLogs(ctx, exec, rel2...) if err != nil { return err } @@ -379,6 +417,54 @@ func (m commsPhoneMods) WithParentsCascading() CommsPhoneMod { }) } +func (m commsPhoneMods) WithDestinationTextJobs(number int, related *CommsTextJobTemplate) CommsPhoneMod { + return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { + o.r.DestinationTextJobs = []*commsPhoneRDestinationTextJobsR{{ + number: number, + o: related, + }} + }) +} + +func (m commsPhoneMods) WithNewDestinationTextJobs(number int, mods ...CommsTextJobMod) CommsPhoneMod { + return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { + related := o.f.NewCommsTextJobWithContext(ctx, mods...) + m.WithDestinationTextJobs(number, related).Apply(ctx, o) + }) +} + +func (m commsPhoneMods) AddDestinationTextJobs(number int, related *CommsTextJobTemplate) CommsPhoneMod { + return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { + o.r.DestinationTextJobs = append(o.r.DestinationTextJobs, &commsPhoneRDestinationTextJobsR{ + number: number, + o: related, + }) + }) +} + +func (m commsPhoneMods) AddNewDestinationTextJobs(number int, mods ...CommsTextJobMod) CommsPhoneMod { + return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { + related := o.f.NewCommsTextJobWithContext(ctx, mods...) + m.AddDestinationTextJobs(number, related).Apply(ctx, o) + }) +} + +func (m commsPhoneMods) AddExistingDestinationTextJobs(existingModels ...*models.CommsTextJob) CommsPhoneMod { + return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { + for _, em := range existingModels { + o.r.DestinationTextJobs = append(o.r.DestinationTextJobs, &commsPhoneRDestinationTextJobsR{ + o: o.f.FromExistingCommsTextJob(em), + }) + } + }) +} + +func (m commsPhoneMods) WithoutDestinationTextJobs() CommsPhoneMod { + return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { + o.r.DestinationTextJobs = nil + }) +} + func (m commsPhoneMods) WithDestinationTextLogs(number int, related *CommsTextLogTemplate) CommsPhoneMod { return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { o.r.DestinationTextLogs = []*commsPhoneRDestinationTextLogsR{{ diff --git a/db/factory/comms.text_job.bob.go b/db/factory/comms.text_job.bob.go new file mode 100644 index 00000000..6d16388f --- /dev/null +++ b/db/factory/comms.text_job.bob.go @@ -0,0 +1,499 @@ +// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package factory + +import ( + "context" + "testing" + "time" + + enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" + models "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/aarondl/opt/omit" + "github.com/jaswdr/faker/v2" + "github.com/stephenafamo/bob" +) + +type CommsTextJobMod interface { + Apply(context.Context, *CommsTextJobTemplate) +} + +type CommsTextJobModFunc func(context.Context, *CommsTextJobTemplate) + +func (f CommsTextJobModFunc) Apply(ctx context.Context, n *CommsTextJobTemplate) { + f(ctx, n) +} + +type CommsTextJobModSlice []CommsTextJobMod + +func (mods CommsTextJobModSlice) Apply(ctx context.Context, n *CommsTextJobTemplate) { + for _, f := range mods { + f.Apply(ctx, n) + } +} + +// CommsTextJobTemplate is an object representing the database table. +// all columns are optional and should be set by mods +type CommsTextJobTemplate struct { + Content func() string + Created func() time.Time + Destination func() string + ID func() int32 + Type func() enums.CommsTextjobtype + + r commsTextJobR + f *Factory + + alreadyPersisted bool +} + +type commsTextJobR struct { + DestinationPhone *commsTextJobRDestinationPhoneR +} + +type commsTextJobRDestinationPhoneR struct { + o *CommsPhoneTemplate +} + +// Apply mods to the CommsTextJobTemplate +func (o *CommsTextJobTemplate) Apply(ctx context.Context, mods ...CommsTextJobMod) { + for _, mod := range mods { + mod.Apply(ctx, o) + } +} + +// setModelRels creates and sets the relationships on *models.CommsTextJob +// according to the relationships in the template. Nothing is inserted into the db +func (t CommsTextJobTemplate) setModelRels(o *models.CommsTextJob) { + if t.r.DestinationPhone != nil { + rel := t.r.DestinationPhone.o.Build() + rel.R.DestinationTextJobs = append(rel.R.DestinationTextJobs, o) + o.Destination = rel.E164 // h2 + o.R.DestinationPhone = rel + } +} + +// BuildSetter returns an *models.CommsTextJobSetter +// this does nothing with the relationship templates +func (o CommsTextJobTemplate) BuildSetter() *models.CommsTextJobSetter { + m := &models.CommsTextJobSetter{} + + if o.Content != nil { + val := o.Content() + m.Content = omit.From(val) + } + if o.Created != nil { + val := o.Created() + m.Created = omit.From(val) + } + if o.Destination != nil { + val := o.Destination() + m.Destination = omit.From(val) + } + if o.ID != nil { + val := o.ID() + m.ID = omit.From(val) + } + if o.Type != nil { + val := o.Type() + m.Type = omit.From(val) + } + + return m +} + +// BuildManySetter returns an []*models.CommsTextJobSetter +// this does nothing with the relationship templates +func (o CommsTextJobTemplate) BuildManySetter(number int) []*models.CommsTextJobSetter { + m := make([]*models.CommsTextJobSetter, number) + + for i := range m { + m[i] = o.BuildSetter() + } + + return m +} + +// Build returns an *models.CommsTextJob +// Related objects are also created and placed in the .R field +// NOTE: Objects are not inserted into the database. Use CommsTextJobTemplate.Create +func (o CommsTextJobTemplate) Build() *models.CommsTextJob { + m := &models.CommsTextJob{} + + if o.Content != nil { + m.Content = o.Content() + } + if o.Created != nil { + m.Created = o.Created() + } + if o.Destination != nil { + m.Destination = o.Destination() + } + if o.ID != nil { + m.ID = o.ID() + } + if o.Type != nil { + m.Type = o.Type() + } + + o.setModelRels(m) + + return m +} + +// BuildMany returns an models.CommsTextJobSlice +// Related objects are also created and placed in the .R field +// NOTE: Objects are not inserted into the database. Use CommsTextJobTemplate.CreateMany +func (o CommsTextJobTemplate) BuildMany(number int) models.CommsTextJobSlice { + m := make(models.CommsTextJobSlice, number) + + for i := range m { + m[i] = o.Build() + } + + return m +} + +func ensureCreatableCommsTextJob(m *models.CommsTextJobSetter) { + if !(m.Content.IsValue()) { + val := random_string(nil) + m.Content = omit.From(val) + } + if !(m.Created.IsValue()) { + val := random_time_Time(nil) + m.Created = omit.From(val) + } + if !(m.Destination.IsValue()) { + val := random_string(nil) + m.Destination = omit.From(val) + } + if !(m.Type.IsValue()) { + val := random_enums_CommsTextjobtype(nil) + m.Type = omit.From(val) + } +} + +// insertOptRels creates and inserts any optional the relationships on *models.CommsTextJob +// according to the relationships in the template. +// any required relationship should have already exist on the model +func (o *CommsTextJobTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m *models.CommsTextJob) error { + var err error + + return err +} + +// Create builds a commsTextJob and inserts it into the database +// Relations objects are also inserted and placed in the .R field +func (o *CommsTextJobTemplate) Create(ctx context.Context, exec bob.Executor) (*models.CommsTextJob, error) { + var err error + opt := o.BuildSetter() + ensureCreatableCommsTextJob(opt) + + if o.r.DestinationPhone == nil { + CommsTextJobMods.WithNewDestinationPhone().Apply(ctx, o) + } + + var rel0 *models.CommsPhone + + if o.r.DestinationPhone.o.alreadyPersisted { + rel0 = o.r.DestinationPhone.o.Build() + } else { + rel0, err = o.r.DestinationPhone.o.Create(ctx, exec) + if err != nil { + return nil, err + } + } + + opt.Destination = omit.From(rel0.E164) + + m, err := models.CommsTextJobs.Insert(opt).One(ctx, exec) + if err != nil { + return nil, err + } + + m.R.DestinationPhone = rel0 + + if err := o.insertOptRels(ctx, exec, m); err != nil { + return nil, err + } + return m, err +} + +// MustCreate builds a commsTextJob and inserts it into the database +// Relations objects are also inserted and placed in the .R field +// panics if an error occurs +func (o *CommsTextJobTemplate) MustCreate(ctx context.Context, exec bob.Executor) *models.CommsTextJob { + m, err := o.Create(ctx, exec) + if err != nil { + panic(err) + } + return m +} + +// CreateOrFail builds a commsTextJob and inserts it into the database +// Relations objects are also inserted and placed in the .R field +// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs +func (o *CommsTextJobTemplate) CreateOrFail(ctx context.Context, tb testing.TB, exec bob.Executor) *models.CommsTextJob { + tb.Helper() + m, err := o.Create(ctx, exec) + if err != nil { + tb.Fatal(err) + return nil + } + return m +} + +// CreateMany builds multiple commsTextJobs and inserts them into the database +// Relations objects are also inserted and placed in the .R field +func (o CommsTextJobTemplate) CreateMany(ctx context.Context, exec bob.Executor, number int) (models.CommsTextJobSlice, error) { + var err error + m := make(models.CommsTextJobSlice, number) + + for i := range m { + m[i], err = o.Create(ctx, exec) + if err != nil { + return nil, err + } + } + + return m, nil +} + +// MustCreateMany builds multiple commsTextJobs and inserts them into the database +// Relations objects are also inserted and placed in the .R field +// panics if an error occurs +func (o CommsTextJobTemplate) MustCreateMany(ctx context.Context, exec bob.Executor, number int) models.CommsTextJobSlice { + m, err := o.CreateMany(ctx, exec, number) + if err != nil { + panic(err) + } + return m +} + +// CreateManyOrFail builds multiple commsTextJobs and inserts them into the database +// Relations objects are also inserted and placed in the .R field +// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs +func (o CommsTextJobTemplate) CreateManyOrFail(ctx context.Context, tb testing.TB, exec bob.Executor, number int) models.CommsTextJobSlice { + tb.Helper() + m, err := o.CreateMany(ctx, exec, number) + if err != nil { + tb.Fatal(err) + return nil + } + return m +} + +// CommsTextJob has methods that act as mods for the CommsTextJobTemplate +var CommsTextJobMods commsTextJobMods + +type commsTextJobMods struct{} + +func (m commsTextJobMods) RandomizeAllColumns(f *faker.Faker) CommsTextJobMod { + return CommsTextJobModSlice{ + CommsTextJobMods.RandomContent(f), + CommsTextJobMods.RandomCreated(f), + CommsTextJobMods.RandomDestination(f), + CommsTextJobMods.RandomID(f), + CommsTextJobMods.RandomType(f), + } +} + +// Set the model columns to this value +func (m commsTextJobMods) Content(val string) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Content = func() string { return val } + }) +} + +// Set the Column from the function +func (m commsTextJobMods) ContentFunc(f func() string) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Content = f + }) +} + +// Clear any values for the column +func (m commsTextJobMods) UnsetContent() CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Content = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextJobMods) RandomContent(f *faker.Faker) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Content = func() string { + return random_string(f) + } + }) +} + +// Set the model columns to this value +func (m commsTextJobMods) Created(val time.Time) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Created = func() time.Time { return val } + }) +} + +// Set the Column from the function +func (m commsTextJobMods) CreatedFunc(f func() time.Time) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Created = f + }) +} + +// Clear any values for the column +func (m commsTextJobMods) UnsetCreated() CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Created = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextJobMods) RandomCreated(f *faker.Faker) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Created = func() time.Time { + return random_time_Time(f) + } + }) +} + +// Set the model columns to this value +func (m commsTextJobMods) Destination(val string) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Destination = func() string { return val } + }) +} + +// Set the Column from the function +func (m commsTextJobMods) DestinationFunc(f func() string) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Destination = f + }) +} + +// Clear any values for the column +func (m commsTextJobMods) UnsetDestination() CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Destination = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextJobMods) RandomDestination(f *faker.Faker) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Destination = func() string { + return random_string(f) + } + }) +} + +// Set the model columns to this value +func (m commsTextJobMods) ID(val int32) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.ID = func() int32 { return val } + }) +} + +// Set the Column from the function +func (m commsTextJobMods) IDFunc(f func() int32) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.ID = f + }) +} + +// Clear any values for the column +func (m commsTextJobMods) UnsetID() CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.ID = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextJobMods) RandomID(f *faker.Faker) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.ID = func() int32 { + return random_int32(f) + } + }) +} + +// Set the model columns to this value +func (m commsTextJobMods) Type(val enums.CommsTextjobtype) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Type = func() enums.CommsTextjobtype { return val } + }) +} + +// Set the Column from the function +func (m commsTextJobMods) TypeFunc(f func() enums.CommsTextjobtype) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Type = f + }) +} + +// Clear any values for the column +func (m commsTextJobMods) UnsetType() CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Type = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextJobMods) RandomType(f *faker.Faker) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Type = func() enums.CommsTextjobtype { + return random_enums_CommsTextjobtype(f) + } + }) +} + +func (m commsTextJobMods) WithParentsCascading() CommsTextJobMod { + return CommsTextJobModFunc(func(ctx context.Context, o *CommsTextJobTemplate) { + if isDone, _ := commsTextJobWithParentsCascadingCtx.Value(ctx); isDone { + return + } + ctx = commsTextJobWithParentsCascadingCtx.WithValue(ctx, true) + { + + related := o.f.NewCommsPhoneWithContext(ctx, CommsPhoneMods.WithParentsCascading()) + m.WithDestinationPhone(related).Apply(ctx, o) + } + }) +} + +func (m commsTextJobMods) WithDestinationPhone(rel *CommsPhoneTemplate) CommsTextJobMod { + return CommsTextJobModFunc(func(ctx context.Context, o *CommsTextJobTemplate) { + o.r.DestinationPhone = &commsTextJobRDestinationPhoneR{ + o: rel, + } + }) +} + +func (m commsTextJobMods) WithNewDestinationPhone(mods ...CommsPhoneMod) CommsTextJobMod { + return CommsTextJobModFunc(func(ctx context.Context, o *CommsTextJobTemplate) { + related := o.f.NewCommsPhoneWithContext(ctx, mods...) + + m.WithDestinationPhone(related).Apply(ctx, o) + }) +} + +func (m commsTextJobMods) WithExistingDestinationPhone(em *models.CommsPhone) CommsTextJobMod { + return CommsTextJobModFunc(func(ctx context.Context, o *CommsTextJobTemplate) { + o.r.DestinationPhone = &commsTextJobRDestinationPhoneR{ + o: o.f.FromExistingCommsPhone(em), + } + }) +} + +func (m commsTextJobMods) WithoutDestinationPhone() CommsTextJobMod { + return CommsTextJobModFunc(func(ctx context.Context, o *CommsTextJobTemplate) { + o.r.DestinationPhone = nil + }) +} diff --git a/db/factory/comms.text_log.bob.go b/db/factory/comms.text_log.bob.go index 6b1cd6e0..d2e88519 100644 --- a/db/factory/comms.text_log.bob.go +++ b/db/factory/comms.text_log.bob.go @@ -40,6 +40,7 @@ type CommsTextLogTemplate struct { Created func() time.Time Destination func() string ID func() int32 + IsWelcome func() bool Origin func() enums.CommsTextorigin Source func() string @@ -107,6 +108,10 @@ func (o CommsTextLogTemplate) BuildSetter() *models.CommsTextLogSetter { val := o.ID() m.ID = omit.From(val) } + if o.IsWelcome != nil { + val := o.IsWelcome() + m.IsWelcome = omit.From(val) + } if o.Origin != nil { val := o.Origin() m.Origin = omit.From(val) @@ -149,6 +154,9 @@ func (o CommsTextLogTemplate) Build() *models.CommsTextLog { if o.ID != nil { m.ID = o.ID() } + if o.IsWelcome != nil { + m.IsWelcome = o.IsWelcome() + } if o.Origin != nil { m.Origin = o.Origin() } @@ -187,6 +195,10 @@ func ensureCreatableCommsTextLog(m *models.CommsTextLogSetter) { val := random_string(nil) m.Destination = omit.From(val) } + if !(m.IsWelcome.IsValue()) { + val := random_bool(nil) + m.IsWelcome = omit.From(val) + } if !(m.Origin.IsValue()) { val := random_enums_CommsTextorigin(nil) m.Origin = omit.From(val) @@ -336,6 +348,7 @@ func (m commsTextLogMods) RandomizeAllColumns(f *faker.Faker) CommsTextLogMod { CommsTextLogMods.RandomCreated(f), CommsTextLogMods.RandomDestination(f), CommsTextLogMods.RandomID(f), + CommsTextLogMods.RandomIsWelcome(f), CommsTextLogMods.RandomOrigin(f), CommsTextLogMods.RandomSource(f), } @@ -465,6 +478,37 @@ func (m commsTextLogMods) RandomID(f *faker.Faker) CommsTextLogMod { }) } +// Set the model columns to this value +func (m commsTextLogMods) IsWelcome(val bool) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.IsWelcome = func() bool { return val } + }) +} + +// Set the Column from the function +func (m commsTextLogMods) IsWelcomeFunc(f func() bool) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.IsWelcome = f + }) +} + +// Clear any values for the column +func (m commsTextLogMods) UnsetIsWelcome() CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.IsWelcome = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextLogMods) RandomIsWelcome(f *faker.Faker) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.IsWelcome = func() bool { + return random_bool(f) + } + }) +} + // Set the model columns to this value func (m commsTextLogMods) Origin(val enums.CommsTextorigin) CommsTextLogMod { return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { diff --git a/db/migrations/00041_text_log_overhaul.sql b/db/migrations/00041_text_log_overhaul.sql index 4e33d846..2a954d31 100644 --- a/db/migrations/00041_text_log_overhaul.sql +++ b/db/migrations/00041_text_log_overhaul.sql @@ -6,18 +6,34 @@ CREATE TYPE comms.TextOrigin AS ENUM ( 'llm', 'website-action' ); +CREATE TYPE comms.TextJobType AS ENUM ( + 'report-confirmation' +); +CREATE TABLE comms.text_job ( + content TEXT NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + destination TEXT NOT NULL REFERENCES comms.phone(e164), + id SERIAL NOT NULL, + type_ comms.TextJobType NOT NULL, + PRIMARY KEY(id) +); +COMMENT ON TABLE comms.text_job IS 'Used to track text messages that should be sent later'; CREATE TABLE comms.text_log ( content TEXT NOT NULL, created TIMESTAMP WITHOUT TIME ZONE NOT NULL, destination TEXT NOT NULL REFERENCES comms.phone(e164), id SERIAL, + is_welcome BOOLEAN NOT NULL, origin comms.TextOrigin NOT NULL, source TEXT NOT NULL REFERENCES comms.phone(e164), PRIMARY KEY(id) ); +COMMENT ON TABLE comms.text_log IS 'Used to track text messages that were sent.'; -- +goose Down DROP TABLE comms.text_log; +DROP TABLE comms.text_job; +DROP TYPE comms.TextJobType; DROP TYPE comms.TextOrigin; CREATE TYPE comms.MessageTypeText AS ENUM ( 'initial-contact', diff --git a/db/models/bob_joins.bob.go b/db/models/bob_joins.bob.go index e6ca18af..a69510f4 100644 --- a/db/models/bob_joins.bob.go +++ b/db/models/bob_joins.bob.go @@ -38,6 +38,7 @@ type joins[Q dialect.Joinable] struct { CommsEmailLogs joinSet[commsEmailLogJoins[Q]] CommsEmailTemplates joinSet[commsEmailTemplateJoins[Q]] CommsPhones joinSet[commsPhoneJoins[Q]] + CommsTextJobs joinSet[commsTextJobJoins[Q]] CommsTextLogs joinSet[commsTextLogJoins[Q]] FieldseekerContainerrelates joinSet[fieldseekerContainerrelateJoins[Q]] FieldseekerFieldscoutinglogs joinSet[fieldseekerFieldscoutinglogJoins[Q]] @@ -104,6 +105,7 @@ func getJoins[Q dialect.Joinable]() joins[Q] { CommsEmailLogs: buildJoinSet[commsEmailLogJoins[Q]](CommsEmailLogs.Columns, buildCommsEmailLogJoins), CommsEmailTemplates: buildJoinSet[commsEmailTemplateJoins[Q]](CommsEmailTemplates.Columns, buildCommsEmailTemplateJoins), CommsPhones: buildJoinSet[commsPhoneJoins[Q]](CommsPhones.Columns, buildCommsPhoneJoins), + CommsTextJobs: buildJoinSet[commsTextJobJoins[Q]](CommsTextJobs.Columns, buildCommsTextJobJoins), CommsTextLogs: buildJoinSet[commsTextLogJoins[Q]](CommsTextLogs.Columns, buildCommsTextLogJoins), FieldseekerContainerrelates: buildJoinSet[fieldseekerContainerrelateJoins[Q]](FieldseekerContainerrelates.Columns, buildFieldseekerContainerrelateJoins), FieldseekerFieldscoutinglogs: buildJoinSet[fieldseekerFieldscoutinglogJoins[Q]](FieldseekerFieldscoutinglogs.Columns, buildFieldseekerFieldscoutinglogJoins), diff --git a/db/models/bob_loaders.bob.go b/db/models/bob_loaders.bob.go index aff45d78..3a003482 100644 --- a/db/models/bob_loaders.bob.go +++ b/db/models/bob_loaders.bob.go @@ -23,6 +23,7 @@ type preloaders struct { CommsEmailLog commsEmailLogPreloader CommsEmailTemplate commsEmailTemplatePreloader CommsPhone commsPhonePreloader + CommsTextJob commsTextJobPreloader CommsTextLog commsTextLogPreloader FieldseekerContainerrelate fieldseekerContainerrelatePreloader FieldseekerFieldscoutinglog fieldseekerFieldscoutinglogPreloader @@ -81,6 +82,7 @@ func getPreloaders() preloaders { CommsEmailLog: buildCommsEmailLogPreloader(), CommsEmailTemplate: buildCommsEmailTemplatePreloader(), CommsPhone: buildCommsPhonePreloader(), + CommsTextJob: buildCommsTextJobPreloader(), CommsTextLog: buildCommsTextLogPreloader(), FieldseekerContainerrelate: buildFieldseekerContainerrelatePreloader(), FieldseekerFieldscoutinglog: buildFieldseekerFieldscoutinglogPreloader(), @@ -145,6 +147,7 @@ type thenLoaders[Q orm.Loadable] struct { CommsEmailLog commsEmailLogThenLoader[Q] CommsEmailTemplate commsEmailTemplateThenLoader[Q] CommsPhone commsPhoneThenLoader[Q] + CommsTextJob commsTextJobThenLoader[Q] CommsTextLog commsTextLogThenLoader[Q] FieldseekerContainerrelate fieldseekerContainerrelateThenLoader[Q] FieldseekerFieldscoutinglog fieldseekerFieldscoutinglogThenLoader[Q] @@ -203,6 +206,7 @@ func getThenLoaders[Q orm.Loadable]() thenLoaders[Q] { CommsEmailLog: buildCommsEmailLogThenLoader[Q](), CommsEmailTemplate: buildCommsEmailTemplateThenLoader[Q](), CommsPhone: buildCommsPhoneThenLoader[Q](), + CommsTextJob: buildCommsTextJobThenLoader[Q](), CommsTextLog: buildCommsTextLogThenLoader[Q](), FieldseekerContainerrelate: buildFieldseekerContainerrelateThenLoader[Q](), FieldseekerFieldscoutinglog: buildFieldseekerFieldscoutinglogThenLoader[Q](), diff --git a/db/models/bob_where.bob.go b/db/models/bob_where.bob.go index fad47793..d25228a0 100644 --- a/db/models/bob_where.bob.go +++ b/db/models/bob_where.bob.go @@ -23,6 +23,7 @@ func Where[Q psql.Filterable]() struct { CommsEmailLogs commsEmailLogWhere[Q] CommsEmailTemplates commsEmailTemplateWhere[Q] CommsPhones commsPhoneWhere[Q] + CommsTextJobs commsTextJobWhere[Q] CommsTextLogs commsTextLogWhere[Q] FieldseekerContainerrelates fieldseekerContainerrelateWhere[Q] FieldseekerFieldscoutinglogs fieldseekerFieldscoutinglogWhere[Q] @@ -87,6 +88,7 @@ func Where[Q psql.Filterable]() struct { CommsEmailLogs commsEmailLogWhere[Q] CommsEmailTemplates commsEmailTemplateWhere[Q] CommsPhones commsPhoneWhere[Q] + CommsTextJobs commsTextJobWhere[Q] CommsTextLogs commsTextLogWhere[Q] FieldseekerContainerrelates fieldseekerContainerrelateWhere[Q] FieldseekerFieldscoutinglogs fieldseekerFieldscoutinglogWhere[Q] @@ -150,6 +152,7 @@ func Where[Q psql.Filterable]() struct { CommsEmailLogs: buildCommsEmailLogWhere[Q](CommsEmailLogs.Columns), CommsEmailTemplates: buildCommsEmailTemplateWhere[Q](CommsEmailTemplates.Columns), CommsPhones: buildCommsPhoneWhere[Q](CommsPhones.Columns), + CommsTextJobs: buildCommsTextJobWhere[Q](CommsTextJobs.Columns), CommsTextLogs: buildCommsTextLogWhere[Q](CommsTextLogs.Columns), FieldseekerContainerrelates: buildFieldseekerContainerrelateWhere[Q](FieldseekerContainerrelates.Columns), FieldseekerFieldscoutinglogs: buildFieldseekerFieldscoutinglogWhere[Q](FieldseekerFieldscoutinglogs.Columns), diff --git a/db/models/comms.phone.bob.go b/db/models/comms.phone.bob.go index ffc12632..6af2d6e7 100644 --- a/db/models/comms.phone.bob.go +++ b/db/models/comms.phone.bob.go @@ -43,6 +43,7 @@ type CommsPhonesQuery = *psql.ViewQuery[*CommsPhone, CommsPhoneSlice] // commsPhoneR is where relationships are stored. type commsPhoneR struct { + DestinationTextJobs CommsTextJobSlice // comms.text_job.text_job_destination_fkey DestinationTextLogs CommsTextLogSlice // comms.text_log.text_log_destination_fkey SourceTextLogs CommsTextLogSlice // comms.text_log.text_log_source_fkey } @@ -371,6 +372,30 @@ func (o CommsPhoneSlice) ReloadAll(ctx context.Context, exec bob.Executor) error return nil } +// DestinationTextJobs starts a query for related objects on comms.text_job +func (o *CommsPhone) DestinationTextJobs(mods ...bob.Mod[*dialect.SelectQuery]) CommsTextJobsQuery { + return CommsTextJobs.Query(append(mods, + sm.Where(CommsTextJobs.Columns.Destination.EQ(psql.Arg(o.E164))), + )...) +} + +func (os CommsPhoneSlice) DestinationTextJobs(mods ...bob.Mod[*dialect.SelectQuery]) CommsTextJobsQuery { + pkE164 := make(pgtypes.Array[string], 0, len(os)) + for _, o := range os { + if o == nil { + continue + } + pkE164 = append(pkE164, o.E164) + } + PKArgExpr := psql.Select(sm.Columns( + psql.F("unnest", psql.Cast(psql.Arg(pkE164), "text[]")), + )) + + return CommsTextJobs.Query(append(mods, + sm.Where(psql.Group(CommsTextJobs.Columns.Destination).OP("IN", PKArgExpr)), + )...) +} + // DestinationTextLogs starts a query for related objects on comms.text_log func (o *CommsPhone) DestinationTextLogs(mods ...bob.Mod[*dialect.SelectQuery]) CommsTextLogsQuery { return CommsTextLogs.Query(append(mods, @@ -419,6 +444,74 @@ func (os CommsPhoneSlice) SourceTextLogs(mods ...bob.Mod[*dialect.SelectQuery]) )...) } +func insertCommsPhoneDestinationTextJobs0(ctx context.Context, exec bob.Executor, commsTextJobs1 []*CommsTextJobSetter, commsPhone0 *CommsPhone) (CommsTextJobSlice, error) { + for i := range commsTextJobs1 { + commsTextJobs1[i].Destination = omit.From(commsPhone0.E164) + } + + ret, err := CommsTextJobs.Insert(bob.ToMods(commsTextJobs1...)).All(ctx, exec) + if err != nil { + return ret, fmt.Errorf("insertCommsPhoneDestinationTextJobs0: %w", err) + } + + return ret, nil +} + +func attachCommsPhoneDestinationTextJobs0(ctx context.Context, exec bob.Executor, count int, commsTextJobs1 CommsTextJobSlice, commsPhone0 *CommsPhone) (CommsTextJobSlice, error) { + setter := &CommsTextJobSetter{ + Destination: omit.From(commsPhone0.E164), + } + + err := commsTextJobs1.UpdateAll(ctx, exec, *setter) + if err != nil { + return nil, fmt.Errorf("attachCommsPhoneDestinationTextJobs0: %w", err) + } + + return commsTextJobs1, nil +} + +func (commsPhone0 *CommsPhone) InsertDestinationTextJobs(ctx context.Context, exec bob.Executor, related ...*CommsTextJobSetter) error { + if len(related) == 0 { + return nil + } + + var err error + + commsTextJobs1, err := insertCommsPhoneDestinationTextJobs0(ctx, exec, related, commsPhone0) + if err != nil { + return err + } + + commsPhone0.R.DestinationTextJobs = append(commsPhone0.R.DestinationTextJobs, commsTextJobs1...) + + for _, rel := range commsTextJobs1 { + rel.R.DestinationPhone = commsPhone0 + } + return nil +} + +func (commsPhone0 *CommsPhone) AttachDestinationTextJobs(ctx context.Context, exec bob.Executor, related ...*CommsTextJob) error { + if len(related) == 0 { + return nil + } + + var err error + commsTextJobs1 := CommsTextJobSlice(related) + + _, err = attachCommsPhoneDestinationTextJobs0(ctx, exec, len(related), commsTextJobs1, commsPhone0) + if err != nil { + return err + } + + commsPhone0.R.DestinationTextJobs = append(commsPhone0.R.DestinationTextJobs, commsTextJobs1...) + + for _, rel := range related { + rel.R.DestinationPhone = commsPhone0 + } + + return nil +} + func insertCommsPhoneDestinationTextLogs0(ctx context.Context, exec bob.Executor, commsTextLogs1 []*CommsTextLogSetter, commsPhone0 *CommsPhone) (CommsTextLogSlice, error) { for i := range commsTextLogs1 { commsTextLogs1[i].Destination = omit.From(commsPhone0.E164) @@ -577,6 +670,20 @@ func (o *CommsPhone) Preload(name string, retrieved any) error { } switch name { + case "DestinationTextJobs": + rels, ok := retrieved.(CommsTextJobSlice) + if !ok { + return fmt.Errorf("commsPhone cannot load %T as %q", retrieved, name) + } + + o.R.DestinationTextJobs = rels + + for _, rel := range rels { + if rel != nil { + rel.R.DestinationPhone = o + } + } + return nil case "DestinationTextLogs": rels, ok := retrieved.(CommsTextLogSlice) if !ok { @@ -617,11 +724,15 @@ func buildCommsPhonePreloader() commsPhonePreloader { } type commsPhoneThenLoader[Q orm.Loadable] struct { + DestinationTextJobs func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] DestinationTextLogs func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] SourceTextLogs func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] } func buildCommsPhoneThenLoader[Q orm.Loadable]() commsPhoneThenLoader[Q] { + type DestinationTextJobsLoadInterface interface { + LoadDestinationTextJobs(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error + } type DestinationTextLogsLoadInterface interface { LoadDestinationTextLogs(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error } @@ -630,6 +741,12 @@ func buildCommsPhoneThenLoader[Q orm.Loadable]() commsPhoneThenLoader[Q] { } return commsPhoneThenLoader[Q]{ + DestinationTextJobs: thenLoadBuilder[Q]( + "DestinationTextJobs", + func(ctx context.Context, exec bob.Executor, retrieved DestinationTextJobsLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { + return retrieved.LoadDestinationTextJobs(ctx, exec, mods...) + }, + ), DestinationTextLogs: thenLoadBuilder[Q]( "DestinationTextLogs", func(ctx context.Context, exec bob.Executor, retrieved DestinationTextLogsLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { @@ -645,6 +762,67 @@ func buildCommsPhoneThenLoader[Q orm.Loadable]() commsPhoneThenLoader[Q] { } } +// LoadDestinationTextJobs loads the commsPhone's DestinationTextJobs into the .R struct +func (o *CommsPhone) LoadDestinationTextJobs(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if o == nil { + return nil + } + + // Reset the relationship + o.R.DestinationTextJobs = nil + + related, err := o.DestinationTextJobs(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, rel := range related { + rel.R.DestinationPhone = o + } + + o.R.DestinationTextJobs = related + return nil +} + +// LoadDestinationTextJobs loads the commsPhone's DestinationTextJobs into the .R struct +func (os CommsPhoneSlice) LoadDestinationTextJobs(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if len(os) == 0 { + return nil + } + + commsTextJobs, err := os.DestinationTextJobs(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, o := range os { + if o == nil { + continue + } + + o.R.DestinationTextJobs = nil + } + + for _, o := range os { + if o == nil { + continue + } + + for _, rel := range commsTextJobs { + + if !(o.E164 == rel.Destination) { + continue + } + + rel.R.DestinationPhone = o + + o.R.DestinationTextJobs = append(o.R.DestinationTextJobs, rel) + } + } + + return nil +} + // LoadDestinationTextLogs loads the commsPhone's DestinationTextLogs into the .R struct func (o *CommsPhone) LoadDestinationTextLogs(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { if o == nil { @@ -769,6 +947,7 @@ func (os CommsPhoneSlice) LoadSourceTextLogs(ctx context.Context, exec bob.Execu // commsPhoneC is where relationship counts are stored. type commsPhoneC struct { + DestinationTextJobs *int64 DestinationTextLogs *int64 SourceTextLogs *int64 } @@ -780,6 +959,8 @@ func (o *CommsPhone) PreloadCount(name string, count int64) error { } switch name { + case "DestinationTextJobs": + o.C.DestinationTextJobs = &count case "DestinationTextLogs": o.C.DestinationTextLogs = &count case "SourceTextLogs": @@ -789,12 +970,30 @@ func (o *CommsPhone) PreloadCount(name string, count int64) error { } type commsPhoneCountPreloader struct { + DestinationTextJobs func(...bob.Mod[*dialect.SelectQuery]) psql.Preloader DestinationTextLogs func(...bob.Mod[*dialect.SelectQuery]) psql.Preloader SourceTextLogs func(...bob.Mod[*dialect.SelectQuery]) psql.Preloader } func buildCommsPhoneCountPreloader() commsPhoneCountPreloader { return commsPhoneCountPreloader{ + DestinationTextJobs: func(mods ...bob.Mod[*dialect.SelectQuery]) psql.Preloader { + return countPreloader[*CommsPhone]("DestinationTextJobs", func(parent string) bob.Expression { + // Build a correlated subquery: (SELECT COUNT(*) FROM related WHERE fk = parent.pk) + if parent == "" { + parent = CommsPhones.Alias() + } + + subqueryMods := []bob.Mod[*dialect.SelectQuery]{ + sm.Columns(psql.Raw("count(*)")), + + sm.From(CommsTextJobs.Name()), + sm.Where(psql.Quote(CommsTextJobs.Alias(), "destination").EQ(psql.Quote(parent, "e164"))), + } + subqueryMods = append(subqueryMods, mods...) + return psql.Group(psql.Select(subqueryMods...).Expression) + }) + }, DestinationTextLogs: func(mods ...bob.Mod[*dialect.SelectQuery]) psql.Preloader { return countPreloader[*CommsPhone]("DestinationTextLogs", func(parent string) bob.Expression { // Build a correlated subquery: (SELECT COUNT(*) FROM related WHERE fk = parent.pk) @@ -833,11 +1032,15 @@ func buildCommsPhoneCountPreloader() commsPhoneCountPreloader { } type commsPhoneCountThenLoader[Q orm.Loadable] struct { + DestinationTextJobs func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] DestinationTextLogs func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] SourceTextLogs func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] } func buildCommsPhoneCountThenLoader[Q orm.Loadable]() commsPhoneCountThenLoader[Q] { + type DestinationTextJobsCountInterface interface { + LoadCountDestinationTextJobs(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error + } type DestinationTextLogsCountInterface interface { LoadCountDestinationTextLogs(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error } @@ -846,6 +1049,12 @@ func buildCommsPhoneCountThenLoader[Q orm.Loadable]() commsPhoneCountThenLoader[ } return commsPhoneCountThenLoader[Q]{ + DestinationTextJobs: countThenLoadBuilder[Q]( + "DestinationTextJobs", + func(ctx context.Context, exec bob.Executor, retrieved DestinationTextJobsCountInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { + return retrieved.LoadCountDestinationTextJobs(ctx, exec, mods...) + }, + ), DestinationTextLogs: countThenLoadBuilder[Q]( "DestinationTextLogs", func(ctx context.Context, exec bob.Executor, retrieved DestinationTextLogsCountInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { @@ -861,6 +1070,36 @@ func buildCommsPhoneCountThenLoader[Q orm.Loadable]() commsPhoneCountThenLoader[ } } +// LoadCountDestinationTextJobs loads the count of DestinationTextJobs into the C struct +func (o *CommsPhone) LoadCountDestinationTextJobs(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if o == nil { + return nil + } + + count, err := o.DestinationTextJobs(mods...).Count(ctx, exec) + if err != nil { + return err + } + + o.C.DestinationTextJobs = &count + return nil +} + +// LoadCountDestinationTextJobs loads the count of DestinationTextJobs for a slice +func (os CommsPhoneSlice) LoadCountDestinationTextJobs(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if len(os) == 0 { + return nil + } + + for _, o := range os { + if err := o.LoadCountDestinationTextJobs(ctx, exec, mods...); err != nil { + return err + } + } + + return nil +} + // LoadCountDestinationTextLogs loads the count of DestinationTextLogs into the C struct func (o *CommsPhone) LoadCountDestinationTextLogs(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { if o == nil { @@ -923,6 +1162,7 @@ func (os CommsPhoneSlice) LoadCountSourceTextLogs(ctx context.Context, exec bob. type commsPhoneJoins[Q dialect.Joinable] struct { typ string + DestinationTextJobs modAs[Q, commsTextJobColumns] DestinationTextLogs modAs[Q, commsTextLogColumns] SourceTextLogs modAs[Q, commsTextLogColumns] } @@ -934,6 +1174,20 @@ func (j commsPhoneJoins[Q]) aliasedAs(alias string) commsPhoneJoins[Q] { func buildCommsPhoneJoins[Q dialect.Joinable](cols commsPhoneColumns, typ string) commsPhoneJoins[Q] { return commsPhoneJoins[Q]{ typ: typ, + DestinationTextJobs: modAs[Q, commsTextJobColumns]{ + c: CommsTextJobs.Columns, + f: func(to commsTextJobColumns) bob.Mod[Q] { + mods := make(mods.QueryMods[Q], 0, 1) + + { + mods = append(mods, dialect.Join[Q](typ, CommsTextJobs.Name().As(to.Alias())).On( + to.Destination.EQ(cols.E164), + )) + } + + return mods + }, + }, DestinationTextLogs: modAs[Q, commsTextLogColumns]{ c: CommsTextLogs.Columns, f: func(to commsTextLogColumns) bob.Mod[Q] { diff --git a/db/models/comms.text_job.bob.go b/db/models/comms.text_job.bob.go new file mode 100644 index 00000000..6a8ce5b5 --- /dev/null +++ b/db/models/comms.text_job.bob.go @@ -0,0 +1,679 @@ +// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +import ( + "context" + "fmt" + "io" + "time" + + enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" + "github.com/aarondl/opt/omit" + "github.com/stephenafamo/bob" + "github.com/stephenafamo/bob/dialect/psql" + "github.com/stephenafamo/bob/dialect/psql/dialect" + "github.com/stephenafamo/bob/dialect/psql/dm" + "github.com/stephenafamo/bob/dialect/psql/sm" + "github.com/stephenafamo/bob/dialect/psql/um" + "github.com/stephenafamo/bob/expr" + "github.com/stephenafamo/bob/mods" + "github.com/stephenafamo/bob/orm" + "github.com/stephenafamo/bob/types/pgtypes" +) + +// CommsTextJob is an object representing the database table. +type CommsTextJob struct { + Content string `db:"content" ` + Created time.Time `db:"created" ` + Destination string `db:"destination" ` + ID int32 `db:"id,pk" ` + Type enums.CommsTextjobtype `db:"type_" ` + + R commsTextJobR `db:"-" ` +} + +// CommsTextJobSlice is an alias for a slice of pointers to CommsTextJob. +// This should almost always be used instead of []*CommsTextJob. +type CommsTextJobSlice []*CommsTextJob + +// CommsTextJobs contains methods to work with the text_job table +var CommsTextJobs = psql.NewTablex[*CommsTextJob, CommsTextJobSlice, *CommsTextJobSetter]("comms", "text_job", buildCommsTextJobColumns("comms.text_job")) + +// CommsTextJobsQuery is a query on the text_job table +type CommsTextJobsQuery = *psql.ViewQuery[*CommsTextJob, CommsTextJobSlice] + +// commsTextJobR is where relationships are stored. +type commsTextJobR struct { + DestinationPhone *CommsPhone // comms.text_job.text_job_destination_fkey +} + +func buildCommsTextJobColumns(alias string) commsTextJobColumns { + return commsTextJobColumns{ + ColumnsExpr: expr.NewColumnsExpr( + "content", "created", "destination", "id", "type_", + ).WithParent("comms.text_job"), + tableAlias: alias, + Content: psql.Quote(alias, "content"), + Created: psql.Quote(alias, "created"), + Destination: psql.Quote(alias, "destination"), + ID: psql.Quote(alias, "id"), + Type: psql.Quote(alias, "type_"), + } +} + +type commsTextJobColumns struct { + expr.ColumnsExpr + tableAlias string + Content psql.Expression + Created psql.Expression + Destination psql.Expression + ID psql.Expression + Type psql.Expression +} + +func (c commsTextJobColumns) Alias() string { + return c.tableAlias +} + +func (commsTextJobColumns) AliasedAs(alias string) commsTextJobColumns { + return buildCommsTextJobColumns(alias) +} + +// CommsTextJobSetter is used for insert/upsert/update operations +// All values are optional, and do not have to be set +// Generated columns are not included +type CommsTextJobSetter struct { + Content omit.Val[string] `db:"content" ` + Created omit.Val[time.Time] `db:"created" ` + Destination omit.Val[string] `db:"destination" ` + ID omit.Val[int32] `db:"id,pk" ` + Type omit.Val[enums.CommsTextjobtype] `db:"type_" ` +} + +func (s CommsTextJobSetter) SetColumns() []string { + vals := make([]string, 0, 5) + if s.Content.IsValue() { + vals = append(vals, "content") + } + if s.Created.IsValue() { + vals = append(vals, "created") + } + if s.Destination.IsValue() { + vals = append(vals, "destination") + } + if s.ID.IsValue() { + vals = append(vals, "id") + } + if s.Type.IsValue() { + vals = append(vals, "type_") + } + return vals +} + +func (s CommsTextJobSetter) Overwrite(t *CommsTextJob) { + if s.Content.IsValue() { + t.Content = s.Content.MustGet() + } + if s.Created.IsValue() { + t.Created = s.Created.MustGet() + } + if s.Destination.IsValue() { + t.Destination = s.Destination.MustGet() + } + if s.ID.IsValue() { + t.ID = s.ID.MustGet() + } + if s.Type.IsValue() { + t.Type = s.Type.MustGet() + } +} + +func (s *CommsTextJobSetter) Apply(q *dialect.InsertQuery) { + q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) { + return CommsTextJobs.BeforeInsertHooks.RunHooks(ctx, exec, s) + }) + + q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { + vals := make([]bob.Expression, 5) + if s.Content.IsValue() { + vals[0] = psql.Arg(s.Content.MustGet()) + } else { + vals[0] = psql.Raw("DEFAULT") + } + + if s.Created.IsValue() { + vals[1] = psql.Arg(s.Created.MustGet()) + } else { + vals[1] = psql.Raw("DEFAULT") + } + + if s.Destination.IsValue() { + vals[2] = psql.Arg(s.Destination.MustGet()) + } else { + vals[2] = psql.Raw("DEFAULT") + } + + if s.ID.IsValue() { + vals[3] = psql.Arg(s.ID.MustGet()) + } else { + vals[3] = psql.Raw("DEFAULT") + } + + if s.Type.IsValue() { + vals[4] = psql.Arg(s.Type.MustGet()) + } else { + vals[4] = psql.Raw("DEFAULT") + } + + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") + })) +} + +func (s CommsTextJobSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { + return um.Set(s.Expressions()...) +} + +func (s CommsTextJobSetter) Expressions(prefix ...string) []bob.Expression { + exprs := make([]bob.Expression, 0, 5) + + if s.Content.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "content")...), + psql.Arg(s.Content), + }}) + } + + if s.Created.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "created")...), + psql.Arg(s.Created), + }}) + } + + if s.Destination.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "destination")...), + psql.Arg(s.Destination), + }}) + } + + if s.ID.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "id")...), + psql.Arg(s.ID), + }}) + } + + if s.Type.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "type_")...), + psql.Arg(s.Type), + }}) + } + + return exprs +} + +// FindCommsTextJob retrieves a single record by primary key +// If cols is empty Find will return all columns. +func FindCommsTextJob(ctx context.Context, exec bob.Executor, IDPK int32, cols ...string) (*CommsTextJob, error) { + if len(cols) == 0 { + return CommsTextJobs.Query( + sm.Where(CommsTextJobs.Columns.ID.EQ(psql.Arg(IDPK))), + ).One(ctx, exec) + } + + return CommsTextJobs.Query( + sm.Where(CommsTextJobs.Columns.ID.EQ(psql.Arg(IDPK))), + sm.Columns(CommsTextJobs.Columns.Only(cols...)), + ).One(ctx, exec) +} + +// CommsTextJobExists checks the presence of a single record by primary key +func CommsTextJobExists(ctx context.Context, exec bob.Executor, IDPK int32) (bool, error) { + return CommsTextJobs.Query( + sm.Where(CommsTextJobs.Columns.ID.EQ(psql.Arg(IDPK))), + ).Exists(ctx, exec) +} + +// AfterQueryHook is called after CommsTextJob is retrieved from the database +func (o *CommsTextJob) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error { + var err error + + switch queryType { + case bob.QueryTypeSelect: + ctx, err = CommsTextJobs.AfterSelectHooks.RunHooks(ctx, exec, CommsTextJobSlice{o}) + case bob.QueryTypeInsert: + ctx, err = CommsTextJobs.AfterInsertHooks.RunHooks(ctx, exec, CommsTextJobSlice{o}) + case bob.QueryTypeUpdate: + ctx, err = CommsTextJobs.AfterUpdateHooks.RunHooks(ctx, exec, CommsTextJobSlice{o}) + case bob.QueryTypeDelete: + ctx, err = CommsTextJobs.AfterDeleteHooks.RunHooks(ctx, exec, CommsTextJobSlice{o}) + } + + return err +} + +// primaryKeyVals returns the primary key values of the CommsTextJob +func (o *CommsTextJob) primaryKeyVals() bob.Expression { + return psql.Arg(o.ID) +} + +func (o *CommsTextJob) pkEQ() dialect.Expression { + return psql.Quote("comms.text_job", "id").EQ(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { + return o.primaryKeyVals().WriteSQL(ctx, w, d, start) + })) +} + +// Update uses an executor to update the CommsTextJob +func (o *CommsTextJob) Update(ctx context.Context, exec bob.Executor, s *CommsTextJobSetter) error { + v, err := CommsTextJobs.Update(s.UpdateMod(), um.Where(o.pkEQ())).One(ctx, exec) + if err != nil { + return err + } + + o.R = v.R + *o = *v + + return nil +} + +// Delete deletes a single CommsTextJob record with an executor +func (o *CommsTextJob) Delete(ctx context.Context, exec bob.Executor) error { + _, err := CommsTextJobs.Delete(dm.Where(o.pkEQ())).Exec(ctx, exec) + return err +} + +// Reload refreshes the CommsTextJob using the executor +func (o *CommsTextJob) Reload(ctx context.Context, exec bob.Executor) error { + o2, err := CommsTextJobs.Query( + sm.Where(CommsTextJobs.Columns.ID.EQ(psql.Arg(o.ID))), + ).One(ctx, exec) + if err != nil { + return err + } + o2.R = o.R + *o = *o2 + + return nil +} + +// AfterQueryHook is called after CommsTextJobSlice is retrieved from the database +func (o CommsTextJobSlice) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error { + var err error + + switch queryType { + case bob.QueryTypeSelect: + ctx, err = CommsTextJobs.AfterSelectHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeInsert: + ctx, err = CommsTextJobs.AfterInsertHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeUpdate: + ctx, err = CommsTextJobs.AfterUpdateHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeDelete: + ctx, err = CommsTextJobs.AfterDeleteHooks.RunHooks(ctx, exec, o) + } + + return err +} + +func (o CommsTextJobSlice) pkIN() dialect.Expression { + if len(o) == 0 { + return psql.Raw("NULL") + } + + return psql.Quote("comms.text_job", "id").In(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { + pkPairs := make([]bob.Expression, len(o)) + for i, row := range o { + pkPairs[i] = row.primaryKeyVals() + } + return bob.ExpressSlice(ctx, w, d, start, pkPairs, "", ", ", "") + })) +} + +// copyMatchingRows finds models in the given slice that have the same primary key +// then it first copies the existing relationships from the old model to the new model +// and then replaces the old model in the slice with the new model +func (o CommsTextJobSlice) copyMatchingRows(from ...*CommsTextJob) { + for i, old := range o { + for _, new := range from { + if new.ID != old.ID { + continue + } + new.R = old.R + o[i] = new + break + } + } +} + +// UpdateMod modifies an update query with "WHERE primary_key IN (o...)" +func (o CommsTextJobSlice) UpdateMod() bob.Mod[*dialect.UpdateQuery] { + return bob.ModFunc[*dialect.UpdateQuery](func(q *dialect.UpdateQuery) { + q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) { + return CommsTextJobs.BeforeUpdateHooks.RunHooks(ctx, exec, o) + }) + + q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error { + var err error + switch retrieved := retrieved.(type) { + case *CommsTextJob: + o.copyMatchingRows(retrieved) + case []*CommsTextJob: + o.copyMatchingRows(retrieved...) + case CommsTextJobSlice: + o.copyMatchingRows(retrieved...) + default: + // If the retrieved value is not a CommsTextJob or a slice of CommsTextJob + // then run the AfterUpdateHooks on the slice + _, err = CommsTextJobs.AfterUpdateHooks.RunHooks(ctx, exec, o) + } + + return err + })) + + q.AppendWhere(o.pkIN()) + }) +} + +// DeleteMod modifies an delete query with "WHERE primary_key IN (o...)" +func (o CommsTextJobSlice) DeleteMod() bob.Mod[*dialect.DeleteQuery] { + return bob.ModFunc[*dialect.DeleteQuery](func(q *dialect.DeleteQuery) { + q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) { + return CommsTextJobs.BeforeDeleteHooks.RunHooks(ctx, exec, o) + }) + + q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error { + var err error + switch retrieved := retrieved.(type) { + case *CommsTextJob: + o.copyMatchingRows(retrieved) + case []*CommsTextJob: + o.copyMatchingRows(retrieved...) + case CommsTextJobSlice: + o.copyMatchingRows(retrieved...) + default: + // If the retrieved value is not a CommsTextJob or a slice of CommsTextJob + // then run the AfterDeleteHooks on the slice + _, err = CommsTextJobs.AfterDeleteHooks.RunHooks(ctx, exec, o) + } + + return err + })) + + q.AppendWhere(o.pkIN()) + }) +} + +func (o CommsTextJobSlice) UpdateAll(ctx context.Context, exec bob.Executor, vals CommsTextJobSetter) error { + if len(o) == 0 { + return nil + } + + _, err := CommsTextJobs.Update(vals.UpdateMod(), o.UpdateMod()).All(ctx, exec) + return err +} + +func (o CommsTextJobSlice) DeleteAll(ctx context.Context, exec bob.Executor) error { + if len(o) == 0 { + return nil + } + + _, err := CommsTextJobs.Delete(o.DeleteMod()).Exec(ctx, exec) + return err +} + +func (o CommsTextJobSlice) ReloadAll(ctx context.Context, exec bob.Executor) error { + if len(o) == 0 { + return nil + } + + o2, err := CommsTextJobs.Query(sm.Where(o.pkIN())).All(ctx, exec) + if err != nil { + return err + } + + o.copyMatchingRows(o2...) + + return nil +} + +// DestinationPhone starts a query for related objects on comms.phone +func (o *CommsTextJob) DestinationPhone(mods ...bob.Mod[*dialect.SelectQuery]) CommsPhonesQuery { + return CommsPhones.Query(append(mods, + sm.Where(CommsPhones.Columns.E164.EQ(psql.Arg(o.Destination))), + )...) +} + +func (os CommsTextJobSlice) DestinationPhone(mods ...bob.Mod[*dialect.SelectQuery]) CommsPhonesQuery { + pkDestination := make(pgtypes.Array[string], 0, len(os)) + for _, o := range os { + if o == nil { + continue + } + pkDestination = append(pkDestination, o.Destination) + } + PKArgExpr := psql.Select(sm.Columns( + psql.F("unnest", psql.Cast(psql.Arg(pkDestination), "text[]")), + )) + + return CommsPhones.Query(append(mods, + sm.Where(psql.Group(CommsPhones.Columns.E164).OP("IN", PKArgExpr)), + )...) +} + +func attachCommsTextJobDestinationPhone0(ctx context.Context, exec bob.Executor, count int, commsTextJob0 *CommsTextJob, commsPhone1 *CommsPhone) (*CommsTextJob, error) { + setter := &CommsTextJobSetter{ + Destination: omit.From(commsPhone1.E164), + } + + err := commsTextJob0.Update(ctx, exec, setter) + if err != nil { + return nil, fmt.Errorf("attachCommsTextJobDestinationPhone0: %w", err) + } + + return commsTextJob0, nil +} + +func (commsTextJob0 *CommsTextJob) InsertDestinationPhone(ctx context.Context, exec bob.Executor, related *CommsPhoneSetter) error { + var err error + + commsPhone1, err := CommsPhones.Insert(related).One(ctx, exec) + if err != nil { + return fmt.Errorf("inserting related objects: %w", err) + } + + _, err = attachCommsTextJobDestinationPhone0(ctx, exec, 1, commsTextJob0, commsPhone1) + if err != nil { + return err + } + + commsTextJob0.R.DestinationPhone = commsPhone1 + + commsPhone1.R.DestinationTextJobs = append(commsPhone1.R.DestinationTextJobs, commsTextJob0) + + return nil +} + +func (commsTextJob0 *CommsTextJob) AttachDestinationPhone(ctx context.Context, exec bob.Executor, commsPhone1 *CommsPhone) error { + var err error + + _, err = attachCommsTextJobDestinationPhone0(ctx, exec, 1, commsTextJob0, commsPhone1) + if err != nil { + return err + } + + commsTextJob0.R.DestinationPhone = commsPhone1 + + commsPhone1.R.DestinationTextJobs = append(commsPhone1.R.DestinationTextJobs, commsTextJob0) + + return nil +} + +type commsTextJobWhere[Q psql.Filterable] struct { + Content psql.WhereMod[Q, string] + Created psql.WhereMod[Q, time.Time] + Destination psql.WhereMod[Q, string] + ID psql.WhereMod[Q, int32] + Type psql.WhereMod[Q, enums.CommsTextjobtype] +} + +func (commsTextJobWhere[Q]) AliasedAs(alias string) commsTextJobWhere[Q] { + return buildCommsTextJobWhere[Q](buildCommsTextJobColumns(alias)) +} + +func buildCommsTextJobWhere[Q psql.Filterable](cols commsTextJobColumns) commsTextJobWhere[Q] { + return commsTextJobWhere[Q]{ + Content: psql.Where[Q, string](cols.Content), + Created: psql.Where[Q, time.Time](cols.Created), + Destination: psql.Where[Q, string](cols.Destination), + ID: psql.Where[Q, int32](cols.ID), + Type: psql.Where[Q, enums.CommsTextjobtype](cols.Type), + } +} + +func (o *CommsTextJob) Preload(name string, retrieved any) error { + if o == nil { + return nil + } + + switch name { + case "DestinationPhone": + rel, ok := retrieved.(*CommsPhone) + if !ok { + return fmt.Errorf("commsTextJob cannot load %T as %q", retrieved, name) + } + + o.R.DestinationPhone = rel + + if rel != nil { + rel.R.DestinationTextJobs = CommsTextJobSlice{o} + } + return nil + default: + return fmt.Errorf("commsTextJob has no relationship %q", name) + } +} + +type commsTextJobPreloader struct { + DestinationPhone func(...psql.PreloadOption) psql.Preloader +} + +func buildCommsTextJobPreloader() commsTextJobPreloader { + return commsTextJobPreloader{ + DestinationPhone: func(opts ...psql.PreloadOption) psql.Preloader { + return psql.Preload[*CommsPhone, CommsPhoneSlice](psql.PreloadRel{ + Name: "DestinationPhone", + Sides: []psql.PreloadSide{ + { + From: CommsTextJobs, + To: CommsPhones, + FromColumns: []string{"destination"}, + ToColumns: []string{"e164"}, + }, + }, + }, CommsPhones.Columns.Names(), opts...) + }, + } +} + +type commsTextJobThenLoader[Q orm.Loadable] struct { + DestinationPhone func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] +} + +func buildCommsTextJobThenLoader[Q orm.Loadable]() commsTextJobThenLoader[Q] { + type DestinationPhoneLoadInterface interface { + LoadDestinationPhone(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error + } + + return commsTextJobThenLoader[Q]{ + DestinationPhone: thenLoadBuilder[Q]( + "DestinationPhone", + func(ctx context.Context, exec bob.Executor, retrieved DestinationPhoneLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { + return retrieved.LoadDestinationPhone(ctx, exec, mods...) + }, + ), + } +} + +// LoadDestinationPhone loads the commsTextJob's DestinationPhone into the .R struct +func (o *CommsTextJob) LoadDestinationPhone(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if o == nil { + return nil + } + + // Reset the relationship + o.R.DestinationPhone = nil + + related, err := o.DestinationPhone(mods...).One(ctx, exec) + if err != nil { + return err + } + + related.R.DestinationTextJobs = CommsTextJobSlice{o} + + o.R.DestinationPhone = related + return nil +} + +// LoadDestinationPhone loads the commsTextJob's DestinationPhone into the .R struct +func (os CommsTextJobSlice) LoadDestinationPhone(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if len(os) == 0 { + return nil + } + + commsPhones, err := os.DestinationPhone(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, o := range os { + if o == nil { + continue + } + + for _, rel := range commsPhones { + + if !(o.Destination == rel.E164) { + continue + } + + rel.R.DestinationTextJobs = append(rel.R.DestinationTextJobs, o) + + o.R.DestinationPhone = rel + break + } + } + + return nil +} + +type commsTextJobJoins[Q dialect.Joinable] struct { + typ string + DestinationPhone modAs[Q, commsPhoneColumns] +} + +func (j commsTextJobJoins[Q]) aliasedAs(alias string) commsTextJobJoins[Q] { + return buildCommsTextJobJoins[Q](buildCommsTextJobColumns(alias), j.typ) +} + +func buildCommsTextJobJoins[Q dialect.Joinable](cols commsTextJobColumns, typ string) commsTextJobJoins[Q] { + return commsTextJobJoins[Q]{ + typ: typ, + DestinationPhone: modAs[Q, commsPhoneColumns]{ + c: CommsPhones.Columns, + f: func(to commsPhoneColumns) bob.Mod[Q] { + mods := make(mods.QueryMods[Q], 0, 1) + + { + mods = append(mods, dialect.Join[Q](typ, CommsPhones.Name().As(to.Alias())).On( + to.E164.EQ(cols.Destination), + )) + } + + return mods + }, + }, + } +} diff --git a/db/models/comms.text_log.bob.go b/db/models/comms.text_log.bob.go index fc1054d3..445ac37f 100644 --- a/db/models/comms.text_log.bob.go +++ b/db/models/comms.text_log.bob.go @@ -29,6 +29,7 @@ type CommsTextLog struct { Created time.Time `db:"created" ` Destination string `db:"destination" ` ID int32 `db:"id,pk" ` + IsWelcome bool `db:"is_welcome" ` Origin enums.CommsTextorigin `db:"origin" ` Source string `db:"source" ` @@ -54,13 +55,14 @@ type commsTextLogR struct { func buildCommsTextLogColumns(alias string) commsTextLogColumns { return commsTextLogColumns{ ColumnsExpr: expr.NewColumnsExpr( - "content", "created", "destination", "id", "origin", "source", + "content", "created", "destination", "id", "is_welcome", "origin", "source", ).WithParent("comms.text_log"), tableAlias: alias, Content: psql.Quote(alias, "content"), Created: psql.Quote(alias, "created"), Destination: psql.Quote(alias, "destination"), ID: psql.Quote(alias, "id"), + IsWelcome: psql.Quote(alias, "is_welcome"), Origin: psql.Quote(alias, "origin"), Source: psql.Quote(alias, "source"), } @@ -73,6 +75,7 @@ type commsTextLogColumns struct { Created psql.Expression Destination psql.Expression ID psql.Expression + IsWelcome psql.Expression Origin psql.Expression Source psql.Expression } @@ -93,12 +96,13 @@ type CommsTextLogSetter struct { Created omit.Val[time.Time] `db:"created" ` Destination omit.Val[string] `db:"destination" ` ID omit.Val[int32] `db:"id,pk" ` + IsWelcome omit.Val[bool] `db:"is_welcome" ` Origin omit.Val[enums.CommsTextorigin] `db:"origin" ` Source omit.Val[string] `db:"source" ` } func (s CommsTextLogSetter) SetColumns() []string { - vals := make([]string, 0, 6) + vals := make([]string, 0, 7) if s.Content.IsValue() { vals = append(vals, "content") } @@ -111,6 +115,9 @@ func (s CommsTextLogSetter) SetColumns() []string { if s.ID.IsValue() { vals = append(vals, "id") } + if s.IsWelcome.IsValue() { + vals = append(vals, "is_welcome") + } if s.Origin.IsValue() { vals = append(vals, "origin") } @@ -133,6 +140,9 @@ func (s CommsTextLogSetter) Overwrite(t *CommsTextLog) { if s.ID.IsValue() { t.ID = s.ID.MustGet() } + if s.IsWelcome.IsValue() { + t.IsWelcome = s.IsWelcome.MustGet() + } if s.Origin.IsValue() { t.Origin = s.Origin.MustGet() } @@ -147,7 +157,7 @@ func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) { }) q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { - vals := make([]bob.Expression, 6) + vals := make([]bob.Expression, 7) if s.Content.IsValue() { vals[0] = psql.Arg(s.Content.MustGet()) } else { @@ -172,18 +182,24 @@ func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) { vals[3] = psql.Raw("DEFAULT") } - if s.Origin.IsValue() { - vals[4] = psql.Arg(s.Origin.MustGet()) + if s.IsWelcome.IsValue() { + vals[4] = psql.Arg(s.IsWelcome.MustGet()) } else { vals[4] = psql.Raw("DEFAULT") } - if s.Source.IsValue() { - vals[5] = psql.Arg(s.Source.MustGet()) + if s.Origin.IsValue() { + vals[5] = psql.Arg(s.Origin.MustGet()) } else { vals[5] = psql.Raw("DEFAULT") } + if s.Source.IsValue() { + vals[6] = psql.Arg(s.Source.MustGet()) + } else { + vals[6] = psql.Raw("DEFAULT") + } + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") })) } @@ -193,7 +209,7 @@ func (s CommsTextLogSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { } func (s CommsTextLogSetter) Expressions(prefix ...string) []bob.Expression { - exprs := make([]bob.Expression, 0, 6) + exprs := make([]bob.Expression, 0, 7) if s.Content.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ @@ -223,6 +239,13 @@ func (s CommsTextLogSetter) Expressions(prefix ...string) []bob.Expression { }}) } + if s.IsWelcome.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "is_welcome")...), + psql.Arg(s.IsWelcome), + }}) + } + if s.Origin.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ psql.Quote(append(prefix, "origin")...), @@ -612,6 +635,7 @@ type commsTextLogWhere[Q psql.Filterable] struct { Created psql.WhereMod[Q, time.Time] Destination psql.WhereMod[Q, string] ID psql.WhereMod[Q, int32] + IsWelcome psql.WhereMod[Q, bool] Origin psql.WhereMod[Q, enums.CommsTextorigin] Source psql.WhereMod[Q, string] } @@ -626,6 +650,7 @@ func buildCommsTextLogWhere[Q psql.Filterable](cols commsTextLogColumns) commsTe Created: psql.Where[Q, time.Time](cols.Created), Destination: psql.Where[Q, string](cols.Destination), ID: psql.Where[Q, int32](cols.ID), + IsWelcome: psql.Where[Q, bool](cols.IsWelcome), Origin: psql.Where[Q, enums.CommsTextorigin](cols.Origin), Source: psql.Where[Q, string](cols.Source), } diff --git a/main.go b/main.go index ed8956b6..ac76e6af 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/auth" "github.com/Gleipnir-Technology/nidus-sync/background" "github.com/Gleipnir-Technology/nidus-sync/comms/email" + "github.com/Gleipnir-Technology/nidus-sync/comms/text" "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/public-report" @@ -46,6 +47,11 @@ func main() { os.Exit(3) } + err = text.StoreSources() + if err != nil { + log.Error().Err(err).Msg("Failed to store text source phone numbers") + os.Exit(4) + } router_logger := log.With().Logger() r := chi.NewRouter()