Return logs on comms public reports

...and start to display them. A bit.
This commit is contained in:
Eli Ribble 2026-03-18 18:56:51 +00:00
parent 21e8b9880d
commit 685b7456b6
No known key found for this signature in database
11 changed files with 259 additions and 60 deletions

View file

@ -15,13 +15,8 @@ import (
//"github.com/rs/zerolog/log" //"github.com/rs/zerolog/log"
) )
type historyEntry struct {
Action string `json:"action"`
Timestamp time.Time `json:"timestamp"`
}
type communication struct { type communication struct {
Created time.Time `json:"created"` Created time.Time `json:"created"`
History []historyEntry `json:"history"`
ID string `json:"id"` ID string `json:"id"`
PublicReport types.PublicReport `json:"public_report"` PublicReport types.PublicReport `json:"public_report"`
Type string `json:"type"` Type string `json:"type"`
@ -38,13 +33,7 @@ func listCommunication(ctx context.Context, r *http.Request, user platform.User,
comms := make([]communication, len(reports)) comms := make([]communication, len(reports))
for i, report := range reports { for i, report := range reports {
comms[i] = communication{ comms[i] = communication{
Created: report.Created, Created: report.Created,
History: []historyEntry{
historyEntry{
Action: "created",
Timestamp: report.Created,
},
},
ID: report.PublicID, ID: report.PublicID,
PublicReport: report, PublicReport: report,
Type: "nuisance", Type: "nuisance",

View file

@ -237,15 +237,6 @@
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
// Add to activity log
if (!this.selectedCommunication.history) {
this.selectedCommunication.history = [];
}
this.selectedCommunication.history.push({
action: "Message sent to reporter",
timestamp: new Date(),
});
this.showNotification( this.showNotification(
"Message Sent", "Message Sent",
`Message successfully sent to ${this.selectedCommunication.public_report.reporter.name}`, `Message successfully sent to ${this.selectedCommunication.public_report.reporter.name}`,
@ -844,19 +835,38 @@
<h6><i class="bi bi-clock-history"></i> Activity Log</h6> <h6><i class="bi bi-clock-history"></i> Activity Log</h6>
<div class="small"> <div class="small">
<template <template
x-for="activity in selectedCommunication.history || []" x-for="entry in selectedCommunication.public_report.log || []"
:key="activity.timestamp" :key="entry.created"
> >
<div class="border-start border-2 ps-2 mb-2"> <div class="border-start border-2 ps-2 mb-2">
<div class="text-muted" x-text="activity.action"></div> <template x-if="entry.type == 'created'">
<small <div>
class="text-muted" <div class="text-muted">Initial Report</div>
x-text="formatDate(activity.timestamp)" <small
></small> class="text-muted"
x-text="formatDate(entry.created)"
></small>
</div>
</template>
<template x-if="entry.type == 'message-text'">
<div>
<div class="text-muted">Text Message</div>
<div x-text="entry.message"></div>
<small
class="text-muted"
x-text="formatDate(entry.created)"
></small>
</div>
</template>
<template
x-if="!(entry.type == 'created' || entry.type == 'message-text')"
>
<div x-text="entry.type"></div>
</template>
</div> </div>
</template> </template>
<template <template
x-if="!selectedCommunication.history || selectedCommunication.history.length === 0" x-if="!selectedCommunication.public_report.log || selectedCommunication.public_report.log.length === 0"
> >
<div class="text-muted">No activity yet</div> <div class="text-muted">No activity yet</div>
</template> </template>

View file

@ -2,6 +2,7 @@ package llm
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
@ -50,6 +51,9 @@ type QueryReportStatusInput struct {
var client *openAIClient var client *openAIClient
func (c *openAIClient) continueConversation(ctx context.Context, tools genai.OptionsTools, msg genai.Message) (Message, error) { func (c *openAIClient) continueConversation(ctx context.Context, tools genai.OptionsTools, msg genai.Message) (Message, error) {
if c.client == nil {
return Message{}, errors.New("Client not initialized")
}
res, _, err := adapters.GenSyncWithToolCallLoop(ctx, c.client, genai.Messages{msg}, &tools) res, _, err := adapters.GenSyncWithToolCallLoop(ctx, c.client, genai.Messages{msg}, &tools)
if err != nil { if err != nil {
return Message{}, fmt.Errorf("Failed to continue conversation: %v", err) return Message{}, fmt.Errorf("Failed to continue conversation: %v", err)

View file

@ -6,16 +6,21 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/aarondl/opt/omit" "github.com/Gleipnir-Technology/bob"
"github.com/aarondl/opt/omitnull" "github.com/Gleipnir-Technology/bob/dialect/psql"
//"github.com/Gleipnir-Technology/bob" "github.com/Gleipnir-Technology/bob/dialect/psql/um"
"github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
//"github.com/Gleipnir-Technology/nidus-sync/platform/background" //"github.com/Gleipnir-Technology/nidus-sync/platform/background"
"github.com/Gleipnir-Technology/nidus-sync/platform/email" "github.com/Gleipnir-Technology/nidus-sync/platform/email"
"github.com/Gleipnir-Technology/nidus-sync/platform/event" "github.com/Gleipnir-Technology/nidus-sync/platform/event"
"github.com/Gleipnir-Technology/nidus-sync/platform/geocode"
"github.com/Gleipnir-Technology/nidus-sync/platform/report"
"github.com/Gleipnir-Technology/nidus-sync/platform/text" "github.com/Gleipnir-Technology/nidus-sync/platform/text"
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -58,7 +63,7 @@ func PublicReportMessageCreate(ctx context.Context, user User, report_id, messag
return nil, fmt.Errorf("send text: %w", err) return nil, fmt.Errorf("send text: %w", err)
} }
txn.Commit(ctx) txn.Commit(ctx)
log.Debug().Int32("msg_id", *msg_id).Msg("Created text.ReportMessage") //log.Debug().Int32("msg_id", *msg_id).Msg("Created text.ReportMessage")
return msg_id, nil return msg_id, nil
} else if report.ReporterEmail != "" { } else if report.ReporterEmail != "" {
msg_id, err := email.ReportMessage(ctx, int32(user.ID), report_id, report.ReporterEmail, message) msg_id, err := email.ReportMessage(ctx, int32(user.ID), report_id, report.ReporterEmail, message)
@ -72,9 +77,134 @@ func PublicReportMessageCreate(ctx context.Context, user User, report_id, messag
return nil, errors.New("no contact methods available") return nil, errors.New("no contact methods available")
} }
} }
func PublicReportReporterUpdated(ctx context.Context, org_id int32, report_id string, tablename string) { func PublicReportReporterUpdated(ctx context.Context, org_id int32, report_id string) {
event.Updated(event.TypeRMOReport, org_id, report_id) event.Updated(event.TypeRMOReport, org_id, report_id)
} }
func ReportNuisanceCreate(ctx context.Context, setter_report models.PublicreportReportSetter, setter_nuisance models.PublicreportNuisanceSetter, latlng LatLng, address Address, images []ImageUpload) (*models.PublicreportReport, error) {
return reportCreate(ctx, setter_report, latlng, address, images, func(ctx context.Context, txn bob.Executor, report_id int32) error {
setter_nuisance.ReportID = omit.From(report_id)
_, err := models.PublicreportNuisances.Insert(&setter_nuisance).One(ctx, txn)
if err != nil {
return fmt.Errorf("Failed to create nuisance database record: %w", err)
}
return nil
})
}
func ReportWaterCreate(ctx context.Context, setter_report models.PublicreportReportSetter, setter_water models.PublicreportWaterSetter, latlng LatLng, address Address, images []ImageUpload) (*models.PublicreportReport, error) {
return reportCreate(ctx, setter_report, latlng, address, images, func(ctx context.Context, txn bob.Executor, report_id int32) error {
setter_water.ReportID = omit.From(report_id)
_, err := models.PublicreportWaters.Insert(&setter_water).One(ctx, txn)
if err != nil {
return fmt.Errorf("Failed to create water database record: %w", err)
}
return nil
})
}
type funcSetReportDetail = func(context.Context, bob.Executor, int32) error
func reportCreate(ctx context.Context, setter_report models.PublicreportReportSetter, latlng LatLng, address Address, images []ImageUpload, detail_setter funcSetReportDetail) (result *models.PublicreportReport, err error) {
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("create txn: %w", err)
}
defer txn.Rollback(ctx)
public_id, err := report.GenerateReportID()
if err != nil {
return nil, fmt.Errorf("create public ID: %w", err)
}
setter_report.PublicID = omit.From(public_id)
// If we've got an locality value it was set by geocoding so we should save it
var a *models.Address
if address.Locality != "" && latlng.Latitude != nil && latlng.Longitude != nil {
a, err = geocode.EnsureAddress(ctx, txn, address, types.Location{
Latitude: *latlng.Latitude,
Longitude: *latlng.Longitude,
})
if err != nil {
return nil, fmt.Errorf("Failed to ensure address: %w", err)
}
}
saved_images, err := saveImageUploads(ctx, txn, images)
if err != nil {
return nil, fmt.Errorf("Failed to save image uploads: %w", err)
}
var organization_id *int32
organization_id, err = MatchDistrict(ctx, latlng.Longitude, latlng.Latitude, images)
if err != nil {
log.Warn().Err(err).Msg("Failed to match district")
}
if a != nil {
setter_report.AddressID = omitnull.From(a.ID)
}
if organization_id != nil {
setter_report.OrganizationID = omit.FromPtr(organization_id)
}
result, err = models.PublicreportReports.Insert(&setter_report).One(ctx, txn)
if err != nil {
return nil, fmt.Errorf("Failed to create report database record: %w", err)
}
if latlng.Latitude != nil && latlng.Longitude != nil {
h3cell, _ := latlng.H3Cell()
geom_query, _ := latlng.GeometryQuery()
_, err = psql.Update(
um.Table("publicreport.report"),
um.SetCol("h3cell").ToArg(h3cell),
um.SetCol("location").To(geom_query),
um.Where(psql.Quote("id").EQ(psql.Arg(result.ID))),
).Exec(ctx, txn)
if err != nil {
return nil, fmt.Errorf("Failed to insert publicreport.report geospatial", err)
}
}
log.Info().Str("public_id", public_id).Int32("id", result.ID).Msg("Created base report")
if len(saved_images) > 0 {
setters := make([]*models.PublicreportReportImageSetter, 0)
for _, image := range saved_images {
setters = append(setters, &models.PublicreportReportImageSetter{
ImageID: omit.From(int32(image.ID)),
ReportID: omit.From(int32(result.ID)),
})
}
_, err = models.PublicreportReportImages.Insert(bob.ToMods(setters...)).Exec(ctx, txn)
if err != nil {
return nil, fmt.Errorf("Failed to save reference to images: %w", err)
}
log.Info().Int("len", len(images)).Msg("saved uploaded images")
}
err = detail_setter(ctx, txn, result.ID)
if err != nil {
return nil, fmt.Errorf("detail setter: %w", err)
}
models.PublicreportReportLogs.Insert(&models.PublicreportReportLogSetter{
Created: omit.From(time.Now()),
EmailLogID: omitnull.FromPtr[int32](nil),
// ID
ReportID: omit.From(result.ID),
TextLogID: omitnull.FromPtr[int32](nil),
Type: omit.From(enums.PublicreportReportlogtypeCreated),
UserID: omitnull.FromPtr[int32](nil),
}).One(ctx, txn)
txn.Commit(ctx)
if organization_id != nil {
event.Created(
event.TypeRMONuisance,
*organization_id,
result.PublicID,
)
}
return result, nil
}
func reportFromID(ctx context.Context, user User, report_id string) (*models.PublicreportReport, error) { func reportFromID(ctx context.Context, user User, report_id string) (*models.PublicreportReport, error) {
report, err := models.PublicreportReports.Query( report, err := models.PublicreportReports.Query(
models.SelectWhere.PublicreportReports.PublicID.EQ(report_id), models.SelectWhere.PublicreportReports.PublicID.EQ(report_id),

View file

@ -32,6 +32,7 @@ func logEntriesByReportID(ctx context.Context, report_ids []int32) (map[int32][]
sm.Columns( sm.Columns(
"l.created", "l.created",
"l.id", "l.id",
"COALESCE(t.content, '') AS message",
"l.report_id", "l.report_id",
"l.type_", "l.type_",
"l.user_id", "l.user_id",

View file

@ -18,6 +18,7 @@ import (
) )
type Report struct { type Report struct {
Log []LogEntry `db:"-" json:"log"`
Address types.Address `db:"address" json:"address"` Address types.Address `db:"address" json:"address"`
AddressRaw string `db:"address_raw" json:"address_raw"` AddressRaw string `db:"address_raw" json:"address_raw"`
Created time.Time `db:"created" json:"created"` Created time.Time `db:"created" json:"created"`
@ -31,7 +32,7 @@ type Report struct {
Water *Water `db:"water" json:"water"` Water *Water `db:"water" json:"water"`
} }
func ReportsForOrganization(ctx context.Context, org_id int32) ([]Report, error) { func ReportsForOrganization(ctx context.Context, org_id int32) ([]*Report, error) {
rows, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select( rows, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select(
sm.Columns( sm.Columns(
"address_country AS \"address.country\"", "address_country AS \"address.country\"",
@ -67,6 +68,10 @@ func ReportsForOrganization(ctx context.Context, org_id int32) ([]Report, error)
if err != nil { if err != nil {
return nil, fmt.Errorf("images for report: %w", err) return nil, fmt.Errorf("images for report: %w", err)
} }
logs_by_report_id, err := logEntriesByReportID(ctx, report_ids)
if err != nil {
return nil, fmt.Errorf("log entries for reports: %w", err)
}
nuisances_by_report_id, err := nuisancesByReportID(ctx, report_ids) nuisances_by_report_id, err := nuisancesByReportID(ctx, report_ids)
if err != nil { if err != nil {
return nil, fmt.Errorf("nuisances: %w", err) return nil, fmt.Errorf("nuisances: %w", err)
@ -76,17 +81,20 @@ func ReportsForOrganization(ctx context.Context, org_id int32) ([]Report, error)
return nil, fmt.Errorf("waters: %w", err) return nil, fmt.Errorf("waters: %w", err)
} }
for _, row := range rows { results := make([]*Report, len(rows))
for i, row := range rows {
images, ok := images_by_id[row.ID] images, ok := images_by_id[row.ID]
if ok { if ok {
row.Images = images row.Images = images
} else { } else {
row.Images = []types.Image{} row.Images = []types.Image{}
} }
row.Log = logs_by_report_id[row.ID]
row.Nuisance = nuisances_by_report_id[row.ID] row.Nuisance = nuisances_by_report_id[row.ID]
row.Water = waters_by_report_id[row.ID] row.Water = waters_by_report_id[row.ID]
results[i] = &row
} }
return rows, nil return results, nil
} }
func ReportsForOrganizationCount(ctx context.Context, org_id int32) (uint, error) { func ReportsForOrganizationCount(ctx context.Context, org_id int32) (uint, error) {
type _Row struct { type _Row struct {

View file

@ -169,7 +169,7 @@ func listenAndDoOneJob(ctx context.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to parse int from payload '%s': %w", notification.Payload, err) return fmt.Errorf("failed to parse int from payload '%s': %w", notification.Payload, err)
} }
log.Debug().Int("job_id", job_id).Msg("got notification for job") //log.Debug().Int("job_id", job_id).Msg("got notification for job")
c := bobpgx.NewConn(conn.Conn()) c := bobpgx.NewConn(conn.Conn())
job, err := models.FindJob(ctx, c, int32(job_id)) job, err := models.FindJob(ctx, c, int32(job_id))
@ -199,6 +199,6 @@ func listenAndDoOneJob(ctx context.Context) error {
return fmt.Errorf("delete job: %w", err) return fmt.Errorf("delete job: %w", err)
} }
txn.Commit(ctx) txn.Commit(ctx)
sublog.Debug().Msg("job complete") //sublog.Debug().Msg("job complete")
} }
} }

View file

@ -7,7 +7,7 @@ import (
"github.com/Gleipnir-Technology/bob" "github.com/Gleipnir-Technology/bob"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform/types" "github.com/Gleipnir-Technology/nidus-sync/platform/types"
"github.com/rs/zerolog/log" //"github.com/rs/zerolog/log"
) )
func JobRespond(ctx context.Context, txn bob.Executor, log_id int32) error { func JobRespond(ctx context.Context, txn bob.Executor, log_id int32) error {
@ -18,7 +18,7 @@ func JobSend(ctx context.Context, txn bob.Executor, job_id int32) error {
if err != nil { if err != nil {
return fmt.Errorf("find text: %w", err) return fmt.Errorf("find text: %w", err)
} }
log.Debug().Int32("job.id", job.ID).Msg("completing text job") //log.Debug().Int32("job.id", job.ID).Msg("completing text job")
return sendTextComplete(ctx, txn, job) return sendTextComplete(ctx, txn, job)
} }
func handleWaitingTextJobs(ctx context.Context, txn bob.Executor, dst types.E164) error { func handleWaitingTextJobs(ctx context.Context, txn bob.Executor, dst types.E164) error {

View file

@ -5,10 +5,14 @@ import (
"fmt" "fmt"
"github.com/Gleipnir-Technology/bob" "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/enums"
"github.com/Gleipnir-Technology/nidus-sync/db/models" //"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform/types" "github.com/Gleipnir-Technology/nidus-sync/platform/types"
//"github.com/rs/zerolog/log" //"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 // Send a message from a district to a public reporter within the context of the public report
@ -29,12 +33,35 @@ func ReportSubscriptionConfirmationText(ctx context.Context, txn bob.Executor, d
} }
return err return err
} }
func reportForTextRecipient(ctx context.Context, txn bob.Executor, destination types.E164) (*models.PublicreportReport, error) {
/*return models.ReportText type reportIDs struct {
psql.Query( ID int32 `db:"id"`
return Addresses.Query( PublicID string `db:"public_id"`
sm.Where(Addresses.Columns.ID.EQ(psql.Arg(IDPK))), OrganizationID int32 `db:"organization_id"`
).Exists(ctx, exec) }
*/
return nil, nil // 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
} }

View file

@ -14,6 +14,7 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform/background" "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/Gleipnir-Technology/nidus-sync/platform/types"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -125,14 +126,11 @@ func sendTextComplete(ctx context.Context, txn bob.Executor, job *models.CommsTe
return fmt.Errorf("Failed to ensure initial text has been sent: %w", err) return fmt.Errorf("Failed to ensure initial text has been sent: %w", err)
} }
return nil return nil
case enums.CommsPhonestatustypeOkToSend: //case enums.CommsPhonestatustypeOkToSend:
_, err = sendTextDirect(ctx, txn, origin, dst.PhoneString(), job.Content, true, false) // allow to drop through
if err != nil {
return fmt.Errorf("Failed to send report subscription confirmation: %w", err)
}
return nil
case enums.CommsPhonestatustypeStopped: case enums.CommsPhonestatustypeStopped:
resendInitialText(ctx, txn, *dst) resendInitialText(ctx, txn, *dst)
return nil
} }
text_log, err := sendTextDirect(ctx, txn, origin, job.Destination, job.Content, true, false) text_log, err := sendTextDirect(ctx, txn, origin, job.Destination, job.Content, true, false)
if err != nil { if err != nil {
@ -145,14 +143,33 @@ func sendTextComplete(ctx context.Context, txn bob.Executor, job *models.CommsTe
return fmt.Errorf("update job: %w", err) return fmt.Errorf("update job: %w", err)
} }
if job.ReportID.IsValue() { if job.ReportID.IsValue() {
creator_id := job.CreatorID.MustGet()
report_id := job.ReportID.MustGet()
log.Debug().Int32("creator", creator_id).Int32("report_id", report_id).Msg("Creating report entries for text message")
_, err := models.ReportTexts.Insert(&models.ReportTextSetter{ _, err := models.ReportTexts.Insert(&models.ReportTextSetter{
CreatorID: omit.From(job.CreatorID.MustGet()), CreatorID: omit.From(creator_id),
ReportID: omit.From(job.ReportID.MustGet()), ReportID: omit.From(report_id),
TextLogID: omit.From(text_log.ID), TextLogID: omit.From(text_log.ID),
}).One(ctx, txn) }).One(ctx, txn)
if err != nil { if err != nil {
return fmt.Errorf("insert report_text: %w", err) return fmt.Errorf("insert report_text: %w", err)
} }
models.PublicreportReportLogs.Insert(&models.PublicreportReportLogSetter{
Created: omit.From(time.Now()),
EmailLogID: omitnull.FromPtr[int32](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)
report, err := models.FindPublicreportReport(ctx, txn, report_id)
if err != nil {
return fmt.Errorf("find public report: %w", err)
}
event.Updated(event.TypeRMOReport, report.OrganizationID, report.PublicID)
} else {
log.Debug().Msg("no report info on text")
} }
return nil return nil
} }

View file

@ -129,13 +129,26 @@ func respondText(ctx context.Context, txn bob.Executor, log_id int32) error {
return nil return nil
} }
// If we've got an open public report from this phone number then we'll let the district respond // If we've got an open public report from this phone number then we'll let the district respond
report, err := reportForTextRecipient(ctx, txn, *src) reports, err := reportsForTextRecipient(ctx, txn, *src)
if err != nil { if err != nil {
return fmt.Errorf("has open report: %w", err) return fmt.Errorf("has open report: %w", err)
} }
if report != nil { for _, report := range reports {
models.PublicreportReportLogs.Insert(&models.PublicreportReportLogSetter{
Created: omit.From(time.Now()),
EmailLogID: omitnull.FromPtr[int32](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)
event.Updated(event.TypeRMOReport, report.OrganizationID, report.PublicID) event.Updated(event.TypeRMOReport, report.OrganizationID, report.PublicID)
} }
// If humans are involved, wait for them.
if len(reports) > 0 {
return nil
}
// Otherwise let the LLM handle the response // Otherwise let the LLM handle the response
return respondTextLLM(ctx, txn, *src) return respondTextLLM(ctx, txn, *src)
} }