+5
.envrc
+5
.envrc
+1
-1
.gitignore
+1
-1
.gitignore
+17
Dockerfile
+17
Dockerfile
+7
Dockerfile-stubby
+7
Dockerfile-stubby
+47
README.org
+47
README.org
···
1
+
* Payment Gateway
2
+
3
+
4
+
** Getting started
5
+
6
+
*** Prerequisites
7
+
8
+
- [[https://hurl.dev][hurl]] :: CLI tool that runs http requests, similar to curl
9
+
- [[https://docs.docker.com/desktop/][docker]] :: Using docker to run project and mocks
10
+
- [[https://just.systems/][just]] :: CLI runner
11
+
- [[https://jqlang.org/][jq]] :: (Optional) CLI tool to query JSON data
12
+
13
+
If you're using nix, you can find a flake in the root of the project and run =nix develop= to download all the tools (or allow [[https://direnv.net/][direnv]] to auto import in your path)
14
+
15
+
*** Usage
16
+
17
+
Run docker compose to start mocks, the mocks will use ports =8002= and =8003= and the were created using [[https://github.com/mrak/stubby4node][stubby4node]]
18
+
19
+
#+begin_src shell
20
+
docker compose up
21
+
#+end_src
22
+
23
+
In another terminal, bulid and run the project with =just=, it will start the project in port =8000=
24
+
25
+
#+begin_src shell
26
+
just run
27
+
#+end_src
28
+
29
+
In other terminal, make the request with hurl
30
+
31
+
#+begin_src shell
32
+
hurl request.hurl
33
+
#+end_src
34
+
35
+
In case you want to see the response with json format, you can use jq
36
+
37
+
#+begin_src shell
38
+
hurl request.hurl | jq .
39
+
#+end_src
40
+
41
+
** Tests
42
+
43
+
To run all tests, you can run the command that it will run all tests from the project:
44
+
45
+
#+begin_src shell
46
+
just test
47
+
#+end_src
+30
-5
cmd/app/main.go
+30
-5
cmd/app/main.go
···
1
1
package main
2
2
3
3
import (
4
-
"log"
4
+
"context"
5
5
"os"
6
+
"os/signal"
6
7
8
+
"github.com/Tulkdan/payment-gateway/internal/providers"
7
9
"github.com/Tulkdan/payment-gateway/internal/service"
8
10
"github.com/Tulkdan/payment-gateway/internal/web"
11
+
"go.uber.org/zap"
9
12
)
10
13
11
14
func getEnv(key, defaultValue string) string {
···
16
19
}
17
20
18
21
func main() {
19
-
paymentsService := service.NewPaymentService()
22
+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
23
+
defer stop()
20
24
21
-
server := web.NewServer(paymentsService, "8000")
25
+
logger, _ := zap.NewDevelopment()
26
+
defer logger.Sync()
27
+
28
+
providers := providers.NewUseProviders([]providers.Provider{
29
+
providers.NewBraintreeProvider(getEnv("BRAINTREE_URL", "http://localhost:8001"), logger),
30
+
providers.NewStripeProvider(getEnv("STRIPE_URL", "http://localhost:8002"), logger),
31
+
}, logger)
32
+
paymentsService := service.NewPaymentService(providers)
33
+
34
+
port := getEnv("PORT", "8000")
35
+
server := web.NewServer(paymentsService, port, logger)
22
36
server.ConfigureRouter()
23
37
24
-
if err := server.Start(); err != nil {
25
-
log.Fatal("Error starting server: ", err)
38
+
srvErr := make(chan error, 1)
39
+
go func() {
40
+
logger.Info("Starting server", zap.String("port", port))
41
+
srvErr <- server.Start(ctx)
42
+
}()
43
+
44
+
select {
45
+
case <-srvErr:
46
+
return
47
+
case <-ctx.Done():
48
+
stop()
26
49
}
50
+
51
+
server.Shutdown()
27
52
}
+16
docker-compose.yml
+16
docker-compose.yml
···
1
+
services:
2
+
stripe:
3
+
build:
4
+
dockerfile: $PWD/Dockerfile-stubby
5
+
ports:
6
+
- "8002:8882"
7
+
volumes:
8
+
- $PWD/mocks/stripe.yml:/app/mock.yml
9
+
10
+
braintree:
11
+
build:
12
+
dockerfile: $PWD/Dockerfile-stubby
13
+
ports:
14
+
- "8001:8882"
15
+
volumes:
16
+
- $PWD/mocks/braintree.yml:/app/mock.yml
+61
flake.lock
+61
flake.lock
···
1
+
{
2
+
"nodes": {
3
+
"flake-utils": {
4
+
"inputs": {
5
+
"systems": "systems"
6
+
},
7
+
"locked": {
8
+
"lastModified": 1731533236,
9
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
10
+
"owner": "numtide",
11
+
"repo": "flake-utils",
12
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
13
+
"type": "github"
14
+
},
15
+
"original": {
16
+
"owner": "numtide",
17
+
"repo": "flake-utils",
18
+
"type": "github"
19
+
}
20
+
},
21
+
"nixpkgs": {
22
+
"locked": {
23
+
"lastModified": 1752012998,
24
+
"narHash": "sha256-Q82Ms+FQmgOBkdoSVm+FBpuFoeUAffNerR5yVV7SgT8=",
25
+
"owner": "NixOS",
26
+
"repo": "nixpkgs",
27
+
"rev": "2a2130494ad647f953593c4e84ea4df839fbd68c",
28
+
"type": "github"
29
+
},
30
+
"original": {
31
+
"owner": "NixOS",
32
+
"ref": "nixpkgs-unstable",
33
+
"repo": "nixpkgs",
34
+
"type": "github"
35
+
}
36
+
},
37
+
"root": {
38
+
"inputs": {
39
+
"flake-utils": "flake-utils",
40
+
"nixpkgs": "nixpkgs"
41
+
}
42
+
},
43
+
"systems": {
44
+
"locked": {
45
+
"lastModified": 1681028828,
46
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
47
+
"owner": "nix-systems",
48
+
"repo": "default",
49
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
50
+
"type": "github"
51
+
},
52
+
"original": {
53
+
"owner": "nix-systems",
54
+
"repo": "default",
55
+
"type": "github"
56
+
}
57
+
}
58
+
},
59
+
"root": "root",
60
+
"version": 7
61
+
}
+28
flake.nix
+28
flake.nix
···
1
+
{
2
+
description = "A Nix-flake-based Golang development environment";
3
+
4
+
inputs = {
5
+
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
6
+
7
+
flake-utils.url = "github:numtide/flake-utils";
8
+
};
9
+
10
+
outputs = { self, nixpkgs, flake-utils }:
11
+
flake-utils.lib.eachDefaultSystem (system:
12
+
let
13
+
inherit (pkgs.lib) optional optionals;
14
+
15
+
pkgs = import nixpkgs { inherit system; };
16
+
in
17
+
{
18
+
devShells.default = pkgs.mkShell {
19
+
buildInputs = with pkgs; [
20
+
go
21
+
hurl
22
+
just
23
+
jq
24
+
];
25
+
};
26
+
}
27
+
);
28
+
}
+5
go.mod
+5
go.mod
+4
go.sum
+4
go.sum
···
1
1
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
2
2
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
3
+
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
4
+
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
5
+
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
6
+
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+13
-1
internal/dto/payments.go
+13
-1
internal/dto/payments.go
···
1
1
package dto
2
2
3
+
import "github.com/google/uuid"
4
+
3
5
type PaymentCardInput struct {
4
6
Number string `json:"number"`
5
7
HolderName string `json:"holderName"`
···
17
19
}
18
20
19
21
type PaymentOutput struct {
20
-
Message string `json:"message"`
22
+
Id uuid.UUID `json:"id"`
23
+
CardId uuid.UUID `json:"cardId"`
24
+
CurrentAmount uint `json:"currentAmount"`
25
+
}
26
+
27
+
func NewPaymentOutput(id, cardId uuid.UUID, currentAmount uint) *PaymentOutput {
28
+
return &PaymentOutput{
29
+
Id: id,
30
+
CardId: cardId,
31
+
CurrentAmount: currentAmount,
32
+
}
21
33
}
+28
-7
internal/providers/braintree.go
+28
-7
internal/providers/braintree.go
···
2
2
3
3
import (
4
4
"bytes"
5
+
"context"
5
6
"encoding/json"
6
7
"net/http"
7
8
8
9
"github.com/Tulkdan/payment-gateway/internal/domain"
9
10
"github.com/google/uuid"
11
+
"go.uber.org/zap"
10
12
)
11
13
12
14
type BraintreeProvider struct {
13
-
Url string
15
+
Url string
16
+
logger *zap.Logger
14
17
}
15
18
16
-
func NewBraintreeProvider(url string) *BraintreeProvider {
17
-
return &BraintreeProvider{Url: url}
19
+
func NewBraintreeProvider(url string, logger *zap.Logger) *BraintreeProvider {
20
+
return &BraintreeProvider{Url: url, logger: logger.Named("BraintreeProvider")}
18
21
}
19
22
20
23
type BraintreeChargeCard struct {
···
37
40
PaymentMethod BraintreeChargePaymentMethod `json:"paymentMethod"`
38
41
}
39
42
40
-
func (b BraintreeProvider) Charge(request *domain.Payment) (*domain.Provider, error) {
43
+
func (b *BraintreeProvider) Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error) {
44
+
url := b.Url + "/transactions"
45
+
46
+
b.logger.Debug("Making request to charge",
47
+
zap.String("url", url))
48
+
41
49
body := b.createChargeBody(request)
42
-
response, err := http.Post(b.Url, "application/json", bytes.NewBuffer(body))
50
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, b.Url+"/charges", bytes.NewBuffer(body))
51
+
if err != nil {
52
+
return nil, err
53
+
}
54
+
55
+
req.Header.Set("Content-Type", "application/json")
56
+
req.Header.Set("transaction-id", ctx.Value("request-id").(string))
57
+
58
+
response, err := http.DefaultClient.Do(req)
43
59
if err != nil {
44
60
return nil, err
45
61
}
62
+
defer response.Body.Close()
46
63
47
64
return b.responseCharge(response)
48
65
}
49
66
50
-
func (b BraintreeProvider) createChargeBody(request *domain.Payment) []byte {
67
+
func (b *BraintreeProvider) createChargeBody(request *domain.Payment) []byte {
51
68
toSend := &BraintreeCharge{
52
69
Amount: request.Amount,
53
70
Currency: request.Currency,
···
79
96
CardId uuid.UUID `json:"cardId"`
80
97
}
81
98
82
-
func (b BraintreeProvider) responseCharge(response *http.Response) (*domain.Provider, error) {
99
+
func (b *BraintreeProvider) responseCharge(response *http.Response) (*domain.Provider, error) {
83
100
var data BraintreeChargeResponse
84
101
if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
85
102
return nil, err
···
108
125
}
109
126
return providerResponse, nil
110
127
}
128
+
129
+
func (b *BraintreeProvider) GetName() string {
130
+
return "Braintree Provider"
131
+
}
+9
-2
internal/providers/braintree_test.go
+9
-2
internal/providers/braintree_test.go
···
1
1
package providers_test
2
2
3
3
import (
4
+
"context"
4
5
"encoding/json"
5
6
"net/http"
6
7
"net/http/httptest"
···
11
12
"github.com/Tulkdan/payment-gateway/internal/domain"
12
13
"github.com/Tulkdan/payment-gateway/internal/providers"
13
14
"github.com/google/uuid"
15
+
"go.uber.org/zap"
14
16
)
15
17
16
18
func TestBraintree(t *testing.T) {
17
19
t.Run("should make request to url", func(t *testing.T) {
20
+
logger, _ := zap.NewDevelopment()
21
+
defer logger.Sync()
22
+
18
23
id, _ := uuid.Parse("2ee70bcb-5cb9-4412-a35f-c2a15fb88ef1")
19
24
cardId, _ := uuid.Parse("ed6ecd4c-81d5-4e63-bb12-99439ae559e7")
25
+
ctx := context.WithValue(t.Context(), "request-id", uuid.New().String())
26
+
20
27
serverResponse := &providers.BraintreeChargeResponse{
21
28
Id: id,
22
29
CreatedAt: time.Now().Format("YYYY-MM-DD"),
···
53
60
CardId: cardId,
54
61
}
55
62
56
-
provider := providers.NewBraintreeProvider(server.URL)
57
-
response, err := provider.Charge(charge)
63
+
provider := providers.NewBraintreeProvider(server.URL, logger)
64
+
response, err := provider.Charge(ctx, charge)
58
65
59
66
if err != nil {
60
67
t.Fatalf("got an error but didn't want one %q", err)
+40
-11
internal/providers/provider.go
+40
-11
internal/providers/provider.go
···
1
1
package providers
2
2
3
3
import (
4
+
"context"
4
5
"errors"
5
6
"time"
6
7
7
8
"github.com/Tulkdan/payment-gateway/internal/domain"
9
+
"go.uber.org/zap"
8
10
)
9
11
10
-
var thirtySecondTimout = 30 * time.Second
12
+
var thirtySecondTimout = 5 * time.Second
11
13
12
14
type Provider interface {
13
-
Charge(request *domain.Payment) (*domain.Provider, error)
15
+
GetName() string
16
+
Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error)
14
17
}
15
18
16
19
type UseProviders struct {
17
20
providers []Provider
18
21
timeout time.Duration
22
+
logger *zap.Logger
19
23
}
20
24
21
-
func NewUseProviders(providers []Provider) *UseProviders {
22
-
return ConfigurableUseProvider(providers, thirtySecondTimout)
25
+
func NewUseProviders(providers []Provider, logger *zap.Logger) *UseProviders {
26
+
return ConfigurableUseProvider(providers, logger, thirtySecondTimout)
23
27
}
24
28
25
-
func ConfigurableUseProvider(providers []Provider, timeout time.Duration) *UseProviders {
29
+
func ConfigurableUseProvider(providers []Provider, logger *zap.Logger, timeout time.Duration) *UseProviders {
26
30
return &UseProviders{
27
31
providers: providers,
32
+
logger: logger.Named("UseProviders"),
28
33
timeout: timeout,
29
34
}
30
35
}
31
36
32
-
func (p *UseProviders) Payment(payment *domain.Payment) (*domain.Provider, error) {
37
+
func (p *UseProviders) Payment(ctx context.Context, payment *domain.Payment) (*domain.Provider, error) {
33
38
var err error = nil
34
39
35
40
for _, provider := range p.providers {
41
+
requestCtx, cancel := context.WithTimeout(ctx, p.timeout)
42
+
defer cancel()
43
+
44
+
dataCh, errCh := p.charge(requestCtx, payment, provider)
36
45
select {
37
-
case data := <-charge(payment, provider):
46
+
case data := <-dataCh:
47
+
p.logger.Debug("[Payment] Received request successfully",
48
+
zap.String("provider", provider.GetName()))
49
+
38
50
return data, nil
51
+
case error := <-errCh:
52
+
p.logger.Error("[Payment] Received request with error",
53
+
zap.String("provider", provider.GetName()),
54
+
zap.String("error", error.Error()))
55
+
56
+
err = error
57
+
continue
39
58
case <-time.After(p.timeout):
59
+
p.logger.Error("[Payment] Timeout for provider to respond",
60
+
zap.String("provider", provider.GetName()))
61
+
62
+
cancel()
63
+
40
64
err = errors.New("Timeout")
41
65
continue
66
+
case <-ctx.Done():
67
+
cancel()
68
+
return nil, ctx.Err()
42
69
}
43
70
}
44
71
45
72
return nil, err
46
73
}
47
74
48
-
func charge(charge *domain.Payment, provider Provider) chan *domain.Provider {
75
+
func (p *UseProviders) charge(ctx context.Context, charge *domain.Payment, provider Provider) (chan *domain.Provider, chan error) {
49
76
ch := make(chan *domain.Provider)
77
+
chError := make(chan error)
50
78
51
79
go func() {
52
-
response, err := provider.Charge(charge)
80
+
response, err := provider.Charge(ctx, charge)
53
81
if err != nil {
54
-
close(ch)
82
+
chError <- err
83
+
return
55
84
}
56
85
ch <- response
57
86
}()
58
87
59
-
return ch
88
+
return ch, chError
60
89
}
+22
-7
internal/providers/provider_test.go
+22
-7
internal/providers/provider_test.go
···
1
1
package providers_test
2
2
3
3
import (
4
+
"context"
4
5
"testing"
5
6
"time"
6
7
7
8
"github.com/Tulkdan/payment-gateway/internal/domain"
8
9
"github.com/Tulkdan/payment-gateway/internal/providers"
10
+
"go.uber.org/zap"
9
11
)
10
12
11
13
type SpyProvider struct {
···
14
16
Response *domain.Provider
15
17
}
16
18
17
-
func (s *SpyProvider) Charge(request *domain.Payment) (*domain.Provider, error) {
19
+
func (s *SpyProvider) Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error) {
18
20
time.Sleep(s.Timeout)
19
21
s.Calls++
20
22
21
23
return s.Response, nil
24
+
}
25
+
26
+
func (s *SpyProvider) GetName() string {
27
+
return "Mock"
22
28
}
23
29
24
30
func TestProvider(t *testing.T) {
25
31
t.Run("should make request for first provider", func(t *testing.T) {
32
+
logger, _ := zap.NewDevelopment()
33
+
defer logger.Sync()
34
+
26
35
spyFirst := &SpyProvider{Timeout: 10 * time.Millisecond, Response: &domain.Provider{Description: "First"}}
27
36
spySecond := &SpyProvider{Timeout: 10 * time.Millisecond, Response: &domain.Provider{Description: "Second"}}
28
37
29
38
payment, _ := domain.NewPayment(1000, "R$", "", "card", domain.PaymentCard{Number: "", HolderName: "", CVV: "", ExpirationDate: "02/2025", Installments: 1})
30
39
31
-
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, 15*time.Millisecond)
32
-
data, err := useProvider.Payment(payment)
40
+
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, logger, 15*time.Millisecond)
41
+
data, err := useProvider.Payment(context.Background(), payment)
33
42
34
43
if err != nil {
35
44
t.Fatal("Got error. didn't want one")
···
44
53
})
45
54
46
55
t.Run("should make request for second provider when first provider timeouts", func(t *testing.T) {
56
+
logger, _ := zap.NewDevelopment()
57
+
defer logger.Sync()
58
+
47
59
spyFirst := &SpyProvider{Timeout: 20 * time.Millisecond, Response: &domain.Provider{Description: "First"}}
48
60
spySecond := &SpyProvider{Timeout: 10 * time.Millisecond, Response: &domain.Provider{Description: "Second"}}
49
61
50
62
payment, _ := domain.NewPayment(1000, "R$", "", "card", domain.PaymentCard{Number: "", HolderName: "", CVV: "", ExpirationDate: "02/2025", Installments: 1})
51
63
52
-
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, 15*time.Millisecond)
53
-
data, err := useProvider.Payment(payment)
64
+
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, logger, 15*time.Millisecond)
65
+
data, err := useProvider.Payment(context.Background(), payment)
54
66
55
67
if err != nil {
56
68
t.Fatal("Got error. didn't want one")
···
65
77
})
66
78
67
79
t.Run("should return error when all providers timeout", func(t *testing.T) {
80
+
logger, _ := zap.NewDevelopment()
81
+
defer logger.Sync()
82
+
68
83
spyFirst := &SpyProvider{Timeout: 20 * time.Millisecond, Response: &domain.Provider{Description: "First"}}
69
84
spySecond := &SpyProvider{Timeout: 20 * time.Millisecond, Response: &domain.Provider{Description: "Second"}}
70
85
71
86
payment, _ := domain.NewPayment(1000, "R$", "", "card", domain.PaymentCard{Number: "", HolderName: "", CVV: "", ExpirationDate: "02/2025", Installments: 1})
72
87
73
-
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, 5*time.Millisecond)
74
-
data, err := useProvider.Payment(payment)
88
+
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, logger, 5*time.Millisecond)
89
+
data, err := useProvider.Payment(context.Background(), payment)
75
90
76
91
if data != nil {
77
92
t.Fatalf("Got data but didn't expected one, got %q", data)
+30
-9
internal/providers/stripe.go
+30
-9
internal/providers/stripe.go
···
2
2
3
3
import (
4
4
"bytes"
5
+
"context"
5
6
"encoding/json"
6
7
"net/http"
7
8
8
9
"github.com/Tulkdan/payment-gateway/internal/domain"
9
10
"github.com/google/uuid"
11
+
"go.uber.org/zap"
10
12
)
11
13
12
14
type StripeProvider struct {
13
-
Url string
15
+
Url string
16
+
logger *zap.Logger
14
17
}
15
18
16
-
func NewStripeProvider(url string) *StripeProvider {
17
-
return &StripeProvider{Url: url}
19
+
func NewStripeProvider(url string, logger *zap.Logger) *StripeProvider {
20
+
return &StripeProvider{Url: url, logger: logger.Named("StripeProvider")}
18
21
}
19
22
20
23
type StripeChargeCard struct {
···
33
36
Card StripeChargeCard `json:"card"`
34
37
}
35
38
36
-
func (b StripeProvider) Charge(request *domain.Payment) (*domain.Provider, error) {
37
-
body := b.createChargeBody(request)
38
-
response, err := http.Post(b.Url, "application/json", bytes.NewBuffer(body))
39
+
func (s *StripeProvider) Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error) {
40
+
url := s.Url + "/transactions"
41
+
42
+
s.logger.Debug("Making request to charge",
43
+
zap.String("url", url))
44
+
45
+
body := s.createChargeBody(request)
46
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body))
47
+
if err != nil {
48
+
return nil, err
49
+
}
50
+
51
+
req.Header.Set("Content-Type", "application/json")
52
+
req.Header.Set("transaction-id", ctx.Value("request-id").(string))
53
+
54
+
response, err := http.DefaultClient.Do(req)
39
55
if err != nil {
40
56
return nil, err
41
57
}
58
+
defer response.Body.Close()
42
59
43
-
return b.responseCharge(response)
60
+
return s.responseCharge(response)
44
61
}
45
62
46
-
func (b StripeProvider) createChargeBody(request *domain.Payment) []byte {
63
+
func (s *StripeProvider) createChargeBody(request *domain.Payment) []byte {
47
64
toSend := &StripeCharge{
48
65
Amount: request.Amount,
49
66
Currency: request.Currency,
···
73
90
CardId uuid.UUID `json:"cardId"`
74
91
}
75
92
76
-
func (b StripeProvider) responseCharge(response *http.Response) (*domain.Provider, error) {
93
+
func (s *StripeProvider) responseCharge(response *http.Response) (*domain.Provider, error) {
77
94
var data StripeChargeResponse
78
95
if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
79
96
return nil, err
···
102
119
}
103
120
return providerResponse, nil
104
121
}
122
+
123
+
func (s *StripeProvider) GetName() string {
124
+
return "Stripe provider"
125
+
}
+9
-2
internal/providers/stripe_test.go
+9
-2
internal/providers/stripe_test.go
···
1
1
package providers_test
2
2
3
3
import (
4
+
"context"
4
5
"encoding/json"
5
6
"net/http"
6
7
"net/http/httptest"
···
11
12
"github.com/Tulkdan/payment-gateway/internal/domain"
12
13
"github.com/Tulkdan/payment-gateway/internal/providers"
13
14
"github.com/google/uuid"
15
+
"go.uber.org/zap"
14
16
)
15
17
16
18
func TestStripe(t *testing.T) {
17
19
t.Run("should make request to url", func(t *testing.T) {
20
+
logger, _ := zap.NewDevelopment()
21
+
defer logger.Sync()
22
+
18
23
id, _ := uuid.Parse("2ee70bcb-5cb9-4412-a35f-c2a15fb88ef1")
19
24
cardId, _ := uuid.Parse("ed6ecd4c-81d5-4e63-bb12-99439ae559e7")
25
+
ctx := context.WithValue(t.Context(), "request-id", uuid.New().String())
26
+
20
27
serverResponse := &providers.StripeChargeResponse{
21
28
Id: id,
22
29
CreatedAt: time.Now().Format("YYYY-MM-DD"),
···
53
60
CardId: cardId,
54
61
}
55
62
56
-
provider := providers.NewStripeProvider(server.URL)
57
-
response, err := provider.Charge(charge)
63
+
provider := providers.NewStripeProvider(server.URL, logger)
64
+
response, err := provider.Charge(ctx, charge)
58
65
59
66
if err != nil {
60
67
t.Fatalf("got an error but didn't want one %q", err)
+14
-6
internal/service/payment_service.go
+14
-6
internal/service/payment_service.go
···
2
2
3
3
import (
4
4
"context"
5
-
"fmt"
6
5
7
6
"github.com/Tulkdan/payment-gateway/internal/domain"
8
7
"github.com/Tulkdan/payment-gateway/internal/dto"
8
+
"github.com/Tulkdan/payment-gateway/internal/providers"
9
9
)
10
10
11
-
type PaymentService struct{}
11
+
type PaymentService struct {
12
+
providers *providers.UseProviders
13
+
}
12
14
13
-
func NewPaymentService() *PaymentService {
14
-
return &PaymentService{}
15
+
func NewPaymentService(providers *providers.UseProviders) *PaymentService {
16
+
return &PaymentService{providers: providers}
15
17
}
16
18
17
19
func (p *PaymentService) CreatePayment(ctx context.Context, input dto.PaymentInput) (*dto.PaymentOutput, error) {
···
20
22
return nil, err
21
23
}
22
24
23
-
fmt.Printf("%+v", payment)
25
+
providerData, err := p.providers.Payment(ctx, payment)
26
+
if err != nil {
27
+
payment.UpdateStatus(domain.StatusRejected)
28
+
return nil, err
29
+
}
30
+
31
+
payment.UpdateStatus(domain.StatusApproved)
24
32
25
-
return &dto.PaymentOutput{Message: "Processed successfully"}, nil
33
+
return dto.NewPaymentOutput(providerData.Id, providerData.CardId, providerData.CurrentAmount), nil
26
34
}
+17
-1
internal/web/handler/payments_handler.go
+17
-1
internal/web/handler/payments_handler.go
···
6
6
7
7
"github.com/Tulkdan/payment-gateway/internal/dto"
8
8
"github.com/Tulkdan/payment-gateway/internal/service"
9
+
"go.uber.org/zap"
9
10
)
10
11
11
12
type PaymentsHandler struct {
12
13
paymentService *service.PaymentService
14
+
15
+
logger *zap.Logger
13
16
}
14
17
15
-
func NewPaymentsHandler(paymentsService *service.PaymentService) *PaymentsHandler {
18
+
func NewPaymentsHandler(paymentsService *service.PaymentService, logger *zap.Logger) *PaymentsHandler {
16
19
return &PaymentsHandler{
17
20
paymentService: paymentsService,
21
+
logger: logger.Named("PaymentHandler"),
18
22
}
19
23
}
20
24
21
25
func (p *PaymentsHandler) Create(w http.ResponseWriter, r *http.Request) {
22
26
var body dto.PaymentInput
23
27
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
28
+
p.logger.Error("Body incomplete",
29
+
zap.String("error", err.Error()),
30
+
zap.String("requestId", r.Context().Value("request-id").(string)))
31
+
24
32
http.Error(w, err.Error(), http.StatusBadRequest)
25
33
return
26
34
}
27
35
28
36
response, err := p.paymentService.CreatePayment(r.Context(), body)
29
37
if err != nil {
38
+
p.logger.Error("Failed to create payment",
39
+
zap.String("error", err.Error()),
40
+
zap.String("requestId", r.Context().Value("request-id").(string)))
41
+
30
42
http.Error(w, err.Error(), http.StatusBadRequest)
31
43
return
32
44
}
45
+
46
+
p.logger.Debug("Processed request",
47
+
zap.Any("response", response),
48
+
zap.String("requestId", r.Context().Value("request-id").(string)))
33
49
34
50
w.Header().Set("Content-Type", "application/json")
35
51
w.WriteHeader(http.StatusOK)
+15
internal/web/middleware/transactionId.go
+15
internal/web/middleware/transactionId.go
···
1
+
package middleware
2
+
3
+
import (
4
+
"context"
5
+
"net/http"
6
+
7
+
"github.com/google/uuid"
8
+
)
9
+
10
+
func WithRequestId(next http.HandlerFunc) http.HandlerFunc {
11
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
12
+
ctx := context.WithValue(r.Context(), "request-id", uuid.New().String())
13
+
next.ServeHTTP(w, r.WithContext(ctx))
14
+
})
15
+
}
+22
-8
internal/web/server.go
+22
-8
internal/web/server.go
···
1
1
package web
2
2
3
3
import (
4
+
"context"
5
+
"net"
4
6
"net/http"
7
+
"time"
5
8
6
9
"github.com/Tulkdan/payment-gateway/internal/service"
7
10
"github.com/Tulkdan/payment-gateway/internal/web/handler"
11
+
"github.com/Tulkdan/payment-gateway/internal/web/middleware"
12
+
"go.uber.org/zap"
8
13
)
9
14
10
15
type Server struct {
11
16
port string
12
17
router *http.ServeMux
13
18
server *http.Server
19
+
logger *zap.Logger
14
20
15
21
paymentsService *service.PaymentService
16
22
}
17
23
18
-
func NewServer(paymentsService *service.PaymentService, port string) *Server {
24
+
func NewServer(paymentsService *service.PaymentService, port string, logger *zap.Logger) *Server {
19
25
return &Server{
20
26
port: port,
21
27
paymentsService: paymentsService,
28
+
logger: logger,
22
29
}
23
30
}
24
31
25
32
func (s *Server) ConfigureRouter() {
26
-
r := &http.ServeMux{}
33
+
mux := http.NewServeMux()
27
34
28
-
paymentsHandler := handler.NewPaymentsHandler(s.paymentsService)
35
+
paymentsHandler := handler.NewPaymentsHandler(s.paymentsService, s.logger)
29
36
30
-
r.HandleFunc("POST /payments", paymentsHandler.Create)
37
+
mux.HandleFunc("POST /payments", middleware.WithRequestId(paymentsHandler.Create))
31
38
// r.HandleFunc("POST /refunds", func(http.ResponseWriter, *http.Request) {})
32
39
// r.HandleFunc("GET /payments/{id}", func(w http.ResponseWriter, r *http.Request) {
33
40
// id := r.PathValue("id")
34
41
// })
35
42
36
-
s.router = r
43
+
s.router = mux
37
44
}
38
45
39
-
func (s *Server) Start() error {
46
+
func (s *Server) Start(ctx context.Context) error {
40
47
s.server = &http.Server{
41
-
Addr: ":" + s.port,
42
-
Handler: s.router,
48
+
Addr: ":" + s.port,
49
+
Handler: s.router,
50
+
BaseContext: func(_ net.Listener) context.Context { return ctx },
51
+
ReadTimeout: time.Second,
52
+
WriteTimeout: 10 * time.Second,
43
53
}
44
54
45
55
return s.server.ListenAndServe()
46
56
}
57
+
58
+
func (s *Server) Shutdown() error {
59
+
return s.server.Shutdown(context.Background())
60
+
}
+27
mocks/braintree.yml
+27
mocks/braintree.yml
···
1
+
- request:
2
+
url: ^/charges$
3
+
method: POST
4
+
headers:
5
+
content-type: application/json
6
+
response:
7
+
- status: 200
8
+
latency: 15000
9
+
headers:
10
+
content-type: application/json
11
+
server: stubbedServer/4.2
12
+
body: >
13
+
{
14
+
"amount": 1000,
15
+
"currency": "BRL",
16
+
"description": "",
17
+
"paymentMethod": {
18
+
"type": "card",
19
+
"card": {
20
+
"number": "",
21
+
"holderName": "",
22
+
"cvv": "",
23
+
"expirationDate": "22/2025",
24
+
"installments": 1
25
+
}
26
+
}
27
+
}
+25
mocks/stripe.yml
+25
mocks/stripe.yml
···
1
+
- request:
2
+
url: ^/transactions$
3
+
method: POST
4
+
headers:
5
+
content-type: application/json
6
+
response:
7
+
- status: 200
8
+
latency: 500
9
+
headers:
10
+
content-type: application/json
11
+
server: stubbedServer/4.2
12
+
body: >
13
+
{
14
+
"amount": 1000,
15
+
"currency": "BRL",
16
+
"statementDescriptor": "",
17
+
"paymentType": "card",
18
+
"card": {
19
+
"number": "",
20
+
"holder": "",
21
+
"cvv": "",
22
+
"expiration": "22/2025",
23
+
"installmentNumber": 1
24
+
}
25
+
}
+16
request.hurl
+16
request.hurl
···
1
+
POST http://localhost:8000/payments
2
+
Content-Type: application/json
3
+
{
4
+
"amount": 10000,
5
+
"currency": "BRL",
6
+
"description": "Pagamento do Pix",
7
+
"paymentType": "card",
8
+
"card": {
9
+
"number": "0000 0000 0000 0000",
10
+
"holderName": "Teste Teste",
11
+
"cvv": "123",
12
+
"expirationDate": "12/2345",
13
+
"installments": 1
14
+
}
15
+
}
16
+
HTTP 200