2026-03-14 01:14:30 +00:00
|
|
|
package platform
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-04-16 10:15:28 +00:00
|
|
|
"crypto/rand"
|
2026-03-15 22:38:36 +00:00
|
|
|
"errors"
|
2026-03-14 01:14:30 +00:00
|
|
|
"fmt"
|
2026-04-16 10:15:28 +00:00
|
|
|
"math/big"
|
|
|
|
|
"strings"
|
2026-03-14 01:14:30 +00:00
|
|
|
"time"
|
|
|
|
|
|
2026-05-09 01:48:56 +00:00
|
|
|
"github.com/Gleipnir-Technology/jet/postgres"
|
2026-03-14 01:14:30 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db"
|
2026-05-01 20:49:37 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/model"
|
2026-05-07 10:39:17 +00:00
|
|
|
tablepublic "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/table"
|
2026-05-01 20:49:37 +00:00
|
|
|
modelpublicreport "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model"
|
2026-05-07 10:39:17 +00:00
|
|
|
tablepublicreport "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/table"
|
2026-05-01 20:49:37 +00:00
|
|
|
querypublic "github.com/Gleipnir-Technology/nidus-sync/db/query/public"
|
|
|
|
|
querypublicreport "github.com/Gleipnir-Technology/nidus-sync/db/query/publicreport"
|
2026-03-15 22:38:36 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/platform/email"
|
2026-03-14 18:14:30 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/platform/event"
|
2026-03-18 18:56:51 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/platform/geocode"
|
2026-04-08 23:37:00 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/platform/publicreport"
|
2026-03-15 22:38:36 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/platform/text"
|
2026-03-18 18:56:51 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
2026-03-14 01:14:30 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-16 10:15:28 +00:00
|
|
|
// GenerateReportID creates a 12-character random string using only unambiguous
|
|
|
|
|
// capital letters and numbers
|
|
|
|
|
func GenerateReportID() (string, error) {
|
|
|
|
|
// Define character set (no O/0, I/l/1, 2/Z to avoid confusion)
|
|
|
|
|
const charset = "ABCDEFGHJKLMNPQRSTUVWXY3456789"
|
|
|
|
|
const length = 12
|
|
|
|
|
|
|
|
|
|
var builder strings.Builder
|
|
|
|
|
builder.Grow(length)
|
|
|
|
|
|
|
|
|
|
// Use crypto/rand for secure randomness
|
|
|
|
|
for i := 0; i < length; i++ {
|
|
|
|
|
// Generate a random index within our charset
|
|
|
|
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("failed to generate random number: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add the randomly selected character to our ID
|
|
|
|
|
builder.WriteByte(charset[n.Int64()])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return builder.String(), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 06:36:55 +00:00
|
|
|
func PublicReportByIDCompliance(ctx context.Context, report_id string, is_public bool) (*types.PublicReportCompliance, error) {
|
|
|
|
|
result, err := publicreport.ByIDCompliance(ctx, report_id, is_public)
|
2026-04-21 21:35:40 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("byidcompliance: %w", err)
|
|
|
|
|
}
|
2026-04-29 20:37:36 +00:00
|
|
|
if result == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
2026-04-21 21:35:40 +00:00
|
|
|
// Check for evidence if this is a mailer-based compliance request
|
|
|
|
|
crr, err := ComplianceReportRequestFromPublicID(ctx, result.PublicID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("compliance report request by public id: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if crr != nil {
|
2026-04-22 21:22:03 +00:00
|
|
|
result.Concerns = []*types.ConcernComplianceReportRequest{
|
|
|
|
|
&types.ConcernComplianceReportRequest{
|
2026-04-21 21:35:40 +00:00
|
|
|
ComplianceReportRequestPublicID: crr.PublicID,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result, nil
|
2026-04-12 17:53:25 +00:00
|
|
|
}
|
2026-04-28 06:36:55 +00:00
|
|
|
func PublicReportByIDNuisance(ctx context.Context, report_id string, is_public bool) (*types.PublicReportNuisance, error) {
|
|
|
|
|
return publicreport.ByIDNuisance(ctx, report_id, is_public)
|
2026-04-12 17:53:25 +00:00
|
|
|
}
|
2026-04-28 06:36:55 +00:00
|
|
|
func PublicReportByIDWater(ctx context.Context, report_id string, is_public bool) (*types.PublicReportWater, error) {
|
|
|
|
|
return publicreport.ByIDWater(ctx, report_id, is_public)
|
2026-04-08 17:49:32 +00:00
|
|
|
}
|
2026-04-28 06:36:55 +00:00
|
|
|
func PublicReportInvalid(ctx context.Context, user User, public_id string) error {
|
2026-05-07 10:39:17 +00:00
|
|
|
report, err := querypublicreport.ReportFromPublicID(ctx, db.PGInstance.PGXPool, public_id)
|
2026-03-14 01:14:30 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("query report existence: %w", err)
|
|
|
|
|
}
|
2026-04-10 16:59:29 +00:00
|
|
|
if report.OrganizationID != user.Organization.ID {
|
|
|
|
|
return fmt.Errorf("user is from a different organization")
|
|
|
|
|
}
|
2026-03-14 01:14:30 +00:00
|
|
|
|
2026-05-07 10:39:17 +00:00
|
|
|
now := time.Now()
|
2026-05-08 22:22:52 +00:00
|
|
|
report_updater := querypublicreport.NewReportUpdater()
|
2026-05-07 10:39:17 +00:00
|
|
|
report_updater.Model.Reviewed = &now
|
|
|
|
|
report_updater.Set(tablepublicreport.Report.Reviewed)
|
|
|
|
|
reporter_id := int32(user.ID)
|
|
|
|
|
report_updater.Model.ReviewerID = &reporter_id
|
|
|
|
|
report_updater.Set(tablepublicreport.Report.ReviewerID)
|
|
|
|
|
report_updater.Model.Status = modelpublicreport.Reportstatustype_Invalidated
|
|
|
|
|
report_updater.Set(tablepublicreport.Report.Status)
|
|
|
|
|
err = report_updater.Execute(ctx, db.PGInstance.PGXPool, report.ID)
|
2026-03-14 01:14:30 +00:00
|
|
|
|
2026-03-18 15:36:20 +00:00
|
|
|
log.Info().Int32("id", report.ID).Msg("Report marked as invalid")
|
2026-04-13 17:19:20 +00:00
|
|
|
event.Updated(event.TypeRMOPublicReport, user.Organization.ID, public_id)
|
2026-03-14 01:14:30 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-03-14 18:14:30 +00:00
|
|
|
|
2026-04-13 17:19:20 +00:00
|
|
|
func PublicReportMessageCreate(ctx context.Context, user User, public_id, message string) (message_id *int32, err error) {
|
2026-03-18 15:36:20 +00:00
|
|
|
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("create txn: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer txn.Rollback(ctx)
|
|
|
|
|
|
2026-05-07 10:39:17 +00:00
|
|
|
report, err := querypublicreport.ReportFromPublicID(ctx, db.PGInstance.PGXPool, public_id)
|
2026-03-15 22:38:36 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("query report existence: %w", err)
|
|
|
|
|
}
|
2026-04-10 16:59:29 +00:00
|
|
|
if report.OrganizationID != user.Organization.ID {
|
|
|
|
|
return nil, fmt.Errorf("user is from a different organization")
|
|
|
|
|
}
|
2026-03-18 15:36:20 +00:00
|
|
|
if report.ReporterPhone != "" {
|
2026-04-13 17:19:20 +00:00
|
|
|
log.Debug().Str("public_id", public_id).Msg("contacting via phone")
|
2026-03-18 15:36:20 +00:00
|
|
|
p, err := text.ParsePhoneNumber(report.ReporterPhone)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("parse phone: %w", err)
|
|
|
|
|
}
|
|
|
|
|
msg_id, err := text.ReportMessage(ctx, txn, int32(user.ID), int32(report.ID), *p, message)
|
2026-03-15 22:38:36 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("send text: %w", err)
|
|
|
|
|
}
|
2026-03-18 15:36:20 +00:00
|
|
|
txn.Commit(ctx)
|
2026-03-18 18:56:51 +00:00
|
|
|
//log.Debug().Int32("msg_id", *msg_id).Msg("Created text.ReportMessage")
|
2026-03-15 22:38:36 +00:00
|
|
|
return msg_id, nil
|
2026-03-18 15:36:20 +00:00
|
|
|
} else if report.ReporterEmail != "" {
|
2026-04-13 17:19:20 +00:00
|
|
|
msg_id, err := email.ReportMessage(ctx, int32(user.ID), public_id, report.ReporterEmail, message)
|
2026-03-15 22:38:36 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("send email: %w", err)
|
|
|
|
|
}
|
2026-03-18 15:36:20 +00:00
|
|
|
txn.Commit(ctx)
|
2026-03-15 22:38:36 +00:00
|
|
|
return msg_id, nil
|
|
|
|
|
} else {
|
2026-04-13 17:19:20 +00:00
|
|
|
log.Debug().Str("public_id", public_id).Msg("contacting via email")
|
2026-03-15 22:38:36 +00:00
|
|
|
return nil, errors.New("no contact methods available")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
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)
|
|
|
|
|
txn, err := db.BeginTxn(ctx)
|
2026-04-10 16:59:29 +00:00
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return fmt.Errorf("create txn: %w", err)
|
2026-04-10 16:59:29 +00:00
|
|
|
}
|
|
|
|
|
defer txn.Rollback(ctx)
|
2026-05-07 10:39:17 +00:00
|
|
|
report, err := querypublicreport.ReportFromPublicID(ctx, db.PGInstance.PGXPool, public_id)
|
2026-04-10 16:59:29 +00:00
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return fmt.Errorf("query report existence: %w", err)
|
2026-04-10 16:59:29 +00:00
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
//compliance, err := models.FindPublicreportCompliance(ctx, txn, report.ID)
|
|
|
|
|
compliance, err := querypublicreport.ComplianceFromID(ctx, txn, int64(report.ID))
|
2026-04-13 20:42:03 +00:00
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return fmt.Errorf("find compliance %d: %w", report.ID, err)
|
2026-04-13 20:42:03 +00:00
|
|
|
}
|
2026-05-01 21:27:17 +00:00
|
|
|
// Don't allow modifying of the submission date if it's set
|
2026-05-07 10:39:17 +00:00
|
|
|
if compliance_updates.Has(tablepublicreport.Compliance.Submitted) {
|
|
|
|
|
if compliance.Submitted != nil {
|
|
|
|
|
compliance_updates.Unset(tablepublicreport.Compliance.Submitted)
|
2026-05-01 21:27:17 +00:00
|
|
|
} else {
|
2026-05-07 10:39:17 +00:00
|
|
|
comm := model.Communication{
|
2026-05-02 00:38:38 +00:00
|
|
|
OrganizationID: report.OrganizationID,
|
2026-05-01 21:27:17 +00:00
|
|
|
SourceReportID: &report.ID,
|
|
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
comm, err = querypublic.CommunicationInsert(ctx, txn, comm)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("insert communication: %w", err)
|
|
|
|
|
}
|
|
|
|
|
comm_log := model.CommunicationLogEntry{
|
|
|
|
|
CommunicationID: comm.ID,
|
|
|
|
|
Created: time.Now(),
|
|
|
|
|
Type: model.Communicationlogentry_Created,
|
|
|
|
|
User: nil,
|
|
|
|
|
}
|
|
|
|
|
comm_log, err = querypublic.CommunicationLogEntryInsert(ctx, txn, comm_log)
|
2026-05-01 21:27:17 +00:00
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return fmt.Errorf("insert communication log entry: %w", err)
|
2026-05-01 21:27:17 +00:00
|
|
|
}
|
|
|
|
|
log.Debug().Int32("id", comm.ID).Msg("inserted new communication")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 22:34:34 +00:00
|
|
|
// Avoid attempting to perform an empty update
|
2026-05-08 22:43:57 +00:00
|
|
|
if address != nil {
|
|
|
|
|
report_updates.Model.AddressGid = address.GID
|
|
|
|
|
report_updates.Set(tablepublicreport.Report.AddressGid)
|
|
|
|
|
report_updates.Model.AddressRaw = address.Raw
|
|
|
|
|
report_updates.Set(tablepublicreport.Report.AddressRaw)
|
|
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
err = report_updates.Execute(ctx, txn, int64(report.ID))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("update report: %w", err)
|
2026-04-10 16:59:29 +00:00
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
err = compliance_updates.Execute(ctx, txn, int64(compliance.ReportID))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("update compliance: %w", err)
|
2026-04-13 20:42:03 +00:00
|
|
|
}
|
2026-04-10 20:29:26 +00:00
|
|
|
if address != nil {
|
2026-05-08 22:43:57 +00:00
|
|
|
err = publicReportUpdateAddressID(ctx, txn, report, *address)
|
2026-04-10 20:29:26 +00:00
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return fmt.Errorf("update address: %w", err)
|
2026-04-10 20:29:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 16:59:29 +00:00
|
|
|
if location != nil {
|
2026-04-12 17:07:14 +00:00
|
|
|
err = publicReportUpdateLocation(ctx, txn, report.ID, *location)
|
2026-04-10 16:59:29 +00:00
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return fmt.Errorf("update location: %w", err)
|
2026-04-10 16:59:29 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
txn.Commit(ctx)
|
2026-05-07 10:39:17 +00:00
|
|
|
return nil
|
2026-04-10 16:59:29 +00:00
|
|
|
}
|
2026-03-18 18:56:51 +00:00
|
|
|
func PublicReportReporterUpdated(ctx context.Context, org_id int32, report_id string) {
|
2026-04-13 16:43:15 +00:00
|
|
|
event.Updated(event.TypeRMOPublicReport, org_id, report_id)
|
2026-03-14 18:14:30 +00:00
|
|
|
}
|
2026-05-07 23:22:50 +00:00
|
|
|
func PublicReportsForOrganization(ctx context.Context, org_id int32, is_public bool) ([]types.PublicReport, error) {
|
|
|
|
|
return publicreport.UnreviewedForOrganization(ctx, db.PGInstance.PGXPool, int64(org_id), is_public)
|
2026-04-12 17:07:14 +00:00
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
func PublicReportsFromIDs(ctx context.Context, report_ids []int64) ([]modelpublicreport.Report, error) {
|
|
|
|
|
return querypublicreport.ReportsFromIDs(ctx, report_ids)
|
2026-05-01 20:49:37 +00:00
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
func PublicReportComplianceCreate(ctx context.Context, setter_report modelpublicreport.Report, setter_compliance modelpublicreport.Compliance, org_id int32) (modelpublicreport.Report, error) {
|
|
|
|
|
return publicReportCreate(ctx, setter_report, nil, nil, nil, org_id, func(ctx context.Context, txn db.Ex, report_id int32) error {
|
|
|
|
|
setter_compliance.ReportID = report_id
|
|
|
|
|
_, err := querypublicreport.ComplianceInsert(ctx, txn, setter_compliance)
|
2026-04-09 23:38:20 +00:00
|
|
|
if err != nil {
|
2026-05-08 01:08:06 +00:00
|
|
|
return fmt.Errorf("Failed to create compliance database record: %w", err)
|
2026-04-09 23:38:20 +00:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-04-13 17:19:20 +00:00
|
|
|
func PublicReportImageCreate(ctx context.Context, public_id string, images []ImageUpload) error {
|
2026-05-07 10:39:17 +00:00
|
|
|
txn, err := db.BeginTxn(ctx)
|
2026-04-10 22:34:14 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("create txn: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer txn.Rollback(ctx)
|
|
|
|
|
|
2026-05-07 10:39:17 +00:00
|
|
|
report, err := querypublicreport.ReportFromPublicID(ctx, db.PGInstance.PGXPool, public_id)
|
2026-04-10 22:34:14 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("report from ID: %w", err)
|
|
|
|
|
}
|
|
|
|
|
saved_images, err := saveImageUploads(ctx, txn, images)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Failed to save image uploads: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if len(saved_images) > 0 {
|
2026-05-07 10:39:17 +00:00
|
|
|
report_images := make([]modelpublicreport.ReportImage, len(saved_images))
|
|
|
|
|
for i, image := range saved_images {
|
|
|
|
|
report_images[i] = modelpublicreport.ReportImage{
|
|
|
|
|
ImageID: image.ID,
|
|
|
|
|
ReportID: report.ID,
|
|
|
|
|
}
|
2026-04-10 22:34:14 +00:00
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
_, err := querypublicreport.ReportImagesInsert(ctx, txn, report_images)
|
2026-04-10 22:34:14 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Failed to save reference to images: %w", err)
|
|
|
|
|
}
|
|
|
|
|
log.Info().Int("len", len(images)).Msg("saved uploaded images")
|
|
|
|
|
}
|
|
|
|
|
txn.Commit(ctx)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
func PublicReportNuisanceCreate(ctx context.Context, setter_report modelpublicreport.Report, setter_nuisance modelpublicreport.Nuisance, location types.Location, address Address, images []ImageUpload) (modelpublicreport.Report, error) {
|
|
|
|
|
return publicReportCreate(ctx, setter_report, &location, &address, images, 0, func(ctx context.Context, txn db.Ex, report_id int32) error {
|
|
|
|
|
setter_nuisance.ReportID = report_id
|
|
|
|
|
_, err := querypublicreport.NuisanceInsert(ctx, txn, setter_nuisance)
|
2026-03-18 18:56:51 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Failed to create nuisance database record: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 10:39:17 +00:00
|
|
|
func PublicReportWaterCreate(ctx context.Context, setter_report modelpublicreport.Report, setter_water modelpublicreport.Water, location types.Location, address Address, images []ImageUpload) (modelpublicreport.Report, error) {
|
|
|
|
|
return publicReportCreate(ctx, setter_report, &location, &address, images, 0, func(ctx context.Context, txn db.Ex, report_id int32) error {
|
|
|
|
|
setter_water.ReportID = report_id
|
|
|
|
|
_, err := querypublicreport.WaterInsert(ctx, txn, setter_water)
|
2026-03-18 18:56:51 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Failed to create water database record: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-04-14 16:07:17 +00:00
|
|
|
func PublicReportTypeByID(ctx context.Context, public_id string) (string, error) {
|
2026-05-07 10:39:17 +00:00
|
|
|
report, err := querypublicreport.ReportFromPublicID(ctx, db.PGInstance.PGXPool, public_id)
|
2026-04-14 16:07:17 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("query report '%s': %w", public_id, err)
|
|
|
|
|
}
|
|
|
|
|
return report.ReportType.String(), nil
|
|
|
|
|
}
|
2026-03-18 18:56:51 +00:00
|
|
|
|
2026-05-07 10:39:17 +00:00
|
|
|
type funcSetReportDetail = func(context.Context, db.Ex, int32) error
|
2026-03-18 18:56:51 +00:00
|
|
|
|
2026-05-07 10:39:17 +00:00
|
|
|
func publicReportCreate(ctx context.Context, setter_report modelpublicreport.Report, location *types.Location, address *Address, images []ImageUpload, organization_id int32, detail_setter funcSetReportDetail) (result modelpublicreport.Report, err error) {
|
|
|
|
|
txn, err := db.BeginTxn(ctx)
|
2026-03-18 18:56:51 +00:00
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return result, fmt.Errorf("create txn: %w", err)
|
2026-03-18 18:56:51 +00:00
|
|
|
}
|
|
|
|
|
defer txn.Rollback(ctx)
|
|
|
|
|
|
2026-05-07 10:39:17 +00:00
|
|
|
if setter_report.PublicID == "" {
|
2026-04-21 14:35:13 +00:00
|
|
|
public_id, err := GenerateReportID()
|
|
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return result, fmt.Errorf("create public ID: %w", err)
|
2026-04-21 14:35:13 +00:00
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
setter_report.PublicID = public_id
|
2026-03-18 18:56:51 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-07 10:39:17 +00:00
|
|
|
var addr *types.Address
|
2026-04-29 13:55:10 +00:00
|
|
|
if address != nil {
|
|
|
|
|
if address.GID != "" {
|
2026-05-07 10:39:17 +00:00
|
|
|
addr_existing, err := geocode.EnsureAddress(ctx, txn, *address)
|
2026-04-29 13:55:10 +00:00
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return result, fmt.Errorf("Failed to ensure address: %w", err)
|
2026-04-29 13:55:10 +00:00
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
addr = &addr_existing
|
2026-04-29 13:55:10 +00:00
|
|
|
} else if address.Raw != "" {
|
|
|
|
|
geo_res, err := geocode.GeocodeRaw(ctx, nil, address.Raw)
|
|
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return result, fmt.Errorf("Failed to geocode raw: %w", err)
|
2026-04-29 13:55:10 +00:00
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
addr = &geo_res.Address
|
2026-04-29 13:55:10 +00:00
|
|
|
} else {
|
2026-05-07 10:39:17 +00:00
|
|
|
return result, fmt.Errorf("empty address")
|
2026-03-18 18:56:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saved_images, err := saveImageUploads(ctx, txn, images)
|
|
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return result, fmt.Errorf("Failed to save image uploads: %w", err)
|
2026-03-18 18:56:51 +00:00
|
|
|
}
|
2026-04-20 16:21:08 +00:00
|
|
|
if organization_id == 0 {
|
2026-04-29 15:01:35 +00:00
|
|
|
organization_id, err = matchDistrict(ctx, location, images, addr)
|
2026-04-20 16:21:08 +00:00
|
|
|
if err != nil {
|
|
|
|
|
log.Warn().Err(err).Msg("Failed to match district")
|
|
|
|
|
}
|
2026-03-18 18:56:51 +00:00
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
setter_report.OrganizationID = organization_id
|
2026-03-18 18:56:51 +00:00
|
|
|
|
2026-04-09 23:38:20 +00:00
|
|
|
if addr != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
setter_report.AddressID = addr.ID
|
2026-03-18 18:56:51 +00:00
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
result, err = querypublicreport.ReportInsert(ctx, txn, setter_report)
|
2026-03-18 18:56:51 +00:00
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return result, fmt.Errorf("Failed to create report database record: %w", err)
|
2026-03-18 18:56:51 +00:00
|
|
|
}
|
2026-04-09 23:38:20 +00:00
|
|
|
if location != nil {
|
|
|
|
|
l := *location
|
|
|
|
|
if l.Latitude != 0 && l.Longitude != 0 {
|
2026-04-12 17:07:14 +00:00
|
|
|
publicReportUpdateLocation(ctx, txn, result.ID, l)
|
2026-03-18 18:56:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
log.Info().Str("public_id", setter_report.PublicID).Int32("id", result.ID).Msg("Created base report")
|
2026-03-18 18:56:51 +00:00
|
|
|
|
|
|
|
|
if len(saved_images) > 0 {
|
2026-05-07 10:39:17 +00:00
|
|
|
setters := make([]modelpublicreport.ReportImage, len(saved_images))
|
|
|
|
|
for i, image := range saved_images {
|
|
|
|
|
setters[i] = modelpublicreport.ReportImage{
|
|
|
|
|
ImageID: int32(image.ID),
|
|
|
|
|
ReportID: int32(result.ID),
|
|
|
|
|
}
|
2026-03-18 18:56:51 +00:00
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
_, err = querypublicreport.ReportImagesInsert(ctx, txn, setters)
|
2026-03-18 18:56:51 +00:00
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return result, fmt.Errorf("Failed to save reference to images: %w", err)
|
2026-03-18 18:56:51 +00:00
|
|
|
}
|
|
|
|
|
log.Info().Int("len", len(images)).Msg("saved uploaded images")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = detail_setter(ctx, txn, result.ID)
|
|
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return result, fmt.Errorf("detail setter: %w", err)
|
2026-03-18 18:56:51 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-07 10:39:17 +00:00
|
|
|
_, err = querypublicreport.ReportLogInsert(ctx, txn, modelpublicreport.ReportLog{
|
|
|
|
|
Created: time.Now(),
|
|
|
|
|
EmailLogID: nil,
|
2026-03-18 18:56:51 +00:00
|
|
|
// ID
|
2026-05-07 10:39:17 +00:00
|
|
|
ReportID: result.ID,
|
|
|
|
|
TextLogID: nil,
|
|
|
|
|
Type: modelpublicreport.Reportlogtype_Created,
|
|
|
|
|
UserID: nil,
|
|
|
|
|
})
|
2026-03-18 18:56:51 +00:00
|
|
|
|
2026-05-01 21:27:17 +00:00
|
|
|
// Only create communication entries for compliance when they're submitted
|
2026-05-07 10:39:17 +00:00
|
|
|
report_type := setter_report.ReportType
|
|
|
|
|
if report_type != modelpublicreport.Reporttype_Compliance {
|
|
|
|
|
comm := model.Communication{
|
2026-05-02 00:38:38 +00:00
|
|
|
OrganizationID: result.OrganizationID,
|
2026-05-01 21:27:17 +00:00
|
|
|
SourceReportID: &result.ID,
|
|
|
|
|
}
|
|
|
|
|
comm, err = querypublic.CommunicationInsert(ctx, txn, comm)
|
|
|
|
|
if err != nil {
|
2026-05-07 10:39:17 +00:00
|
|
|
return result, fmt.Errorf("insert communication: %w", err)
|
2026-05-01 21:27:17 +00:00
|
|
|
}
|
|
|
|
|
log.Debug().Int32("id", comm.ID).Msg("inserted new communication")
|
2026-05-01 20:49:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 18:56:51 +00:00
|
|
|
txn.Commit(ctx)
|
|
|
|
|
|
2026-04-20 16:21:08 +00:00
|
|
|
event.Created(
|
|
|
|
|
event.TypeRMOPublicReport,
|
|
|
|
|
organization_id,
|
|
|
|
|
result.PublicID,
|
|
|
|
|
)
|
2026-03-18 18:56:51 +00:00
|
|
|
return result, nil
|
|
|
|
|
}
|
2026-05-08 22:43:57 +00:00
|
|
|
func publicReportUpdateAddressID(ctx context.Context, txn db.Tx, report *modelpublicreport.Report, address types.Address) error {
|
|
|
|
|
var err error
|
|
|
|
|
if address.GID == "" && address.Raw != "" {
|
|
|
|
|
geo_res, err := geocode.GeocodeRaw(ctx, nil, address.Raw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Failed to geocode raw: %w", err)
|
|
|
|
|
}
|
|
|
|
|
statement := tablepublicreport.Report.UPDATE(
|
|
|
|
|
tablepublicreport.Report.AddressID,
|
|
|
|
|
).SET(
|
|
|
|
|
tablepublicreport.Report.AddressID.SET(postgres.Int(int64(*geo_res.Address.ID))),
|
|
|
|
|
).WHERE(
|
2026-05-07 10:39:17 +00:00
|
|
|
tablepublicreport.Report.ID.EQ(postgres.Int(int64(report.ID))),
|
|
|
|
|
)
|
2026-05-08 22:43:57 +00:00
|
|
|
err = db.ExecuteNoneTx(ctx, txn, statement)
|
|
|
|
|
} else {
|
|
|
|
|
statement := tablepublicreport.Report.UPDATE(
|
|
|
|
|
tablepublicreport.Report.AddressID,
|
|
|
|
|
).SET(
|
|
|
|
|
tablepublic.Address.SELECT(
|
|
|
|
|
tablepublic.Address.ID,
|
|
|
|
|
).WHERE(
|
|
|
|
|
tablepublic.Address.Gid.EQ(postgres.String(address.GID)),
|
|
|
|
|
).LIMIT(1),
|
2026-05-07 10:39:17 +00:00
|
|
|
).WHERE(
|
2026-05-08 22:43:57 +00:00
|
|
|
tablepublicreport.Report.ID.EQ(postgres.Int(int64(report.ID))),
|
|
|
|
|
)
|
|
|
|
|
err = db.ExecuteNoneTx(ctx, txn, statement)
|
|
|
|
|
}
|
2026-04-13 19:22:41 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("update report address_id: %w", err)
|
|
|
|
|
}
|
2026-04-10 20:29:26 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-05-07 10:39:17 +00:00
|
|
|
func publicReportUpdateLocation(ctx context.Context, txn db.Tx, id int32, location types.Location) error {
|
2026-04-10 16:59:29 +00:00
|
|
|
h3cell, _ := location.H3Cell()
|
2026-05-07 10:39:17 +00:00
|
|
|
if h3cell == nil {
|
|
|
|
|
return fmt.Errorf("nil h3 cell")
|
|
|
|
|
}
|
2026-04-10 16:59:29 +00:00
|
|
|
geom_query, _ := location.GeometryQuery()
|
2026-05-07 10:39:17 +00:00
|
|
|
statement := tablepublicreport.Report.UPDATE(
|
|
|
|
|
tablepublicreport.Report.H3cell,
|
|
|
|
|
tablepublicreport.Report.Location,
|
|
|
|
|
).SET(
|
|
|
|
|
postgres.Int(int64(*h3cell)),
|
|
|
|
|
postgres.Raw(geom_query),
|
|
|
|
|
).WHERE(
|
|
|
|
|
tablepublicreport.Report.ID.EQ(postgres.Int(int64(id))),
|
|
|
|
|
)
|
|
|
|
|
err := db.ExecuteNoneTx(ctx, txn, statement)
|
2026-04-10 16:59:29 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Failed to insert publicreport.report geospatial", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|