package privacy import ( "context" "encoding/json" "errors" "fmt" "time" "data-terminal-cloud/internal/audit" "data-terminal-cloud/internal/database" "data-terminal-cloud/internal/outbox" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) type AccessProcessor struct { pool *pgxpool.Pool audit *audit.Repository } func NewAccessProcessor(pool *pgxpool.Pool) (*AccessProcessor, error) { if pool == nil { return nil, errors.New("database pool is required") } return &AccessProcessor{pool: pool, audit: audit.NewRepository()}, nil } func (p *AccessProcessor) Handle(ctx context.Context, event outbox.Event) error { return database.Transact(ctx, p.pool, func(tx pgx.Tx) error { var tenantID, subjectID, publicID, status, requestType string var scopeJSON []byte if err := tx.QueryRow(ctx, `SELECT tenant_id::text,subject_id::text,public_id::text,status,request_type,scope FROM privacy_request WHERE id=$1::uuid FOR UPDATE`, event.AggregateID).Scan(&tenantID, &subjectID, &publicID, &status, &requestType, &scopeJSON); errors.Is(err, pgx.ErrNoRows) { return nil } else if err != nil { return fmt.Errorf("load privacy access request: %w", err) } if status == "COMPLETED" { return nil } if requestType != "ACCESS" { return fmt.Errorf("privacy request type %s is not handled by access processor", requestType) } if _, err := tx.Exec(ctx, `UPDATE privacy_request SET status='VERIFYING',version=version+1,updated_at=now() WHERE id=$1::uuid AND status='SUBMITTED'`, event.AggregateID); err != nil { return err } if _, err := tx.Exec(ctx, `UPDATE privacy_request SET status='EXECUTING',version=version+1,updated_at=now() WHERE id=$1::uuid AND status IN ('VERIFYING','PARTIAL_FAILED')`, event.AggregateID); err != nil { return err } if _, err := tx.Exec(ctx, `UPDATE privacy_subtask SET status='RUNNING',attempts=attempts+1,last_error=NULL,updated_at=now() WHERE privacy_request_id=$1::uuid AND subsystem='postgresql' AND target_type='subject_data_inventory'`, event.AggregateID); err != nil { return err } counts, err := collectInventory(ctx, tx, tenantID, subjectID) if err != nil { return err } var scope map[string]any if err := json.Unmarshal(scopeJSON, &scope); err != nil { return fmt.Errorf("decode privacy scope: %w", err) } result := map[string]any{ "inventory_counts": filterInventory(counts, scope), "generated_at": time.Now().UTC().Format(time.RFC3339Nano), "content_included": false, } resultJSON, err := json.Marshal(result) if err != nil { return err } if _, err := tx.Exec(ctx, `UPDATE privacy_subtask SET status='COMPLETED',completed_at=now(),updated_at=now() WHERE privacy_request_id=$1::uuid AND subsystem='postgresql' AND target_type='subject_data_inventory'`, event.AggregateID); err != nil { return err } if _, err := tx.Exec(ctx, `UPDATE privacy_request SET status='COMPLETED',result_summary=$2::jsonb,completed_at=now(),version=version+1,updated_at=now() WHERE id=$1::uuid`, event.AggregateID, resultJSON); err != nil { return err } return p.audit.Record(ctx, tx, audit.Event{TenantID: &tenantID, ActorType: "system", Action: "privacy.access.complete", ResourceType: "privacy_request", ResourceID: &publicID, Result: "success", Detail: map[string]any{"subject_digest_scope": "tenant_subject", "content_included": false}}) }) } func collectInventory(ctx context.Context, tx pgx.Tx, tenantID, subjectID string) (map[string]int64, error) { queries := map[string]string{ "profile": `SELECT count(*) FROM subject_profile WHERE tenant_id=$1::uuid AND id=$2::uuid`, "consents": `SELECT count(*) FROM consent_record WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`, "device_bindings": `SELECT count(*) FROM device_binding WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`, "conversations": `SELECT count(*) FROM conversation WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`, "transcripts": `SELECT count(*) FROM conversation_turn WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`, "summaries": `SELECT count(*) FROM conversation_summary WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`, "stories": `SELECT count(*) FROM story_draft WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`, "memories": `SELECT count(*) FROM curated_memory WHERE tenant_id=$1::uuid AND subject_id=$2::uuid`, } result := make(map[string]int64, len(queries)) for category, query := range queries { var count int64 if err := tx.QueryRow(ctx, query, tenantID, subjectID).Scan(&count); err != nil { return nil, fmt.Errorf("count privacy inventory category %s: %w", category, err) } result[category] = count } return result, nil } func filterInventory(counts map[string]int64, scope map[string]any) map[string]int64 { requested, ok := scope["categories"].([]any) if !ok || len(requested) == 0 { return counts } result := make(map[string]int64, len(requested)) for _, value := range requested { category, ok := value.(string) if !ok { continue } if count, exists := counts[category]; exists { result[category] = count } } return result }