205 lines
6.7 KiB
Go
205 lines
6.7 KiB
Go
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')
|
|
}
|