package store import ( "bytes" "crypto/sha256" "database/sql" "encoding/json" "errors" "fmt" "time" "git.ipao.vip/rogee/go-sip/internal/configread" "git.ipao.vip/rogee/go-sip/internal/contract" "git.ipao.vip/rogee/go-sip/internal/tenant" ) var ErrCurrentUploadUnverified = errors.New("recording outcome is not verified against its Dispatcher-approved upload") var ErrCurrentResultConflict = errors.New("call already has a different final result") var ErrCurrentEndUnconfirmed = errors.New("final result requires confirmed call end and its frozen snapshot") var ErrCurrentResultInvalid = errors.New("final result input violates the approved contract") func currentResultEventID(dispatcherID, sourceEventID string) string { identity := sha256.Sum256([]byte("call.execute.result\x00" + dispatcherID + "\x00" + sourceEventID)) return fmt.Sprintf("result-%x", identity[:]) } // RecordCallResult stores one no-recording final result only after confirmed // call end. A durable upload grant cannot be bypassed with an empty recording. func (s *CurrentStore) RecordCallResult(dispatcherID, sourceEventID string, payload []byte) (CurrentOutboxEvent, bool, error) { return s.recordCallResult(dispatcherID, sourceEventID, payload, nil) } // RecordUploadedCallResult accepts the authenticated Agent's successful PUT // observation only for the precise, previously bound OSS asset. Both the // upload fact and sole SaaS result outbox entry commit in the same transaction. func (s *CurrentStore) RecordUploadedCallResult(dispatcherID, sourceEventID string, payload []byte, proof CurrentUploadProof) (CurrentOutboxEvent, bool, error) { return s.recordCallResult(dispatcherID, sourceEventID, payload, &proof) } func (s *CurrentStore) recordCallResult(dispatcherID, sourceEventID string, payload []byte, proof *CurrentUploadProof) (CurrentOutboxEvent, bool, error) { if dispatcherID == "" || sourceEventID == "" || len(sourceEventID) > 255 || len(payload) == 0 { return CurrentOutboxEvent{}, false, fmt.Errorf("%w: durable call identity and payload are required", ErrCurrentResultInvalid) } var result struct { TaskID string `json:"task_id"` CallerProfileID string `json:"caller_profile_id"` Callee string `json:"callee"` TrunkID string `json:"trunk_id"` StartedAt string `json:"started_at"` EndedAt string `json:"ended_at"` Recording json.RawMessage `json:"recording"` } if err := json.Unmarshal(payload, &result); err != nil { return CurrentOutboxEvent{}, false, fmt.Errorf("%w: decode JSON: %v", ErrCurrentResultInvalid, err) } start, err := time.Parse(time.RFC3339Nano, result.StartedAt) if err != nil { return CurrentOutboxEvent{}, false, fmt.Errorf("%w: invalid start time", ErrCurrentResultInvalid) } end, err := time.Parse(time.RFC3339Nano, result.EndedAt) if err != nil || end.Before(start) { return CurrentOutboxEvent{}, false, fmt.Errorf("%w: end precedes start or is invalid", ErrCurrentResultInvalid) } tx, err := s.db.Begin() if err != nil { return CurrentOutboxEvent{}, false, err } defer tx.Rollback() var tenantID int64 var taskID, callee, trunkID, status string var snapshotJSON []byte err = tx.QueryRow(`SELECT tenant_id,task_id,callee,COALESCE(selected_trunk_id,''),status,snapshot_json FROM dispatcher_inbox WHERE dispatcher_id=? AND event_id=?`, dispatcherID, sourceEventID).Scan(&tenantID, &taskID, &callee, &trunkID, &status, &snapshotJSON) if errors.Is(err, sql.ErrNoRows) { return CurrentOutboxEvent{}, false, errors.New("no durable approved call matches final result") } if err != nil { return CurrentOutboxEvent{}, false, fmt.Errorf("load completed call identity: %w", err) } if status != "finished" || trunkID == "" || len(snapshotJSON) == 0 { return CurrentOutboxEvent{}, false, ErrCurrentEndUnconfirmed } var snapshot struct { Task configread.CurrentTask `json:"task"` } if err := json.Unmarshal(snapshotJSON, &snapshot); err != nil { return CurrentOutboxEvent{}, false, fmt.Errorf("decode frozen call task: %w", err) } if snapshot.Task.DispatcherID != dispatcherID || snapshot.Task.TenantID != tenantID || snapshot.Task.TaskID != taskID || result.TaskID != taskID || result.Callee != callee || result.TrunkID != trunkID || result.CallerProfileID != snapshot.Task.CallerProfileID { return CurrentOutboxEvent{}, false, errors.New("final result identity differs from the approved call") } var recording struct { Status string `json:"status"` Bucket string `json:"bucket"` ObjectKey string `json:"object_key"` Format string `json:"format"` Channels int `json:"channels"` SampleRateHz int `json:"sample_rate_hz"` DurationMS int64 `json:"duration_ms"` SizeBytes int64 `json:"size_bytes"` ChecksumSHA256 string `json:"checksum_sha256"` } if err := json.Unmarshal(result.Recording, &recording); err != nil { return CurrentOutboxEvent{}, false, fmt.Errorf("%w: recording fact is invalid", ErrCurrentResultInvalid) } grant, grantErr := loadRecordingUpload(tx.QueryRow(`SELECT dispatcher_id,source_event_id,upload_id,recording_id,bucket,object_key,checksum_sha256,size_bytes,format,channels,sample_rate_hz,duration_ms,COALESCE(confirmed_at,'') FROM dispatcher_recordings WHERE dispatcher_id=? AND source_event_id=?`, dispatcherID, sourceEventID)) if grantErr != nil && !errors.Is(grantErr, ErrCurrentUploadNotFound) { return CurrentOutboxEvent{}, false, fmt.Errorf("inspect original recording target: %w", grantErr) } if recording.Status == "uploaded" { if proof == nil || grantErr != nil || proof.StatusCode < 200 || proof.StatusCode >= 300 || proof.UploadID != grant.UploadID || proof.RecordingID != grant.RecordingID || proof.SizeBytes != grant.SizeBytes || proof.SHA256 != grant.ChecksumSHA256 || recording.Bucket != grant.Bucket || recording.ObjectKey != grant.ObjectKey || recording.Format != grant.Format || recording.Channels != grant.Channels || recording.SampleRateHz != grant.SampleRateHz || recording.DurationMS != grant.DurationMS || recording.SizeBytes != grant.SizeBytes || recording.ChecksumSHA256 != grant.ChecksumSHA256 { return CurrentOutboxEvent{}, false, ErrCurrentUploadUnverified } } else if proof != nil || grantErr == nil { return CurrentOutboxEvent{}, false, ErrCurrentUploadUnverified } route, err := tenant.CurrentResultRoute(dispatcherID) if err != nil { return CurrentOutboxEvent{}, false, err } eventID := currentResultEventID(dispatcherID, sourceEventID) body, err := json.Marshal(struct { EventID string `json:"event_id"` EventType string `json:"event_type"` DispatcherID string `json:"dispatcher_id"` TenantID int64 `json:"tenant_id"` IssuedAt string `json:"issued_at"` Payload json.RawMessage `json:"payload"` }{eventID, "call.execute.result", dispatcherID, tenantID, result.EndedAt, payload}) if err != nil { return CurrentOutboxEvent{}, false, fmt.Errorf("encode final result: %w", err) } if err := contract.ValidateCurrent("mq", body); err != nil { return CurrentOutboxEvent{}, false, fmt.Errorf("%w: MQ contract: %v", ErrCurrentResultInvalid, err) } inserted, err := tx.Exec(`INSERT INTO dispatcher_outbox(dispatcher_id,event_id,event_type,routing_key,body) VALUES(?,?,?,?,?) ON CONFLICT(dispatcher_id,event_id) DO NOTHING`, dispatcherID, eventID, "call.execute.result", route.BindingKey, body) if err != nil { return CurrentOutboxEvent{}, false, fmt.Errorf("persist final result outbox: %w", err) } count, err := inserted.RowsAffected() if err != nil { return CurrentOutboxEvent{}, false, err } var stored CurrentOutboxEvent stored.EventID = eventID if err := tx.QueryRow(`SELECT event_type,routing_key,body FROM dispatcher_outbox WHERE dispatcher_id=? AND event_id=?`, dispatcherID, eventID).Scan(&stored.EventType, &stored.RoutingKey, &stored.Body); err != nil { return CurrentOutboxEvent{}, false, fmt.Errorf("inspect existing final result: %w", err) } if stored.EventType != "call.execute.result" || stored.RoutingKey != route.BindingKey || !bytes.Equal(stored.Body, body) { return CurrentOutboxEvent{}, false, ErrCurrentResultConflict } if proof != nil { if grant.ConfirmedAt != "" && count == 1 { return CurrentOutboxEvent{}, false, ErrCurrentResultConflict } confirmed, err := tx.Exec(`UPDATE dispatcher_recordings SET confirmed_at=COALESCE(confirmed_at,?) WHERE dispatcher_id=? AND source_event_id=?`, time.Now().UTC().Format(time.RFC3339Nano), dispatcherID, sourceEventID) if err != nil { return CurrentOutboxEvent{}, false, fmt.Errorf("persist confirmed recording with result: %w", err) } affected, err := confirmed.RowsAffected() if err != nil { return CurrentOutboxEvent{}, false, fmt.Errorf("inspect recording confirmation: %w", err) } if affected != 1 { return CurrentOutboxEvent{}, false, errors.New("recording confirmation lost its original target") } } if err := tx.Commit(); err != nil { return CurrentOutboxEvent{}, false, fmt.Errorf("commit final result outbox: %w", err) } return stored, count == 1, nil }