From f1fe8b4d2b44ab9b9a88743cc1ff651cb2d305f4 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 15 May 2026 16:58:28 +0000 Subject: [PATCH] Add contacts, rework comms schema This in a pretty huge change. At a high level we're adding the concept of a 'contact' which is a person or organization that has zero or more contact methods (email, phone). This ended up cascading a number of changes, including critically to the publicreprt schema. In the end it seemed safer to get to the point where I'm confident we aren't using any of the old fields for storing reporter information (though I haven't deleted the columns yet) so I removed the code for defining those columns. At this point I think it's not possible for me to regenerate the bob schema due to the interdependencies between my various schemas, so the migration is well-and-truly happening. --- db/bobgen.yaml | 1 - db/gen/nidus-sync/comms/model/contact.go | 19 ++ .../nidus-sync/comms/model/contact_email.go | 16 ++ .../nidus-sync/comms/model/contact_phone.go | 17 ++ db/gen/nidus-sync/comms/table/contact.go | 87 ++++++++ .../nidus-sync/comms/table/contact_email.go | 90 ++++++++ .../nidus-sync/comms/table/contact_phone.go | 93 +++++++++ .../comms/table/table_use_schema.go | 3 + .../nidus-sync/publicreport/model/report.go | 1 + .../nidus-sync/publicreport/table/report.go | 7 +- db/migrations/00151_comms_contact.sql | 119 +++++++++++ db/models/publicreport.report.bob.go | 134 ++---------- db/query/comms/contact.go | 47 +++++ db/query/comms/contact_phone.go | 59 ++++++ db/query/comms/text_job.go | 42 ++++ db/query/comms/text_log.go | 32 ++- db/query/public/communication.go | 13 +- db/query/public/organization.go | 36 ++++ db/query/public/report_text.go | 18 ++ db/query/publicreport/notify_email.go | 26 +++ db/query/publicreport/notify_phone.go | 26 +++ db/query/publicreport/report.go | 19 +- db/query/publicreport/subscribe_email.go | 19 ++ db/query/publicreport/subscribe_phone.go | 18 ++ platform/background/background.go | 4 +- platform/communication.go | 11 +- platform/csv/pool.go | 24 +-- platform/email/email.go | 3 +- platform/email/initial.go | 5 +- .../email/report_notification_confirmation.go | 8 +- platform/publicreport.go | 30 ++- platform/publicreport/notification.go | 105 ++++++++++ platform/publicreport/report.go | 2 +- platform/publicreport_notification.go | 26 +-- platform/report/notification.go | 197 ------------------ platform/report/some_report.go | 37 ---- platform/text/job.go | 13 +- platform/text/llm.go | 4 +- platform/text/phone_number.go | 47 ++--- platform/text/report.go | 47 +---- platform/text/send.go | 148 ++++++------- platform/text/text.go | 67 +++--- platform/types/contact.go | 2 +- platform/types/site.go | 2 +- resource/communication.go | 2 +- resource/publicreport_compliance.go | 34 +-- 46 files changed, 1127 insertions(+), 633 deletions(-) create mode 100644 db/gen/nidus-sync/comms/model/contact.go create mode 100644 db/gen/nidus-sync/comms/model/contact_email.go create mode 100644 db/gen/nidus-sync/comms/model/contact_phone.go create mode 100644 db/gen/nidus-sync/comms/table/contact.go create mode 100644 db/gen/nidus-sync/comms/table/contact_email.go create mode 100644 db/gen/nidus-sync/comms/table/contact_phone.go create mode 100644 db/migrations/00151_comms_contact.sql create mode 100644 db/query/comms/contact.go create mode 100644 db/query/comms/contact_phone.go create mode 100644 db/query/comms/text_job.go create mode 100644 db/query/public/organization.go create mode 100644 db/query/public/report_text.go create mode 100644 db/query/publicreport/notify_email.go create mode 100644 db/query/publicreport/notify_phone.go create mode 100644 db/query/publicreport/subscribe_email.go create mode 100644 db/query/publicreport/subscribe_phone.go create mode 100644 platform/publicreport/notification.go delete mode 100644 platform/report/notification.go delete mode 100644 platform/report/some_report.go diff --git a/db/bobgen.yaml b/db/bobgen.yaml index 184c0432..8cb9b619 100644 --- a/db/bobgen.yaml +++ b/db/bobgen.yaml @@ -18,7 +18,6 @@ aliases: no_tests: true psql: schemas: - - "comms" - "fieldseeker" - "fileupload" - "lob" diff --git a/db/gen/nidus-sync/comms/model/contact.go b/db/gen/nidus-sync/comms/model/contact.go new file mode 100644 index 00000000..8b21a73f --- /dev/null +++ b/db/gen/nidus-sync/comms/model/contact.go @@ -0,0 +1,19 @@ +// +// 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 + +import ( + "time" +) + +type Contact struct { + Created time.Time + ID int32 `sql:"primary_key"` + Name string + OrganizationID int32 +} diff --git a/db/gen/nidus-sync/comms/model/contact_email.go b/db/gen/nidus-sync/comms/model/contact_email.go new file mode 100644 index 00000000..0e83dba0 --- /dev/null +++ b/db/gen/nidus-sync/comms/model/contact_email.go @@ -0,0 +1,16 @@ +// +// 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 ContactEmail struct { + Address string `sql:"primary_key"` + Confirmed bool + ContactID int32 + ID int32 + IsSubscribed bool +} diff --git a/db/gen/nidus-sync/comms/model/contact_phone.go b/db/gen/nidus-sync/comms/model/contact_phone.go new file mode 100644 index 00000000..0477da3c --- /dev/null +++ b/db/gen/nidus-sync/comms/model/contact_phone.go @@ -0,0 +1,17 @@ +// +// 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 ContactPhone struct { + CanSms bool + ConfirmedMessageID *int32 + ContactID int32 + E164 string `sql:"primary_key"` + IsSubscribed bool + StopMessageID *int32 +} diff --git a/db/gen/nidus-sync/comms/table/contact.go b/db/gen/nidus-sync/comms/table/contact.go new file mode 100644 index 00000000..5dd2a7b5 --- /dev/null +++ b/db/gen/nidus-sync/comms/table/contact.go @@ -0,0 +1,87 @@ +// +// 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 Contact = newContactTable("comms", "contact", "") + +type contactTable struct { + postgres.Table + + // Columns + Created postgres.ColumnTimestamp + ID postgres.ColumnInteger + Name postgres.ColumnString + OrganizationID postgres.ColumnInteger + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList + DefaultColumns postgres.ColumnList +} + +type ContactTable struct { + contactTable + + EXCLUDED contactTable +} + +// AS creates new ContactTable with assigned alias +func (a ContactTable) AS(alias string) *ContactTable { + return newContactTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new ContactTable with assigned schema name +func (a ContactTable) FromSchema(schemaName string) *ContactTable { + return newContactTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new ContactTable with assigned table prefix +func (a ContactTable) WithPrefix(prefix string) *ContactTable { + return newContactTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new ContactTable with assigned table suffix +func (a ContactTable) WithSuffix(suffix string) *ContactTable { + return newContactTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newContactTable(schemaName, tableName, alias string) *ContactTable { + return &ContactTable{ + contactTable: newContactTableImpl(schemaName, tableName, alias), + EXCLUDED: newContactTableImpl("", "excluded", ""), + } +} + +func newContactTableImpl(schemaName, tableName, alias string) contactTable { + var ( + CreatedColumn = postgres.TimestampColumn("created") + IDColumn = postgres.IntegerColumn("id") + NameColumn = postgres.StringColumn("name") + OrganizationIDColumn = postgres.IntegerColumn("organization_id") + allColumns = postgres.ColumnList{CreatedColumn, IDColumn, NameColumn, OrganizationIDColumn} + mutableColumns = postgres.ColumnList{CreatedColumn, NameColumn, OrganizationIDColumn} + defaultColumns = postgres.ColumnList{IDColumn} + ) + + return contactTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + Created: CreatedColumn, + ID: IDColumn, + Name: NameColumn, + OrganizationID: OrganizationIDColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + DefaultColumns: defaultColumns, + } +} diff --git a/db/gen/nidus-sync/comms/table/contact_email.go b/db/gen/nidus-sync/comms/table/contact_email.go new file mode 100644 index 00000000..33f47522 --- /dev/null +++ b/db/gen/nidus-sync/comms/table/contact_email.go @@ -0,0 +1,90 @@ +// +// 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 ContactEmail = newContactEmailTable("comms", "contact_email", "") + +type contactEmailTable struct { + postgres.Table + + // Columns + Address postgres.ColumnString + Confirmed postgres.ColumnBool + ContactID postgres.ColumnInteger + ID postgres.ColumnInteger + IsSubscribed postgres.ColumnBool + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList + DefaultColumns postgres.ColumnList +} + +type ContactEmailTable struct { + contactEmailTable + + EXCLUDED contactEmailTable +} + +// AS creates new ContactEmailTable with assigned alias +func (a ContactEmailTable) AS(alias string) *ContactEmailTable { + return newContactEmailTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new ContactEmailTable with assigned schema name +func (a ContactEmailTable) FromSchema(schemaName string) *ContactEmailTable { + return newContactEmailTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new ContactEmailTable with assigned table prefix +func (a ContactEmailTable) WithPrefix(prefix string) *ContactEmailTable { + return newContactEmailTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new ContactEmailTable with assigned table suffix +func (a ContactEmailTable) WithSuffix(suffix string) *ContactEmailTable { + return newContactEmailTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newContactEmailTable(schemaName, tableName, alias string) *ContactEmailTable { + return &ContactEmailTable{ + contactEmailTable: newContactEmailTableImpl(schemaName, tableName, alias), + EXCLUDED: newContactEmailTableImpl("", "excluded", ""), + } +} + +func newContactEmailTableImpl(schemaName, tableName, alias string) contactEmailTable { + var ( + AddressColumn = postgres.StringColumn("address") + ConfirmedColumn = postgres.BoolColumn("confirmed") + ContactIDColumn = postgres.IntegerColumn("contact_id") + IDColumn = postgres.IntegerColumn("id") + IsSubscribedColumn = postgres.BoolColumn("is_subscribed") + allColumns = postgres.ColumnList{AddressColumn, ConfirmedColumn, ContactIDColumn, IDColumn, IsSubscribedColumn} + mutableColumns = postgres.ColumnList{ConfirmedColumn, ContactIDColumn, IDColumn, IsSubscribedColumn} + defaultColumns = postgres.ColumnList{IDColumn} + ) + + return contactEmailTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + Address: AddressColumn, + Confirmed: ConfirmedColumn, + ContactID: ContactIDColumn, + ID: IDColumn, + IsSubscribed: IsSubscribedColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + DefaultColumns: defaultColumns, + } +} diff --git a/db/gen/nidus-sync/comms/table/contact_phone.go b/db/gen/nidus-sync/comms/table/contact_phone.go new file mode 100644 index 00000000..1d59e635 --- /dev/null +++ b/db/gen/nidus-sync/comms/table/contact_phone.go @@ -0,0 +1,93 @@ +// +// 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 ContactPhone = newContactPhoneTable("comms", "contact_phone", "") + +type contactPhoneTable struct { + postgres.Table + + // Columns + CanSms postgres.ColumnBool + ConfirmedMessageID postgres.ColumnInteger + ContactID postgres.ColumnInteger + E164 postgres.ColumnString + IsSubscribed postgres.ColumnBool + StopMessageID postgres.ColumnInteger + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList + DefaultColumns postgres.ColumnList +} + +type ContactPhoneTable struct { + contactPhoneTable + + EXCLUDED contactPhoneTable +} + +// AS creates new ContactPhoneTable with assigned alias +func (a ContactPhoneTable) AS(alias string) *ContactPhoneTable { + return newContactPhoneTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new ContactPhoneTable with assigned schema name +func (a ContactPhoneTable) FromSchema(schemaName string) *ContactPhoneTable { + return newContactPhoneTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new ContactPhoneTable with assigned table prefix +func (a ContactPhoneTable) WithPrefix(prefix string) *ContactPhoneTable { + return newContactPhoneTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new ContactPhoneTable with assigned table suffix +func (a ContactPhoneTable) WithSuffix(suffix string) *ContactPhoneTable { + return newContactPhoneTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newContactPhoneTable(schemaName, tableName, alias string) *ContactPhoneTable { + return &ContactPhoneTable{ + contactPhoneTable: newContactPhoneTableImpl(schemaName, tableName, alias), + EXCLUDED: newContactPhoneTableImpl("", "excluded", ""), + } +} + +func newContactPhoneTableImpl(schemaName, tableName, alias string) contactPhoneTable { + var ( + CanSmsColumn = postgres.BoolColumn("can_sms") + ConfirmedMessageIDColumn = postgres.IntegerColumn("confirmed_message_id") + ContactIDColumn = postgres.IntegerColumn("contact_id") + 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{} + ) + + return contactPhoneTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + CanSms: CanSmsColumn, + ConfirmedMessageID: ConfirmedMessageIDColumn, + ContactID: ContactIDColumn, + E164: E164Column, + IsSubscribed: IsSubscribedColumn, + StopMessageID: StopMessageIDColumn, + + 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 7264d7c9..83ad79b1 100644 --- a/db/gen/nidus-sync/comms/table/table_use_schema.go +++ b/db/gen/nidus-sync/comms/table/table_use_schema.go @@ -10,6 +10,9 @@ package table // UseSchema sets a new schema name for all generated table SQL builder types. It is recommended to invoke // this method only once at the beginning of the program. func UseSchema(schema string) { + Contact = Contact.FromSchema(schema) + ContactEmail = ContactEmail.FromSchema(schema) + ContactPhone = ContactPhone.FromSchema(schema) EmailContact = EmailContact.FromSchema(schema) EmailLog = EmailLog.FromSchema(schema) EmailTemplate = EmailTemplate.FromSchema(schema) diff --git a/db/gen/nidus-sync/publicreport/model/report.go b/db/gen/nidus-sync/publicreport/model/report.go index 1265ae09..e1d330e6 100644 --- a/db/gen/nidus-sync/publicreport/model/report.go +++ b/db/gen/nidus-sync/publicreport/model/report.go @@ -36,4 +36,5 @@ type Report struct { AddressGid string ClientUUID *uuid.UUID ReporterPhoneCanSms bool + ReporterContactID *int32 } diff --git a/db/gen/nidus-sync/publicreport/table/report.go b/db/gen/nidus-sync/publicreport/table/report.go index bd04cf1f..22bdefa7 100644 --- a/db/gen/nidus-sync/publicreport/table/report.go +++ b/db/gen/nidus-sync/publicreport/table/report.go @@ -39,6 +39,7 @@ type reportTable struct { AddressGid postgres.ColumnString ClientUUID postgres.ColumnString ReporterPhoneCanSms postgres.ColumnBool + ReporterContactID postgres.ColumnInteger AllColumns postgres.ColumnList MutableColumns postgres.ColumnList @@ -102,8 +103,9 @@ func newReportTableImpl(schemaName, tableName, alias string) reportTable { AddressGidColumn = postgres.StringColumn("address_gid") ClientUUIDColumn = postgres.StringColumn("client_uuid") ReporterPhoneCanSmsColumn = postgres.BoolColumn("reporter_phone_can_sms") - allColumns = postgres.ColumnList{AddressRawColumn, AddressIDColumn, CreatedColumn, LocationColumn, H3cellColumn, IDColumn, LatlngAccuracyTypeColumn, LatlngAccuracyValueColumn, MapZoomColumn, OrganizationIDColumn, PublicIDColumn, ReporterNameColumn, ReporterEmailColumn, ReporterPhoneColumn, ReporterContactConsentColumn, ReportTypeColumn, ReviewedColumn, ReviewerIDColumn, StatusColumn, AddressGidColumn, ClientUUIDColumn, ReporterPhoneCanSmsColumn} - mutableColumns = postgres.ColumnList{AddressRawColumn, AddressIDColumn, CreatedColumn, LocationColumn, H3cellColumn, LatlngAccuracyTypeColumn, LatlngAccuracyValueColumn, MapZoomColumn, OrganizationIDColumn, PublicIDColumn, ReporterNameColumn, ReporterEmailColumn, ReporterPhoneColumn, ReporterContactConsentColumn, ReportTypeColumn, ReviewedColumn, ReviewerIDColumn, StatusColumn, AddressGidColumn, ClientUUIDColumn, ReporterPhoneCanSmsColumn} + ReporterContactIDColumn = postgres.IntegerColumn("reporter_contact_id") + allColumns = postgres.ColumnList{AddressRawColumn, AddressIDColumn, CreatedColumn, LocationColumn, H3cellColumn, IDColumn, LatlngAccuracyTypeColumn, LatlngAccuracyValueColumn, MapZoomColumn, OrganizationIDColumn, PublicIDColumn, ReporterNameColumn, ReporterEmailColumn, ReporterPhoneColumn, ReporterContactConsentColumn, ReportTypeColumn, ReviewedColumn, ReviewerIDColumn, StatusColumn, AddressGidColumn, ClientUUIDColumn, ReporterPhoneCanSmsColumn, ReporterContactIDColumn} + mutableColumns = postgres.ColumnList{AddressRawColumn, AddressIDColumn, CreatedColumn, LocationColumn, H3cellColumn, LatlngAccuracyTypeColumn, LatlngAccuracyValueColumn, MapZoomColumn, OrganizationIDColumn, PublicIDColumn, ReporterNameColumn, ReporterEmailColumn, ReporterPhoneColumn, ReporterContactConsentColumn, ReportTypeColumn, ReviewedColumn, ReviewerIDColumn, StatusColumn, AddressGidColumn, ClientUUIDColumn, ReporterPhoneCanSmsColumn, ReporterContactIDColumn} defaultColumns = postgres.ColumnList{IDColumn} ) @@ -133,6 +135,7 @@ func newReportTableImpl(schemaName, tableName, alias string) reportTable { AddressGid: AddressGidColumn, ClientUUID: ClientUUIDColumn, ReporterPhoneCanSms: ReporterPhoneCanSmsColumn, + ReporterContactID: ReporterContactIDColumn, AllColumns: allColumns, MutableColumns: mutableColumns, diff --git a/db/migrations/00151_comms_contact.sql b/db/migrations/00151_comms_contact.sql new file mode 100644 index 00000000..55cd3251 --- /dev/null +++ b/db/migrations/00151_comms_contact.sql @@ -0,0 +1,119 @@ +-- +goose Up +CREATE TABLE comms.contact ( + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + id SERIAL NOT NULL, + name TEXT NOT NULL, + organization_id INTEGER NOT NULL REFERENCES organization(id), + PRIMARY KEY(id) +); +CREATE TABLE comms.contact_email ( + address TEXT NOT NULL, + confirmed BOOLEAN NOT NULL, + contact_id INTEGER NOT NULL REFERENCES comms.contact(id), + id SERIAL NOT NULL, + is_subscribed BOOLEAN NOT NULL, + PRIMARY KEY(address) +); +CREATE TABLE comms.contact_phone ( + can_sms BOOLEAN NOT NULL, + confirmed_message_id INTEGER REFERENCES comms.text_log(id), + contact_id INTEGER NOT NULL REFERENCES comms.contact(id), + e164 TEXT NOT NULL, + is_subscribed BOOLEAN NOT NULL, + stop_message_id INTEGER REFERENCES comms.text_log(id), + PRIMARY KEY(e164) +); + +ALTER TABLE publicreport.report + ADD COLUMN reporter_contact_id INTEGER REFERENCES comms.contact(id); + +-- insert a placeholder contact for the system +INSERT INTO comms.contact ( + created, + name, + organization_id +) SELECT + CURRENT_TIMESTAMP, + 'Nidus', + id +FROM organization WHERE is_catchall = TRUE; + +-- insert contacts where we have names, dropping duplicates +INSERT INTO comms.contact ( + created, + name, + organization_id +) SELECT DISTINCT ON (reporter_name, organization_id) + created, + reporter_name, + organization_id +FROM publicreport.report +WHERE reporter_name != '' +ORDER BY reporter_name, organization_id, created ASC; + +UPDATE publicreport.report +SET reporter_contact_id = comms.contact.id +FROM comms.contact +WHERE comms.contact.name = report.reporter_name +AND reporter_name != ''; + +-- insert contacts where we don't have names +INSERT INTO comms.contact ( + created, + name, + organization_id +) SELECT + created, + reporter_name, + organization_id +FROM publicreport.report +WHERE report.reporter_name = ''; + +UPDATE publicreport.report +SET reporter_contact_id = comms.contact.id +FROM comms.contact +WHERE comms.contact.created = report.created; + +-- At this point every publicreport.report should have an associated contact ID. +-- We'll make sure of this via constraint before moving the email and phone data into +-- the contacts +ALTER TABLE publicreport.report + ALTER COLUMN reporter_contact_id SET NOT NULL; + +-- Now copy over all of the contact information we have into the new contact rows +INSERT INTO comms.contact_email ( + address, + confirmed, + contact_id, + is_subscribed +) SELECT + reporter_email, + COALESCE(reporter_contact_consent, false), + reporter_contact_id, + FALSE +FROM publicreport.report WHERE report.reporter_email != '' +ON CONFLICT DO NOTHING; + +INSERT INTO comms.contact_phone ( + can_sms, + confirmed_message_id, + contact_id, + e164, + is_subscribed, + stop_message_id +) SELECT + reporter_phone_can_sms, + NULL, + reporter_contact_id, + reporter_phone, + FALSE, + NULL +FROM publicreport.report WHERE publicreport.report.reporter_phone != '' +ON CONFLICT DO NOTHING; + +-- +goose Down +ALTER TABLE publicreport.report + DROP COLUMN reporter_contact_id; +DROP TABLE comms.contact_phone; +DROP TABLE comms.contact_email; +DROP TABLE comms.contact; diff --git a/db/models/publicreport.report.bob.go b/db/models/publicreport.report.bob.go index a444d640..8accf66e 100644 --- a/db/models/publicreport.report.bob.go +++ b/db/models/publicreport.report.bob.go @@ -153,28 +153,23 @@ func (publicreportReportColumns) AliasedAs(alias string) publicreportReportColum // All values are optional, and do not have to be set // Generated columns are not included type PublicreportReportSetter struct { - AddressRaw omit.Val[string] `db:"address_raw" ` - AddressID omitnull.Val[int32] `db:"address_id" ` - Created omit.Val[time.Time] `db:"created" ` - Location omitnull.Val[string] `db:"location" ` - H3cell omitnull.Val[string] `db:"h3cell" ` - ID omit.Val[int32] `db:"id,pk" ` - LatlngAccuracyType omit.Val[enums.PublicreportAccuracytype] `db:"latlng_accuracy_type" ` - LatlngAccuracyValue omit.Val[float32] `db:"latlng_accuracy_value" ` - MapZoom omit.Val[float32] `db:"map_zoom" ` - OrganizationID omit.Val[int32] `db:"organization_id" ` - PublicID omit.Val[string] `db:"public_id" ` - ReporterName omit.Val[string] `db:"reporter_name" ` - ReporterEmail omit.Val[string] `db:"reporter_email" ` - ReporterPhone omit.Val[string] `db:"reporter_phone" ` - ReporterContactConsent omitnull.Val[bool] `db:"reporter_contact_consent" ` - ReportType omit.Val[enums.PublicreportReporttype] `db:"report_type" ` - Reviewed omitnull.Val[time.Time] `db:"reviewed" ` - ReviewerID omitnull.Val[int32] `db:"reviewer_id" ` - Status omit.Val[enums.PublicreportReportstatustype] `db:"status" ` - AddressGid omit.Val[string] `db:"address_gid" ` - ClientUUID omitnull.Val[uuid.UUID] `db:"client_uuid" ` - ReporterPhoneCanSMS omit.Val[bool] `db:"reporter_phone_can_sms" ` + AddressRaw omit.Val[string] `db:"address_raw" ` + AddressID omitnull.Val[int32] `db:"address_id" ` + Created omit.Val[time.Time] `db:"created" ` + Location omitnull.Val[string] `db:"location" ` + H3cell omitnull.Val[string] `db:"h3cell" ` + ID omit.Val[int32] `db:"id,pk" ` + LatlngAccuracyType omit.Val[enums.PublicreportAccuracytype] `db:"latlng_accuracy_type" ` + LatlngAccuracyValue omit.Val[float32] `db:"latlng_accuracy_value" ` + MapZoom omit.Val[float32] `db:"map_zoom" ` + OrganizationID omit.Val[int32] `db:"organization_id" ` + PublicID omit.Val[string] `db:"public_id" ` + ReportType omit.Val[enums.PublicreportReporttype] `db:"report_type" ` + Reviewed omitnull.Val[time.Time] `db:"reviewed" ` + ReviewerID omitnull.Val[int32] `db:"reviewer_id" ` + Status omit.Val[enums.PublicreportReportstatustype] `db:"status" ` + AddressGid omit.Val[string] `db:"address_gid" ` + ClientUUID omitnull.Val[uuid.UUID] `db:"client_uuid" ` } func (s PublicreportReportSetter) SetColumns() []string { @@ -212,18 +207,6 @@ func (s PublicreportReportSetter) SetColumns() []string { if s.PublicID.IsValue() { vals = append(vals, "public_id") } - if s.ReporterName.IsValue() { - vals = append(vals, "reporter_name") - } - if s.ReporterEmail.IsValue() { - vals = append(vals, "reporter_email") - } - if s.ReporterPhone.IsValue() { - vals = append(vals, "reporter_phone") - } - if !s.ReporterContactConsent.IsUnset() { - vals = append(vals, "reporter_contact_consent") - } if s.ReportType.IsValue() { vals = append(vals, "report_type") } @@ -242,9 +225,6 @@ func (s PublicreportReportSetter) SetColumns() []string { if !s.ClientUUID.IsUnset() { vals = append(vals, "client_uuid") } - if s.ReporterPhoneCanSMS.IsValue() { - vals = append(vals, "reporter_phone_can_sms") - } return vals } @@ -282,18 +262,6 @@ func (s PublicreportReportSetter) Overwrite(t *PublicreportReport) { if s.PublicID.IsValue() { t.PublicID = s.PublicID.MustGet() } - if s.ReporterName.IsValue() { - t.ReporterName = s.ReporterName.MustGet() - } - if s.ReporterEmail.IsValue() { - t.ReporterEmail = s.ReporterEmail.MustGet() - } - if s.ReporterPhone.IsValue() { - t.ReporterPhone = s.ReporterPhone.MustGet() - } - if !s.ReporterContactConsent.IsUnset() { - t.ReporterContactConsent = s.ReporterContactConsent.MustGetNull() - } if s.ReportType.IsValue() { t.ReportType = s.ReportType.MustGet() } @@ -312,9 +280,6 @@ func (s PublicreportReportSetter) Overwrite(t *PublicreportReport) { if !s.ClientUUID.IsUnset() { t.ClientUUID = s.ClientUUID.MustGetNull() } - if s.ReporterPhoneCanSMS.IsValue() { - t.ReporterPhoneCanSMS = s.ReporterPhoneCanSMS.MustGet() - } } func (s *PublicreportReportSetter) Apply(q *dialect.InsertQuery) { @@ -390,29 +355,13 @@ func (s *PublicreportReportSetter) Apply(q *dialect.InsertQuery) { vals[10] = psql.Raw("DEFAULT") } - if s.ReporterName.IsValue() { - vals[11] = psql.Arg(s.ReporterName.MustGet()) - } else { - vals[11] = psql.Raw("DEFAULT") - } + vals[11] = psql.Raw("DEFAULT") - if s.ReporterEmail.IsValue() { - vals[12] = psql.Arg(s.ReporterEmail.MustGet()) - } else { - vals[12] = psql.Raw("DEFAULT") - } + vals[12] = psql.Raw("DEFAULT") - if s.ReporterPhone.IsValue() { - vals[13] = psql.Arg(s.ReporterPhone.MustGet()) - } else { - vals[13] = psql.Raw("DEFAULT") - } + vals[13] = psql.Raw("DEFAULT") - if !s.ReporterContactConsent.IsUnset() { - vals[14] = psql.Arg(s.ReporterContactConsent.MustGetNull()) - } else { - vals[14] = psql.Raw("DEFAULT") - } + vals[14] = psql.Raw("DEFAULT") if s.ReportType.IsValue() { vals[15] = psql.Arg(s.ReportType.MustGet()) @@ -450,11 +399,7 @@ func (s *PublicreportReportSetter) Apply(q *dialect.InsertQuery) { vals[20] = psql.Raw("DEFAULT") } - if s.ReporterPhoneCanSMS.IsValue() { - vals[21] = psql.Arg(s.ReporterPhoneCanSMS.MustGet()) - } else { - vals[21] = psql.Raw("DEFAULT") - } + vals[21] = psql.Raw("DEFAULT") return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") })) @@ -544,34 +489,6 @@ func (s PublicreportReportSetter) Expressions(prefix ...string) []bob.Expression }}) } - if s.ReporterName.IsValue() { - exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ - psql.Quote(append(prefix, "reporter_name")...), - psql.Arg(s.ReporterName), - }}) - } - - if s.ReporterEmail.IsValue() { - exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ - psql.Quote(append(prefix, "reporter_email")...), - psql.Arg(s.ReporterEmail), - }}) - } - - if s.ReporterPhone.IsValue() { - exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ - psql.Quote(append(prefix, "reporter_phone")...), - psql.Arg(s.ReporterPhone), - }}) - } - - if !s.ReporterContactConsent.IsUnset() { - exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ - psql.Quote(append(prefix, "reporter_contact_consent")...), - psql.Arg(s.ReporterContactConsent), - }}) - } - if s.ReportType.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ psql.Quote(append(prefix, "report_type")...), @@ -614,13 +531,6 @@ func (s PublicreportReportSetter) Expressions(prefix ...string) []bob.Expression }}) } - if s.ReporterPhoneCanSMS.IsValue() { - exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ - psql.Quote(append(prefix, "reporter_phone_can_sms")...), - psql.Arg(s.ReporterPhoneCanSMS), - }}) - } - return exprs } diff --git a/db/query/comms/contact.go b/db/query/comms/contact.go new file mode 100644 index 00000000..8b6bf80f --- /dev/null +++ b/db/query/comms/contact.go @@ -0,0 +1,47 @@ +package comms + +import ( + "context" + + //"github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/nidus-sync/db" + //"github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/enum" + "github.com/Gleipnir-Technology/jet/postgres" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/table" +) + +func ContactInsert(ctx context.Context, txn db.Ex, m model.Contact) (model.Contact, error) { + statement := table.Contact.INSERT(table.Contact.MutableColumns). + MODEL(m). + RETURNING(table.Contact.AllColumns) + return db.ExecuteOneTx[model.Contact](ctx, txn, statement) +} + +func ContactFromID(ctx context.Context, txn db.Ex, id int64) (model.Contact, error) { + statement := table.Contact.SELECT( + table.Contact.AllColumns, + ).FROM(table.Contact). + WHERE(table.Contact.ID.EQ(postgres.Int(id))) + return db.ExecuteOne[model.Contact](ctx, statement) +} + +func ContactUpdateName(ctx context.Context, txn db.Ex, id int64, name string) error { + statement := table.Contact.UPDATE(). + SET( + table.Contact.Name.SET(postgres.String(name)), + ). + WHERE(table.Contact.OrganizationID.EQ(postgres.Int(id))) + return db.ExecuteNoneTx(ctx, txn, statement) +} + +/* +func ContactsFromAddress(ctx context.Context, address string) ([]model.Contact, error) { + statement := table.Contact.SELECT( + table.Contact.AllColumns, + ).FROM(table.Contact). + WHERE(table.Contact.Source.EQ(postgres.String(address)).OR( + table.Contact.Destination.EQ(postgres.String(address)))) + return db.ExecuteMany[model.Contact](ctx, statement) +} +*/ diff --git a/db/query/comms/contact_phone.go b/db/query/comms/contact_phone.go new file mode 100644 index 00000000..001f2ba5 --- /dev/null +++ b/db/query/comms/contact_phone.go @@ -0,0 +1,59 @@ +package comms + +import ( + "context" + + //"github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/nidus-sync/db" + //"github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/enum" + "github.com/Gleipnir-Technology/jet/postgres" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/table" +) + +func ContactPhoneInsert(ctx context.Context, txn db.Ex, m model.ContactPhone) (model.ContactPhone, error) { + statement := table.ContactPhone.INSERT(table.ContactPhone.MutableColumns). + MODEL(m). + RETURNING(table.ContactPhone.AllColumns) + return db.ExecuteOneTx[model.ContactPhone](ctx, txn, statement) +} + +func ContactPhoneFromE164(ctx context.Context, txn db.Ex, e164 string) (model.ContactPhone, error) { + statement := table.ContactPhone.SELECT( + table.ContactPhone.AllColumns, + ).FROM(table.ContactPhone). + WHERE(table.ContactPhone.E164.EQ(postgres.String(e164))) + return db.ExecuteOneTx[model.ContactPhone](ctx, txn, statement) +} + +func ContactPhoneUpdateConfirmedMessageID(ctx context.Context, txn db.Ex, e164 string, message_id *int32) error { + statement := table.ContactPhone.UPDATE(). + SET(table.ContactPhone.ConfirmedMessageID.SET(postgres.IntExp(postgres.NULL))). + WHERE(table.ContactPhone.E164.EQ(postgres.String(e164))) + return db.ExecuteNoneTx(ctx, txn, statement) +} +func ContactPhoneUpdateStopMessageID(ctx context.Context, txn db.Ex, e164 string, message_id *int32) error { + /* + m := model.ContactPhone{} + m.StopMessageID = message_id + statement := table.ContactPhone.UPDATE( + table.ContactPhone.StopMessageID, + ).MODEL(m). + WHERE(table.ContactPhone.E164.EQ(postgres.String(e164))) + */ + statement := table.ContactPhone.UPDATE(). + SET(table.ContactPhone.StopMessageID.SET(postgres.IntExp(postgres.NULL))). + WHERE(table.ContactPhone.E164.EQ(postgres.String(e164))) + return db.ExecuteNoneTx(ctx, txn, statement) +} + +/* +func ContactPhonesFromAddress(ctx context.Context, address string) ([]model.ContactPhone, error) { + statement := table.ContactPhone.SELECT( + table.ContactPhone.AllColumns, + ).FROM(table.ContactPhone). + WHERE(table.ContactPhone.Source.EQ(postgres.String(address)).OR( + table.ContactPhone.Destination.EQ(postgres.String(address)))) + return db.ExecuteMany[model.ContactPhone](ctx, statement) +} +*/ diff --git a/db/query/comms/text_job.go b/db/query/comms/text_job.go new file mode 100644 index 00000000..6e545a20 --- /dev/null +++ b/db/query/comms/text_job.go @@ -0,0 +1,42 @@ +package comms + +import ( + "context" + + "github.com/Gleipnir-Technology/jet/postgres" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/table" +) + +func TextJobComplete(ctx context.Context, txn db.Ex, id int64) error { + statement := table.TextJob.UPDATE( + table.TextJob.Completed, + ).SET( + table.TextJob.Completed.SET(postgres.LOCALTIMESTAMP()), + ).WHERE( + table.TextJob.ID.EQ(postgres.Int(id)), + ) + return db.ExecuteNoneTx(ctx, txn, statement) +} +func TextJobFromID(ctx context.Context, txn db.Ex, id int64) (model.TextJob, error) { + statement := table.TextJob.SELECT( + table.TextJob.AllColumns, + ).FROM(table.TextJob). + WHERE(table.TextJob.ID.EQ(postgres.Int(id))) + return db.ExecuteOneTx[model.TextJob](ctx, txn, statement) +} +func TextJobInsert(ctx context.Context, txn db.Ex, m model.TextJob) (model.TextJob, error) { + statement := table.TextJob.INSERT( + table.TextJob.MutableColumns, + ).MODEL(m) + return db.ExecuteOneTx[model.TextJob](ctx, txn, statement) +} +func TextJobsWaitingFromDestination(ctx context.Context, txn db.Ex, destination string) ([]model.TextJob, error) { + statement := table.TextJob.SELECT( + table.TextJob.AllColumns, + ).FROM(table.TextJob). + WHERE(table.TextJob.Destination.EQ(postgres.String(destination)).AND( + table.TextJob.Completed.IS_NULL())) + return db.ExecuteManyTx[model.TextJob](ctx, txn, statement) +} diff --git a/db/query/comms/text_log.go b/db/query/comms/text_log.go index 1bfbf18a..86705481 100644 --- a/db/query/comms/text_log.go +++ b/db/query/comms/text_log.go @@ -9,18 +9,42 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/table" ) -func TextLogFromID(ctx context.Context, id int64) (model.TextLog, error) { +func TextLogInsert(ctx context.Context, txn db.Ex, m model.TextLog) (model.TextLog, error) { + statement := table.TextLog.INSERT( + table.TextLog.MutableColumns, + ).MODEL(m) + return db.ExecuteOneTx[model.TextLog](ctx, txn, statement) +} +func TextLogFromID(ctx context.Context, txn db.Ex, id int64) (model.TextLog, error) { statement := table.TextLog.SELECT( table.TextLog.AllColumns, ).FROM(table.TextLog). WHERE(table.TextLog.ID.EQ(postgres.Int(id))) - return db.ExecuteOne[model.TextLog](ctx, statement) + return db.ExecuteOneTx[model.TextLog](ctx, txn, statement) } -func TextLogsFromPhoneNumber(ctx context.Context, number string) ([]model.TextLog, error) { +func TextLogsFromPhoneNumber(ctx context.Context, txn db.Ex, number string) ([]model.TextLog, error) { statement := table.TextLog.SELECT( table.TextLog.AllColumns, ).FROM(table.TextLog). WHERE(table.TextLog.Source.EQ(postgres.String(number)).OR( table.TextLog.Destination.EQ(postgres.String(number)))) - return db.ExecuteMany[model.TextLog](ctx, statement) + return db.ExecuteManyTx[model.TextLog](ctx, txn, statement) +} +func TextLogWelcomeFromDestination(ctx context.Context, txn db.Ex, destination string) ([]model.TextLog, error) { + statement := table.TextLog.SELECT( + table.TextLog.AllColumns, + ).FROM(table.TextLog). + WHERE(table.TextLog.Destination.EQ(postgres.String(destination)).AND( + table.TextLog.IsWelcome.EQ(postgres.Bool(true)))) + return db.ExecuteManyTx[model.TextLog](ctx, txn, statement) +} +func TextLogUpdate(ctx context.Context, txn db.Ex, id int64, twilio_sid string, twilio_status string) error { + statement := table.TextLog.UPDATE( + table.TextLog.TwilioSid, + table.TextLog.TwilioStatus, + ).SET( + table.TextLog.TwilioSid.SET(postgres.String(twilio_sid)), + table.TextLog.TwilioStatus.SET(postgres.String(twilio_status)), + ).WHERE(table.TextLog.ID.EQ(postgres.Int(id))) + return db.ExecuteNoneTx(ctx, txn, statement) } diff --git a/db/query/public/communication.go b/db/query/public/communication.go index e251feb1..6da6c2a8 100644 --- a/db/query/public/communication.go +++ b/db/query/public/communication.go @@ -3,7 +3,6 @@ package public import ( "context" - //"github.com/Gleipnir-Technology/bob" "github.com/Gleipnir-Technology/nidus-sync/db" //"github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/enum" "github.com/Gleipnir-Technology/jet/postgres" @@ -11,28 +10,28 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/table" ) -func CommunicationInsert(ctx context.Context, txn db.Tx, m model.Communication) (model.Communication, error) { +func CommunicationInsert(ctx context.Context, txn db.Ex, m model.Communication) (model.Communication, error) { statement := table.Communication.INSERT(table.Communication.MutableColumns). MODEL(m). RETURNING(table.Communication.AllColumns) return db.ExecuteOneTx[model.Communication](ctx, txn, statement) } -func CommunicationFromID(ctx context.Context, comm_id int64) (model.Communication, error) { +func CommunicationFromID(ctx context.Context, txn db.Ex, comm_id int64) (model.Communication, error) { statement := table.Communication.SELECT( table.Communication.AllColumns, ).FROM(table.Communication). WHERE(table.Communication.ID.EQ(postgres.Int(comm_id))) - return db.ExecuteOne[model.Communication](ctx, statement) + return db.ExecuteOneTx[model.Communication](ctx, txn, statement) } -func CommunicationsFromOrganization(ctx context.Context, org_id int64) ([]model.Communication, error) { +func CommunicationsFromOrganization(ctx context.Context, txn db.Ex, org_id int64) ([]model.Communication, error) { statement := table.Communication.SELECT( table.Communication.AllColumns, ).FROM(table.Communication). WHERE(table.Communication.OrganizationID.EQ(postgres.Int(org_id))). ORDER_BY(table.Communication.Created.DESC()) - return db.ExecuteMany[model.Communication](ctx, statement) + return db.ExecuteManyTx[model.Communication](ctx, txn, statement) } -func CommunicationSetStatus(ctx context.Context, txn db.Tx, org_id int64, comm_id int64, status model.Communicationstatus) error { +func CommunicationSetStatus(ctx context.Context, txn db.Ex, org_id int64, comm_id int64, status model.Communicationstatus) error { statement := table.Communication.UPDATE(). SET( table.Communication.Status.SET(postgres.NewEnumValue(status.String())), diff --git a/db/query/public/organization.go b/db/query/public/organization.go new file mode 100644 index 00000000..c406c462 --- /dev/null +++ b/db/query/public/organization.go @@ -0,0 +1,36 @@ +package public + +import ( + "context" + + "github.com/Gleipnir-Technology/jet/postgres" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/model" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/table" +) + +func OrganizationInsert(ctx context.Context, txn db.Ex, m model.Organization) (model.Organization, error) { + statement := table.Organization.INSERT(table.Organization.MutableColumns). + MODEL(m). + RETURNING(table.Organization.AllColumns) + return db.ExecuteOneTx[model.Organization](ctx, txn, statement) +} +func OrganizationFromID(ctx context.Context, txn db.Ex, org_id int64) (model.Organization, error) { + statement := table.Organization.SELECT( + table.Organization.AllColumns, + ).FROM(table.Organization). + WHERE(table.Organization.ID.EQ(postgres.Int(org_id))) + return db.ExecuteOne[model.Organization](ctx, statement) +} + +/* +func OrganizationSetStatus(ctx context.Context, txn db.Tx, org_id int64, org_id int64, status model.Organizationstatus) error { + statement := table.Organization.UPDATE(). + SET( + table.Organization.Status.SET(postgres.NewEnumValue(status.String())), + ). + WHERE(table.Organization.OrganizationID.EQ(postgres.Int(org_id)).AND( + table.Organization.ID.EQ(postgres.Int(org_id)))) + return db.ExecuteNoneTx(ctx, txn, statement) +} +*/ diff --git a/db/query/public/report_text.go b/db/query/public/report_text.go new file mode 100644 index 00000000..031870fd --- /dev/null +++ b/db/query/public/report_text.go @@ -0,0 +1,18 @@ +package public + +import ( + "context" + + "github.com/Gleipnir-Technology/nidus-sync/db" + //"github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/enum" + //"github.com/Gleipnir-Technology/jet/postgres" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/model" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/table" +) + +func ReportTextInsert(ctx context.Context, txn db.Tx, m model.ReportText) (model.ReportText, error) { + statement := table.ReportText.INSERT(table.ReportText.MutableColumns). + MODEL(m). + RETURNING(table.ReportText.AllColumns) + return db.ExecuteOneTx[model.ReportText](ctx, txn, statement) +} diff --git a/db/query/publicreport/notify_email.go b/db/query/publicreport/notify_email.go new file mode 100644 index 00000000..35c9de61 --- /dev/null +++ b/db/query/publicreport/notify_email.go @@ -0,0 +1,26 @@ +package publicreport + +import ( + "context" + "time" + + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/table" + //"github.com/Gleipnir-Technology/jet/postgres" +) + +func NotifyEmailInsert(ctx context.Context, txn db.Ex, m model.NotifyEmail) (model.NotifyEmail, error) { + statement := table.NotifyEmail.INSERT(table.NotifyEmail.MutableColumns). + MODEL(m). + RETURNING(table.NotifyEmail.AllColumns) + return db.ExecuteOneTx[model.NotifyEmail](ctx, txn, statement) +} +func NotifyEmailCreate(ctx context.Context, txn db.Ex, report_id int32, destination string) (model.NotifyEmail, error) { + return NotifyEmailInsert(ctx, txn, model.NotifyEmail{ + Created: time.Now(), + Deleted: nil, + EmailAddress: destination, + ReportID: report_id, + }) +} diff --git a/db/query/publicreport/notify_phone.go b/db/query/publicreport/notify_phone.go new file mode 100644 index 00000000..acc2c1cf --- /dev/null +++ b/db/query/publicreport/notify_phone.go @@ -0,0 +1,26 @@ +package publicreport + +import ( + "context" + "time" + + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/table" + //"github.com/Gleipnir-Technology/jet/postgres" +) + +func NotifyPhoneInsert(ctx context.Context, txn db.Ex, m model.NotifyPhone) (model.NotifyPhone, error) { + statement := table.NotifyPhone.INSERT(table.NotifyPhone.MutableColumns). + MODEL(m). + RETURNING(table.NotifyPhone.AllColumns) + return db.ExecuteOneTx[model.NotifyPhone](ctx, txn, statement) +} +func NotifyPhoneCreate(ctx context.Context, txn db.Ex, report_id int32, destination string) (model.NotifyPhone, error) { + return NotifyPhoneInsert(ctx, txn, model.NotifyPhone{ + Created: time.Now(), + Deleted: nil, + PhoneE164: destination, + ReportID: report_id, + }) +} diff --git a/db/query/publicreport/report.go b/db/query/publicreport/report.go index 349d1b31..8a9896fe 100644 --- a/db/query/publicreport/report.go +++ b/db/query/publicreport/report.go @@ -9,6 +9,7 @@ import ( //"github.com/Gleipnir-Technology/bob" "github.com/Gleipnir-Technology/jet/postgres" "github.com/Gleipnir-Technology/nidus-sync/db" + tablecomms "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/table" "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model" "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/table" ) @@ -36,12 +37,12 @@ func ReportsFromAddressID(ctx context.Context, txn db.Ex, org_id int64, address_ table.Report.OrganizationID.EQ(postgres.Int(org_id)))) return db.ExecuteManyTx[model.Report](ctx, txn, statement) } -func ReportFromID(ctx context.Context, report_id int64) (model.Report, error) { +func ReportFromID(ctx context.Context, txn db.Ex, report_id int64) (model.Report, error) { statement := table.Report.SELECT( table.Report.AllColumns, ).FROM(table.Report). WHERE(table.Report.ID.EQ(postgres.Int(report_id))) - return db.ExecuteOne[model.Report](ctx, statement) + return db.ExecuteOneTx[model.Report](ctx, txn, statement) } func ReportsFromIDs(ctx context.Context, report_ids []int64) ([]model.Report, error) { sql_ids := make([]postgres.Expression, len(report_ids)) @@ -111,3 +112,17 @@ func ReportsUnreviewedForOrganization(ctx context.Context, txn db.Ex, org_id int table.Report.OrganizationID.EQ(postgres.Int(org_id)))) return db.ExecuteManyTx[model.Report](ctx, txn, statement) } +func ReportsFromReporterPhone(ctx context.Context, txn db.Ex, destination string) ([]model.Report, error) { + statement := table.Report.SELECT( + table.Report.AllColumns, + ).FROM(table.Report). + FROM(table.Report.INNER_JOIN( + tablecomms.TextJob, + tablecomms.TextJob.ReportID.EQ(table.Report.ID), + )).WHERE( + tablecomms.TextJob.ReportID.IS_NOT_NULL().AND( + tablecomms.TextJob.Destination.EQ(postgres.String(destination))).AND( + table.Report.Status.EQ(postgres.NewEnumValue(model.Reportstatustype_Reported.String()))), + ) + return db.ExecuteManyTx[model.Report](ctx, txn, statement) +} diff --git a/db/query/publicreport/subscribe_email.go b/db/query/publicreport/subscribe_email.go new file mode 100644 index 00000000..c73a21d6 --- /dev/null +++ b/db/query/publicreport/subscribe_email.go @@ -0,0 +1,19 @@ +package publicreport + +import ( + "context" + //"time" + + //"github.com/Gleipnir-Technology/bob" + //"github.com/Gleipnir-Technology/jet/postgres" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/table" +) + +func SubscribeEmailInsert(ctx context.Context, txn db.Ex, m model.SubscribeEmail) (model.SubscribeEmail, error) { + statement := table.SubscribeEmail.INSERT(table.SubscribeEmail.AllColumns). + MODEL(m). + RETURNING(table.SubscribeEmail.AllColumns) + return db.ExecuteOneTx[model.SubscribeEmail](ctx, txn, statement) +} diff --git a/db/query/publicreport/subscribe_phone.go b/db/query/publicreport/subscribe_phone.go new file mode 100644 index 00000000..bb6457b7 --- /dev/null +++ b/db/query/publicreport/subscribe_phone.go @@ -0,0 +1,18 @@ +package publicreport + +import ( + "context" + //"time" + + //"github.com/Gleipnir-Technology/jet/postgres" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/table" +) + +func SubscribePhoneInsert(ctx context.Context, txn db.Ex, m model.SubscribePhone) (model.SubscribePhone, error) { + statement := table.SubscribePhone.INSERT(table.SubscribePhone.AllColumns). + MODEL(m). + RETURNING(table.SubscribePhone.AllColumns) + return db.ExecuteOneTx[model.SubscribePhone](ctx, txn, statement) +} diff --git a/platform/background/background.go b/platform/background/background.go index d43cd3a6..0ae832ff 100644 --- a/platform/background/background.go +++ b/platform/background/background.go @@ -36,8 +36,8 @@ func NewLabelStudioAudioCreate(ctx context.Context, txn bob.Executor, note_audio func NewTextRespond(ctx context.Context, txn bob.Executor, text_id int32) error { return newJob(ctx, txn, enums.JobtypeTextRespond, text_id) } -func NewTextSend(ctx context.Context, txn bob.Executor, job_id int32) error { - return newJob(ctx, txn, enums.JobtypeTextSend, job_id) +func NewTextSend(ctx context.Context, txn db.Ex, job_id int32) error { + return newJob2(ctx, txn, model.Jobtype_TextSend, job_id) } func newJob(ctx context.Context, txn bob.Executor, t enums.Jobtype, id int32) error { _, err := models.Jobs.Insert(&models.JobSetter{ diff --git a/platform/communication.go b/platform/communication.go index 629891fa..788d58f2 100644 --- a/platform/communication.go +++ b/platform/communication.go @@ -53,6 +53,7 @@ func CommunicationRelatedRecords(ctx context.Context, user User, comm *modelpubl // * phone number // * email // * name + txn := db.PGInstance.PGXPool result := make([]RelatedRecord, 0) if comm.SourceEmailLogID != nil { email_log, err := querycomms.EmailLogFromID(ctx, int64(*comm.SourceEmailLogID)) @@ -71,7 +72,7 @@ func CommunicationRelatedRecords(ctx context.Context, user User, comm *modelpubl }) } } else if comm.SourceTextLogID != nil { - text_log, err := querycomms.TextLogFromID(ctx, int64(*comm.SourceTextLogID)) + text_log, err := querycomms.TextLogFromID(ctx, txn, int64(*comm.SourceTextLogID)) if err != nil { return result, fmt.Errorf("text log from ID: %w", err) } @@ -87,7 +88,7 @@ func CommunicationRelatedRecords(ctx context.Context, user User, comm *modelpubl }) } } else if comm.SourceReportID != nil { - report, err := querypublicreport.ReportFromID(ctx, int64(*comm.SourceReportID)) + report, err := querypublicreport.ReportFromID(ctx, txn, int64(*comm.SourceReportID)) if err != nil { return result, fmt.Errorf("report from ID: %w", err) } @@ -123,10 +124,12 @@ func CommunicationRelatedRecords(ctx context.Context, user User, comm *modelpubl return result, nil } func CommunicationsForOrganization(ctx context.Context, org_id int64) ([]modelpublic.Communication, error) { - return querypublic.CommunicationsFromOrganization(ctx, org_id) + txn := db.PGInstance.PGXPool + return querypublic.CommunicationsFromOrganization(ctx, txn, org_id) } func CommunicationFromID(ctx context.Context, user User, comm_id int64) (*modelpublic.Communication, error) { - comm, err := querypublic.CommunicationFromID(ctx, comm_id) + txn := db.PGInstance.PGXPool + comm, err := querypublic.CommunicationFromID(ctx, txn, comm_id) if err != nil { return nil, err } diff --git a/platform/csv/pool.go b/platform/csv/pool.go index dafc3f5d..e95d6f56 100644 --- a/platform/csv/pool.go +++ b/platform/csv/pool.go @@ -13,10 +13,10 @@ import ( "github.com/Gleipnir-Technology/bob/dialect/psql" "github.com/Gleipnir-Technology/bob/dialect/psql/um" "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/lint" "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/Gleipnir-Technology/nidus-sync/lint" "github.com/Gleipnir-Technology/nidus-sync/platform/file" "github.com/Gleipnir-Technology/nidus-sync/platform/geocode" "github.com/Gleipnir-Technology/nidus-sync/platform/geom" @@ -260,8 +260,8 @@ func parseCSVPoollist(ctx context.Context, txn bob.Tx, f *models.FileuploadFile, parts := strings.SplitN(col, " ", 2) if len(parts) != 2 { lint.LogOnErrCtx(func(ctx context.Context) error { - return addError(ctx, txn, c, int32(line_number), int32(i), fmt.Sprintf("'%s' is not a house number and street. It needs to be in the form '123 main'", col)) - }, ctx, "add address parse error") + return addError(ctx, txn, c, int32(line_number), int32(i), fmt.Sprintf("'%s' is not a house number and street. It needs to be in the form '123 main'", col)) + }, ctx, "add address parse error") continue } setter.AddressNumber = omit.From(parts[0]) @@ -277,8 +277,8 @@ func parseCSVPoollist(ctx context.Context, txn bob.Tx, f *models.FileuploadFile, err := condition.Scan(col_translated) if err != nil { lint.LogOnErrCtx(func(ctx context.Context) error { - return addError(ctx, txn, c, int32(line_number), int32(i), fmt.Sprintf("'%s' is not a pool condition that we recognize. It should be one of %s", col, poolConditionValidValues())) - }, ctx, "add pool condition error") + return addError(ctx, txn, c, int32(line_number), int32(i), fmt.Sprintf("'%s' is not a pool condition that we recognize. It should be one of %s", col, poolConditionValidValues())) + }, ctx, "add pool condition error") setter.Condition = omit.From(enums.PoolconditiontypeUnknown) continue } @@ -291,13 +291,8 @@ func parseCSVPoollist(ctx context.Context, txn bob.Tx, f *models.FileuploadFile, phone, err := text.ParsePhoneNumber(col) if err != nil { lint.LogOnErrCtx(func(ctx context.Context) error { - return addError(ctx, txn, c, int32(line_number), int32(i), fmt.Sprintf("'%s' is not a phone number that we recognize. Ideally it should be of the form '+12223334444'", col)) - }, ctx, "add phone number error") - continue - } - err = text.EnsureInDB(ctx, txn, *phone) - if err != nil { - log.Error().Err(err).Str("phone", col).Msg("ensure in DB failure") + return addError(ctx, txn, c, int32(line_number), int32(i), fmt.Sprintf("'%s' is not a phone number that we recognize. Ideally it should be of the form '+12223334444'", col)) + }, ctx, "add phone number error") continue } setter.PropertyOwnerPhoneE164 = omitnull.From(phone.PhoneString()) @@ -318,11 +313,6 @@ func parseCSVPoollist(ctx context.Context, txn bob.Tx, f *models.FileuploadFile, }, ctx, "add phone number error") continue } - err = text.EnsureInDB(ctx, txn, *phone) - if err != nil { - log.Error().Err(err).Str("phone", col).Msg("ensure in DB failure") - continue - } setter.ResidentPhoneE164 = omitnull.From(phone.PhoneString()) case headerPoolTag: tags[header_names[i]] = col diff --git a/platform/email/email.go b/platform/email/email.go index 4f9b5e1d..d016c348 100644 --- a/platform/email/email.go +++ b/platform/email/email.go @@ -13,6 +13,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" + modelcomms "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/platform/background" "github.com/aarondl/opt/omit" @@ -20,7 +21,7 @@ import ( "github.com/rs/zerolog/log" ) -func EnsureInDB(ctx context.Context, destination string) (err error) { +func EnsureInDB(ctx context.Context, txn db.Ex, contact modelcomms.Contact, destination string) (err error) { _, err = models.FindCommsEmailContact(ctx, db.PGInstance.BobDB, destination) if err != nil { // doesn't exist diff --git a/platform/email/initial.go b/platform/email/initial.go index 9cba8eba..c546998b 100644 --- a/platform/email/initial.go +++ b/platform/email/initial.go @@ -6,12 +6,13 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" + modelcomms "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model" "github.com/Gleipnir-Technology/nidus-sync/db/models" //"github.com/rs/zerolog/log" ) -func maybeSendInitialEmail(ctx context.Context, destination string) error { - err := EnsureInDB(ctx, destination) +func maybeSendInitialEmail(ctx context.Context, txn db.Ex, contact modelcomms.Contact, destination string) error { + err := EnsureInDB(ctx, txn, contact, destination) if err != nil { return fmt.Errorf("Failed to add email recipient to database: %w", err) } diff --git a/platform/email/report_notification_confirmation.go b/platform/email/report_notification_confirmation.go index 7578e148..081d2310 100644 --- a/platform/email/report_notification_confirmation.go +++ b/platform/email/report_notification_confirmation.go @@ -5,11 +5,13 @@ import ( "fmt" "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/Gleipnir-Technology/nidus-sync/db" + modelcomms "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model" //"github.com/rs/zerolog/log" ) -func SendReportConfirmation(ctx context.Context, destination, report_id string) error { - err := maybeSendInitialEmail(ctx, destination) +func SendReportConfirmation(ctx context.Context, txn db.Ex, contact modelcomms.Contact, destination, report_id string) error { + err := maybeSendInitialEmail(ctx, txn, contact, destination) if err != nil { return fmt.Errorf("Failed to handle initial email: %w", err) } @@ -25,5 +27,3 @@ func SendReportConfirmation(ctx context.Context, destination, report_id string) subject := fmt.Sprintf("Mosquito Report Submission - %s", report_id_str) return sendEmailBegin(ctx, config.ForwardEmailRMOAddress, destination, templateReportNotificationConfirmationID, subject, data) } - - diff --git a/platform/publicreport.go b/platform/publicreport.go index 8321c1b2..9e90cbab 100644 --- a/platform/publicreport.go +++ b/platform/publicreport.go @@ -11,13 +11,15 @@ import ( "github.com/Gleipnir-Technology/jet/postgres" "github.com/Gleipnir-Technology/nidus-sync/db" - "github.com/Gleipnir-Technology/nidus-sync/lint" + modelcomms "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model" "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/model" tablepublic "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/table" modelpublicreport "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model" tablepublicreport "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/table" + querycomms "github.com/Gleipnir-Technology/nidus-sync/db/query/comms" querypublic "github.com/Gleipnir-Technology/nidus-sync/db/query/public" querypublicreport "github.com/Gleipnir-Technology/nidus-sync/db/query/publicreport" + "github.com/Gleipnir-Technology/nidus-sync/lint" "github.com/Gleipnir-Technology/nidus-sync/platform/email" "github.com/Gleipnir-Technology/nidus-sync/platform/event" "github.com/Gleipnir-Technology/nidus-sync/platform/geocode" @@ -106,7 +108,7 @@ func PublicReportInvalid(ctx context.Context, user User, public_id string) error } func PublicReportMessageCreate(ctx context.Context, user User, public_id, message string) (message_id *int32, err error) { - txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil) + txn, err := db.BeginTxn(ctx) if err != nil { return nil, fmt.Errorf("create txn: %w", err) } @@ -148,8 +150,7 @@ func PublicReportMessageCreate(ctx context.Context, user User, public_id, messag return nil, errors.New("no contact methods available") } } -func PublicReportUpdateCompliance(ctx context.Context, public_id string, report_updates querypublicreport.ReportUpdater, compliance_updates querypublicreport.ComplianceUpdater, address *types.Address, location *types.Location) error { - //txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil) +func PublicReportUpdateCompliance(ctx context.Context, public_id string, report_updates querypublicreport.ReportUpdater, compliance_updates querypublicreport.ComplianceUpdater, address *types.Address, location *types.Location, reporter *types.Contact) error { txn, err := db.BeginTxn(ctx) if err != nil { return fmt.Errorf("create txn: %w", err) @@ -159,7 +160,6 @@ func PublicReportUpdateCompliance(ctx context.Context, public_id string, report_ if err != nil { return fmt.Errorf("query report existence: %w", err) } - //compliance, err := models.FindPublicreportCompliance(ctx, txn, report.ID) compliance, err := querypublicreport.ComplianceFromID(ctx, txn, int64(report.ID)) if err != nil { return fmt.Errorf("find compliance %d: %w", report.ID, err) @@ -224,6 +224,26 @@ func PublicReportUpdateCompliance(ctx context.Context, public_id string, report_ return fmt.Errorf("update location: %w", err) } } + if reporter != nil { + if report.ReporterContactID == nil { + contact := modelcomms.Contact{ + Created: time.Now(), + Name: reporter.Name, + OrganizationID: report.OrganizationID, + } + _, err = querycomms.ContactInsert(ctx, txn, contact) + if err != nil { + return fmt.Errorf("insert contact: %w", err) + } + } else if reporter.Name != "" { + err = querycomms.ContactUpdateName(ctx, txn, int64(*report.ReporterContactID), reporter.Name) + if err != nil { + return fmt.Errorf("update contact: %w", err) + } + } else { + log.Warn().Int32("report.id", report.ID).Msg("not sure what to do with empty reporter name") + } + } if err := txn.Commit(ctx); err != nil { return fmt.Errorf("commit: %w", err) } diff --git a/platform/publicreport/notification.go b/platform/publicreport/notification.go new file mode 100644 index 00000000..3dd73b5c --- /dev/null +++ b/platform/publicreport/notification.go @@ -0,0 +1,105 @@ +package publicreport + +import ( + "context" + "fmt" + "time" + + "github.com/Gleipnir-Technology/nidus-sync/db" + modelcomms "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model" + modelpublic "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/model" + modelpublicreport "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model" + querycomms "github.com/Gleipnir-Technology/nidus-sync/db/query/comms" + querypublic "github.com/Gleipnir-Technology/nidus-sync/db/query/public" + querypublicreport "github.com/Gleipnir-Technology/nidus-sync/db/query/publicreport" + "github.com/Gleipnir-Technology/nidus-sync/platform/email" + "github.com/Gleipnir-Technology/nidus-sync/platform/text" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" + //"github.com/rs/zerolog/log" +) + +func DistrictForReport(ctx context.Context, report_id string) (modelpublic.Organization, error) { + report, err := querypublicreport.ReportFromPublicID(ctx, db.PGInstance.PGXPool, report_id) + if err != nil { + return modelpublic.Organization{}, fmt.Errorf("Failed to find report %s: %w", report_id, err) + } + result, e := querypublic.OrganizationFromID(ctx, db.PGInstance.PGXPool, int64(report.OrganizationID)) + if e != nil { + return modelpublic.Organization{}, fmt.Errorf("Failed to load organization %d: %w", report.OrganizationID, e) + } + return result, nil +} + +func RegisterNotificationEmail(ctx context.Context, txn db.Ex, report modelpublicreport.Report, contact modelcomms.Contact, destination string) error { + err := email.EnsureInDB(ctx, txn, contact, destination) + if err != nil { + return fmt.Errorf("Failed to ensure phone is in DB: %w", err) + } + _, err = querypublicreport.NotifyEmailCreate(ctx, txn, report.ID, destination) + if err != nil { + return err + } + return email.SendReportConfirmation(ctx, txn, contact, destination, report.PublicID) +} + +func RegisterNotificationPhone(ctx context.Context, txn db.Ex, report modelpublicreport.Report, contact modelcomms.Contact, phone types.E164) error { + err := text.EnsureInDB(ctx, txn, contact, phone) + if err != nil { + return fmt.Errorf("Failed to ensure phone is in DB: %w", err) + } + _, err = querypublicreport.NotifyPhoneCreate(ctx, txn, report.ID, phone.PhoneString()) + if err != nil { + return err + } + return text.ReportSubscriptionConfirmationText(ctx, db.PGInstance.PGXPool, phone, report.PublicID) +} + +func RegisterSubscriptionEmail(ctx context.Context, txn db.Ex, contact modelcomms.Contact, destination string) error { + _, err := querypublicreport.SubscribeEmailInsert(ctx, txn, modelpublicreport.SubscribeEmail{ + Created: time.Now(), + Deleted: nil, + EmailAddress: destination, + }) + if err != nil { + return fmt.Errorf("Failed to save new subscription email row: %w", err) + } + + return nil +} +func RegisterSubscriptionPhone(ctx context.Context, txn db.Ex, contact modelcomms.Contact, phone types.E164) error { + _, err := querypublicreport.SubscribePhoneInsert(ctx, txn, modelpublicreport.SubscribePhone{ + Created: time.Now(), + Deleted: nil, + PhoneE164: phone.PhoneString(), + }) + if err != nil { + return fmt.Errorf("Failed to save new subscription phone row: %w", err) + } + return nil +} + +func SaveReporter(ctx context.Context, txn db.Ex, report modelpublicreport.Report, name string) (contact modelcomms.Contact, err error) { + if report.ReporterContactID == nil { + contact = modelcomms.Contact{ + Created: time.Now(), + Name: name, + OrganizationID: report.OrganizationID, + } + contact, err = querycomms.ContactInsert(ctx, txn, contact) + if err != nil { + return contact, fmt.Errorf("contact insert: %w", err) + } + } else { + contact, err = querycomms.ContactFromID(ctx, txn, int64(*report.ReporterContactID)) + if err != nil { + return contact, fmt.Errorf("contact query: %w", err) + } + if name != "" && contact.Name != name { + err = querycomms.ContactUpdateName(ctx, txn, int64(contact.ID), name) + if err != nil { + return contact, fmt.Errorf("contact update name: %w", err) + } + } + } + return contact, nil +} diff --git a/platform/publicreport/report.go b/platform/publicreport/report.go index 7af42b39..704ff68a 100644 --- a/platform/publicreport/report.go +++ b/platform/publicreport/report.go @@ -148,7 +148,7 @@ func reportQueryToRows(ctx context.Context, reports []modelpublicreport.Report, Email: &row.ReporterEmail, HasEmail: row.ReporterEmail != "", HasPhone: row.ReporterPhone != "", - Name: &row.ReporterName, + Name: row.ReporterName, Phone: &row.ReporterPhone, }, Status: row.Status.String(), diff --git a/platform/publicreport_notification.go b/platform/publicreport_notification.go index f60dfb38..b1509a92 100644 --- a/platform/publicreport_notification.go +++ b/platform/publicreport_notification.go @@ -5,9 +5,9 @@ import ( "fmt" "github.com/Gleipnir-Technology/nidus-sync/db" + querypublicreport "github.com/Gleipnir-Technology/nidus-sync/db/query/publicreport" "github.com/Gleipnir-Technology/nidus-sync/lint" - "github.com/Gleipnir-Technology/nidus-sync/db/models" - "github.com/Gleipnir-Technology/nidus-sync/platform/report" + "github.com/Gleipnir-Technology/nidus-sync/platform/publicreport" "github.com/Gleipnir-Technology/nidus-sync/platform/types" //"github.com/rs/zerolog/log" ) @@ -23,31 +23,33 @@ type PublicreportNotification struct { } func PublicreportNotificationCreate(ctx context.Context, pn PublicreportNotification) error { - txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil) + txn, err := db.BeginTxn(ctx) if err != nil { return fmt.Errorf("begin txn: %w", err) } defer lint.LogOnErrRollback(txn.Rollback, ctx, "rollback") - rep, err := models.PublicreportReports.Query( - models.SelectWhere.PublicreportReports.PublicID.EQ(pn.ReportID), - ).One(ctx, db.PGInstance.BobDB) + + report, err := querypublicreport.ReportFromPublicID(ctx, txn, pn.ReportID) if err != nil { return fmt.Errorf("find report '%s': %w", pn.ReportID, err) } + if report == nil { + return fmt.Errorf("no such report '%s'", pn.ReportID) + } - err = report.SaveReporter(ctx, txn, pn.ReportID, pn.Name, pn.Email, pn.Phone, pn.Consent) + contact, err := publicreport.SaveReporter(ctx, txn, *report, pn.Name) if err != nil { return fmt.Errorf("save reporter: %w", err) } if pn.Email != "" { if pn.Subscription { - err = report.RegisterSubscriptionEmail(ctx, txn, pn.Email) + err = publicreport.RegisterSubscriptionEmail(ctx, txn, contact, pn.Email) if err != nil { return fmt.Errorf("register subscription email: %w", err) } } if pn.Notification { - err = report.RegisterNotificationEmail(ctx, txn, pn.ReportID, pn.Email) + err = publicreport.RegisterNotificationEmail(ctx, txn, *report, contact, pn.Email) if err != nil { return fmt.Errorf("register notification email: %w", err) } @@ -55,13 +57,13 @@ func PublicreportNotificationCreate(ctx context.Context, pn PublicreportNotifica } if pn.Phone != nil { if pn.Subscription { - err = report.RegisterSubscriptionPhone(ctx, txn, *pn.Phone) + err = publicreport.RegisterSubscriptionPhone(ctx, txn, contact, *pn.Phone) if err != nil { return fmt.Errorf("register subscription phone: %w", err) } } if pn.Notification { - err = report.RegisterNotificationPhone(ctx, txn, pn.ReportID, *pn.Phone) + err = publicreport.RegisterNotificationPhone(ctx, txn, *report, contact, *pn.Phone) if err != nil { return fmt.Errorf("register notification phone: %w", err) } @@ -70,6 +72,6 @@ func PublicreportNotificationCreate(ctx context.Context, pn PublicreportNotifica if err := txn.Commit(ctx); err != nil { return fmt.Errorf("commit: %w", err) } - PublicReportReporterUpdated(ctx, rep.OrganizationID, pn.ReportID) + PublicReportReporterUpdated(ctx, report.OrganizationID, pn.ReportID) return nil } diff --git a/platform/report/notification.go b/platform/report/notification.go deleted file mode 100644 index 4da5c053..00000000 --- a/platform/report/notification.go +++ /dev/null @@ -1,197 +0,0 @@ -package report - -import ( - "context" - "fmt" - "time" - - "github.com/Gleipnir-Technology/bob" - "github.com/Gleipnir-Technology/nidus-sync/db" - "github.com/Gleipnir-Technology/nidus-sync/lint" - "github.com/Gleipnir-Technology/nidus-sync/db/models" - "github.com/Gleipnir-Technology/nidus-sync/platform/email" - "github.com/Gleipnir-Technology/nidus-sync/platform/text" - "github.com/Gleipnir-Technology/nidus-sync/platform/types" - "github.com/aarondl/opt/omit" - "github.com/aarondl/opt/omitnull" - //"github.com/rs/zerolog/log" -) - -func DistrictForReport(ctx context.Context, report_id string) (*models.Organization, error) { - report, err := reportByPublicID(ctx, db.PGInstance.BobDB, report_id) - if err != nil { - return nil, fmt.Errorf("Failed to find report %s: %w", report_id, err) - } - result, e := models.FindOrganization(ctx, db.PGInstance.BobDB, report.OrganizationID) - if e != nil { - return nil, fmt.Errorf("Failed to load organization %d: %w", report.OrganizationID, e) - } - return result, nil -} - -func RegisterNotificationEmail(ctx context.Context, txn bob.Executor, report_id string, destination string) error { - report, e := reportByPublicID(ctx, db.PGInstance.BobDB, report_id) - if e != nil { - return fmt.Errorf("Failed to find report: %w", e) - } - e = email.EnsureInDB(ctx, destination) - if e != nil { - return fmt.Errorf("Failed to ensure phone is in DB: %w", e) - } - err := addNotificationEmail(ctx, txn, report, destination) - if err != nil { - return err - } - lint.LogOnErrCtx(func(ctx context.Context) error { - return email.SendReportConfirmation(ctx, destination, report_id) - }, ctx, "send report confirmation") - return nil -} - -func RegisterNotificationPhone(ctx context.Context, txn bob.Executor, report_id string, phone types.E164) error { - report, e := reportByPublicID(ctx, db.PGInstance.BobDB, report_id) - if e != nil { - return fmt.Errorf("Failed to find report: %w", e) - } - e = text.EnsureInDB(ctx, db.PGInstance.BobDB, phone) - if e != nil { - return fmt.Errorf("Failed to ensure phone is in DB: %w", e) - } - err := addNotificationPhone(ctx, txn, report, phone) - if err != nil { - return err - } - lint.LogOnErrCtx(func(ctx context.Context) error { - return text.ReportSubscriptionConfirmationText(ctx, db.PGInstance.BobDB, phone, report.PublicID) - }, ctx, "report subscription confirmation text") - return nil -} - -func RegisterSubscriptionEmail(ctx context.Context, txn bob.Executor, destination string) error { - e := email.EnsureInDB(ctx, destination) - if e != nil { - return fmt.Errorf("Failed to ensure email is in DB: %w", e) - } - setter := models.PublicreportSubscribeEmailSetter{ - Created: omit.From(time.Now()), - Deleted: omitnull.FromPtr[time.Time](nil), - //DistrictID: omit.FromPtr[int32](nil), - EmailAddress: omit.From(destination), - } - _, err := models.PublicreportSubscribeEmails.Insert(&setter).Exec(ctx, txn) - if err != nil { - return fmt.Errorf("Failed to save new subscription email row: %w", err) - } - - return nil -} -func RegisterSubscriptionPhone(ctx context.Context, txn bob.Executor, phone types.E164) error { - e := text.EnsureInDB(ctx, db.PGInstance.BobDB, phone) - if e != nil { - return fmt.Errorf("Failed to ensure phone is in DB: %w", e) - } - setter := models.PublicreportSubscribePhoneSetter{ - Created: omit.From(time.Now()), - Deleted: omitnull.FromPtr[time.Time](nil), - //DistrictID: omitnull.FromPtr[int32](nil), - PhoneE164: omit.From(phone.PhoneString()), - } - _, err := models.PublicreportSubscribePhones.Insert(&setter).Exec(ctx, txn) - if err != nil { - return fmt.Errorf("Failed to save new subscription phone row: %w", err) - } - return nil -} - -func SaveReporter(ctx context.Context, txn bob.Executor, report_id string, name string, email string, phone *types.E164, has_consent bool) error { - report, e := reportByPublicID(ctx, db.PGInstance.BobDB, report_id) - if e != nil { - return fmt.Errorf("Failed to find report: %w", e) - } - if name != "" { - err := updateReporterName(ctx, txn, report, name) - if err != nil { - return err - } - } - if phone != nil { - err := updateReporterPhone(ctx, txn, report, *phone) - if err != nil { - return err - } - } - if email != "" { - err := updateReporterEmail(ctx, txn, report, email) - if err != nil { - return err - } - } - err := updateReporterConsent(ctx, txn, report, has_consent) - if err != nil { - return err - } - return nil -} -func reportByPublicID(ctx context.Context, txn bob.Executor, public_id string) (*models.PublicreportReport, error) { - return models.PublicreportReports.Query( - models.SelectWhere.PublicreportReports.PublicID.EQ(public_id), - ).One(ctx, txn) -} -func addNotificationEmail(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, email string) error { - setter := models.PublicreportNotifyEmailSetter{ - Created: omit.From(time.Now()), - Deleted: omitnull.FromPtr[time.Time](nil), - EmailAddress: omit.From(email), - ReportID: omit.From(report.ID), - } - _, err := models.PublicreportNotifyEmails.Insert(&setter).Exec(ctx, txn) - if err != nil { - return fmt.Errorf("Failed to save new notification email row: %w", err) - } - return nil -} -func addNotificationPhone(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, phone types.E164) error { - var err error - setter := models.PublicreportNotifyPhoneSetter{ - Created: omit.From(time.Now()), - Deleted: omitnull.FromPtr[time.Time](nil), - PhoneE164: omit.From(phone.PhoneString()), - ReportID: omit.From(report.ID), - } - _, err = models.PublicreportNotifyPhones.Insert(&setter).Exec(ctx, txn) - if err != nil { - return fmt.Errorf("Failed to save new notification phone row: %w", err) - } - return nil -} -func updateReporterConsent(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, has_consent bool) error { - return updateReportCol(ctx, txn, report, &models.PublicreportReportSetter{ - ReporterContactConsent: omitnull.From(has_consent), - }) -} -func updateReporterEmail(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, email string) error { - return updateReportCol(ctx, txn, report, &models.PublicreportReportSetter{ - ReporterEmail: omit.From(email), - }) -} -func updateReporterName(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, name string) error { - return updateReportCol(ctx, txn, report, &models.PublicreportReportSetter{ - ReporterName: omit.From(name), - }) -} -func updateReportCol(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, setter *models.PublicreportReportSetter) error { - err := report.Update(ctx, txn, setter) - if err != nil { - return fmt.Errorf("Failed to update nuisance report in the database: %w", err) - } - return nil -} -func updateReporterPhone(ctx context.Context, txn bob.Executor, report *models.PublicreportReport, phone types.E164) error { - err := report.Update(ctx, txn, &models.PublicreportReportSetter{ - ReporterPhone: omit.From(phone.PhoneString()), - }) - if err != nil { - return fmt.Errorf("Failed to update report: %w", err) - } - return nil -} diff --git a/platform/report/some_report.go b/platform/report/some_report.go deleted file mode 100644 index 2cbd2d77..00000000 --- a/platform/report/some_report.go +++ /dev/null @@ -1,37 +0,0 @@ -package report - -import ( - "context" - //"crypto/rand" - //"fmt" - //"math/big" - //"strconv" - //"strings" - //"time" - - //"github.com/aarondl/opt/omit" - //"github.com/aarondl/opt/omitnull" - "github.com/Gleipnir-Technology/bob" - //"github.com/Gleipnir-Technology/bob/dialect/psql" - //"github.com/Gleipnir-Technology/bob/dialect/psql/sm" - //"github.com/Gleipnir-Technology/bob/dialect/psql/um" - //"github.com/Gleipnir-Technology/nidus-sync/background" - //"github.com/Gleipnir-Technology/nidus-sync/db" - //"github.com/Gleipnir-Technology/nidus-sync/db/models" - //"github.com/Gleipnir-Technology/nidus-sync/db/sql" - "github.com/Gleipnir-Technology/nidus-sync/platform/types" - //"github.com/rs/zerolog/log" - //"github.com/stephenafamo/scan" -) - -type SomeReport interface { - addNotificationEmail(context.Context, bob.Executor, string) error - addNotificationPhone(context.Context, bob.Executor, types.E164) error - districtID(context.Context) *int32 - updateReporterConsent(context.Context, bob.Executor, bool) error - updateReporterEmail(context.Context, bob.Executor, string) error - updateReporterName(context.Context, bob.Executor, string) error - updateReporterPhone(context.Context, bob.Executor, types.E164) error - PublicReportID() string - reportID() int32 -} diff --git a/platform/text/job.go b/platform/text/job.go index 476932c5..f07fb600 100644 --- a/platform/text/job.go +++ b/platform/text/job.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/Gleipnir-Technology/nidus-sync/db" - "github.com/Gleipnir-Technology/nidus-sync/db/models" + querycomms "github.com/Gleipnir-Technology/nidus-sync/db/query/comms" "github.com/Gleipnir-Technology/nidus-sync/platform/types" //"github.com/rs/zerolog/log" ) @@ -14,8 +14,8 @@ func JobRespond(ctx context.Context, log_id int32) error { return respondText(ctx, log_id) } func JobSend(ctx context.Context, job_id int32) error { - bxn := db.PGInstance.BobDB - job, err := models.FindCommsTextJob(ctx, bxn, job_id) + bxn := db.PGInstance.PGXPool + job, err := querycomms.TextJobFromID(ctx, bxn, int64(job_id)) if err != nil { return fmt.Errorf("find text: %w", err) } @@ -23,11 +23,8 @@ func JobSend(ctx context.Context, job_id int32) error { return sendTextComplete(ctx, job) } func handleWaitingTextJobs(ctx context.Context, dst types.E164) error { - bxn := db.PGInstance.BobDB - jobs, err := models.CommsTextJobs.Query( - models.SelectWhere.CommsTextJobs.Destination.EQ(dst.PhoneString()), - models.SelectWhere.CommsTextJobs.Completed.IsNull(), - ).All(ctx, bxn) + bxn := db.PGInstance.PGXPool + jobs, err := querycomms.TextJobsWaitingFromDestination(ctx, bxn, dst.PhoneString()) if err != nil { return fmt.Errorf("query jobs: %w", err) } diff --git a/platform/text/llm.go b/platform/text/llm.go index 6acf58ec..313d95bb 100644 --- a/platform/text/llm.go +++ b/platform/text/llm.go @@ -6,12 +6,10 @@ import ( "fmt" "github.com/rs/zerolog/log" - "github.com/Gleipnir-Technology/bob" "github.com/Gleipnir-Technology/bob/dialect/psql" "github.com/Gleipnir-Technology/bob/dialect/psql/um" "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/Gleipnir-Technology/nidus-sync/db/sql" "github.com/Gleipnir-Technology/nidus-sync/llm" @@ -34,7 +32,7 @@ func generateNextMessage(ctx context.Context, history []llm.Message, customer_ph } return llm.GenerateNextMessage(ctx, history, _handle_report_status, _handle_contact_district, _handle_contact_supervisor) } -func handleResetConversation(ctx context.Context, txn bob.Executor, src types.E164) error { +func handleResetConversation(ctx context.Context, txn db.Ex, src types.E164) error { err := wipeLLMMemory(ctx, src) sublog := log.With().Str("src", src.PhoneString()).Logger() if err != nil { diff --git a/platform/text/phone_number.go b/platform/text/phone_number.go index 9e261f53..970843b7 100644 --- a/platform/text/phone_number.go +++ b/platform/text/phone_number.go @@ -4,31 +4,28 @@ import ( "context" "fmt" - "github.com/Gleipnir-Technology/bob" - "github.com/Gleipnir-Technology/bob/dialect/psql" - "github.com/Gleipnir-Technology/bob/dialect/psql/im" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" + modelcomms "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model" "github.com/Gleipnir-Technology/nidus-sync/db/models" + querycomms "github.com/Gleipnir-Technology/nidus-sync/db/query/comms" "github.com/Gleipnir-Technology/nidus-sync/platform/types" - "github.com/aarondl/opt/omit" - "github.com/rs/zerolog/log" + //"github.com/rs/zerolog/log" ) -func EnsureInDB(ctx context.Context, txn bob.Executor, dst types.E164) (err error) { - return ensureInDB(ctx, txn, dst.PhoneString()) +func EnsureInDB(ctx context.Context, txn db.Ex, contact modelcomms.Contact, dst types.E164) (err error) { + return ensureInDB(ctx, txn, contact, dst.PhoneString()) } -func ensureInDB(ctx context.Context, txn bob.Executor, destination string) (err error) { - _, err = psql.Insert( - im.Into("comms.phone", "can_sms", "e164", "is_subscribed", "status"), - im.Values( - psql.Arg(true), - psql.Arg(destination), - psql.Arg(false), - psql.Arg("unconfirmed"), - ), - im.OnConflict("e164").DoNothing(), - ).Exec(ctx, txn) +func ensureInDB(ctx context.Context, txn db.Ex, contact modelcomms.Contact, destination string) (err error) { + contact_phone := modelcomms.ContactPhone{ + CanSms: true, + ConfirmedMessageID: nil, + ContactID: contact.ID, + E164: destination, + IsSubscribed: false, + StopMessageID: nil, + } + _, err = querycomms.ContactPhoneInsert(ctx, txn, contact_phone) return err } func phoneStatus(ctx context.Context, src types.E164) (enums.CommsPhonestatustype, error) { @@ -38,17 +35,3 @@ func phoneStatus(ctx context.Context, src types.E164) (enums.CommsPhonestatustyp } return phone.Status, nil } -func setPhoneStatus(ctx context.Context, txn bob.Executor, src types.E164, status enums.CommsPhonestatustype) error { - phone, err := models.FindCommsPhone(ctx, txn, src.PhoneString()) - if err != nil { - return fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err) - } - err = phone.Update(ctx, txn, &models.CommsPhoneSetter{ - Status: omit.From(status), - }) - if err != nil { - return fmt.Errorf("update phone status: %w", err) - } - log.Info().Str("src", src.PhoneString()).Str("status", string(status)).Msg("Set number subscribed") - return nil -} diff --git a/platform/text/report.go b/platform/text/report.go index 2a836d6d..d20bd899 100644 --- a/platform/text/report.go +++ b/platform/text/report.go @@ -4,20 +4,15 @@ import ( "context" "fmt" - "github.com/Gleipnir-Technology/bob" - "github.com/Gleipnir-Technology/bob/dialect/psql" - "github.com/Gleipnir-Technology/bob/dialect/psql/sm" "github.com/Gleipnir-Technology/nidus-sync/db" - "github.com/Gleipnir-Technology/nidus-sync/db/enums" - //"github.com/Gleipnir-Technology/nidus-sync/db/models" + modelcomms "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model" "github.com/Gleipnir-Technology/nidus-sync/platform/types" //"github.com/rs/zerolog/log" - "github.com/stephenafamo/scan" ) // Send a message from a district to a public reporter within the context of the public report -func ReportMessage(ctx context.Context, txn bob.Executor, user_id int32, report_id int32, destination types.E164, content string) (*int32, error) { - job_id, err := sendTextBegin(ctx, txn, &user_id, &report_id, destination, content, enums.CommsTextjobtypeReportMessage) +func ReportMessage(ctx context.Context, txn db.Ex, user_id int32, report_id int32, destination types.E164, content string) (*int32, error) { + job_id, err := sendTextBegin(ctx, txn, &user_id, &report_id, destination, content, modelcomms.Textjobtype_ReportMessage) if err != nil { return nil, fmt.Errorf("Failed to send initial confirmation: %w", err) } @@ -25,43 +20,11 @@ func ReportMessage(ctx context.Context, txn bob.Executor, user_id int32, report_ } // Send a message from the system to a public reporter indicating they are subscribed to updates on the report -func ReportSubscriptionConfirmationText(ctx context.Context, txn bob.Executor, destination types.E164, report_id string) error { +func ReportSubscriptionConfirmationText(ctx context.Context, txn db.Ex, destination types.E164, report_id string) error { content := fmt.Sprintf("Thanks for submitting mosquito report %s. Text for any questions. We'll send you updates as we get them.", report_id) - _, err := sendTextBegin(ctx, txn, nil, nil, destination, content, enums.CommsTextjobtypeReportConfirmation) + _, err := sendTextBegin(ctx, txn, nil, nil, destination, content, modelcomms.Textjobtype_ReportConfirmation) if err != nil { return fmt.Errorf("Failed to send initial confirmation: %w", err) } return err } - -type reportIDs struct { - ID int32 `db:"id"` - PublicID string `db:"public_id"` - OrganizationID int32 `db:"organization_id"` -} - -// Get the list of reports that are still open for a particular text message recipient -// 'still open' is not well-defined throughout the system, but for now we'll go with -// 'not reviewed in any way'. -func reportsForTextRecipient(ctx context.Context, txn bob.Executor, destination types.E164) ([]reportIDs, error) { - rows, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select( - sm.Columns( - "r.id", - "r.public_id", - "r.organization_id", - ), - sm.From("comms.text_job").As("t"), - sm.InnerJoin("publicreport.report").As("r").OnEQ( - psql.Quote("t", "report_id"), - psql.Quote("r", "id"), - ), - sm.Where(psql.Quote("t", "report_id").IsNotNull()), - sm.Where(psql.Quote("t", "destination").EQ(psql.Arg(destination.PhoneString()))), - sm.Where(psql.Quote("r", "status").EQ(psql.Arg(enums.PublicreportReportstatustypeReported))), - ), scan.StructMapper[reportIDs]()) - if err != nil { - return []reportIDs{}, fmt.Errorf("query reports: %w", err) - } - - return rows, nil -} diff --git a/platform/text/send.go b/platform/text/send.go index 3cbbc193..b669c6a6 100644 --- a/platform/text/send.go +++ b/platform/text/send.go @@ -5,50 +5,47 @@ import ( "fmt" "time" - "github.com/Gleipnir-Technology/bob" "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/db/enums" + modelcomms "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model" + modelpublic "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/model" + modelpublicreport "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model" + querycomms "github.com/Gleipnir-Technology/nidus-sync/db/query/comms" + querypublic "github.com/Gleipnir-Technology/nidus-sync/db/query/public" + querypublicreport "github.com/Gleipnir-Technology/nidus-sync/db/query/publicreport" "github.com/Gleipnir-Technology/nidus-sync/lint" - "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/platform/background" "github.com/Gleipnir-Technology/nidus-sync/platform/event" "github.com/Gleipnir-Technology/nidus-sync/platform/types" - "github.com/aarondl/opt/omit" - "github.com/aarondl/opt/omitnull" "github.com/rs/zerolog/log" ) -func ensureInitialText(ctx context.Context, txn bob.Executor, dst types.E164) error { - rows, err := models.CommsTextLogs.Query( - models.SelectWhere.CommsTextLogs.Destination.EQ(dst.PhoneString()), - models.SelectWhere.CommsTextLogs.IsWelcome.EQ(true), - ).All(ctx, txn) +func ensureInitialText(ctx context.Context, txn db.Ex, dst types.E164) error { + logs, err := querycomms.TextLogWelcomeFromDestination(ctx, txn, dst.PhoneString()) if err != nil { return fmt.Errorf("Failed to query text logs: %w", err) } - if len(rows) > 0 { + if len(logs) > 0 { return nil } return sendInitialText(ctx, txn, dst) } -func resendInitialText(ctx context.Context, txn bob.Executor, dst types.E164) error { - phone, err := models.FindCommsPhone(ctx, txn, dst.PhoneString()) +func resendInitialText(ctx context.Context, txn db.Ex, dst types.E164) error { + phone, err := querycomms.ContactPhoneFromE164(ctx, txn, dst.PhoneString()) if err != nil { return fmt.Errorf("Failed to find phone %s: %w", dst, err) } - err = phone.Update(ctx, txn, &models.CommsPhoneSetter{ - Status: omit.From(enums.CommsPhonestatustypeUnconfirmed), - }) + err = querycomms.ContactPhoneUpdateStopMessageID(ctx, txn, phone.E164, nil) if err != nil { return fmt.Errorf("Failed to clear subscription on phone %s: %w", dst, err) } return nil } -func sendInitialText(ctx context.Context, txn bob.Executor, dst types.E164) error { +func sendInitialText(ctx context.Context, txn db.Ex, dst types.E164) error { 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 := sendTextDirect(ctx, txn, enums.CommsTextoriginWebsiteAction, dst.PhoneString(), content, false, true) + _, err := sendTextDirect(ctx, txn, modelcomms.Textorigin_WebsiteAction, dst.PhoneString(), content, false, true) if err != nil { return fmt.Errorf("send text: %w", err) } @@ -57,21 +54,17 @@ func sendInitialText(ctx context.Context, txn bob.Executor, dst types.E164) erro // Begin the process of sending the text message, but only get as far as adding it to // the database, then let the backend finish sending. -func sendTextBegin(ctx context.Context, txn bob.Executor, user_id *int32, report_id *int32, destination types.E164, content string, type_ enums.CommsTextjobtype) (*int32, error) { - err := EnsureInDB(ctx, txn, destination) - if err != nil { - return nil, fmt.Errorf("Failed to ensure text message destination is in the DB: %w", err) - } - job, err := models.CommsTextJobs.Insert(&models.CommsTextJobSetter{ - Content: omit.From(content), - CreatorID: omitnull.FromPtr(user_id), - Created: omit.From(time.Now()), - Destination: omit.From(destination.PhoneString()), +func sendTextBegin(ctx context.Context, txn db.Ex, user_id *int32, report_id *int32, destination types.E164, content string, type_ modelcomms.Textjobtype) (*int32, error) { + job, err := querycomms.TextJobInsert(ctx, txn, modelcomms.TextJob{ + Content: content, + CreatorID: user_id, + Created: time.Now(), + Destination: destination.PhoneString(), //ID: - ReportID: omitnull.FromPtr(report_id), - Source: omit.From(enums.CommsTextjobsourceRmo), - Type: omit.From(type_), - }).One(ctx, txn) + ReportID: report_id, + Source: modelcomms.Textjobsource_Rmo, + Type: type_, + }) if err != nil { return nil, fmt.Errorf("Failed to add delayed text job: %w", err) } @@ -81,12 +74,12 @@ func sendTextBegin(ctx context.Context, txn bob.Executor, user_id *int32, report } return &job.ID, nil } -func sendTextCommandResponse(ctx context.Context, txn bob.Executor, dst types.E164, content string) error { - _, err := sendTextDirect(ctx, txn, enums.CommsTextoriginCommandResponse, dst.PhoneString(), content, false, false) +func sendTextCommandResponse(ctx context.Context, txn db.Ex, dst types.E164, content string) error { + _, err := sendTextDirect(ctx, txn, modelcomms.Textorigin_CommandResponse, dst.PhoneString(), content, false, false) return err } -func sendTextComplete(ctx context.Context, job *models.CommsTextJob) error { - txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil) +func sendTextComplete(ctx context.Context, job modelcomms.TextJob) error { + txn, err := db.BeginTxn(ctx) if err != nil { return fmt.Errorf("begin tx: %w", err) } @@ -95,12 +88,12 @@ func sendTextComplete(ctx context.Context, job *models.CommsTextJob) error { if err != nil { return fmt.Errorf("parse phone: %w", err) } - var origin enums.CommsTextorigin + var origin modelcomms.Textorigin switch job.Type { - case enums.CommsTextjobtypeReportConfirmation: - origin = enums.CommsTextoriginWebsiteAction - case enums.CommsTextjobtypeReportMessage: - origin = enums.CommsTextoriginDistrict + case modelcomms.Textjobtype_ReportConfirmation: + origin = modelcomms.Textorigin_WebsiteAction + case modelcomms.Textjobtype_ReportMessage: + origin = modelcomms.Textorigin_District default: return fmt.Errorf("incomplete switch: %s", string(job.Type)) } @@ -128,37 +121,35 @@ func sendTextComplete(ctx context.Context, job *models.CommsTextJob) error { if err != nil { return fmt.Errorf("send text direct: %w", err) } - err = job.Update(ctx, txn, &models.CommsTextJobSetter{ - Completed: omitnull.From(time.Now()), - }) + err = querycomms.TextJobComplete(ctx, txn, int64(job.ID)) if err != nil { return fmt.Errorf("update job: %w", err) } - if job.ReportID.IsValue() { - creator_id := job.CreatorID.MustGet() - report_id := job.ReportID.MustGet() + if job.ReportID != nil { + creator_id := *job.CreatorID + report_id := *job.ReportID log.Debug().Int32("creator", creator_id).Int32("report_id", report_id).Msg("Creating report entries for text message") - _, err := models.ReportTexts.Insert(&models.ReportTextSetter{ - CreatorID: omit.From(creator_id), - ReportID: omit.From(report_id), - TextLogID: omit.From(text_log.ID), - }).One(ctx, txn) + querypublic.ReportTextInsert(ctx, txn, modelpublic.ReportText{ + CreatorID: creator_id, + ReportID: report_id, + TextLogID: text_log.ID, + }) if err != nil { return fmt.Errorf("insert report_text: %w", err) } - _, err = models.PublicreportReportLogs.Insert(&models.PublicreportReportLogSetter{ - Created: omit.From(time.Now()), - EmailLogID: omitnull.FromPtr[int32](nil), + _, err = querypublicreport.ReportLogInsert(ctx, txn, modelpublicreport.ReportLog{ + Created: time.Now(), + EmailLogID: nil, // ID - ReportID: omit.From(report_id), - TextLogID: omitnull.From(text_log.ID), - Type: omit.From(enums.PublicreportReportlogtypeMessageText), - UserID: omitnull.From(creator_id), - }).One(ctx, txn) + ReportID: report_id, + TextLogID: &text_log.ID, + Type: modelpublicreport.Reportlogtype_MessageText, + UserID: &creator_id, + }) if err != nil { return fmt.Errorf("insert report log: %w", err) } - report, err := models.FindPublicreportReport(ctx, txn, report_id) + report, err := querypublicreport.ReportFromID(ctx, txn, int64(report_id)) if err != nil { return fmt.Errorf("find public report: %w", err) } @@ -174,33 +165,28 @@ func sendTextComplete(ctx context.Context, job *models.CommsTextJob) error { // Send a text message and save the appropriate database records. // Send immediately using the current goroutine -func sendTextDirect(ctx context.Context, txn bob.Executor, origin enums.CommsTextorigin, destination, content string, is_visible_to_llm, is_welcome bool) (*models.CommsTextLog, error) { - text_log, err := models.CommsTextLogs.Insert(&models.CommsTextLogSetter{ - //ID: - Content: omit.From(content), - Created: omit.From(time.Now()), - Destination: omit.From(destination), - IsVisibleToLLM: omit.From(is_visible_to_llm), - IsWelcome: omit.From(is_welcome), - Origin: omit.From(origin), - Source: omit.From(config.PhoneNumberReportStr), - TwilioSid: omitnull.FromPtr[string](nil), - TwilioStatus: omit.From(""), - }).One(ctx, txn) +func sendTextDirect(ctx context.Context, txn db.Ex, origin modelcomms.Textorigin, destination, content string, is_visible_to_llm, is_welcome bool) (modelcomms.TextLog, error) { + text_log, err := querycomms.TextLogInsert(ctx, txn, modelcomms.TextLog{ + Content: content, + Created: time.Now(), + Destination: destination, + IsVisibleToLlm: is_visible_to_llm, + IsWelcome: is_welcome, + Origin: origin, + Source: config.PhoneNumberReportStr, + TwilioSid: nil, + TwilioStatus: "", + }) if err != nil { - return nil, fmt.Errorf("insert text log: %w", err) + return modelcomms.TextLog{}, fmt.Errorf("insert text log: %w", err) } pid, err := text.SendText(ctx, config.VoipMSNumber, destination, content) if err != nil { - return nil, fmt.Errorf("send text: %w", err) + return modelcomms.TextLog{}, fmt.Errorf("send text: %w", err) } - err = text_log.Update(ctx, txn, &models.CommsTextLogSetter{ - TwilioSid: omitnull.From(pid), - TwilioStatus: omit.From("created"), - }) + err = querycomms.TextLogUpdate(ctx, txn, int64(text_log.ID), pid, "created") if err != nil { - return nil, fmt.Errorf("update %w", err) + return modelcomms.TextLog{}, fmt.Errorf("update %w", err) } - return text_log, nil } diff --git a/platform/text/text.go b/platform/text/text.go index 1031600f..f800003c 100644 --- a/platform/text/text.go +++ b/platform/text/text.go @@ -9,8 +9,12 @@ import ( "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/lint" + modelcomms "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/comms/model" + modelpublicreport "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model" "github.com/Gleipnir-Technology/nidus-sync/db/models" + querycomms "github.com/Gleipnir-Technology/nidus-sync/db/query/comms" + querypublicreport "github.com/Gleipnir-Technology/nidus-sync/db/query/publicreport" + "github.com/Gleipnir-Technology/nidus-sync/lint" "github.com/Gleipnir-Technology/nidus-sync/platform/background" "github.com/Gleipnir-Technology/nidus-sync/platform/event" "github.com/Gleipnir-Technology/nidus-sync/platform/types" @@ -68,12 +72,12 @@ func HandleTextMessage(ctx context.Context, source string, destination string, c } func respondText(ctx context.Context, log_id int32) error { - txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil) + txn, err := db.BeginTxn(ctx) if err != nil { return fmt.Errorf("begin tx: %w", err) } defer lint.LogOnErrRollback(txn.Rollback, ctx, "rollback") - l, err := models.FindCommsTextLog(ctx, txn, log_id) + l, err := querycomms.TextLogFromID(ctx, txn, int64(log_id)) if err != nil { return fmt.Errorf("find comms: %w", err) } @@ -82,19 +86,19 @@ func respondText(ctx context.Context, log_id int32) error { return fmt.Errorf("parse source: %w", err) } - status, err := phoneStatus(ctx, *src) + contact_phone, err := querycomms.ContactPhoneFromE164(ctx, txn, src.PhoneString()) if err != nil { - return fmt.Errorf("Failed to get phone status") + return fmt.Errorf("Failed to get contact phone") } body_l := strings.TrimSpace(strings.ToLower(l.Content)) // If the user isn't confirmed for sending regular texts ensure they get a reprompt - if status == enums.CommsPhonestatustypeUnconfirmed { + if contact_phone.ConfirmedMessageID == nil { switch body_l { case "yes": - err = setPhoneStatus(ctx, txn, *src, enums.CommsPhonestatustypeOkToSend) + err = querycomms.ContactPhoneUpdateConfirmedMessageID(ctx, txn, src.PhoneString(), &l.ID) if err != nil { - return fmt.Errorf("set phone status: %w", err) + return fmt.Errorf("set phone confirmed message ID: %w", err) } content := "Thanks, we've confirmed your phone number. You can text STOP at any time if you change your mind" err = sendTextCommandResponse(ctx, txn, *src, content) @@ -118,14 +122,15 @@ func respondText(ctx context.Context, log_id int32) error { } switch body_l { case "stop": + err = querycomms.ContactPhoneUpdateStopMessageID(ctx, txn, src.PhoneString(), &l.ID) + if err != nil { + return fmt.Errorf("set phone stop message ID: %w", err) + } content := "You have successfully been unsubscribed. You will not receive any more messages from this number. Reply START to resubscribe." err = sendTextCommandResponse(ctx, txn, *src, content) if err != nil { log.Error().Err(err).Msg("Failed to send unsubscribe acknowledgement.") } - lint.LogOnErrCtx(func(ctx context.Context) error { - return setPhoneStatus(ctx, txn, *src, enums.CommsPhonestatustypeStopped) - }, ctx, "set phone status") return nil case "reset conversation": err = handleResetConversation(ctx, txn, *src) @@ -140,20 +145,23 @@ func respondText(ctx context.Context, log_id int32) error { return nil } // If we've got an open public report from this phone number then we'll let the district respond - reports, err := reportsForTextRecipient(ctx, txn, *src) + // Get the list of reports that are still open for a particular text message recipient + // 'still open' is not well-defined throughout the system, but for now we'll go with + // 'not reviewed in any way'. + reports, err := querypublicreport.ReportsFromReporterPhone(ctx, txn, src.PhoneString()) if err != nil { return fmt.Errorf("has open report: %w", err) } for _, report := range reports { - _, err = models.PublicreportReportLogs.Insert(&models.PublicreportReportLogSetter{ - Created: omit.From(time.Now()), - EmailLogID: omitnull.FromPtr[int32](nil), + _, err = querypublicreport.ReportLogInsert(ctx, txn, modelpublicreport.ReportLog{ + Created: time.Now(), + EmailLogID: nil, // ID - ReportID: omit.From(report.ID), - TextLogID: omitnull.From(log_id), - Type: omit.From(enums.PublicreportReportlogtypeMessageText), - UserID: omitnull.FromPtr[int32](nil), - }).One(ctx, txn) + ReportID: report.ID, + TextLogID: &log_id, + Type: modelpublicreport.Reportlogtype_MessageText, + UserID: nil, + }) if err != nil { return fmt.Errorf("insert report log: %w", err) } @@ -164,7 +172,8 @@ func respondText(ctx context.Context, log_id int32) error { return nil } // Otherwise let the LLM handle the response - return respondTextLLM(ctx, *src) + //return respondTextLLM(ctx, *src) + return nil } func respondTextLLM(ctx context.Context, src types.E164) error { @@ -177,12 +186,12 @@ func respondTextLLM(ctx context.Context, src types.E164) error { if err != nil { return fmt.Errorf("Failed to generate next message: %w", 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") - _, err = sendTextDirect(ctx, txn, enums.CommsTextoriginLLM, src.PhoneString(), next_message.Content, true, false) + _, err = sendTextDirect(ctx, txn, modelcomms.Textorigin_Llm, src.PhoneString(), next_message.Content, true, false) if err != nil { return fmt.Errorf("Failed to send response text: %w", err) } @@ -201,22 +210,28 @@ func ParsePhoneNumber(input string) (*types.E164, error) { } func StoreSources() error { + var err error ctx := context.TODO() + txn := db.PGInstance.PGXPool + // Magical id 1 is set in migration 00151 + contact, err := querycomms.ContactFromID(ctx, txn, 1) + if err != nil { + return fmt.Errorf("contact from ID 1: %w", err) + } for _, n := range []string{config.PhoneNumberReportStr, config.PhoneNumberSupportStr, config.VoipMSNumber} { - var err error // Deal with Voip.ms not expecting API calls with the prefixed +1 if !strings.HasPrefix(n, "+1") { dest, err := ParsePhoneNumber("+1" + n) if err != nil { return fmt.Errorf("Failed to parse +1'%s' as phone number: %w", n, err) } - err = EnsureInDB(ctx, db.PGInstance.BobDB, *dest) + err = EnsureInDB(ctx, txn, contact, *dest) } else { dest, err := ParsePhoneNumber(n) if err != nil { return fmt.Errorf("Failed to parse '%s' as phone number: %w", n, err) } - err = EnsureInDB(ctx, db.PGInstance.BobDB, *dest) + err = EnsureInDB(ctx, txn, contact, *dest) } if err != nil { return fmt.Errorf("Failed to add number '%s' to DB: %w", n, err) diff --git a/platform/types/contact.go b/platform/types/contact.go index c7b07bef..be045d82 100644 --- a/platform/types/contact.go +++ b/platform/types/contact.go @@ -10,7 +10,7 @@ type Contact struct { Email *string `db:"email" json:"email"` HasEmail bool `json:"has_email"` HasPhone bool `json:"has_phone"` - Name *string `db:"name" json:"name"` + Name string `db:"name" json:"name"` Phone *string `db:"phone" json:"phone"` } diff --git a/platform/types/site.go b/platform/types/site.go index 8ec819ce..ae66811c 100644 --- a/platform/types/site.go +++ b/platform/types/site.go @@ -40,7 +40,7 @@ func SiteFromModel(s *models.Site) Site { Notes: s.Notes, OrganizationID: s.OrganizationID, Owner: Contact{ - Name: &s.OwnerName, + Name: s.OwnerName, Phone: &owner_phone, }, ResidentOwned: resident_owned, diff --git a/resource/communication.go b/resource/communication.go index f47a8f70..58b6edb1 100644 --- a/resource/communication.go +++ b/resource/communication.go @@ -11,7 +11,7 @@ import ( modelpublicreport "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/Gleipnir-Technology/nidus-sync/platform" - //"github.com/rs/zerolog/log" + //"github.com/rs/zerolog/log":q ) type communicationR struct { diff --git a/resource/publicreport_compliance.go b/resource/publicreport_compliance.go index 2a9b9fd8..3d8960ea 100644 --- a/resource/publicreport_compliance.go +++ b/resource/publicreport_compliance.go @@ -78,12 +78,8 @@ func (res *complianceR) Create(ctx context.Context, r *http.Request, n publicRep MapZoom: float32(0.0), //OrganizationID: , //PublicID: - ReporterEmail: "", - ReporterName: "", - ReporterPhone: "", - ReporterPhoneCanSms: true, - ReportType: modelpublicreport.Reporttype_Compliance, - Status: modelpublicreport.Reportstatustype_Reported, + ReportType: modelpublicreport.Reporttype_Compliance, + Status: modelpublicreport.Reportstatustype_Reported, } setter_compliance := modelpublicreport.Compliance{ AccessInstructions: "", @@ -166,28 +162,10 @@ func (res *complianceR) Update(ctx context.Context, r *http.Request, prf publicR report_updater.Set(tablepublicreport.Report.LatlngAccuracyValue) } } + var reporter *types.Contact if prf.Reporter.IsValue() { - reporter := prf.Reporter.MustGet() - if reporter.Email != nil { - //report_setter.ReporterEmail = omit.From(*reporter.Email) - report_updater.Model.ReporterEmail = *reporter.Email - report_updater.Set(tablepublicreport.Report.ReporterEmail) - } - if reporter.Name != nil { - //report_setter.ReporterName = omit.From(*reporter.Name) - report_updater.Model.ReporterName = *reporter.Name - report_updater.Set(tablepublicreport.Report.ReporterName) - } - if reporter.Phone != nil { - //report_setter.ReporterPhone = omit.From(*reporter.Phone) - report_updater.Model.ReporterPhone = *reporter.Phone - report_updater.Set(tablepublicreport.Report.ReporterPhone) - } - if reporter.CanSMS != nil { - //report_setter.ReporterPhoneCanSMS = omit.FromPtr(reporter.CanSMS) - report_updater.Model.ReporterPhoneCanSms = *reporter.CanSMS - report_updater.Set(tablepublicreport.Report.ReporterPhoneCanSms) - } + l := prf.Reporter.MustGet() + reporter = &l } var address *types.Address if prf.Address.IsValue() { @@ -244,7 +222,7 @@ func (res *complianceR) Update(ctx context.Context, r *http.Request, prf publicR compliance_updater.Model.Submitted = &now compliance_updater.Set(tablepublicreport.Compliance.Submitted) } - err = platform.PublicReportUpdateCompliance(ctx, public_id, report_updater, compliance_updater, address, location) + err = platform.PublicReportUpdateCompliance(ctx, public_id, report_updater, compliance_updater, address, location, reporter) if err != nil { return nil, nhttp.NewError("platform update report compliance: %w", err) }