Skip to content

Commit

Permalink
Add CreateTransaction endpoint (#334)
Browse files Browse the repository at this point in the history
  • Loading branch information
AmirAgassi authored Jan 8, 2025
2 parents 6cfb2ad + 7badeff commit 8f08b68
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 3 deletions.
12 changes: 12 additions & 0 deletions backend/.sqlc/queries/transactions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- name: AddTransaction :one
INSERT INTO transactions (
id,
project_id,
company_id,
tx_hash,
from_address,
to_address,
value_amount
) VALUES (
$1, $2, $3, $4, $5, $6, $7
) RETURNING *;
59 changes: 59 additions & 0 deletions backend/db/transactions.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

162 changes: 162 additions & 0 deletions backend/internal/tests/transactions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package tests

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"bytes"
"github.com/google/uuid"
"fmt"
"golang.org/x/crypto/bcrypt"
"time"

"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"KonferCA/SPUR/internal/server"
"KonferCA/SPUR/internal/v1/v1_transactions"
"KonferCA/SPUR/db"
)

func TestTransactionEndpoints(t *testing.T) {
// Setup test environment
setupEnv()
s, err := server.New()
assert.NoError(t, err)

ctx := context.Background()

// Create test user
userID := uuid.New().String()
email := fmt.Sprintf("test-%[email protected]", uuid.New().String())
password := "password"

// Hash the password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
assert.NoError(t, err)

// Create investor user directly
_, err = s.DBPool.Exec(ctx, `
INSERT INTO users (
id,
email,
password,
role,
email_verified,
token_salt
)
VALUES ($1, $2, $3, $4, $5, gen_random_bytes(32))`,
userID, email, string(hashedPassword), db.UserRoleInvestor, true)
assert.NoError(t, err)

// Create test company
companyID, err := createTestCompany(ctx, s, userID)
assert.NoError(t, err)

// Create test project with status
projectID := uuid.New().String()
now := time.Now().Unix()

_, err = s.DBPool.Exec(ctx, `
INSERT INTO projects (
id,
company_id,
title,
description,
status,
created_at,
updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
projectID, companyID, "Test Project", "Test Description", db.ProjectStatusPending, now, now)
assert.NoError(t, err)

// Get access token
accessToken := loginAndGetToken(t, s, email, password)

t.Run("Create Transaction", func(t *testing.T) {
testCases := []struct {
name string
req v1_transactions.CreateTransactionRequest
wantCode int
wantError bool
}{
{
name: "valid transaction",
req: v1_transactions.CreateTransactionRequest{
ProjectID: projectID,
TxHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
FromAddress: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
ToAddress: "0x0987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba",
ValueAmount: "1.5",
},
wantCode: http.StatusCreated,
wantError: false,
},
{
name: "invalid project ID",
req: v1_transactions.CreateTransactionRequest{
ProjectID: "invalid-uuid",
TxHash: "0x1234567890abcdef",
FromAddress: "0xabcdef1234567890",
ToAddress: "0x0987654321fedcba",
ValueAmount: "1.5",
},
wantCode: http.StatusBadRequest,
wantError: true,
},
{
name: "missing required fields",
req: v1_transactions.CreateTransactionRequest{},
wantCode: http.StatusBadRequest,
wantError: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
jsonBody, err := json.Marshal(tc.req)
assert.NoError(t, err)

req := httptest.NewRequest(http.MethodPost, "/api/v1/transactions", bytes.NewBuffer(jsonBody))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.Header.Set(echo.HeaderAuthorization, "Bearer "+accessToken)

rec := httptest.NewRecorder()
s.GetEcho().ServeHTTP(rec, req)

assert.Equal(t, tc.wantCode, rec.Code)

if !tc.wantError {
var response v1_transactions.TransactionResponse
err = json.NewDecoder(rec.Body).Decode(&response)
assert.NoError(t, err)
assert.NotEmpty(t, response.ID)
assert.Equal(t, tc.req.ProjectID, response.ProjectID)
assert.Equal(t, companyID, response.CompanyID)
assert.Equal(t, tc.req.TxHash, response.TxHash)
assert.Equal(t, tc.req.FromAddress, response.FromAddress)
assert.Equal(t, tc.req.ToAddress, response.ToAddress)
assert.Equal(t, tc.req.ValueAmount, response.ValueAmount)
}
})
}
})

// Delete transactions
_, err = s.DBPool.Exec(ctx, "DELETE FROM transactions WHERE project_id = $1", projectID)
assert.NoError(t, err)

// Delete project
_, err = s.DBPool.Exec(ctx, "DELETE FROM projects WHERE id = $1", projectID)
assert.NoError(t, err)

// Delete company
err = removeTestCompany(ctx, companyID, s)
assert.NoError(t, err)

// Delete user
err = removeTestUser(ctx, email, s)
assert.NoError(t, err)
}
2 changes: 2 additions & 0 deletions backend/internal/v1/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"KonferCA/SPUR/internal/v1/v1_health"
"KonferCA/SPUR/internal/v1/v1_projects"
"KonferCA/SPUR/internal/v1/v1_teams"
"KonferCA/SPUR/internal/v1/v1_transactions"
)

func SetupRoutes(s interfaces.CoreServer) {
Expand All @@ -18,4 +19,5 @@ func SetupRoutes(s interfaces.CoreServer) {
v1_companies.SetupCompanyRoutes(g, s)
v1_projects.SetupRoutes(g, s)
v1_teams.SetupRoutes(g, s)
v1_transactions.SetupTransactionRoutes(g, s)
}
17 changes: 16 additions & 1 deletion backend/internal/v1/v1_transactions/routes.go
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
package v1transactions
package v1_transactions

import (
"github.com/labstack/echo/v4"
"KonferCA/SPUR/internal/interfaces"
"KonferCA/SPUR/internal/middleware"
"KonferCA/SPUR/db"
)

func SetupTransactionRoutes(g *echo.Group, s interfaces.CoreServer) {
h := &Handler{server: s}

// POST /api/v1/transactions
transactions := g.Group("/transactions")
transactions.POST("", h.handleCreateTransaction, middleware.Auth(s.GetDB(), db.UserRoleInvestor, db.UserRoleAdmin))
}
55 changes: 54 additions & 1 deletion backend/internal/v1/v1_transactions/transactions.go
Original file line number Diff line number Diff line change
@@ -1 +1,54 @@
package v1transactions
package v1_transactions

import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"KonferCA/SPUR/internal/v1/v1_common"
"KonferCA/SPUR/db"
)

func (h *Handler) handleCreateTransaction(c echo.Context) error {
var req CreateTransactionRequest
if err := v1_common.BindandValidate(c, &req); err != nil {
return err
}

// Get project to verify it exists and get company_id
project, err := h.server.GetQueries().GetProjectByIDAdmin(c.Request().Context(), req.ProjectID)
if err != nil {
return v1_common.Fail(c, http.StatusNotFound, "Project not found", err)
}

// Create numeric value for amount
var numericAmount pgtype.Numeric
if err := numericAmount.Scan(req.ValueAmount); err != nil {
return v1_common.Fail(c, http.StatusBadRequest, "Invalid value amount", err)
}

// Create transaction
tx, err := h.server.GetQueries().AddTransaction(c.Request().Context(), db.AddTransactionParams{
ID: uuid.New().String(),
ProjectID: req.ProjectID,
CompanyID: project.CompanyID,
TxHash: req.TxHash,
FromAddress: req.FromAddress,
ToAddress: req.ToAddress,
ValueAmount: numericAmount,
})
if err != nil {
return v1_common.Fail(c, http.StatusInternalServerError, "Failed to create transaction", err)
}

// Format response
return c.JSON(http.StatusCreated, TransactionResponse{
ID: tx.ID,
ProjectID: tx.ProjectID,
CompanyID: tx.CompanyID,
TxHash: tx.TxHash,
FromAddress: tx.FromAddress,
ToAddress: tx.ToAddress,
ValueAmount: req.ValueAmount,
})
}
26 changes: 25 additions & 1 deletion backend/internal/v1/v1_transactions/types.go
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
package v1transactions
package v1_transactions

import "KonferCA/SPUR/internal/interfaces"

type Handler struct {
server interfaces.CoreServer
}

type CreateTransactionRequest struct {
ProjectID string `json:"project_id" validate:"required,uuid4"`
TxHash string `json:"tx_hash" validate:"required,wallet_address"`
FromAddress string `json:"from_address" validate:"required,wallet_address"`
ToAddress string `json:"to_address" validate:"required,wallet_address"`
ValueAmount string `json:"value_amount" validate:"required,numeric"`
}

type TransactionResponse struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
CompanyID string `json:"company_id"`
TxHash string `json:"tx_hash"`
FromAddress string `json:"from_address"`
ToAddress string `json:"to_address"`
ValueAmount string `json:"value_amount"`
}

0 comments on commit 8f08b68

Please sign in to comment.