2026-03-18 15:36:20 +00:00
package text
import (
"context"
"fmt"
"time"
"github.com/Gleipnir-Technology/bob"
"github.com/Gleipnir-Technology/nidus-sync/comms/text"
"github.com/Gleipnir-Technology/nidus-sync/config"
2026-04-17 22:53:23 +00:00
"github.com/Gleipnir-Technology/nidus-sync/db"
2026-03-18 15:36:20 +00:00
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform/background"
2026-03-18 18:56:51 +00:00
"github.com/Gleipnir-Technology/nidus-sync/platform/event"
2026-03-18 15:36:20 +00:00
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
2026-04-17 22:53:23 +00:00
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
2026-03-18 15:36:20 +00:00
"github.com/rs/zerolog/log"
)
func ensureInitialText ( ctx context . Context , txn bob . Executor , dst types . E164 ) error {
rows , err := models . CommsTextLogs . Query (
models . SelectWhere . CommsTextLogs . Destination . EQ ( dst . PhoneString ( ) ) ,
models . SelectWhere . CommsTextLogs . IsWelcome . EQ ( true ) ,
) . All ( ctx , txn )
if err != nil {
return fmt . Errorf ( "Failed to query text logs: %w" , err )
}
if len ( rows ) > 0 {
return nil
}
return sendInitialText ( ctx , txn , dst )
}
func insertTextLog ( ctx context . Context , txn bob . Executor , destination types . E164 , source types . E164 , origin enums . CommsTextorigin , content string , is_welcome bool , is_visible_to_llm bool ) ( l * models . CommsTextLog , err error ) {
l , err = models . CommsTextLogs . Insert ( & models . CommsTextLogSetter {
//ID:
Content : omit . From ( content ) ,
Created : omit . From ( time . Now ( ) ) ,
Destination : omit . From ( destination . PhoneString ( ) ) ,
IsVisibleToLLM : omit . From ( is_visible_to_llm ) ,
IsWelcome : omit . From ( is_welcome ) ,
Origin : omit . From ( origin ) ,
Source : omit . From ( source . PhoneString ( ) ) ,
TwilioSid : omitnull . FromPtr [ string ] ( nil ) ,
TwilioStatus : omit . From ( "" ) ,
} ) . One ( ctx , txn )
return l , err
}
func resendInitialText ( ctx context . Context , txn bob . Executor , dst types . E164 ) error {
phone , err := models . FindCommsPhone ( ctx , txn , dst . PhoneString ( ) )
if err != nil {
return fmt . Errorf ( "Failed to find phone %s: %w" , dst , err )
}
err = phone . Update ( ctx , txn , & models . CommsPhoneSetter {
Status : omit . From ( enums . CommsPhonestatustypeUnconfirmed ) ,
} )
if err != nil {
return fmt . Errorf ( "Failed to clear subscription on phone %s: %w" , dst , err )
}
return nil
}
func sendInitialText ( ctx context . Context , txn bob . Executor , dst types . E164 ) error {
content := "Welcome to Report Mosquitoes Online. We received your request and want to confirm text updates. Reply YES to continue. Reply STOP at any time to unsubscribe"
_ , err := sendTextDirect ( ctx , txn , enums . CommsTextoriginWebsiteAction , dst . PhoneString ( ) , content , false , true )
if err != nil {
return fmt . Errorf ( "send text: %w" , err )
}
return nil
}
// Begin the process of sending the text message, but only get as far as adding it to
// the database, then let the backend finish sending.
func sendTextBegin ( ctx context . Context , txn bob . Executor , user_id * int32 , report_id * int32 , destination types . E164 , content string , type_ enums . CommsTextjobtype ) ( * int32 , error ) {
err := EnsureInDB ( ctx , txn , destination )
if err != nil {
return nil , fmt . Errorf ( "Failed to ensure text message destination is in the DB: %w" , err )
}
job , err := models . CommsTextJobs . Insert ( & models . CommsTextJobSetter {
Content : omit . From ( content ) ,
CreatorID : omitnull . FromPtr ( user_id ) ,
Created : omit . From ( time . Now ( ) ) ,
Destination : omit . From ( destination . PhoneString ( ) ) ,
//ID:
ReportID : omitnull . FromPtr ( report_id ) ,
Source : omit . From ( enums . CommsTextjobsourceRmo ) ,
Type : omit . From ( type_ ) ,
} ) . One ( ctx , txn )
if err != nil {
return nil , fmt . Errorf ( "Failed to add delayed text job: %w" , err )
}
err = background . NewTextSend ( ctx , txn , job . ID )
if err != nil {
return nil , fmt . Errorf ( "new background job: %w" , err )
}
return & job . ID , nil
}
func sendTextCommandResponse ( ctx context . Context , txn bob . Executor , dst types . E164 , content string ) error {
_ , err := sendTextDirect ( ctx , txn , enums . CommsTextoriginCommandResponse , dst . PhoneString ( ) , content , false , false )
return err
}
2026-04-17 22:53:23 +00:00
func sendTextComplete ( ctx context . Context , job * models . CommsTextJob ) error {
txn , err := db . PGInstance . BobDB . BeginTx ( ctx , nil )
if err != nil {
return fmt . Errorf ( "begin tx: %w" , err )
}
defer txn . Rollback ( ctx )
2026-03-18 15:36:20 +00:00
dst , err := ParsePhoneNumber ( job . Destination )
if err != nil {
return fmt . Errorf ( "parse phone: %w" , err )
}
var origin enums . CommsTextorigin
switch job . Type {
case enums . CommsTextjobtypeReportConfirmation :
origin = enums . CommsTextoriginWebsiteAction
case enums . CommsTextjobtypeReportMessage :
origin = enums . CommsTextoriginDistrict
default :
return fmt . Errorf ( "incomplete switch: %s" , string ( job . Type ) )
}
status , err := phoneStatus ( ctx , * dst )
if err != nil {
return fmt . Errorf ( "Failed to check if subscribed: %w" , err )
}
log . Debug ( ) . Str ( "phone status" , string ( status ) ) . Str ( "destination" , job . Destination ) . Send ( )
switch status {
case enums . CommsPhonestatustypeUnconfirmed :
err := ensureInitialText ( ctx , txn , * dst )
if err != nil {
return fmt . Errorf ( "Failed to ensure initial text has been sent: %w" , err )
}
return nil
2026-03-18 18:56:51 +00:00
//case enums.CommsPhonestatustypeOkToSend:
// allow to drop through
2026-03-18 15:36:20 +00:00
case enums . CommsPhonestatustypeStopped :
resendInitialText ( ctx , txn , * dst )
2026-03-18 18:56:51 +00:00
return nil
2026-03-18 15:36:20 +00:00
}
text_log , err := sendTextDirect ( ctx , txn , origin , job . Destination , job . Content , true , false )
if err != nil {
return fmt . Errorf ( "send text direct: %w" , err )
}
err = job . Update ( ctx , txn , & models . CommsTextJobSetter {
Completed : omitnull . From ( time . Now ( ) ) ,
} )
if err != nil {
return fmt . Errorf ( "update job: %w" , err )
}
if job . ReportID . IsValue ( ) {
2026-03-18 18:56:51 +00:00
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" )
2026-03-18 15:36:20 +00:00
_ , err := models . ReportTexts . Insert ( & models . ReportTextSetter {
2026-03-18 18:56:51 +00:00
CreatorID : omit . From ( creator_id ) ,
ReportID : omit . From ( report_id ) ,
2026-03-18 15:36:20 +00:00
TextLogID : omit . From ( text_log . ID ) ,
} ) . One ( ctx , txn )
if err != nil {
return fmt . Errorf ( "insert report_text: %w" , err )
}
2026-03-18 18:56:51 +00:00
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 )
}
2026-04-13 16:43:15 +00:00
event . Updated ( event . TypeRMOPublicReport , report . OrganizationID , report . PublicID )
2026-03-18 18:56:51 +00:00
} else {
log . Debug ( ) . Msg ( "no report info on text" )
2026-03-18 15:36:20 +00:00
}
2026-04-17 22:53:23 +00:00
txn . Commit ( ctx )
2026-03-18 15:36:20 +00:00
return nil
}
// Send a text message and save the appropriate database records.
// Send immediately using the current goroutine
func sendTextDirect ( ctx context . Context , txn bob . Executor , origin enums . CommsTextorigin , destination , content string , is_visible_to_llm , is_welcome bool ) ( * models . CommsTextLog , error ) {
text_log , err := models . CommsTextLogs . Insert ( & models . CommsTextLogSetter {
//ID:
Content : omit . From ( content ) ,
Created : omit . From ( time . Now ( ) ) ,
Destination : omit . From ( destination ) ,
IsVisibleToLLM : omit . From ( is_visible_to_llm ) ,
IsWelcome : omit . From ( is_welcome ) ,
Origin : omit . From ( origin ) ,
Source : omit . From ( config . PhoneNumberReportStr ) ,
TwilioSid : omitnull . FromPtr [ string ] ( nil ) ,
TwilioStatus : omit . From ( "" ) ,
} ) . One ( ctx , txn )
if err != nil {
return nil , fmt . Errorf ( "insert text log: %w" , err )
}
pid , err := text . SendText ( ctx , config . VoipMSNumber , destination , content )
if err != nil {
return nil , fmt . Errorf ( "send text: %w" , err )
}
err = text_log . Update ( ctx , txn , & models . CommsTextLogSetter {
TwilioSid : omitnull . From ( pid ) ,
TwilioStatus : omit . From ( "created" ) ,
} )
if err != nil {
return nil , fmt . Errorf ( "update %w" , err )
}
return text_log , nil
}