package mysql import ( "context" "database/sql" "encoding/json" "errors" "hyapp/pkg/appcode" "hyapp/pkg/xerr" ) type transactionRow struct { TransactionID string MetadataJSON string } func (r *Repository) lookupTransaction(ctx context.Context, tx *sql.Tx, commandID string, requestHash string, bizType string) (transactionRow, bool, error) { return r.lookupTransactionWithConflictCode(ctx, tx, commandID, requestHash, bizType, xerr.LedgerConflict) } func (r *Repository) lookupTransactionWithConflictCode(ctx context.Context, tx *sql.Tx, commandID string, requestHash string, bizType string, conflictCode xerr.Code) (transactionRow, bool, error) { row := tx.QueryRowContext(ctx, `SELECT transaction_id, request_hash, biz_type, COALESCE(CAST(metadata_json AS CHAR), '{}') FROM wallet_transactions WHERE app_code = ? AND command_id = ? FOR UPDATE`, appcode.FromContext(ctx), commandID, ) var txRow transactionRow var storedHash string var storedBizType string if err := row.Scan(&txRow.TransactionID, &storedHash, &storedBizType, &txRow.MetadataJSON); err != nil { if errors.Is(err, sql.ErrNoRows) { return transactionRow{}, false, nil } return transactionRow{}, false, err } if storedHash != requestHash || storedBizType != bizType { return transactionRow{}, true, xerr.New(conflictCode, "wallet command idempotency conflict") } return txRow, true, nil } func (r *Repository) insertTransaction(ctx context.Context, tx *sql.Tx, transactionID string, commandID string, bizType string, requestHash string, externalRef string, metadata any, nowMs int64) error { metadataJSON, err := json.Marshal(metadata) if err != nil { return err } _, err = tx.ExecContext(ctx, `INSERT INTO wallet_transactions ( app_code, transaction_id, command_id, biz_type, status, request_hash, external_ref, metadata_json, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, appcode.FromContext(ctx), transactionID, commandID, bizType, transactionStatusSucceeded, requestHash, externalRef, string(metadataJSON), nowMs, nowMs, ) return err }