Skip to content

Commit

Permalink
✨ feat: balance endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
perebaj committed Sep 27, 2023
1 parent 2cfa09e commit f882e09
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 74 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ test:
## Run integration tests
.PHONY: integration-test
integration-test:
go test -tags integration -run="$(testcase)" ./...
go test -tags integration -run="$(testcase)" -cover ./...

## Run lint
.PHONY: lint
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The testcase variable could be used to run a specific test

## Ship a new version
`make image/publish`
`heroky container:release web -a contractus`
`heroku container:release web -a contractus`

## Logs in production
`heroku logs --tail -a contractus`
Expand Down
15 changes: 12 additions & 3 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -23,13 +24,20 @@ type Error struct {
func (e Error) Error() string {
return fmt.Sprintf("%s: %s", e.Code, e.Msg)
}

func sendErr(w http.ResponseWriter, statusCode int, err error) {
var httpErr Error
if !errors.As(err, &httpErr) {
httpErr = Error{
Code: "unknown_error",
Msg: "An unexpected error happened",
}
}
if statusCode >= 500 {
slog.Error("Internal server error", "error", err)
slog.Error("Unable to process request", "error", err.Error(), "status_code", statusCode)
}
err = Error{Code: "internal_server_error error", Msg: "Internal server error"}

send(w, statusCode, err)
send(w, statusCode, httpErr)
}

func send(w http.ResponseWriter, statusCode int, body interface{}) {
Expand Down Expand Up @@ -64,6 +72,7 @@ func parseFile(r *http.Request) (content string, err error) {
return string(contentByte), nil
}

// convert is an internal function responsible for converting the file content into transactions.
func convert(content string) (t []contractus.Transaction, err error) {
content = strings.Replace(content, "\t", "", -1)
var re = regexp.MustCompile(`(?m).*$\n`)
Expand Down
62 changes: 62 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/perebaj/contractus"
)

// TODO(JOJO) assert the body
Expand All @@ -26,3 +29,62 @@ func TestSend(t *testing.T) {
t.Errorf("expected status code %d, got %d", http.StatusOK, w.Code)
}
}

func TestConvert(t *testing.T) {
content := `12022-01-15T19:20:30-03:00CURSO DE BEM-ESTAR 0000012750JOSE CARLOS
12021-12-03T11:46:02-03:00DOMINANDO INVESTIMENTOS 0000050000MARIA CANDIDA
`
transac, err := convert(content)
if err != nil {
t.Fatal(err)
}

want := []contractus.Transaction{
{
Type: 1,
Date: time.Date(2022, 01, 15, 22, 20, 30, 0, time.UTC),
ProductDescription: "CURSO DE BEM-ESTAR",
ProductPriceCents: 12750,
SellerName: "JOSE CARLOS",
SellerType: "producer",
Action: "venda produtor",
},
{
Type: 1,
Date: time.Date(2021, 12, 03, 14, 46, 02, 0, time.UTC),
ProductDescription: "DOMINANDO INVESTIMENTOS",
ProductPriceCents: 50000,
SellerName: "MARIA CANDIDA",
SellerType: "producer",
Action: "venda produtor",
},
}

if len(transac) == len(want) {
assert(t, transac[0].Type, want[0].Type)
assert(t, transac[0].Date.Format(time.RFC3339), want[0].Date.Format(time.RFC3339))
assert(t, transac[0].ProductDescription, want[0].ProductDescription)
assert(t, transac[0].ProductPriceCents, want[0].ProductPriceCents)
assert(t, transac[0].SellerName, want[0].SellerName)
assert(t, transac[0].SellerType, want[0].SellerType)
assert(t, transac[0].Action, want[0].Action)

assert(t, transac[1].Type, want[1].Type)
assert(t, transac[1].Date.Format(time.RFC3339), want[1].Date.Format(time.RFC3339))
assert(t, transac[1].ProductDescription, want[1].ProductDescription)
assert(t, transac[1].ProductPriceCents, want[1].ProductPriceCents)
assert(t, transac[1].SellerName, want[1].SellerName)
assert(t, transac[1].SellerType, want[1].SellerType)
assert(t, transac[1].Action, want[1].Action)
} else {
t.Fatalf("expected %d transactions, got %d", len(want), len(transac))
}
}

func assert(t *testing.T, got, want interface{}) {
t.Helper()

if got != want {
t.Fatalf("got %v want %v", got, want)
}
}
51 changes: 45 additions & 6 deletions api/transactionhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import (

"github.com/go-chi/chi/v5"
"github.com/perebaj/contractus"
"github.com/perebaj/contractus/postgres"
)

type transactionStorage interface {
SaveTransaction(ctx context.Context, t []contractus.Transaction) error
Balance(ctx context.Context, sellerType, sellerName string) (*contractus.BalanceResponse, error)
}

type transactionHandler struct {
Expand All @@ -29,18 +31,55 @@ func RegisterHandler(r chi.Router, storage transactionStorage) {
}

const balanceProducer = "/balance/producer"
r.Method(http.MethodGet, balanceProducer, http.HandlerFunc(h.balance))
r.Method(http.MethodGet, balanceProducer, http.HandlerFunc(h.producerBalance))

const balanceAffiliate = "/balance/affiliate"
r.Method(http.MethodGet, balanceAffiliate, http.HandlerFunc(h.affiliateBalance))

const upload = "/upload"
r.Method(http.MethodPost, upload, http.HandlerFunc(h.upload))
}

func (s transactionHandler) balance(w http.ResponseWriter, _ *http.Request) {
t := struct {
ProducerID string `json:"producer_id"`
}{ProducerID: "123"}
func (s transactionHandler) producerBalance(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
name := query.Get("name")
if name == "" {
sendErr(w, http.StatusBadRequest, fmt.Errorf("name is required"))
return
}

b, err := s.storage.Balance(r.Context(), "producer", name)
if err != nil {
if err == postgres.ErrSellerNotFound {
sendErr(w, http.StatusNotFound, postgres.ErrSellerNotFound)
return
}
sendErr(w, http.StatusInternalServerError, err)
return
}

send(w, http.StatusOK, b)
}

func (s transactionHandler) affiliateBalance(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
name := query.Get("name")
if name == "" {
sendErr(w, http.StatusBadRequest, fmt.Errorf("name is required"))
return
}

b, err := s.storage.Balance(r.Context(), "affiliate", name)
if err != nil {
if err == postgres.ErrSellerNotFound {
sendErr(w, http.StatusNotFound, postgres.ErrSellerNotFound)
return
}
sendErr(w, http.StatusInternalServerError, err)
return
}

send(w, http.StatusOK, t)
send(w, http.StatusOK, b)
}

func (s transactionHandler) upload(w http.ResponseWriter, r *http.Request) {
Expand Down
76 changes: 25 additions & 51 deletions api/transactionhandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/go-chi/chi/v5"
"github.com/perebaj/contractus"
Expand All @@ -20,6 +19,10 @@ func (m *mockTransactionStorage) SaveTransaction(_ context.Context, _ []contract
return nil
}

func (m *mockTransactionStorage) Balance(_ context.Context, _ string, _ string) (*contractus.BalanceResponse, error) {
return nil, nil
}

func TestTransactionHandlerUpload(t *testing.T) {
var b bytes.Buffer
w := multipart.NewWriter(&b)
Expand Down Expand Up @@ -54,61 +57,32 @@ func TestTransactionHandlerUpload(t *testing.T) {
}
}

func TestConvert(t *testing.T) {
content := `12022-01-15T19:20:30-03:00CURSO DE BEM-ESTAR 0000012750JOSE CARLOS
12021-12-03T11:46:02-03:00DOMINANDO INVESTIMENTOS 0000050000MARIA CANDIDA
`
transac, err := convert(content)
if err != nil {
t.Fatal(err)
}
func TestTransactionHandlerBalanceProducer(t *testing.T) {
m := &mockTransactionStorage{}
r := chi.NewRouter()
RegisterHandler(r, m)

want := []contractus.Transaction{
{
Type: 1,
Date: time.Date(2022, 01, 15, 22, 20, 30, 0, time.UTC),
ProductDescription: "CURSO DE BEM-ESTAR",
ProductPriceCents: 12750,
SellerName: "JOSE CARLOS",
SellerType: "producer",
Action: "venda produtor",
},
{
Type: 1,
Date: time.Date(2021, 12, 03, 14, 46, 02, 0, time.UTC),
ProductDescription: "DOMINANDO INVESTIMENTOS",
ProductPriceCents: 50000,
SellerName: "MARIA CANDIDA",
SellerType: "producer",
Action: "venda produtor",
},
}
req := httptest.NewRequest(http.MethodGet, "/balance/producer?name=JOSE", nil)
resp := httptest.NewRecorder()

r.ServeHTTP(resp, req)

if len(transac) == len(want) {
assert(t, transac[0].Type, want[0].Type)
assert(t, transac[0].Date.Format(time.RFC3339), want[0].Date.Format(time.RFC3339))
assert(t, transac[0].ProductDescription, want[0].ProductDescription)
assert(t, transac[0].ProductPriceCents, want[0].ProductPriceCents)
assert(t, transac[0].SellerName, want[0].SellerName)
assert(t, transac[0].SellerType, want[0].SellerType)
assert(t, transac[0].Action, want[0].Action)

assert(t, transac[1].Type, want[1].Type)
assert(t, transac[1].Date.Format(time.RFC3339), want[1].Date.Format(time.RFC3339))
assert(t, transac[1].ProductDescription, want[1].ProductDescription)
assert(t, transac[1].ProductPriceCents, want[1].ProductPriceCents)
assert(t, transac[1].SellerName, want[1].SellerName)
assert(t, transac[1].SellerType, want[1].SellerType)
assert(t, transac[1].Action, want[1].Action)
} else {
t.Fatalf("expected %d transactions, got %d", len(want), len(transac))
if resp.Code != http.StatusOK {
t.Fatalf("expected status code %d, got %d", http.StatusOK, resp.Code)
}
}

func assert(t *testing.T, got, want interface{}) {
t.Helper()
func TestTransactionHandlerBalanceAffiliate(t *testing.T) {
m := &mockTransactionStorage{}
r := chi.NewRouter()
RegisterHandler(r, m)

if got != want {
t.Fatalf("got %v want %v", got, want)
req := httptest.NewRequest(http.MethodGet, "/balance/affiliate?name=JOSE", nil)
resp := httptest.NewRecorder()

r.ServeHTTP(resp, req)

if resp.Code != http.StatusOK {
t.Fatalf("expected status code %d, got %d", http.StatusOK, resp.Code)
}
}
40 changes: 39 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,48 @@ Basic usage examples of the contractus API

Request:

```
```bash
curl -i -X 'POST' \
'http://localhost:8080/upload' \
-H 'accept: application/json' \
-H 'Content-Type: multipart/form-data' \
-F '[email protected];type=text/plain'
```

## `/balance/affiliate?name=<>`

Request:

```bash
curl -X 'GET' \
'http://localhost:8080/balance/affiliate?name=<>' \
-H 'accept: application/json'
```

Response:

```JSON
{
"balance": 0,
"seller_name": "string"
}
```

## `/balance/producer?name=<>`

Request:

```bash
curl -X 'GET' \
'http://localhost:8080/balance/producer?name=<>' \
-H 'accept: application/json'
```

Response:

```JSON
{
"balance": 0,
"seller_name": "string"
}
```
Loading

0 comments on commit f882e09

Please sign in to comment.