diff --git a/db/gen/nidus-sync/comms/model/contact_email.go b/db/gen/nidus-sync/comms/model/contact_email.go index 15814316..eaad47c7 100644 --- a/db/gen/nidus-sync/comms/model/contact_email.go +++ b/db/gen/nidus-sync/comms/model/contact_email.go @@ -8,8 +8,9 @@ package model type ContactEmail struct { - Address string `sql:"primary_key"` + Address string Confirmed bool ContactID int32 IsSubscribed bool + ID int32 `sql:"primary_key"` } diff --git a/db/gen/nidus-sync/comms/model/contact_phone.go b/db/gen/nidus-sync/comms/model/contact_phone.go index 0477da3c..8f471443 100644 --- a/db/gen/nidus-sync/comms/model/contact_phone.go +++ b/db/gen/nidus-sync/comms/model/contact_phone.go @@ -11,7 +11,8 @@ type ContactPhone struct { CanSms bool ConfirmedMessageID *int32 ContactID int32 - E164 string `sql:"primary_key"` + E164 string IsSubscribed bool StopMessageID *int32 + ID int32 `sql:"primary_key"` } diff --git a/db/gen/nidus-sync/comms/model/email.go b/db/gen/nidus-sync/comms/model/email.go new file mode 100644 index 00000000..b4aae2fb --- /dev/null +++ b/db/gen/nidus-sync/comms/model/email.go @@ -0,0 +1,13 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +type Email struct { + Address string `sql:"primary_key"` + IsSubscribed bool +} diff --git a/db/gen/nidus-sync/comms/table/contact_email.go b/db/gen/nidus-sync/comms/table/contact_email.go index 7e5341e3..bb4c7eed 100644 --- a/db/gen/nidus-sync/comms/table/contact_email.go +++ b/db/gen/nidus-sync/comms/table/contact_email.go @@ -21,6 +21,7 @@ type contactEmailTable struct { Confirmed postgres.ColumnBool ContactID postgres.ColumnInteger IsSubscribed postgres.ColumnBool + ID postgres.ColumnInteger AllColumns postgres.ColumnList MutableColumns postgres.ColumnList @@ -66,9 +67,10 @@ func newContactEmailTableImpl(schemaName, tableName, alias string) contactEmailT ConfirmedColumn = postgres.BoolColumn("confirmed") ContactIDColumn = postgres.IntegerColumn("contact_id") IsSubscribedColumn = postgres.BoolColumn("is_subscribed") - allColumns = postgres.ColumnList{AddressColumn, ConfirmedColumn, ContactIDColumn, IsSubscribedColumn} - mutableColumns = postgres.ColumnList{ConfirmedColumn, ContactIDColumn, IsSubscribedColumn} - defaultColumns = postgres.ColumnList{} + IDColumn = postgres.IntegerColumn("id") + allColumns = postgres.ColumnList{AddressColumn, ConfirmedColumn, ContactIDColumn, IsSubscribedColumn, IDColumn} + mutableColumns = postgres.ColumnList{AddressColumn, ConfirmedColumn, ContactIDColumn, IsSubscribedColumn} + defaultColumns = postgres.ColumnList{IDColumn} ) return contactEmailTable{ @@ -79,6 +81,7 @@ func newContactEmailTableImpl(schemaName, tableName, alias string) contactEmailT Confirmed: ConfirmedColumn, ContactID: ContactIDColumn, IsSubscribed: IsSubscribedColumn, + ID: IDColumn, AllColumns: allColumns, MutableColumns: mutableColumns, diff --git a/db/gen/nidus-sync/comms/table/contact_phone.go b/db/gen/nidus-sync/comms/table/contact_phone.go index 1d59e635..b2f3fb26 100644 --- a/db/gen/nidus-sync/comms/table/contact_phone.go +++ b/db/gen/nidus-sync/comms/table/contact_phone.go @@ -23,6 +23,7 @@ type contactPhoneTable struct { E164 postgres.ColumnString IsSubscribed postgres.ColumnBool StopMessageID postgres.ColumnInteger + ID postgres.ColumnInteger AllColumns postgres.ColumnList MutableColumns postgres.ColumnList @@ -70,9 +71,10 @@ func newContactPhoneTableImpl(schemaName, tableName, alias string) contactPhoneT E164Column = postgres.StringColumn("e164") IsSubscribedColumn = postgres.BoolColumn("is_subscribed") StopMessageIDColumn = postgres.IntegerColumn("stop_message_id") - allColumns = postgres.ColumnList{CanSmsColumn, ConfirmedMessageIDColumn, ContactIDColumn, E164Column, IsSubscribedColumn, StopMessageIDColumn} - mutableColumns = postgres.ColumnList{CanSmsColumn, ConfirmedMessageIDColumn, ContactIDColumn, IsSubscribedColumn, StopMessageIDColumn} - defaultColumns = postgres.ColumnList{} + IDColumn = postgres.IntegerColumn("id") + allColumns = postgres.ColumnList{CanSmsColumn, ConfirmedMessageIDColumn, ContactIDColumn, E164Column, IsSubscribedColumn, StopMessageIDColumn, IDColumn} + mutableColumns = postgres.ColumnList{CanSmsColumn, ConfirmedMessageIDColumn, ContactIDColumn, E164Column, IsSubscribedColumn, StopMessageIDColumn} + defaultColumns = postgres.ColumnList{IDColumn} ) return contactPhoneTable{ @@ -85,6 +87,7 @@ func newContactPhoneTableImpl(schemaName, tableName, alias string) contactPhoneT E164: E164Column, IsSubscribed: IsSubscribedColumn, StopMessageID: StopMessageIDColumn, + ID: IDColumn, AllColumns: allColumns, MutableColumns: mutableColumns, diff --git a/db/gen/nidus-sync/comms/table/email.go b/db/gen/nidus-sync/comms/table/email.go new file mode 100644 index 00000000..fd8ae520 --- /dev/null +++ b/db/gen/nidus-sync/comms/table/email.go @@ -0,0 +1,81 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/Gleipnir-Technology/jet/postgres" +) + +var Email = newEmailTable("comms", "email", "") + +type emailTable struct { + postgres.Table + + // Columns + Address postgres.ColumnString + IsSubscribed postgres.ColumnBool + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList + DefaultColumns postgres.ColumnList +} + +type EmailTable struct { + emailTable + + EXCLUDED emailTable +} + +// AS creates new EmailTable with assigned alias +func (a EmailTable) AS(alias string) *EmailTable { + return newEmailTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new EmailTable with assigned schema name +func (a EmailTable) FromSchema(schemaName string) *EmailTable { + return newEmailTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new EmailTable with assigned table prefix +func (a EmailTable) WithPrefix(prefix string) *EmailTable { + return newEmailTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new EmailTable with assigned table suffix +func (a EmailTable) WithSuffix(suffix string) *EmailTable { + return newEmailTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newEmailTable(schemaName, tableName, alias string) *EmailTable { + return &EmailTable{ + emailTable: newEmailTableImpl(schemaName, tableName, alias), + EXCLUDED: newEmailTableImpl("", "excluded", ""), + } +} + +func newEmailTableImpl(schemaName, tableName, alias string) emailTable { + var ( + AddressColumn = postgres.StringColumn("address") + IsSubscribedColumn = postgres.BoolColumn("is_subscribed") + allColumns = postgres.ColumnList{AddressColumn, IsSubscribedColumn} + mutableColumns = postgres.ColumnList{IsSubscribedColumn} + defaultColumns = postgres.ColumnList{} + ) + + return emailTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + Address: AddressColumn, + IsSubscribed: IsSubscribedColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + DefaultColumns: defaultColumns, + } +} diff --git a/db/gen/nidus-sync/comms/table/table_use_schema.go b/db/gen/nidus-sync/comms/table/table_use_schema.go index 83ad79b1..4cecef69 100644 --- a/db/gen/nidus-sync/comms/table/table_use_schema.go +++ b/db/gen/nidus-sync/comms/table/table_use_schema.go @@ -13,6 +13,7 @@ func UseSchema(schema string) { Contact = Contact.FromSchema(schema) ContactEmail = ContactEmail.FromSchema(schema) ContactPhone = ContactPhone.FromSchema(schema) + Email = Email.FromSchema(schema) EmailContact = EmailContact.FromSchema(schema) EmailLog = EmailLog.FromSchema(schema) EmailTemplate = EmailTemplate.FromSchema(schema) diff --git a/db/migrations/00153_comms_phone_log_contact.sql b/db/migrations/00153_comms_phone_log_contact.sql new file mode 100644 index 00000000..58913f26 --- /dev/null +++ b/db/migrations/00153_comms_phone_log_contact.sql @@ -0,0 +1,65 @@ +-- +goose Up +-- We need to make it possible to have multiple relationships relating phone numbers, a global asset, +-- to cantacts, an organization-specific asset. + +-- Step 1: Drop the existing primary key constraint on e164 +ALTER TABLE comms.contact_phone +DROP CONSTRAINT contact_phone_pkey; + +-- Step 2: Add new ID column with SERIAL type (auto-generates sequence) +ALTER TABLE comms.contact_phone +ADD COLUMN id SERIAL; + +-- Step 3: Set the new ID column as the primary key +ALTER TABLE comms.contact_phone +ADD PRIMARY KEY (id); + +-- Step 4: Add foreign key constraint on e164 referencing comms.phone +ALTER TABLE comms.contact_phone +ADD CONSTRAINT contact_phone_e164_fkey +FOREIGN KEY (e164) REFERENCES comms.phone(e164); + +-- Step 5 (Optional but recommended): Add an index on e164 for query performance +CREATE INDEX idx_contact_phone_e164 ON comms.contact_phone(e164); + +CREATE TABLE comms.email ( + address TEXT NOT NULL, + is_subscribed BOOLEAN NOT NULL, + PRIMARY KEY(address) +); +INSERT INTO comms.email ( + address, + is_subscribed +) SELECT + address, + FALSE +FROM comms.contact_email; +INSERT INTO comms.email ( + address, + is_subscribed +) SELECT + source, + FALSE +FROM comms.email_log +ON CONFLICT (address) DO NOTHING; + +-- Do the same thing to email, for exactly the same reasons +-- Step 1: Drop the existing primary key constraint on address +ALTER TABLE comms.contact_email +DROP CONSTRAINT contact_email_pkey; + +-- Step 2: Add new ID column with SERIAL type (auto-generates sequence) +ALTER TABLE comms.contact_email +ADD COLUMN id SERIAL; + +-- Step 3: Set the new ID column as the primary key +ALTER TABLE comms.contact_email +ADD PRIMARY KEY (id); + +-- Step 4: Add foreign key constraint on address referencing comms.email +ALTER TABLE comms.contact_email +ADD CONSTRAINT contact_email_address_fkey +FOREIGN KEY (address) REFERENCES comms.email(address); + +-- Step 5 (Optional but recommended): Add an index on address for query performance +CREATE INDEX idx_contact_email_address ON comms.contact_email(address); diff --git a/db/query/comms/phone.go b/db/query/comms/phone.go new file mode 100644 index 00000000..404fa45f --- /dev/null +++ b/db/query/comms/phone.go @@ -0,0 +1,42 @@ +package comms + +import ( + "context" + + "github.com/Gleipnir-Technology/jet/postgres" + "source.gleipnir.technology/Gleipnir/nidus-sync/db" + "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/comms/model" + "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/comms/table" +) + +func PhoneInsertIfNotExists(ctx context.Context, txn db.Ex, m model.Phone) (model.Phone, error) { + inserted := postgres.CTE("inserted") + insert := table.Phone.INSERT( + table.Phone.AllColumns, + ).MODEL(m). + ON_CONFLICT(table.Phone.E164).DO_NOTHING(). + RETURNING(table.Phone.AllColumns) + + statement := postgres.WITH(inserted.AS(insert))( + postgres.SELECT(inserted.AllColumns()). + FROM(inserted). + UNION_ALL( + postgres.SELECT(table.Phone.AllColumns). + FROM(table.Phone). + WHERE(postgres.AND( + table.Phone.E164.EQ(postgres.String(m.E164)), + postgres.NOT(postgres.EXISTS( + postgres.SELECT(postgres.STAR).FROM(inserted), + )), + )), + ), + ) + return db.ExecuteOneTx[model.Phone](ctx, txn, statement) +} +func PhoneFromE164(ctx context.Context, txn db.Ex, e164 string) (model.Phone, error) { + statement := table.Phone.SELECT( + table.Phone.AllColumns, + ).FROM(table.Phone). + WHERE(table.Phone.E164.EQ(postgres.String(e164))) + return db.ExecuteOneTx[model.Phone](ctx, txn, statement) +} diff --git a/platform/background/background.go b/platform/background/background.go index d08ffdd6..d75a1269 100644 --- a/platform/background/background.go +++ b/platform/background/background.go @@ -33,8 +33,8 @@ func NewEmailSend(ctx context.Context, txn bob.Executor, email_id int32) error { func NewLabelStudioAudioCreate(ctx context.Context, txn bob.Executor, note_audio_id int32) error { return newJob(ctx, txn, enums.JobtypeLabelStudioAudioCreate, note_audio_id) } -func NewTextRespond(ctx context.Context, txn bob.Executor, text_id int32) error { - return newJob(ctx, txn, enums.JobtypeTextRespond, text_id) +func NewTextRespond(ctx context.Context, txn db.Ex, text_id int32) error { + return newJob2(ctx, txn, model.Jobtype_TextRespond, text_id) } func NewTextSend(ctx context.Context, txn db.Ex, job_id int32) error { return newJob2(ctx, txn, model.Jobtype_TextSend, job_id) diff --git a/platform/text/phone_number.go b/platform/text/phone_number.go index 86773358..f9af38d8 100644 --- a/platform/text/phone_number.go +++ b/platform/text/phone_number.go @@ -2,12 +2,9 @@ package text import ( "context" - "fmt" "source.gleipnir.technology/Gleipnir/nidus-sync/db" - "source.gleipnir.technology/Gleipnir/nidus-sync/db/enums" modelcomms "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/comms/model" - "source.gleipnir.technology/Gleipnir/nidus-sync/db/models" querycomms "source.gleipnir.technology/Gleipnir/nidus-sync/db/query/comms" "source.gleipnir.technology/Gleipnir/nidus-sync/platform/types" //"github.com/rs/zerolog/log" @@ -28,10 +25,11 @@ func ensureInDB(ctx context.Context, txn db.Ex, contact modelcomms.Contact, dest _, err = querycomms.ContactPhoneInsert(ctx, txn, contact_phone) return err } -func phoneStatus(ctx context.Context, src types.E164) (enums.CommsPhonestatustype, error) { - phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, src.PhoneString()) - if err != nil { - return enums.CommsPhonestatustypeUnconfirmed, fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src.PhoneString(), err) - } - return phone.Status, nil +func ensurePhoneInDB(ctx context.Context, txn db.Ex, number *types.E164) (modelcomms.Phone, error) { + return querycomms.PhoneInsertIfNotExists(ctx, txn, modelcomms.Phone{ + CanSms: false, + E164: number.PhoneString(), + IsSubscribed: false, + Status: modelcomms.Phonestatustype_Unconfirmed, + }) } diff --git a/platform/text/send.go b/platform/text/send.go index b8acbab4..1aa99c0f 100644 --- a/platform/text/send.go +++ b/platform/text/send.go @@ -9,7 +9,6 @@ import ( "source.gleipnir.technology/Gleipnir/nidus-sync/comms/text" "source.gleipnir.technology/Gleipnir/nidus-sync/config" "source.gleipnir.technology/Gleipnir/nidus-sync/db" - "source.gleipnir.technology/Gleipnir/nidus-sync/db/enums" modelcomms "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/comms/model" modelpublic "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/public/model" modelpublicreport "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/publicreport/model" @@ -97,13 +96,13 @@ func sendTextComplete(ctx context.Context, job modelcomms.TextJob) error { default: return fmt.Errorf("incomplete switch: %s", string(job.Type)) } - status, err := phoneStatus(ctx, *dst) + destination, err := querycomms.PhoneFromE164(ctx, txn, dst.PhoneString()) if err != nil { - return fmt.Errorf("Failed to check if subscribed: %w", err) + return fmt.Errorf("destination phone from e164: %w", err) } - log.Debug().Str("phone status", string(status)).Str("destination", job.Destination).Send() - switch status { - case enums.CommsPhonestatustypeUnconfirmed: + log.Debug().Str("phone status", string(destination.Status)).Str("destination", job.Destination).Send() + switch destination.Status { + case modelcomms.Phonestatustype_Unconfirmed: err := ensureInitialText(ctx, txn, *dst) if err != nil { return fmt.Errorf("Failed to ensure initial text has been sent: %w", err) @@ -111,7 +110,7 @@ func sendTextComplete(ctx context.Context, job modelcomms.TextJob) error { return nil //case enums.CommsPhonestatustypeOkToSend: // allow to drop through - case enums.CommsPhonestatustypeStopped: + case modelcomms.Phonestatustype_Stopped: lint.LogOnErrCtx(func(ctx context.Context) error { return resendInitialText(ctx, txn, *dst) }, ctx, "resend initial text") diff --git a/platform/text/text.go b/platform/text/text.go index 1c1c0cff..471cbd9b 100644 --- a/platform/text/text.go +++ b/platform/text/text.go @@ -7,12 +7,10 @@ import ( "time" "github.com/aarondl/opt/omit" - "github.com/aarondl/opt/omitnull" "github.com/nyaruka/phonenumbers" "github.com/rs/zerolog/log" "source.gleipnir.technology/Gleipnir/nidus-sync/config" "source.gleipnir.technology/Gleipnir/nidus-sync/db" - "source.gleipnir.technology/Gleipnir/nidus-sync/db/enums" modelcomms "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/comms/model" modelpublicreport "source.gleipnir.technology/Gleipnir/nidus-sync/db/gen/nidus-sync/publicreport/model" "source.gleipnir.technology/Gleipnir/nidus-sync/db/models" @@ -33,30 +31,29 @@ func HandleTextMessage(ctx context.Context, source string, destination string, c if err != nil { return fmt.Errorf("parse destination '%s': %w", destination, err) } - txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil) + txn, err := db.BeginTxn(ctx) if err != nil { return fmt.Errorf("start txn: %w", err) } defer lint.LogOnErrRollback(txn.Rollback, ctx, "rollback") - status, err := phoneStatus(ctx, *src) + s, err := ensurePhoneInDB(ctx, txn, src) if err != nil { - return fmt.Errorf("Failed to get phone status") + return fmt.Errorf("ensure source in DB: %w", err) } - is_visible_to_llm := status != enums.CommsPhonestatustypeUnconfirmed + is_visible_to_llm := s.Status != modelcomms.Phonestatustype_Unconfirmed - l, err := models.CommsTextLogs.Insert(&models.CommsTextLogSetter{ - //ID: - Content: omit.From(content), - Created: omit.From(time.Now()), - Destination: omit.From(dst.PhoneString()), - IsVisibleToLLM: omit.From(is_visible_to_llm), - IsWelcome: omit.From(false), - Origin: omit.From(enums.CommsTextoriginCustomer), - Source: omit.From(src.PhoneString()), - TwilioSid: omitnull.FromPtr[string](nil), - TwilioStatus: omit.From(""), - }).One(ctx, txn) + l, err := querycomms.TextLogInsert(ctx, txn, modelcomms.TextLog{ + Content: content, + Created: time.Now(), + Destination: dst.PhoneString(), + IsVisibleToLlm: is_visible_to_llm, + IsWelcome: false, + Origin: modelcomms.Textorigin_Customer, + Source: s.E164, + TwilioSid: nil, + TwilioStatus: "", + }) if err != nil { return fmt.Errorf("insert text log: %w", err) } diff --git a/ts/components/ReviewContactColumnDetail.vue b/ts/components/ReviewContactColumnDetail.vue index 123065b1..7ad588b1 100644 --- a/ts/components/ReviewContactColumnDetail.vue +++ b/ts/components/ReviewContactColumnDetail.vue @@ -6,6 +6,7 @@
+

Email Addresses

@@ -14,6 +15,7 @@

No email addresses

+

Phone Numbers