package store import ( "database/sql" "encoding/json" "errors" "fmt" "time" "git.ipao.vip/rogee/go-sip/internal/contract" ) var ErrQuerySnapshotTooLarge = errors.New("query snapshot exceeds MQ message budget") func callQuerySnapshot(tx *sql.Tx, request contract.ServiceMessage, callID string, now time.Time) (map[string]any, error) { rows, err := tx.Query(`SELECT body,status FROM outbox WHERE tenant_key=? AND json_extract(body,'$.tenant_id')=? AND json_extract(body,'$.dispatcher_id')=? AND json_extract(body,'$.event_type') IS NOT NULL AND json_extract(body,'$.payload.call_id')=? ORDER BY json_extract(body,'$.occurred_at'),event_id`, request.TenantKey, request.TenantID, request.DispatcherID, callID) if err != nil { return nil, err } defer rows.Close() attempts, transcripts, recordings := []json.RawMessage{}, []json.RawMessage{}, []json.RawMessage{} delivery := map[string]int{"pending": 0, "retry": 0, "dispatching": 0, "published": 0} var current map[string]any var currentVersion int64 = -1 var finished map[string]any var finishedVersion int64 = -1 count, size := 0, 0 for rows.Next() { var raw []byte var status string if err := rows.Scan(&raw, &status); err != nil { return nil, err } if _, ok := delivery[status]; !ok { return nil, fmt.Errorf("unknown outbox delivery state %q", status) } delivery[status]++ count++ var event struct { EventType string `json:"event_type"` Payload json.RawMessage `json:"payload"` } if err := json.Unmarshal(raw, &event); err != nil { return nil, err } switch event.EventType { case "call.status", "call.finished": var fields map[string]any if err := json.Unmarshal(event.Payload, &fields); err != nil { return nil, err } var state struct { Version int64 `json:"call_version"` } if err := json.Unmarshal(event.Payload, &state); err != nil { return nil, err } if state.Version < 1 { return nil, errors.New("call fact has no positive call_version") } if event.EventType == "call.status" { attempts = append(attempts, json.RawMessage(raw)) size += len(raw) if state.Version >= currentVersion { current, currentVersion = fields, state.Version } } else if state.Version >= finishedVersion { finished, finishedVersion = fields, state.Version } case "transcript.updated", "transcript.failed": transcripts = append(transcripts, json.RawMessage(raw)) size += len(raw) case "recording.uploaded", "recording.failed": recordings = append(recordings, json.RawMessage(raw)) size += len(raw) } if size > 256<<10 { return nil, ErrQuerySnapshotTooLarge } } if err := rows.Err(); err != nil { return nil, err } if count == 0 { return nil, sql.ErrNoRows } if finished != nil && finishedVersion >= currentVersion { current, currentVersion = finished, finishedVersion // A persisted call.finished fact establishes that the call has ended. current["call_state"] = "ended" } if current == nil { return nil, errors.New("call facts contain no state/version evidence") } result := map[string]any{"call_id": callID, "call_version": currentVersion, "snapshot_at": now.Format(time.RFC3339Nano)} result["attempts"] = attempts result["transcript"] = map[string]any{"events": transcripts} result["recordings"] = recordings result["delivery"] = delivery for _, key := range []string{"execution_id", "task_id", "task_item_id", "call_state", "reason_code", "outcome", "started_at", "ended_at", "duration_ms"} { if value, ok := current[key]; ok { result[key] = value } } return result, nil }