Make username unique, make is_subscribed nullable
This commit is contained in:
parent
1cd4a31404
commit
e8e840ec44
11 changed files with 149 additions and 74 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
2
db/migrations/00042_text_subscribe_nullable.sql
Normal file
2
db/migrations/00042_text_subscribe_nullable.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
-- +goose Up
|
||||
ALTER TABLE comms.phone ALTER COLUMN is_subscribed DROP NOT NULL;
|
||||
2
db/migrations/00043_username_unique.sql
Normal file
2
db/migrations/00043_username_unique.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
-- +goose Up
|
||||
ALTER TABLE user_ ADD CONSTRAINT user_username_unique UNIQUE (username);
|
||||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue