nidus-sync/platform/communication.go
Eli Ribble f1fe8b4d2b
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.
2026-05-15 16:58:28 +00:00

182 lines
6.5 KiB
Go

package platform
import (
"context"
"fmt"
"strconv"
"time"
"github.com/Gleipnir-Technology/nidus-sync/db"
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/platform/event"
"github.com/rs/zerolog/log"
)
type RelatedRecordType int
const (
RelatedRecordTypeUnknown RelatedRecordType = iota
RelatedRecordTypeEmail
RelatedRecordTypeReportCompliance
RelatedRecordTypeReportNuisance
RelatedRecordTypeReportWater
RelatedRecordTypeText
)
func recordTypeFromReportType(t modelpublicreport.Reporttype) RelatedRecordType {
switch t {
case modelpublicreport.Reporttype_Compliance:
return RelatedRecordTypeReportCompliance
case modelpublicreport.Reporttype_Nuisance:
return RelatedRecordTypeReportNuisance
case modelpublicreport.Reporttype_Water:
return RelatedRecordTypeReportWater
default:
return RelatedRecordTypeUnknown
}
}
type RelatedRecord struct {
Created time.Time
ID string
Type RelatedRecordType
}
func CommunicationRelatedRecords(ctx context.Context, user User, comm *modelpublic.Communication) ([]RelatedRecord, error) {
// Gather associated records
// * address
// * 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))
if err != nil {
return result, fmt.Errorf("email log from ID: %w", err)
}
email_logs, err := querycomms.EmailLogsFromAddress(ctx, email_log.Source)
if err != nil {
return result, fmt.Errorf("email log from ID: %w", err)
}
for _, l := range email_logs {
result = append(result, RelatedRecord{
Created: l.Created,
ID: strconv.Itoa(int(l.ID)),
Type: RelatedRecordTypeEmail,
})
}
} else if comm.SourceTextLogID != nil {
text_log, err := querycomms.TextLogFromID(ctx, txn, int64(*comm.SourceTextLogID))
if err != nil {
return result, fmt.Errorf("text log from ID: %w", err)
}
text_logs, err := querycomms.EmailLogsFromAddress(ctx, text_log.Source)
if err != nil {
return result, fmt.Errorf("text log from ID: %w", err)
}
for _, l := range text_logs {
result = append(result, RelatedRecord{
Created: l.Created,
ID: strconv.Itoa(int(l.ID)),
Type: RelatedRecordTypeText,
})
}
} else if comm.SourceReportID != nil {
report, err := querypublicreport.ReportFromID(ctx, txn, int64(*comm.SourceReportID))
if err != nil {
return result, fmt.Errorf("report from ID: %w", err)
}
if report.ReporterName != "" {
reports_by_name, err := querypublicreport.ReportsFromReporterName(ctx, db.PGInstance.PGXPool, int64(user.Organization.ID), report.ReporterName)
if err != nil {
return result, fmt.Errorf("reports from reporter name '%s': %w", report.ReporterName, err)
}
for _, r := range reports_by_name {
record_type := recordTypeFromReportType(r.ReportType)
result = append(result, RelatedRecord{
Created: r.Created,
ID: r.PublicID,
Type: record_type,
})
}
}
if report.AddressID != nil {
reports_by_address, err := querypublicreport.ReportsFromAddressID(ctx, db.PGInstance.PGXPool, int64(user.Organization.ID), int64(*report.AddressID))
if err != nil {
return result, fmt.Errorf("reports from reporter name '%s': %w", report.ReporterName, err)
}
for _, r := range reports_by_address {
record_type := recordTypeFromReportType(r.ReportType)
result = append(result, RelatedRecord{
Created: r.Created,
ID: r.PublicID,
Type: record_type,
})
}
}
}
return result, nil
}
func CommunicationsForOrganization(ctx context.Context, org_id int64) ([]modelpublic.Communication, error) {
txn := db.PGInstance.PGXPool
return querypublic.CommunicationsFromOrganization(ctx, txn, org_id)
}
func CommunicationFromID(ctx context.Context, user User, comm_id int64) (*modelpublic.Communication, error) {
txn := db.PGInstance.PGXPool
comm, err := querypublic.CommunicationFromID(ctx, txn, comm_id)
if err != nil {
return nil, err
}
if comm.OrganizationID != user.Organization.ID {
return nil, nil
}
return &comm, nil
}
func CommunicationMarkInvalid(ctx context.Context, user User, comm_id int32) error {
return communicationMark(ctx, user, comm_id, modelpublic.Communicationstatus_Invalid, modelpublic.Communicationlogentry_StatusInvalidated)
}
func CommunicationMarkPendingResponse(ctx context.Context, user User, comm_id int32) error {
return communicationMark(ctx, user, comm_id, modelpublic.Communicationstatus_Pending, modelpublic.Communicationlogentry_StatusPending)
}
func CommunicationMarkPossibleIssue(ctx context.Context, user User, comm_id int32) error {
return communicationMark(ctx, user, comm_id, modelpublic.Communicationstatus_PossibleIssue, modelpublic.Communicationlogentry_StatusPossibleIssue)
}
func CommunicationMarkPossibleResolved(ctx context.Context, user User, comm_id int32) error {
return communicationMark(ctx, user, comm_id, modelpublic.Communicationstatus_PossibleResolved, modelpublic.Communicationlogentry_StatusPossibleResolved)
}
func communicationMark(ctx context.Context, user User, comm_id int32, status modelpublic.Communicationstatus, log_type modelpublic.Communicationlogentry) error {
txn, err := db.BeginTxn(ctx)
if err != nil {
return fmt.Errorf("begin txn: %w", err)
}
defer lint.LogOnErrRollback(txn.Rollback, ctx, "rollback")
err = querypublic.CommunicationSetStatus(ctx, txn, int64(user.Organization.ID), int64(comm_id), status)
if err != nil {
return fmt.Errorf("mark: %w", err)
}
user_id := int32(user.ID)
log_entry := modelpublic.CommunicationLogEntry{
CommunicationID: comm_id,
Created: time.Now(),
Type: log_type,
User: &user_id,
}
_, err = querypublic.CommunicationLogEntryInsert(ctx, txn, log_entry)
if err != nil {
return fmt.Errorf("insert communication log entry: %w", err)
}
if err := txn.Commit(ctx); err != nil {
return fmt.Errorf("commit: %w", err)
}
log.Info().Int32("communication", comm_id).Str("status", status.String()).Msg("Marked communication")
event.Updated(event.TypeCommunication, user.Organization.ID, strconv.Itoa(int(comm_id)))
return nil
}