From 6175e1a8115aeeaea06f9761e69cdddb08380475 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 21 May 2026 18:44:28 +0000 Subject: [PATCH] Update reporter contact on report update This is as a complicated one because it involves merging contact information in tricky cases. I assume that phone should override email, primarily because setting up phones is more tightly regulated. This may be a terrible assumption. Issue: #13 --- db/query/comms/contact.go | 37 ++++++++- db/query/comms/contact_email.go | 8 ++ db/query/comms/contact_phone.go | 2 +- db/query/publicreport/report.go | 8 ++ platform/publicreport/notification.go | 114 +++++++++++++++++++------- 5 files changed, 139 insertions(+), 30 deletions(-) diff --git a/db/query/comms/contact.go b/db/query/comms/contact.go index c4b506ef..98b0ebbc 100644 --- a/db/query/comms/contact.go +++ b/db/query/comms/contact.go @@ -53,6 +53,24 @@ func ContactEmptyForOrganization(ctx context.Context, txn db.Ex, org_id int64) ( } return &row, nil } +func ContactFromEmail(ctx context.Context, txn db.Ex, email string) (*model.Contact, error) { + statement := table.Contact.SELECT( + table.Contact.AllColumns, + ).FROM( + table.Contact.INNER_JOIN( + table.ContactEmail, + table.ContactEmail.ContactID.EQ(table.Contact.ID), + ), + ).WHERE(table.ContactEmail.Address.EQ(postgres.String(email))) + row, err := db.ExecuteOne[model.Contact](ctx, statement) + if err != nil { + if errors.Is(err, db.ErrNoRows) { + return nil, nil + } + return nil, fmt.Errorf("query contact from email '%s': %w", email, err) + } + return &row, nil +} func ContactFromID(ctx context.Context, txn db.Ex, id int64) (model.Contact, error) { statement := table.Contact.SELECT( table.Contact.AllColumns, @@ -74,7 +92,24 @@ func ContactsFromIDs(ctx context.Context, txn db.Ex, contact_ids []int64) ([]mod WHERE(table.Contact.ID.IN(sql_ids...)) return db.ExecuteManyTx[model.Contact](ctx, txn, statement) } - +func ContactFromPhone(ctx context.Context, txn db.Ex, e164 string) (*model.Contact, error) { + statement := table.Contact.SELECT( + table.Contact.AllColumns, + ).FROM( + table.Contact.INNER_JOIN( + table.ContactPhone, + table.ContactPhone.ContactID.EQ(table.Contact.ID), + ), + ).WHERE(table.ContactPhone.E164.EQ(postgres.String(e164))) + row, err := db.ExecuteOne[model.Contact](ctx, statement) + if err != nil { + if errors.Is(err, db.ErrNoRows) { + return nil, nil + } + return nil, fmt.Errorf("query contact from phone '%s': %w", e164, err) + } + return &row, nil +} func ContactUpdateName(ctx context.Context, txn db.Ex, id int64, name string) error { statement := table.Contact.UPDATE(). SET( diff --git a/db/query/comms/contact_email.go b/db/query/comms/contact_email.go index 35618dca..eb2c4b7b 100644 --- a/db/query/comms/contact_email.go +++ b/db/query/comms/contact_email.go @@ -51,3 +51,11 @@ func ContactEmailByContactIDs(ctx context.Context, txn db.Ex, contact_ids []int6 } return result, nil } +func ContactEmailUpdateContactID(ctx context.Context, txn db.Ex, address string, contact_id int64) error { + statement := table.ContactEmail.UPDATE(). + SET( + table.ContactEmail.ContactID.SET(postgres.Int(contact_id)), + ). + WHERE(table.ContactEmail.Address.EQ(postgres.String(address))) + return db.ExecuteNoneTx(ctx, txn, statement) +} diff --git a/db/query/comms/contact_phone.go b/db/query/comms/contact_phone.go index 72c4fd75..c995fbfa 100644 --- a/db/query/comms/contact_phone.go +++ b/db/query/comms/contact_phone.go @@ -13,7 +13,7 @@ import ( ) func ContactPhoneInsert(ctx context.Context, txn db.Ex, m model.ContactPhone) (model.ContactPhone, error) { - statement := table.ContactPhone.INSERT(table.ContactPhone.MutableColumns). + statement := table.ContactPhone.INSERT(table.ContactPhone.AllColumns). MODEL(m). RETURNING(table.ContactPhone.AllColumns) return db.ExecuteOneTx[model.ContactPhone](ctx, txn, statement) diff --git a/db/query/publicreport/report.go b/db/query/publicreport/report.go index 2692bb40..b039b969 100644 --- a/db/query/publicreport/report.go +++ b/db/query/publicreport/report.go @@ -126,3 +126,11 @@ func ReportsFromReporterPhone(ctx context.Context, txn db.Ex, destination string ) return db.ExecuteManyTx[model.Report](ctx, txn, statement) } +func ReportUpdateReporterContactID(ctx context.Context, txn db.Ex, id int64, contact_id int64) error { + statement := table.Report.UPDATE(). + SET( + table.Report.ReporterContactID.SET(postgres.Int(contact_id)), + ). + WHERE(table.Report.ID.EQ(postgres.Int(id))) + return db.ExecuteNoneTx(ctx, txn, statement) +} diff --git a/platform/publicreport/notification.go b/platform/publicreport/notification.go index 10aae0fc..a90cb776 100644 --- a/platform/publicreport/notification.go +++ b/platform/publicreport/notification.go @@ -79,43 +79,101 @@ func RegisterSubscriptionPhone(ctx context.Context, txn db.Ex, contact modelcomm } func SaveReporter(ctx context.Context, txn db.Ex, report modelpublicreport.Report, name string, email_address string, phone *types.E164, can_sms bool) (contact modelcomms.Contact, err error) { - contact, err = querycomms.ContactFromID(ctx, txn, int64(report.ReporterContactID)) + report_contact, err := querycomms.ContactFromID(ctx, txn, int64(report.ReporterContactID)) if err != nil { return contact, fmt.Errorf("contact query: %w", err) } - log.Debug().Str("name", name).Str("old name", contact.Name).Int32("id", contact.ID).Msg("contact updated") - 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) + email_contact, err := querycomms.ContactFromEmail(ctx, txn, email_address) + if err != nil { + return contact, fmt.Errorf("contact query: %w", err) + } + phone_contact, err := querycomms.ContactFromPhone(ctx, txn, phone.PhoneString()) + if err != nil { + return contact, fmt.Errorf("contact query: %w", err) + } + var new_contact_name string + new_contact_id := report_contact.ID + if phone_contact != nil { + // Whether or not the contacts match, we're going to pick the phone contact + // as the authoritative one, for no particular reason. + new_contact_id = phone_contact.ID + if phone_contact.Name != "" { + new_contact_name = phone_contact.Name + } + if email_contact != nil && email_contact.ID != phone_contact.ID { + err = querycomms.ContactEmailUpdateContactID(ctx, txn, email_address, int64(phone_contact.ID)) + if err != nil { + return contact, fmt.Errorf("update email contact %d (%d) to %d: %w", email_contact.ID, email_contact.ID, phone_contact.ID, err) + } + log.Info().Str("address", email_address).Str("e164", phone.PhoneString()).Int32("from-id", email_contact.ID).Int32("to-id", phone_contact.ID).Msg("merged email contact with phone contact") + } + } else if email_contact != nil { + new_contact_id = email_contact.ID + if email_contact.Name != "" { + new_contact_name = email_contact.Name } } - if email_address != "" { - e, err := querycomms.ContactEmailInsert(ctx, txn, modelcomms.ContactEmail{ - Address: email_address, - Confirmed: false, - ContactID: contact.ID, - IsSubscribed: false, - }) - if err != nil { - return contact, fmt.Errorf("contact add email: %w", err) - } - log.Info().Str("address", e.Address).Msg("Created contact email") + if name != "" { + new_contact_name = name } - if phone != nil { - p, err := querycomms.ContactPhoneInsert(ctx, txn, modelcomms.ContactPhone{ - CanSms: can_sms, - ConfirmedMessageID: nil, - ContactID: contact.ID, - E164: phone.PhoneString(), - IsSubscribed: false, - StopMessageID: nil, - }) + if new_contact_id != 0 && new_contact_id != report_contact.ID { + err = querypublicreport.ReportUpdateReporterContactID(ctx, txn, int64(report.ID), int64(new_contact_id)) if err != nil { - return contact, fmt.Errorf("contact add phone: %w", err) + return contact, fmt.Errorf("update report %d reporter contact from %d to %d: %w", report.ID, report.ReporterContactID, new_contact_id, err) + } + } + if new_contact_name != "" { + err = querycomms.ContactUpdateName(ctx, txn, int64(new_contact_id), new_contact_name) + if err != nil { + return contact, fmt.Errorf("update contact %d to name %s: %w", new_contact_id, new_contact_name, err) } - log.Info().Str("e164", p.E164).Msg("Created contact phone") } + log.Debug().Str("name", name).Str("old name", contact.Name).Int32("id", contact.ID).Msg("contact updated") + + if email_address != "" && email_contact == nil { + err = saveReporterEmail(ctx, txn, new_contact_id, email_address) + if err != nil { + return contact, fmt.Errorf("new contact email for '%s' on %d: %w", email_address, new_contact_id, err) + } + } + if phone != nil && phone_contact == nil { + err = saveReporterPhone(ctx, txn, new_contact_id, phone, can_sms) + if err != nil { + return contact, fmt.Errorf("new contact phone for '%s' on %d: %w", phone.PhoneString(), new_contact_id, err) + } + } return contact, nil } +func saveReporterEmail(ctx context.Context, txn db.Ex, contact_id int32, email_address string) error { + e, err := querycomms.ContactEmailInsert(ctx, txn, modelcomms.ContactEmail{ + Address: email_address, + Confirmed: false, + ContactID: contact_id, + IsSubscribed: false, + }) + if err != nil { + return fmt.Errorf("contact add email: %w", err) + } + log.Info().Str("address", e.Address).Int32("contact_id", contact_id).Msg("Created contact email") + return nil +} + +func saveReporterPhone(ctx context.Context, txn db.Ex, contact_id int32, phone *types.E164, can_sms bool) error { + if phone == nil { + return nil + } + p, err := querycomms.ContactPhoneInsert(ctx, txn, modelcomms.ContactPhone{ + CanSms: can_sms, + ConfirmedMessageID: nil, + ContactID: contact_id, + E164: phone.PhoneString(), + IsSubscribed: false, + StopMessageID: nil, + }) + if err != nil { + return fmt.Errorf("contact add phone: %w", err) + } + log.Info().Str("e164", p.E164).Int32("contact_id", contact_id).Msg("Created contact phone") + return nil +}