fix: deliver validated AI insight responses
This commit is contained in:
173
cmd/admin-reset-ai/main.go
Normal file
173
cmd/admin-reset-ai/main.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/config"
|
||||
"git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/database"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type rowCount struct {
|
||||
Before int64 `json:"before"`
|
||||
After int64 `json:"after"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
apply := flag.Bool("apply", false, "commit the reset; without this flag the transaction is rolled back")
|
||||
keysFlag := flag.String("terminals", "T83,T88", "comma-separated exact terminal source keys")
|
||||
flag.Parse()
|
||||
keys := splitKeys(*keysFlag)
|
||||
if len(keys) != 2 || keys[0] != "T83" || keys[1] != "T88" {
|
||||
log.Fatal("this operation is deliberately scoped to exactly T83,T88")
|
||||
}
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
pool, err := database.Open(ctx, cfg.DatabaseURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer pool.Close()
|
||||
tx, err := pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
if _, err := tx.Exec(ctx, `CREATE TEMP TABLE reset_targets(terminal_id bigint PRIMARY KEY,merchant_id bigint NOT NULL,source_key text NOT NULL UNIQUE) ON COMMIT DROP`); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, key := range keys {
|
||||
var count int
|
||||
if err := tx.QueryRow(ctx, `SELECT count(*) FROM terminals WHERE source_key=$1`, key).Scan(&count); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if count != 1 {
|
||||
log.Fatalf("terminal %s resolved to %d records; refusing reset", key, count)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO reset_targets SELECT id,merchant_id,source_key FROM terminals WHERE source_key=$1`, key); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `CREATE TEMP TABLE reset_entity_ids(entity_id text PRIMARY KEY) ON COMMIT DROP`); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO reset_entity_ids
|
||||
SELECT id FROM growth_coach_profiles WHERE terminal_id IN(SELECT terminal_id FROM reset_targets)
|
||||
UNION SELECT id FROM growth_coach_plans WHERE terminal_id IN(SELECT terminal_id FROM reset_targets)
|
||||
UNION SELECT id FROM growth_coach_recommendations WHERE terminal_id IN(SELECT terminal_id FROM reset_targets)
|
||||
UNION SELECT id FROM golden_steps WHERE terminal_id IN(SELECT terminal_id FROM reset_targets)
|
||||
UNION SELECT id FROM monthly_goals WHERE terminal_id IN(SELECT terminal_id FROM reset_targets)
|
||||
UNION SELECT s.id FROM goal_subtasks s JOIN monthly_goals g ON g.id=s.goal_id WHERE g.terminal_id IN(SELECT terminal_id FROM reset_targets)
|
||||
UNION SELECT p.id FROM goal_deadline_proposals p JOIN monthly_goals g ON g.id=p.goal_id WHERE g.terminal_id IN(SELECT terminal_id FROM reset_targets)
|
||||
UNION SELECT id FROM growth_coach_jobs WHERE terminal_id IN(SELECT terminal_id FROM reset_targets)
|
||||
UNION SELECT id FROM growth_coach_difficulty_proposals WHERE terminal_id IN(SELECT terminal_id FROM reset_targets)`); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
counts := map[string]rowCount{}
|
||||
for table, predicate := range resetScopes() {
|
||||
before, err := countRows(ctx, tx, table, predicate)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
counts[table] = rowCount{Before: before}
|
||||
}
|
||||
statements := []string{
|
||||
`INSERT INTO ai_generation_holds(terminal_id,merchant_id,reason) SELECT terminal_id,merchant_id,'ONBOARDING_REQUIRED' FROM reset_targets ON CONFLICT(terminal_id) DO UPDATE SET reason='ONBOARDING_REQUIRED',created_at=now()`,
|
||||
`DELETE FROM growth_report_idempotency i USING growth_reports r,reset_targets t WHERE i.report_id=r.id AND r.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM growth_reports r USING reset_targets t WHERE r.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM ai_insight_refresh_attempts a USING reset_targets t WHERE a.merchant_id=t.merchant_id AND a.terminal_source_key=t.source_key`,
|
||||
`DELETE FROM ai_insight_snapshots s USING reset_targets t WHERE s.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM goal_deadline_proposals p USING monthly_goals g,reset_targets t WHERE p.goal_id=g.id AND g.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM goal_subtasks s USING monthly_goals g,reset_targets t WHERE s.goal_id=g.id AND g.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM monthly_goals g USING reset_targets t WHERE g.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM growth_coach_progress_snapshots p USING golden_steps s,reset_targets t WHERE p.action_id=s.id AND s.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM golden_steps s USING reset_targets t WHERE s.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM growth_coach_outcome_reviews o USING growth_coach_plans p,reset_targets t WHERE o.plan_id=p.id AND p.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM growth_coach_recommendations r USING reset_targets t WHERE r.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM growth_coach_events e USING reset_targets t WHERE e.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM growth_coach_difficulty_proposals p USING reset_targets t WHERE p.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM growth_coach_jobs j USING reset_targets t WHERE j.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM growth_coach_plans p USING reset_targets t WHERE p.terminal_id=t.terminal_id`,
|
||||
`DELETE FROM growth_coach_idempotency i USING reset_targets t WHERE i.merchant_id=t.merchant_id AND (i.response::text LIKE '%"terminalId":"'||t.source_key||'"%' OR EXISTS(SELECT 1 FROM reset_entity_ids e WHERE i.response::text LIKE '%'||e.entity_id||'%'))`,
|
||||
`DELETE FROM growth_coach_profiles p USING reset_targets t WHERE p.terminal_id=t.terminal_id`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := tx.Exec(ctx, statement); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
for table, predicate := range resetScopes() {
|
||||
after, err := countRows(ctx, tx, table, predicate)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
value := counts[table]
|
||||
value.After = after
|
||||
counts[table] = value
|
||||
}
|
||||
var holds int64
|
||||
if err := tx.QueryRow(ctx, `SELECT count(*) FROM ai_generation_holds h JOIN reset_targets t ON t.terminal_id=h.terminal_id WHERE h.reason='ONBOARDING_REQUIRED'`).Scan(&holds); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
result := map[string]any{"mode": "dry-run", "terminals": keys, "rows": counts, "paymentsDeleted": 0, "generationHolds": holds}
|
||||
if *apply {
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
result["mode"] = "applied"
|
||||
} else if err := tx.Rollback(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
encoded, _ := json.MarshalIndent(result, "", " ")
|
||||
_, _ = os.Stdout.Write(append(encoded, '\n'))
|
||||
}
|
||||
|
||||
func splitKeys(value string) []string {
|
||||
parts := strings.Split(value, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if item := strings.TrimSpace(part); item != "" {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func resetScopes() map[string]string {
|
||||
return map[string]string{
|
||||
"growth_reports": `terminal_id IN(SELECT terminal_id FROM reset_targets)`,
|
||||
"growth_report_idempotency": `report_id IN(SELECT id FROM growth_reports WHERE terminal_id IN(SELECT terminal_id FROM reset_targets))`,
|
||||
"ai_insight_snapshots": `terminal_id IN(SELECT terminal_id FROM reset_targets)`,
|
||||
"ai_insight_jobs": `snapshot_id IN(SELECT id FROM ai_insight_snapshots WHERE terminal_id IN(SELECT terminal_id FROM reset_targets))`,
|
||||
"ai_insight_events": `job_id IN(SELECT id FROM ai_insight_jobs WHERE snapshot_id IN(SELECT id FROM ai_insight_snapshots WHERE terminal_id IN(SELECT terminal_id FROM reset_targets)))`,
|
||||
"ai_insight_refresh_attempts": `(merchant_id,terminal_source_key) IN(SELECT merchant_id,source_key FROM reset_targets)`,
|
||||
"growth_coach_profiles": `terminal_id IN(SELECT terminal_id FROM reset_targets)`,
|
||||
"growth_coach_plans": `terminal_id IN(SELECT terminal_id FROM reset_targets)`,
|
||||
"growth_coach_recommendations": `terminal_id IN(SELECT terminal_id FROM reset_targets)`,
|
||||
"golden_steps": `terminal_id IN(SELECT terminal_id FROM reset_targets)`,
|
||||
"monthly_goals": `terminal_id IN(SELECT terminal_id FROM reset_targets)`,
|
||||
"goal_subtasks": `goal_id IN(SELECT id FROM monthly_goals WHERE terminal_id IN(SELECT terminal_id FROM reset_targets))`,
|
||||
"goal_deadline_proposals": `goal_id IN(SELECT id FROM monthly_goals WHERE terminal_id IN(SELECT terminal_id FROM reset_targets))`,
|
||||
"growth_coach_progress_snapshots": `action_id IN(SELECT id FROM golden_steps WHERE terminal_id IN(SELECT terminal_id FROM reset_targets))`,
|
||||
"growth_coach_jobs": `terminal_id IN(SELECT terminal_id FROM reset_targets)`,
|
||||
"growth_coach_events": `terminal_id IN(SELECT terminal_id FROM reset_targets)`,
|
||||
"growth_coach_difficulty_proposals": `terminal_id IN(SELECT terminal_id FROM reset_targets)`,
|
||||
"growth_coach_outcome_reviews": `plan_id IN(SELECT id FROM growth_coach_plans WHERE terminal_id IN(SELECT terminal_id FROM reset_targets))`,
|
||||
"growth_coach_idempotency": `EXISTS(SELECT 1 FROM reset_targets t WHERE growth_coach_idempotency.merchant_id=t.merchant_id AND (growth_coach_idempotency.response::text LIKE '%"terminalId":"'||t.source_key||'"%' OR EXISTS(SELECT 1 FROM reset_entity_ids e WHERE growth_coach_idempotency.response::text LIKE '%'||e.entity_id||'%')))`,
|
||||
}
|
||||
}
|
||||
|
||||
func countRows(ctx context.Context, tx pgx.Tx, table, predicate string) (int64, error) {
|
||||
var count int64
|
||||
err := tx.QueryRow(ctx, fmt.Sprintf("SELECT count(*) FROM %s WHERE %s", table, predicate)).Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
Reference in New Issue
Block a user