-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add CreateTransaction endpoint (#334)
- Loading branch information
Showing
7 changed files
with
330 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 *; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |