Alter report submission page to request reporter name and consent

This also adds the new mechanism for handling notifications on reports
This commit is contained in:
Eli Ribble 2026-02-06 15:39:49 +00:00
parent 9328e7a2f8
commit 57191fa222
No known key found for this signature in database
45 changed files with 10337 additions and 573 deletions

View file

@ -0,0 +1,33 @@
package report
import (
"fmt"
)
type ErrorWithCode struct {
code string
err error
message string
}
func (e *ErrorWithCode) Code() string {
return e.code
}
func (e *ErrorWithCode) Error() string {
return e.message
}
func newErrorWithCode(code string, format string, args ...any) *ErrorWithCode {
if len(args) > 0 {
return &ErrorWithCode{
err: fmt.Errorf(format, args...),
code: code,
}
} else {
return &ErrorWithCode{
code: code,
err: nil,
message: format,
}
}
}

View file

@ -5,119 +5,25 @@ import (
"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/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/text"
"github.com/rs/zerolog/log"
"github.com/stephenafamo/scan"
//"github.com/stephenafamo/scan"
)
type ErrorWithCode struct {
code string
err error
message string
}
func (e *ErrorWithCode) Code() string {
return e.code
}
func (e *ErrorWithCode) Error() string {
return e.message
}
type SomeReport struct {
reportID string
tableName string
}
func (sr SomeReport) districtID(ctx context.Context) *int32 {
type _Row struct {
OrganizationID *int32
}
from := sm.From("no-such-table")
switch sr.tableName {
case "nuisance":
from = sm.From("publicreport.nuisance")
case "pool":
from = sm.From("publicreport.pool")
default:
log.Error().Str("table-name", sr.tableName).Msg("Programmer error, non-exhaustive switch statement in SomeReport.districtID")
}
row, err := bob.One(ctx, db.PGInstance.BobDB, psql.Select(
from,
sm.Columns("organization_id"),
sm.Where(psql.Quote("public_id").EQ(psql.Arg(sr.reportID))),
), scan.StructMapper[_Row]())
if err != nil {
log.Warn().Err(err).Msg("Failed to query for organization_id")
return nil
}
return row.OrganizationID
}
func (sr SomeReport) updateReporterEmail(ctx context.Context, email string) *ErrorWithCode {
table := um.Table("so-such-table")
switch sr.tableName {
case "nuisance":
table = um.Table("publicreport.nuisance")
case "pool":
table = um.Table("publicreport.pool")
default:
return newErrorWithCode("internal-error", "Programmer error: unrecognized table")
}
result, err := psql.Update(
table,
um.SetCol("reporter_email").ToArg(email),
um.Where(psql.Quote("public_id").EQ(psql.Arg(sr.reportID))),
).Exec(ctx, db.PGInstance.BobDB)
if err != nil {
return newErrorWithCode("internal-error", "Failed to update report: %w", err)
}
rowcount, err := result.RowsAffected()
if err != nil {
return newErrorWithCode("internal-error", "Failed to get rows affected: %w", err)
}
if rowcount != 1 {
log.Warn().Str("report_id", sr.reportID).Msg("updated more than one row, which is a programmer error")
}
return nil
}
func (sr SomeReport) updateReporterPhone(ctx context.Context, phone text.E164) *ErrorWithCode {
table := um.Table("so-such-table")
switch sr.tableName {
case "nuisance":
table = um.Table("publicreport.nuisance")
case "pool":
table = um.Table("publicreport.pool")
default:
return newErrorWithCode("internal-error", "Programmer error: unrecognized table")
}
result, err := psql.Update(
table,
um.SetCol("reporter_phone").ToArg(text.PhoneString(phone)),
um.Where(psql.Quote("public_id").EQ(psql.Arg(sr.reportID))),
).Exec(ctx, db.PGInstance.BobDB)
if err != nil {
return newErrorWithCode("internal-error", "Failed to update report: %w", err)
}
rowcount, err := result.RowsAffected()
if err != nil {
return newErrorWithCode("internal-error", "Failed to get rows affected: %w", err)
}
if rowcount != 1 {
log.Warn().Str("report_id", sr.reportID).Msg("updated more than one row, which is a programmer error")
}
return nil
}
func DistrictForReport(ctx context.Context, report_id string) (*models.Organization, error) {
some_report, err := findSomeReport(ctx, report_id)
if err != nil {
@ -159,12 +65,12 @@ func GenerateReportID() (string, error) {
return builder.String(), nil
}
func RegisterNotificationEmail(ctx context.Context, report_id string, email string) *ErrorWithCode {
func RegisterNotificationEmail(ctx context.Context, txn bob.Tx, report_id string, email string) *ErrorWithCode {
some_report, err := findSomeReport(ctx, report_id)
if err != nil {
return err
}
err = some_report.updateReporterEmail(ctx, email)
err = some_report.addNotificationEmail(ctx, txn, email)
if err != nil {
return err
}
@ -172,12 +78,12 @@ func RegisterNotificationEmail(ctx context.Context, report_id string, email stri
return nil
}
func RegisterNotificationPhone(ctx context.Context, report_id string, phone text.E164) *ErrorWithCode {
func RegisterNotificationPhone(ctx context.Context, txn bob.Tx, report_id string, phone text.E164) *ErrorWithCode {
some_report, err := findSomeReport(ctx, report_id)
if err != nil {
return err
}
err = some_report.updateReporterPhone(ctx, phone)
err = some_report.addNotificationPhone(ctx, txn, phone)
if err != nil {
return err
}
@ -185,18 +91,48 @@ func RegisterNotificationPhone(ctx context.Context, report_id string, phone text
return nil
}
func RegisterSubscriptionEmail(ctx context.Context, email string) *ErrorWithCode {
func RegisterSubscriptionEmail(ctx context.Context, txn bob.Tx, email string) *ErrorWithCode {
log.Warn().Msg("RegisterSubscription not implemented yet")
return nil
}
func RegisterSubscriptionPhone(ctx context.Context, phone text.E164) *ErrorWithCode {
func RegisterSubscriptionPhone(ctx context.Context, txn bob.Tx, phone text.E164) *ErrorWithCode {
log.Warn().Msg("RegisterSubscription not implemented yet")
return nil
}
func SaveReporter(ctx context.Context, txn bob.Tx, report_id string, name string, email string, phone *text.E164, has_consent bool) *ErrorWithCode {
some_report, err := findSomeReport(ctx, report_id)
if err != nil {
return err
}
if name != "" {
err = some_report.updateReporterName(ctx, txn, name)
if err != nil {
return err
}
}
if phone != nil {
err = some_report.updateReporterPhone(ctx, txn, *phone)
if err != nil {
return err
}
}
if email != "" {
err = some_report.updateReporterEmail(ctx, txn, email)
if err != nil {
return err
}
}
err = some_report.updateReporterConsent(ctx, txn, has_consent)
if err != nil {
return err
}
return nil
}
func findSomeReport(ctx context.Context, report_id string) (result SomeReport, err *ErrorWithCode) {
rows, e := sql.PublicreportIDTable(report_id).All(ctx, db.PGInstance.BobDB)
if e != nil {
log.Error().Err(e).Str("report_id", report_id).Msg("failed to query report ID table")
return result, newErrorWithCode("internal-error", "Failed to query report ID table: %w", e)
}
switch len(rows) {
@ -205,25 +141,24 @@ func findSomeReport(ctx context.Context, report_id string) (result SomeReport, e
case 1:
break
default:
log.Error().Err(e).Str("report_id", report_id).Msg("More than one report with the provided ID, which shouldn't happen")
return result, newErrorWithCode("internal-error", "More than one report with the provided ID, which shouldn't happen")
}
row := rows[0]
result.reportID = report_id
result.tableName = row.FoundInTables[0]
return result, nil
}
report_id_str := row.ReportIds[0]
t, e := strconv.ParseInt(report_id_str, 10, 32)
if e != nil {
log.Error().Err(e).Str("report_id_str", report_id_str).Msg("Unable to parse integer reponse from database")
return result, newErrorWithCode("internal-error", "Unable to parse integer response from database")
}
func newErrorWithCode(code string, format string, args ...any) *ErrorWithCode {
if len(args) > 0 {
return &ErrorWithCode{
err: fmt.Errorf(format, args...),
code: code,
}
} else {
return &ErrorWithCode{
code: code,
err: nil,
message: format,
}
switch row.FoundInTables[0] {
case "nuisance":
return newNuisance(ctx, report_id, int32(t))
case "pool":
return newPool(ctx, report_id, int32(t))
default:
log.Error().Err(e).Str("table_name", row.FoundInTables[0]).Msg("Unrecognized table")
return Nuisance{}, newErrorWithCode("internal-error", fmt.Sprintf("Unrecognized table '%s'", row.FoundInTables[0]))
}
}

View file

@ -0,0 +1,156 @@
package report
import (
"context"
//"crypto/rand"
//"fmt"
//"math/big"
//"strconv"
//"strings"
"time"
"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/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
//"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/text"
"github.com/rs/zerolog/log"
"github.com/stephenafamo/scan"
)
type Nuisance struct {
id int32
publicReportID string
row *models.PublicreportNuisance
}
func (sr Nuisance) PublicReportID() string {
return sr.publicReportID
}
func (sr Nuisance) addNotificationEmail(ctx context.Context, txn bob.Tx, email string) *ErrorWithCode {
setter := models.PublicreportNotifyEmailNuisanceSetter{
Created: omit.From(time.Now()),
Deleted: omitnull.FromPtr[time.Time](nil),
NuisanceID: omit.From(sr.id),
EmailAddress: omit.From(email),
}
_, err := models.PublicreportNotifyPhoneNuisances.Insert(&setter).Exec(ctx, txn)
if err != nil {
return newErrorWithCode("internal-error", "Failed to save new notification row")
}
return nil
}
func (sr Nuisance) addNotificationPhone(ctx context.Context, txn bob.Tx, phone text.E164) *ErrorWithCode {
var err error
setter := models.PublicreportNotifyPhoneNuisanceSetter{
Created: omit.From(time.Now()),
Deleted: omitnull.FromPtr[time.Time](nil),
NuisanceID: omit.From(sr.id),
PhoneE164: omit.From(text.PhoneString(phone)),
}
_, err = models.PublicreportNotifyPhoneNuisances.Insert(&setter).Exec(ctx, txn)
if err != nil {
return newErrorWithCode("internal-error", "Failed to save new notification row")
}
return nil
}
func (sr Nuisance) districtID(ctx context.Context) *int32 {
type _Row struct {
OrganizationID *int32
}
row, err := bob.One(ctx, db.PGInstance.BobDB, psql.Select(
sm.From("publicreport.nuisance"),
sm.Columns("organization_id"),
sm.Where(psql.Quote("public_id").EQ(psql.Arg(sr.publicReportID))),
), scan.StructMapper[_Row]())
if err != nil {
log.Warn().Err(err).Msg("Failed to query for organization_id")
return nil
}
return row.OrganizationID
}
func (sr Nuisance) reportID() int32 {
return sr.id
}
func (sr Nuisance) updateReporterConsent(ctx context.Context, txn bob.Tx, has_consent bool) *ErrorWithCode {
setter := models.PublicreportNuisanceSetter{
ReporterContactConsent: omitnull.From(has_consent),
}
_, err := models.PublicreportNotifyPhoneNuisances.Insert(&setter).Exec(ctx, txn)
if err != nil {
return newErrorWithCode("internal-error", "Failed to save new notification row")
}
return nil
}
func (sr Nuisance) updateReporterEmail(ctx context.Context, txn bob.Tx, email string) *ErrorWithCode {
result, err := psql.Update(
um.Table("publicreport.nuisance"),
um.SetCol("reporter_email").ToArg(email),
um.Where(psql.Quote("public_id").EQ(psql.Arg(sr.publicReportID))),
).Exec(ctx, txn)
if err != nil {
return newErrorWithCode("internal-error", "Failed to update report: %w", err)
}
rowcount, err := result.RowsAffected()
if err != nil {
return newErrorWithCode("internal-error", "Failed to get rows affected: %w", err)
}
if rowcount != 1 {
log.Warn().Str("public_report_id", sr.publicReportID).Msg("updated more than one row, which is a programmer error")
}
return nil
}
func (sr Nuisance) updateReporterName(ctx context.Context, txn bob.Tx, name string) *ErrorWithCode {
result, err := psql.Update(
um.Table("publicreport.nuisance"),
um.SetCol("reporter_name").ToArg(name),
um.Where(psql.Quote("public_id").EQ(psql.Arg(sr.publicReportID))),
).Exec(ctx, txn)
if err != nil {
return newErrorWithCode("internal-error", "Failed to update report: %w", err)
}
rowcount, err := result.RowsAffected()
if err != nil {
return newErrorWithCode("internal-error", "Failed to get rows affected: %w", err)
}
if rowcount != 1 {
log.Warn().Str("public_report_id", sr.publicReportID).Msg("updated more than one row, which is a programmer error")
}
return nil
}
func (sr Nuisance) updateReporterPhone(ctx context.Context, txn bob.Tx, phone text.E164) *ErrorWithCode {
result, err := psql.Update(
um.Table("publicreport.nuisance"),
um.SetCol("reporter_phone").ToArg(text.PhoneString(phone)),
um.Where(psql.Quote("public_id").EQ(psql.Arg(sr.publicReportID))),
).Exec(ctx, txn)
if err != nil {
return newErrorWithCode("internal-error", "Failed to update report: %w", err)
}
rowcount, err := result.RowsAffected()
if err != nil {
return newErrorWithCode("internal-error", "Failed to get rows affected: %w", err)
}
if rowcount != 1 {
log.Warn().Str("public_report_id", sr.publicReportID).Msg("updated more than one row, which is a programmer error")
}
return nil
}
func newNuisance(ctx context.Context, public_id string, report_id int32) (Nuisance, *ErrorWithCode) {
row, err := models.FindPublicreportNuisance(ctx, db.PGInstance.BobDB, report_id)
if err != nil {
return Nuisance{}, newErrorWithCode("internal-error", "Failed to find nuisance report %d: %w", public_id, err)
}
return Nuisance{
id: report_id,
publicReportID: public_id,
row: row,
}, nil
}

View file

@ -0,0 +1,119 @@
package report
import (
"context"
//"crypto/rand"
//"fmt"
//"math/big"
//"strconv"
//"strings"
"time"
"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/models"
"github.com/Gleipnir-Technology/nidus-sync/platform/text"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/rs/zerolog/log"
"github.com/stephenafamo/scan"
)
type Pool struct {
id int32
publicReportID string
row *models.PublicreportPool
}
func (sr Pool) PublicReportID() string {
return sr.publicReportID
}
func (sr Pool) addNotificationEmail(ctx context.Context, txn bob.Tx, email string) *ErrorWithCode {
setter := models.PublicreportNotifyEmailPoolSetter{
Created: omit.From(time.Now()),
Deleted: omitnull.FromPtr[time.Time](nil),
PoolID: omit.From(sr.id),
EmailAddress: omit.From(email),
}
_, err := models.PublicreportNotifyEmailPools.Insert(&setter).Exec(ctx, txn)
if err != nil {
log.Error().Err(err).Msg("Failed to save new notification email row")
return newErrorWithCode("internal-error", "Failed to save new notification email row")
}
return nil
}
func (sr Pool) addNotificationPhone(ctx context.Context, txn bob.Tx, phone text.E164) *ErrorWithCode {
setter := models.PublicreportNotifyPhonePoolSetter{
Created: omit.From(time.Now()),
Deleted: omitnull.FromPtr[time.Time](nil),
PoolID: omit.From(sr.id),
PhoneE164: omit.From(text.PhoneString(phone)),
}
_, err := models.PublicreportNotifyPhonePools.Insert(&setter).Exec(ctx, txn)
if err != nil {
log.Error().Err(err).Msg("Failed to save new notification phone row")
return newErrorWithCode("internal-error", "Failed to save new notification phone row")
}
return nil
}
func (sr Pool) districtID(ctx context.Context) *int32 {
type _Row struct {
OrganizationID *int32
}
row, err := bob.One(ctx, db.PGInstance.BobDB, psql.Select(
sm.From("publicreport.pool"),
sm.Columns("organization_id"),
sm.Where(psql.Quote("public_id").EQ(psql.Arg(sr.publicReportID))),
), scan.StructMapper[_Row]())
if err != nil {
log.Warn().Err(err).Msg("Failed to query for organization_id")
return nil
}
return row.OrganizationID
}
func (sr Pool) reportID() int32 {
return sr.id
}
func (sr Pool) updateReporterConsent(ctx context.Context, txn bob.Tx, has_consent bool) *ErrorWithCode {
return sr.updateReportCol(ctx, txn, &models.PublicreportPoolSetter{
ReporterContactConsent: omitnull.From(has_consent),
})
}
func (sr Pool) updateReporterEmail(ctx context.Context, txn bob.Tx, email string) *ErrorWithCode {
return sr.updateReportCol(ctx, txn, &models.PublicreportPoolSetter{
ReporterEmail: omit.From(email),
})
}
func (sr Pool) updateReporterName(ctx context.Context, txn bob.Tx, name string) *ErrorWithCode {
return sr.updateReportCol(ctx, txn, &models.PublicreportPoolSetter{
ReporterName: omit.From(name),
})
}
func (sr Pool) updateReportCol(ctx context.Context, txn bob.Tx, setter *models.PublicreportPoolSetter) *ErrorWithCode {
err := sr.row.Update(ctx, txn, setter)
if err != nil {
log.Error().Err(err).Str("public_id", sr.publicReportID).Int32("report_id", sr.id).Msg("Failed to update report")
return newErrorWithCode("internal-error", "Failed to update pool report in the database")
}
return nil
}
func (sr Pool) updateReporterPhone(ctx context.Context, txn bob.Tx, phone text.E164) *ErrorWithCode {
return sr.updateReportCol(ctx, txn, &models.PublicreportPoolSetter{
ReporterPhone: omit.From(text.PhoneString(phone)),
})
}
func newPool(ctx context.Context, public_id string, report_id int32) (Pool, *ErrorWithCode) {
row, err := models.FindPublicreportPool(ctx, db.PGInstance.BobDB, report_id)
if err != nil {
log.Error().Err(err).Msg("Failed to find pool report")
return Pool{}, newErrorWithCode("internal-error", "Failed to find pool report %d: %w", public_id, err)
}
return Pool{
id: report_id,
publicReportID: public_id,
row: row,
}, nil
}

View file

@ -0,0 +1,37 @@
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/text"
//"github.com/rs/zerolog/log"
//"github.com/stephenafamo/scan"
)
type SomeReport interface {
addNotificationEmail(context.Context, bob.Tx, string) *ErrorWithCode
addNotificationPhone(context.Context, bob.Tx, text.E164) *ErrorWithCode
districtID(context.Context) *int32
updateReporterConsent(context.Context, bob.Tx, bool) *ErrorWithCode
updateReporterEmail(context.Context, bob.Tx, string) *ErrorWithCode
updateReporterName(context.Context, bob.Tx, string) *ErrorWithCode
updateReporterPhone(context.Context, bob.Tx, text.E164) *ErrorWithCode
PublicReportID() string
reportID() int32
}