+5
.envrc
+5
.envrc
+1
-1
.gitignore
+1
-1
.gitignore
+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
+2
-2
cmd/app/main.go
+2
-2
cmd/app/main.go
···
26
26
defer logger.Sync()
27
27
28
28
providers := providers.NewUseProviders([]providers.Provider{
29
-
providers.NewBraintreeProvider("http://" + getEnv("BRAINTREE_URL", "localhost:8001")),
30
-
providers.NewStripeProvider("http://" + getEnv("STRIPE_URL", "localhost:8002")),
29
+
providers.NewBraintreeProvider(getEnv("BRAINTREE_URL", "http://localhost:8001"), logger),
30
+
providers.NewStripeProvider(getEnv("STRIPE_URL", "http://localhost:8002"), logger),
31
31
}, logger)
32
32
paymentsService := service.NewPaymentService(providers)
33
33
-13
docker-compose.yml
-13
docker-compose.yml
···
1
1
services:
2
-
api-gateway:
3
-
build:
4
-
dockerfile: $PWD/Dockerfile
5
-
ports:
6
-
- "8000:8000"
7
-
environment:
8
-
PORT: 8000
9
-
BRAINTREE_URL: stripe
10
-
STRIPE_URL: braintree
11
-
depends_on:
12
-
- stripe
13
-
- braintree
14
-
15
2
stripe:
16
3
build:
17
4
dockerfile: $PWD/Dockerfile-stubby
+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
+
}
+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
}
+10
-3
internal/providers/braintree.go
+10
-3
internal/providers/braintree.go
···
8
8
9
9
"github.com/Tulkdan/payment-gateway/internal/domain"
10
10
"github.com/google/uuid"
11
+
"go.uber.org/zap"
11
12
)
12
13
13
14
type BraintreeProvider struct {
14
-
Url string
15
+
Url string
16
+
logger *zap.Logger
15
17
}
16
18
17
-
func NewBraintreeProvider(url string) *BraintreeProvider {
18
-
return &BraintreeProvider{Url: url}
19
+
func NewBraintreeProvider(url string, logger *zap.Logger) *BraintreeProvider {
20
+
return &BraintreeProvider{Url: url, logger: logger.Named("BraintreeProvider")}
19
21
}
20
22
21
23
type BraintreeChargeCard struct {
···
39
41
}
40
42
41
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
+
42
49
body := b.createChargeBody(request)
43
50
req, err := http.NewRequestWithContext(ctx, http.MethodPost, b.Url+"/charges", bytes.NewBuffer(body))
44
51
if err != nil {
+5
-1
internal/providers/braintree_test.go
+5
-1
internal/providers/braintree_test.go
···
12
12
"github.com/Tulkdan/payment-gateway/internal/domain"
13
13
"github.com/Tulkdan/payment-gateway/internal/providers"
14
14
"github.com/google/uuid"
15
+
"go.uber.org/zap"
15
16
)
16
17
17
18
func TestBraintree(t *testing.T) {
18
19
t.Run("should make request to url", func(t *testing.T) {
20
+
logger, _ := zap.NewDevelopment()
21
+
defer logger.Sync()
22
+
19
23
id, _ := uuid.Parse("2ee70bcb-5cb9-4412-a35f-c2a15fb88ef1")
20
24
cardId, _ := uuid.Parse("ed6ecd4c-81d5-4e63-bb12-99439ae559e7")
21
25
ctx := context.WithValue(t.Context(), "request-id", uuid.New().String())
···
56
60
CardId: cardId,
57
61
}
58
62
59
-
provider := providers.NewBraintreeProvider(server.URL)
63
+
provider := providers.NewBraintreeProvider(server.URL, logger)
60
64
response, err := provider.Charge(ctx, charge)
61
65
62
66
if err != nil {
+4
-7
internal/providers/provider.go
+4
-7
internal/providers/provider.go
···
29
29
func ConfigurableUseProvider(providers []Provider, logger *zap.Logger, timeout time.Duration) *UseProviders {
30
30
return &UseProviders{
31
31
providers: providers,
32
-
logger: logger,
32
+
logger: logger.Named("UseProviders"),
33
33
timeout: timeout,
34
34
}
35
35
}
36
36
37
37
func (p *UseProviders) Payment(ctx context.Context, payment *domain.Payment) (*domain.Provider, error) {
38
38
var err error = nil
39
-
attempts := 0
40
39
41
40
for _, provider := range p.providers {
42
41
requestCtx, cancel := context.WithTimeout(ctx, p.timeout)
···
46
45
select {
47
46
case data := <-dataCh:
48
47
p.logger.Debug("[Payment] Received request successfully",
49
-
zap.String("provider", provider.GetName()),
50
-
zap.Int("attempt", attempts))
48
+
zap.String("provider", provider.GetName()))
51
49
52
50
return data, nil
53
51
case error := <-errCh:
54
52
p.logger.Error("[Payment] Received request with error",
55
53
zap.String("provider", provider.GetName()),
56
-
zap.Int("attempt", attempts),
57
54
zap.String("error", error.Error()))
58
55
59
56
err = error
60
57
continue
61
58
case <-time.After(p.timeout):
62
59
p.logger.Error("[Payment] Timeout for provider to respond",
63
-
zap.String("provider", provider.GetName()),
64
-
zap.Int("attempt", attempts))
60
+
zap.String("provider", provider.GetName()))
65
61
66
62
cancel()
67
63
68
64
err = errors.New("Timeout")
69
65
continue
70
66
case <-ctx.Done():
67
+
cancel()
71
68
return nil, ctx.Err()
72
69
}
73
70
}
+2
-2
internal/providers/provider_test.go
+2
-2
internal/providers/provider_test.go
···
53
53
})
54
54
55
55
t.Run("should make request for second provider when first provider timeouts", func(t *testing.T) {
56
-
logger, _ := zap.NewProduction()
56
+
logger, _ := zap.NewDevelopment()
57
57
defer logger.Sync()
58
58
59
59
spyFirst := &SpyProvider{Timeout: 20 * time.Millisecond, Response: &domain.Provider{Description: "First"}}
···
77
77
})
78
78
79
79
t.Run("should return error when all providers timeout", func(t *testing.T) {
80
-
logger, _ := zap.NewProduction()
80
+
logger, _ := zap.NewDevelopment()
81
81
defer logger.Sync()
82
82
83
83
spyFirst := &SpyProvider{Timeout: 20 * time.Millisecond, Response: &domain.Provider{Description: "First"}}
+16
-9
internal/providers/stripe.go
+16
-9
internal/providers/stripe.go
···
8
8
9
9
"github.com/Tulkdan/payment-gateway/internal/domain"
10
10
"github.com/google/uuid"
11
+
"go.uber.org/zap"
11
12
)
12
13
13
14
type StripeProvider struct {
14
-
Url string
15
+
Url string
16
+
logger *zap.Logger
15
17
}
16
18
17
-
func NewStripeProvider(url string) *StripeProvider {
18
-
return &StripeProvider{Url: url}
19
+
func NewStripeProvider(url string, logger *zap.Logger) *StripeProvider {
20
+
return &StripeProvider{Url: url, logger: logger.Named("StripeProvider")}
19
21
}
20
22
21
23
type StripeChargeCard struct {
···
34
36
Card StripeChargeCard `json:"card"`
35
37
}
36
38
37
-
func (b *StripeProvider) Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error) {
38
-
body := b.createChargeBody(request)
39
-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, b.Url+"/transactions", 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))
40
47
if err != nil {
41
48
return nil, err
42
49
}
···
50
57
}
51
58
defer response.Body.Close()
52
59
53
-
return b.responseCharge(response)
60
+
return s.responseCharge(response)
54
61
}
55
62
56
-
func (b *StripeProvider) createChargeBody(request *domain.Payment) []byte {
63
+
func (s *StripeProvider) createChargeBody(request *domain.Payment) []byte {
57
64
toSend := &StripeCharge{
58
65
Amount: request.Amount,
59
66
Currency: request.Currency,
···
83
90
CardId uuid.UUID `json:"cardId"`
84
91
}
85
92
86
-
func (b *StripeProvider) responseCharge(response *http.Response) (*domain.Provider, error) {
93
+
func (s *StripeProvider) responseCharge(response *http.Response) (*domain.Provider, error) {
87
94
var data StripeChargeResponse
88
95
if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
89
96
return nil, err
+5
-1
internal/providers/stripe_test.go
+5
-1
internal/providers/stripe_test.go
···
12
12
"github.com/Tulkdan/payment-gateway/internal/domain"
13
13
"github.com/Tulkdan/payment-gateway/internal/providers"
14
14
"github.com/google/uuid"
15
+
"go.uber.org/zap"
15
16
)
16
17
17
18
func TestStripe(t *testing.T) {
18
19
t.Run("should make request to url", func(t *testing.T) {
20
+
logger, _ := zap.NewDevelopment()
21
+
defer logger.Sync()
22
+
19
23
id, _ := uuid.Parse("2ee70bcb-5cb9-4412-a35f-c2a15fb88ef1")
20
24
cardId, _ := uuid.Parse("ed6ecd4c-81d5-4e63-bb12-99439ae559e7")
21
25
ctx := context.WithValue(t.Context(), "request-id", uuid.New().String())
···
56
60
CardId: cardId,
57
61
}
58
62
59
-
provider := providers.NewStripeProvider(server.URL)
63
+
provider := providers.NewStripeProvider(server.URL, logger)
60
64
response, err := provider.Charge(ctx, charge)
61
65
62
66
if err != nil {
+5
-2
internal/service/payment_service.go
+5
-2
internal/service/payment_service.go
···
22
22
return nil, err
23
23
}
24
24
25
-
_, err = p.providers.Payment(ctx, payment)
25
+
providerData, err := p.providers.Payment(ctx, payment)
26
26
if err != nil {
27
+
payment.UpdateStatus(domain.StatusRejected)
27
28
return nil, err
28
29
}
29
30
30
-
return &dto.PaymentOutput{Message: "Processed successfully"}, nil
31
+
payment.UpdateStatus(domain.StatusApproved)
32
+
33
+
return dto.NewPaymentOutput(providerData.Id, providerData.CardId, providerData.CurrentAmount), nil
31
34
}