Make username unique, make is_subscribed nullable

This commit is contained in:
Eli Ribble 2026-01-26 21:11:31 +00:00
parent 1cd4a31404
commit e8e840ec44
No known key found for this signature in database
11 changed files with 149 additions and 74 deletions

View file

@ -15,6 +15,7 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/nyaruka/phonenumbers"
"github.com/rs/zerolog/log"
"github.com/stephenafamo/bob/types/pgtypes"
@ -55,7 +56,7 @@ func ensureInDB(ctx context.Context, destination string) (err error) {
if err.Error() == "sql: no rows in result set" {
_, err = models.CommsPhones.Insert(&models.CommsPhoneSetter{
E164: omit.From(destination),
IsSubscribed: omit.From(false),
IsSubscribed: omitnull.FromPtr[bool](nil),
}).One(ctx, db.PGInstance.BobDB)
if err != nil {
return fmt.Errorf("Failed to insert new phone contact: %w", err)
@ -81,16 +82,6 @@ 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 {

View file

@ -4,7 +4,8 @@ import (
"context"
"fmt"
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
//"github.com/Gleipnir-Technology/nidus-sync/db/enums"
//"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/nyaruka/phonenumbers"
//"github.com/rs/zerolog/log"
)
@ -43,29 +44,32 @@ func (j jobReportSubscription) source() string {
}
func sendReportSubscription(ctx context.Context, job Job) error {
j, ok := job.(jobReportSubscription)
if !ok {
return fmt.Errorf("job is not for report subscription confirmation")
}
/*
j, ok := job.(jobReportSubscription)
if !ok {
return fmt.Errorf("job is not for report subscription confirmation")
}
sub, err := isSubscribed(ctx, job.destination())
if err != nil {
return fmt.Errorf("Failed to check if subscribed: %w", err)
}
if !sub {
err = sendText(ctx, j.source(), j.destination(), j.content(), enums.CommsTextoriginWebsiteAction, false)
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)
}
} 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)
if !sub {
err = sendText(ctx, j.source(), j.destination(), j.content(), enums.CommsTextoriginWebsiteAction, false)
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)
}
}
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
*/
return nil
}

View file

@ -10,8 +10,17 @@ var UserErrors = &userErrors{
columns: []string{"id"},
s: "user__pkey",
},
ErrUniqueUserUsernameUnique: &UniqueConstraintError{
schema: "",
table: "user_",
columns: []string{"username"},
s: "user_username_unique",
},
}
type userErrors struct {
ErrUniqueUser_Pkey *UniqueConstraintError
ErrUniqueUserUsernameUnique *UniqueConstraintError
}

View file

@ -27,9 +27,9 @@ var CommsPhones = Table[
IsSubscribed: column{
Name: "is_subscribed",
DBType: "boolean",
Default: "",
Default: "NULL",
Comment: "",
Nullable: false,
Nullable: true,
Generated: false,
AutoIncr: false,
},

View file

@ -142,6 +142,23 @@ var Users = Table[
Where: "",
Include: []string{},
},
UserUsernameUnique: index{
Type: "btree",
Name: "user_username_unique",
Columns: []indexColumn{
{
Name: "username",
Desc: null.FromCond(false, true),
IsExpression: false,
},
},
Unique: true,
Comment: "",
NullsFirst: []bool{false},
NullsDistinct: false,
Where: "",
Include: []string{},
},
},
PrimaryKey: &constraint{
Name: "user__pkey",
@ -159,6 +176,13 @@ var Users = Table[
ForeignColumns: []string{"id"},
},
},
Uniques: userUniques{
UserUsernameUnique: constraint{
Name: "user_username_unique",
Columns: []string{"username"},
Comment: "",
},
},
Comment: "",
}
@ -185,12 +209,13 @@ func (c userColumns) AsSlice() []column {
}
type userIndexes struct {
UserPkey index
UserPkey index
UserUsernameUnique index
}
func (i userIndexes) AsSlice() []index {
return []index{
i.UserPkey,
i.UserPkey, i.UserUsernameUnique,
}
}
@ -204,10 +229,14 @@ func (f userForeignKeys) AsSlice() []foreignKey {
}
}
type userUniques struct{}
type userUniques struct {
UserUsernameUnique constraint
}
func (u userUniques) AsSlice() []constraint {
return []constraint{}
return []constraint{
u.UserUsernameUnique,
}
}
type userChecks struct{}

View file

@ -293,7 +293,7 @@ func (f *Factory) FromExistingCommsPhone(m *models.CommsPhone) *CommsPhoneTempla
o := &CommsPhoneTemplate{f: f, alreadyPersisted: true}
o.E164 = func() string { return m.E164 }
o.IsSubscribed = func() bool { return m.IsSubscribed }
o.IsSubscribed = func() null.Val[bool] { return m.IsSubscribed }
ctx := context.Background()
if len(m.R.DestinationTextJobs) > 0 {

View file

@ -8,7 +8,9 @@ import (
"testing"
models "github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/aarondl/opt/null"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/jaswdr/faker/v2"
"github.com/stephenafamo/bob"
)
@ -35,7 +37,7 @@ func (mods CommsPhoneModSlice) Apply(ctx context.Context, n *CommsPhoneTemplate)
// all columns are optional and should be set by mods
type CommsPhoneTemplate struct {
E164 func() string
IsSubscribed func() bool
IsSubscribed func() null.Val[bool]
r commsPhoneR
f *Factory
@ -123,7 +125,7 @@ func (o CommsPhoneTemplate) BuildSetter() *models.CommsPhoneSetter {
}
if o.IsSubscribed != nil {
val := o.IsSubscribed()
m.IsSubscribed = omit.From(val)
m.IsSubscribed = omitnull.FromNull(val)
}
return m
@ -177,10 +179,6 @@ func ensureCreatableCommsPhone(m *models.CommsPhoneSetter) {
val := random_string(nil)
m.E164 = omit.From(val)
}
if !(m.IsSubscribed.IsValue()) {
val := random_bool(nil)
m.IsSubscribed = omit.From(val)
}
}
// insertOptRels creates and inserts any optional the relationships on *models.CommsPhone
@ -378,14 +376,14 @@ func (m commsPhoneMods) RandomE164(f *faker.Faker) CommsPhoneMod {
}
// Set the model columns to this value
func (m commsPhoneMods) IsSubscribed(val bool) CommsPhoneMod {
func (m commsPhoneMods) IsSubscribed(val null.Val[bool]) CommsPhoneMod {
return CommsPhoneModFunc(func(_ context.Context, o *CommsPhoneTemplate) {
o.IsSubscribed = func() bool { return val }
o.IsSubscribed = func() null.Val[bool] { return val }
})
}
// Set the Column from the function
func (m commsPhoneMods) IsSubscribedFunc(f func() bool) CommsPhoneMod {
func (m commsPhoneMods) IsSubscribedFunc(f func() null.Val[bool]) CommsPhoneMod {
return CommsPhoneModFunc(func(_ context.Context, o *CommsPhoneTemplate) {
o.IsSubscribed = f
})
@ -400,10 +398,32 @@ func (m commsPhoneMods) UnsetIsSubscribed() CommsPhoneMod {
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
// The generated value is sometimes null
func (m commsPhoneMods) RandomIsSubscribed(f *faker.Faker) CommsPhoneMod {
return CommsPhoneModFunc(func(_ context.Context, o *CommsPhoneTemplate) {
o.IsSubscribed = func() bool {
return random_bool(f)
o.IsSubscribed = func() null.Val[bool] {
if f == nil {
f = &defaultFaker
}
val := random_bool(f)
return null.From(val)
}
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
// The generated value is never null
func (m commsPhoneMods) RandomIsSubscribedNotNull(f *faker.Faker) CommsPhoneMod {
return CommsPhoneModFunc(func(_ context.Context, o *CommsPhoneTemplate) {
o.IsSubscribed = func() null.Val[bool] {
if f == nil {
f = &defaultFaker
}
val := random_bool(f)
return null.From(val)
}
})
}

View file

@ -0,0 +1,2 @@
-- +goose Up
ALTER TABLE comms.phone ALTER COLUMN is_subscribed DROP NOT NULL;

View file

@ -0,0 +1,2 @@
-- +goose Up
ALTER TABLE user_ ADD CONSTRAINT user_username_unique UNIQUE (username);

View file

@ -8,7 +8,9 @@ import (
"fmt"
"io"
"github.com/aarondl/opt/null"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/psql"
"github.com/stephenafamo/bob/dialect/psql/dialect"
@ -23,8 +25,8 @@ import (
// CommsPhone is an object representing the database table.
type CommsPhone struct {
E164 string `db:"e164,pk" `
IsSubscribed bool `db:"is_subscribed" `
E164 string `db:"e164,pk" `
IsSubscribed null.Val[bool] `db:"is_subscribed" `
R commsPhoneR `db:"-" `
@ -78,8 +80,8 @@ func (commsPhoneColumns) AliasedAs(alias string) commsPhoneColumns {
// All values are optional, and do not have to be set
// Generated columns are not included
type CommsPhoneSetter struct {
E164 omit.Val[string] `db:"e164,pk" `
IsSubscribed omit.Val[bool] `db:"is_subscribed" `
E164 omit.Val[string] `db:"e164,pk" `
IsSubscribed omitnull.Val[bool] `db:"is_subscribed" `
}
func (s CommsPhoneSetter) SetColumns() []string {
@ -87,7 +89,7 @@ func (s CommsPhoneSetter) SetColumns() []string {
if s.E164.IsValue() {
vals = append(vals, "e164")
}
if s.IsSubscribed.IsValue() {
if !s.IsSubscribed.IsUnset() {
vals = append(vals, "is_subscribed")
}
return vals
@ -97,8 +99,8 @@ func (s CommsPhoneSetter) Overwrite(t *CommsPhone) {
if s.E164.IsValue() {
t.E164 = s.E164.MustGet()
}
if s.IsSubscribed.IsValue() {
t.IsSubscribed = s.IsSubscribed.MustGet()
if !s.IsSubscribed.IsUnset() {
t.IsSubscribed = s.IsSubscribed.MustGetNull()
}
}
@ -115,8 +117,8 @@ func (s *CommsPhoneSetter) Apply(q *dialect.InsertQuery) {
vals[0] = psql.Raw("DEFAULT")
}
if s.IsSubscribed.IsValue() {
vals[1] = psql.Arg(s.IsSubscribed.MustGet())
if !s.IsSubscribed.IsUnset() {
vals[1] = psql.Arg(s.IsSubscribed.MustGetNull())
} else {
vals[1] = psql.Raw("DEFAULT")
}
@ -139,7 +141,7 @@ func (s CommsPhoneSetter) Expressions(prefix ...string) []bob.Expression {
}})
}
if s.IsSubscribed.IsValue() {
if !s.IsSubscribed.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
psql.Quote(append(prefix, "is_subscribed")...),
psql.Arg(s.IsSubscribed),
@ -650,7 +652,7 @@ func (commsPhone0 *CommsPhone) AttachSourceTextLogs(ctx context.Context, exec bo
type commsPhoneWhere[Q psql.Filterable] struct {
E164 psql.WhereMod[Q, string]
IsSubscribed psql.WhereMod[Q, bool]
IsSubscribed psql.WhereNullMod[Q, bool]
}
func (commsPhoneWhere[Q]) AliasedAs(alias string) commsPhoneWhere[Q] {
@ -660,7 +662,7 @@ func (commsPhoneWhere[Q]) AliasedAs(alias string) commsPhoneWhere[Q] {
func buildCommsPhoneWhere[Q psql.Filterable](cols commsPhoneColumns) commsPhoneWhere[Q] {
return commsPhoneWhere[Q]{
E164: psql.Where[Q, string](cols.E164),
IsSubscribed: psql.Where[Q, bool](cols.IsSubscribed),
IsSubscribed: psql.WhereNull[Q, bool](cols.IsSubscribed),
}
}

View file

@ -11,7 +11,7 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/db/sql"
"github.com/Gleipnir-Technology/nidus-sync/llm"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/rs/zerolog/log"
)
@ -66,12 +66,16 @@ func splitPhoneSource(s string) (string, string) {
}
func isSubscribed(ctx context.Context, src string) (bool, error) {
func isSubscribed(ctx context.Context, src string) (*bool, error) {
phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, src)
if err != nil {
return false, fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err)
return nil, fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err)
}
return phone.IsSubscribed, nil
if phone.IsSubscribed.IsNull() {
return nil, nil
}
result := phone.IsSubscribed.MustGet()
return &result, nil
}
func setSubscribed(ctx context.Context, src string, is_subscribed bool) error {
@ -80,11 +84,16 @@ func setSubscribed(ctx context.Context, src string, is_subscribed bool) error {
return fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err)
}
phone.Update(ctx, db.PGInstance.BobDB, &models.CommsPhoneSetter{
IsSubscribed: omit.From(is_subscribed),
IsSubscribed: omitnull.From(is_subscribed),
})
log.Info().Str("src", src).Bool("is_subscribed", is_subscribed).Msg("Set number subscribed")
return nil
}
func handleWaitingTextJobs(ctx context.Context, src string) {
log.Info().Str("src", src).Msg("Pretend handle waiting jobs")
}
func HandleTextMessage(from string, to string, body string) {
ctx := context.Background()
type_, src := splitPhoneSource(from)
@ -98,18 +107,25 @@ func HandleTextMessage(from string, to string, body string) {
log.Error().Err(err).Msg("Failed to handle message")
return
}
if !subscribed {
// We don't know if they're subscribed or not.
if subscribed == nil {
body_l := strings.TrimSpace(strings.ToLower(body))
if body_l == "stop" {
switch body_l {
case "stop":
setSubscribed(ctx, src, false)
return
}
err = text.SendInitialReprompt(ctx, dst, src)
if err != nil {
log.Error().Err(err).Msg("Failed to resend initial prompt.")
case "yes":
setSubscribed(ctx, src, true)
handleWaitingTextJobs(ctx, src)
default:
err = text.SendInitialReprompt(ctx, dst, src)
if err != nil {
log.Error().Err(err).Msg("Failed to resend initial prompt.")
}
}
return
}
if !(*subscribed) {
}
previous_messages, err := loadPreviousMessages(ctx, dst, src)
if err != nil {
log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to get previous messages")