package taskdetail import ( "context" "database/sql" "errors" "fmt" "time" ) type SQLiteStore struct{ database *sql.DB } func NewSQLiteStore(database *sql.DB) (*SQLiteStore, error) { if database == nil { return nil, errors.New("task detail database is required") } if _, err := database.Exec("SELECT storage_key FROM evidence_assets LIMIT 1"); err != nil { return nil, fmt.Errorf("task detail migration is not available: %w", err) } return &SQLiteStore{database: database}, nil } func (store *SQLiteStore) Get(ctx context.Context, id string) (Detail, error) { if !validUUID(id) { return Detail{}, ErrNotFound } tx, err := store.database.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}) if err != nil { return Detail{}, err } defer tx.Rollback() var detail Detail var created, updated string err = tx.QueryRowContext(ctx, `SELECT id, source, title, goods_id, sku_color, sku_size, quantity, max_total_price, status, version, created_at, updated_at FROM tasks WHERE id = ?`, id).Scan( &detail.Task.ID, &detail.Task.Source, &detail.Task.Title, &detail.Task.GoodsID, &detail.Task.SKUColor, &detail.Task.SKUSize, &detail.Task.Quantity, &detail.Task.MaxTotalPrice, &detail.Task.Status, &detail.Task.Version, &created, &updated, ) if errors.Is(err, sql.ErrNoRows) { return Detail{}, ErrNotFound } if err != nil { return Detail{}, err } if detail.Task.CreatedAt, err = parseTime(created); err != nil { return Detail{}, err } if detail.Task.UpdatedAt, err = parseTime(updated); err != nil { return Detail{}, err } if detail.Authorizations, err = readAuthorizations(ctx, tx, id); err != nil { return Detail{}, err } if detail.Attempts, err = readAttempts(ctx, tx, id); err != nil { return Detail{}, err } if detail.Submissions, err = readSubmissions(ctx, tx, id); err != nil { return Detail{}, err } if detail.Evidence, err = readEvidence(ctx, tx, id); err != nil { return Detail{}, err } if err := tx.Commit(); err != nil { return Detail{}, err } return detail, nil } func readAuthorizations(ctx context.Context, tx *sql.Tx, taskID string) ([]Authorization, error) { rows, err := tx.QueryContext(ctx, `SELECT id, task_version, total_price_cap, status, created_by, created_at, expires_at FROM order_authorizations WHERE task_id = ? ORDER BY created_at DESC, id DESC`, taskID) if err != nil { return nil, err } defer rows.Close() result := []Authorization{} for rows.Next() { var item Authorization var created, expires string if err := rows.Scan(&item.ID, &item.TaskVersion, &item.TotalPriceCap, &item.Status, &item.CreatedBy, &created, &expires); err != nil { return nil, err } if item.CreatedAt, err = parseTime(created); err != nil { return nil, err } if item.ExpiresAt, err = parseTime(expires); err != nil { return nil, err } result = append(result, item) } return result, rows.Err() } func readAttempts(ctx context.Context, tx *sql.Tx, taskID string) ([]Attempt, error) { rows, err := tx.QueryContext(ctx, `SELECT id, authorization_id, claim_generation, status, gate1_unit_price, gate2_unit_price, quantity_read, confirm_amount, failure_code, started_at, finished_at FROM purchase_attempts WHERE task_id = ? ORDER BY started_at DESC, id DESC`, taskID) if err != nil { return nil, err } defer rows.Close() result := []Attempt{} for rows.Next() { var item Attempt var gate1, gate2, confirm, failure, finished sql.NullString var quantity sql.NullInt64 var started string if err := rows.Scan(&item.ID, &item.AuthorizationID, &item.ClaimGeneration, &item.Status, &gate1, &gate2, &quantity, &confirm, &failure, &started, &finished); err != nil { return nil, err } item.Gate1UnitPrice, item.Gate2UnitPrice, item.ConfirmAmount, item.FailureCode = stringPointer(gate1), stringPointer(gate2), stringPointer(confirm), stringPointer(failure) if quantity.Valid { value := int(quantity.Int64) item.QuantityRead = &value } if item.StartedAt, err = parseTime(started); err != nil { return nil, err } if finished.Valid { value, parseErr := parseTime(finished.String) if parseErr != nil { return nil, parseErr } item.FinishedAt = &value } result = append(result, item) } return result, rows.Err() } func readSubmissions(ctx context.Context, tx *sql.Tx, taskID string) ([]Submission, error) { rows, err := tx.QueryContext(ctx, `SELECT id, authorization_id, attempt_id, status, gate1_unit_price, gate2_unit_price, quantity_read, confirm_amount, created_at, resolved_at FROM order_submissions WHERE task_id = ? ORDER BY created_at DESC, id DESC`, taskID) if err != nil { return nil, err } defer rows.Close() result := []Submission{} for rows.Next() { var item Submission var created string var resolved sql.NullString if err := rows.Scan(&item.ID, &item.AuthorizationID, &item.AttemptID, &item.Status, &item.Gate1UnitPrice, &item.Gate2UnitPrice, &item.QuantityRead, &item.ConfirmAmount, &created, &resolved); err != nil { return nil, err } if item.CreatedAt, err = parseTime(created); err != nil { return nil, err } if resolved.Valid { value, parseErr := parseTime(resolved.String) if parseErr != nil { return nil, parseErr } item.ResolvedAt = &value } result = append(result, item) } return result, rows.Err() } func readEvidence(ctx context.Context, tx *sql.Tx, taskID string) ([]Evidence, error) { rows, err := tx.QueryContext(ctx, `SELECT id, attempt_id, kind, privacy_tier, sha256, byte_size, content_type, width_px, height_px, captured_at FROM evidence_assets WHERE task_id = ? ORDER BY captured_at, created_at, id`, taskID) if err != nil { return nil, err } defer rows.Close() result := []Evidence{} for rows.Next() { var item Evidence var captured string if err := rows.Scan(&item.ID, &item.AttemptID, &item.Kind, &item.PrivacyTier, &item.SHA256, &item.ByteSize, &item.ContentType, &item.Width, &item.Height, &captured); err != nil { return nil, err } if item.CapturedAt, err = parseTime(captured); err != nil { return nil, err } result = append(result, item) } return result, rows.Err() } func parseTime(value string) (time.Time, error) { return time.Parse(time.RFC3339Nano, value) } func stringPointer(value sql.NullString) *string { if !value.Valid { return nil } copy := value.String return © } func validUUID(value string) bool { if len(value) != 36 { return false } for index, character := range value { if index == 8 || index == 13 || index == 18 || index == 23 { if character != '-' { return false } continue } if !(character >= '0' && character <= '9' || character >= 'a' && character <= 'f') { return false } } return value[14] == '4' && (value[19] == '8' || value[19] == '9' || value[19] == 'a' || value[19] == 'b') }