+9
-4
cmd/app/main.go
+9
-4
cmd/app/main.go
···
8
8
"github.com/Tulkdan/payment-gateway/internal/providers"
9
9
"github.com/Tulkdan/payment-gateway/internal/service"
10
10
"github.com/Tulkdan/payment-gateway/internal/web"
11
+
"go.uber.org/zap"
11
12
)
12
13
13
14
func getEnv(key, defaultValue string) string {
···
21
22
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
22
23
defer stop()
23
24
25
+
logger, _ := zap.NewDevelopment()
26
+
defer logger.Sync()
27
+
sugar := logger.Sugar()
28
+
24
29
providers := providers.NewUseProviders([]providers.Provider{
25
-
providers.NewBraintreeProvider(getEnv("BRAINTREE_URL", "localhost:8001")),
26
-
providers.NewStripeProvider(getEnv("STRIPE_URL", "localhost:8002")),
27
-
})
30
+
providers.NewBraintreeProvider(getEnv("BRAINTREE_URL", "http://localhost:8001")),
31
+
providers.NewStripeProvider(getEnv("STRIPE_URL", "http://localhost:8002")),
32
+
}, sugar)
28
33
paymentsService := service.NewPaymentService(providers)
29
34
30
35
port := getEnv("PORT", "8000")
31
-
server := web.NewServer(paymentsService, port)
36
+
server := web.NewServer(paymentsService, port, sugar)
32
37
server.ConfigureRouter()
33
38
34
39
srvErr := make(chan error, 1)
+4
docker-compose.yml
+4
docker-compose.yml
···
15
15
stripe:
16
16
build:
17
17
dockerfile: $PWD/Dockerfile-stubby
18
+
ports:
19
+
- "8002:8882"
18
20
environment:
19
21
PORT: 8882
20
22
volumes:
···
23
25
braintree:
24
26
build:
25
27
dockerfile: $PWD/Dockerfile-stubby
28
+
ports:
29
+
- "8001:8882"
26
30
environment:
27
31
PORT: 8882
28
32
volumes:
+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=
+7
-3
internal/providers/braintree.go
+7
-3
internal/providers/braintree.go
···
38
38
PaymentMethod BraintreeChargePaymentMethod `json:"paymentMethod"`
39
39
}
40
40
41
-
func (b BraintreeProvider) Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error) {
41
+
func (b *BraintreeProvider) Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error) {
42
42
body := b.createChargeBody(request)
43
43
req, err := http.NewRequestWithContext(ctx, http.MethodPost, b.Url+"/charges", bytes.NewBuffer(body))
44
44
if err != nil {
···
55
55
return b.responseCharge(response)
56
56
}
57
57
58
-
func (b BraintreeProvider) createChargeBody(request *domain.Payment) []byte {
58
+
func (b *BraintreeProvider) createChargeBody(request *domain.Payment) []byte {
59
59
toSend := &BraintreeCharge{
60
60
Amount: request.Amount,
61
61
Currency: request.Currency,
···
87
87
CardId uuid.UUID `json:"cardId"`
88
88
}
89
89
90
-
func (b BraintreeProvider) responseCharge(response *http.Response) (*domain.Provider, error) {
90
+
func (b *BraintreeProvider) responseCharge(response *http.Response) (*domain.Provider, error) {
91
91
var data BraintreeChargeResponse
92
92
if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
93
93
return nil, err
···
116
116
}
117
117
return providerResponse, nil
118
118
}
119
+
120
+
func (b *BraintreeProvider) GetName() string {
121
+
return "Braintree Provider"
122
+
}
+18
-5
internal/providers/provider.go
+18
-5
internal/providers/provider.go
···
6
6
"time"
7
7
8
8
"github.com/Tulkdan/payment-gateway/internal/domain"
9
+
"go.uber.org/zap"
9
10
)
10
11
11
12
var thirtySecondTimout = 30 * time.Second
12
13
13
14
type Provider interface {
15
+
GetName() string
14
16
Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error)
15
17
}
16
18
17
19
type UseProviders struct {
18
20
providers []Provider
19
21
timeout time.Duration
22
+
logger *zap.SugaredLogger
20
23
}
21
24
22
-
func NewUseProviders(providers []Provider) *UseProviders {
23
-
return ConfigurableUseProvider(providers, thirtySecondTimout)
25
+
func NewUseProviders(providers []Provider, logger *zap.SugaredLogger) *UseProviders {
26
+
return ConfigurableUseProvider(providers, logger, thirtySecondTimout)
24
27
}
25
28
26
-
func ConfigurableUseProvider(providers []Provider, timeout time.Duration) *UseProviders {
29
+
func ConfigurableUseProvider(providers []Provider, logger *zap.SugaredLogger, timeout time.Duration) *UseProviders {
27
30
return &UseProviders{
28
31
providers: providers,
32
+
logger: logger,
29
33
timeout: timeout,
30
34
}
31
35
}
32
36
33
37
func (p *UseProviders) Payment(ctx context.Context, payment *domain.Payment) (*domain.Provider, error) {
34
38
var err error = nil
39
+
attempts := 0
35
40
36
41
for _, provider := range p.providers {
37
42
select {
38
-
case data := <-charge(ctx, payment, provider):
43
+
case data := <-p.charge(ctx, payment, provider):
44
+
p.logger.Debugw("[Payment] Sending request successfully",
45
+
"provider", provider.GetName(),
46
+
"attempt", attempts)
47
+
39
48
return data, nil
40
49
case <-time.After(p.timeout):
50
+
p.logger.Errorw("[Payment] Timeout for provider to respond",
51
+
"provider", provider.GetName(),
52
+
"attempt", attempts)
53
+
41
54
err = errors.New("Timeout")
42
55
continue
43
56
}
···
46
59
return nil, err
47
60
}
48
61
49
-
func charge(ctx context.Context, charge *domain.Payment, provider Provider) chan *domain.Provider {
62
+
func (p *UseProviders) charge(ctx context.Context, charge *domain.Payment, provider Provider) chan *domain.Provider {
50
63
ch := make(chan *domain.Provider)
51
64
52
65
go func() {
+20
-3
internal/providers/provider_test.go
+20
-3
internal/providers/provider_test.go
···
7
7
8
8
"github.com/Tulkdan/payment-gateway/internal/domain"
9
9
"github.com/Tulkdan/payment-gateway/internal/providers"
10
+
"go.uber.org/zap"
10
11
)
11
12
12
13
type SpyProvider struct {
···
22
23
return s.Response, nil
23
24
}
24
25
26
+
func (s *SpyProvider) GetName() string {
27
+
return "Mock"
28
+
}
29
+
25
30
func TestProvider(t *testing.T) {
26
31
t.Run("should make request for first provider", func(t *testing.T) {
32
+
logger, _ := zap.NewDevelopment()
33
+
defer logger.Sync()
34
+
sugar := logger.Sugar()
35
+
27
36
spyFirst := &SpyProvider{Timeout: 10 * time.Millisecond, Response: &domain.Provider{Description: "First"}}
28
37
spySecond := &SpyProvider{Timeout: 10 * time.Millisecond, Response: &domain.Provider{Description: "Second"}}
29
38
30
39
payment, _ := domain.NewPayment(1000, "R$", "", "card", domain.PaymentCard{Number: "", HolderName: "", CVV: "", ExpirationDate: "02/2025", Installments: 1})
31
40
32
-
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, 15*time.Millisecond)
41
+
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, sugar, 15*time.Millisecond)
33
42
data, err := useProvider.Payment(context.Background(), payment)
34
43
35
44
if err != nil {
···
45
54
})
46
55
47
56
t.Run("should make request for second provider when first provider timeouts", func(t *testing.T) {
57
+
logger, _ := zap.NewProduction()
58
+
defer logger.Sync()
59
+
sugar := logger.Sugar()
60
+
48
61
spyFirst := &SpyProvider{Timeout: 20 * time.Millisecond, Response: &domain.Provider{Description: "First"}}
49
62
spySecond := &SpyProvider{Timeout: 10 * time.Millisecond, Response: &domain.Provider{Description: "Second"}}
50
63
51
64
payment, _ := domain.NewPayment(1000, "R$", "", "card", domain.PaymentCard{Number: "", HolderName: "", CVV: "", ExpirationDate: "02/2025", Installments: 1})
52
65
53
-
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, 15*time.Millisecond)
66
+
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, sugar, 15*time.Millisecond)
54
67
data, err := useProvider.Payment(context.Background(), payment)
55
68
56
69
if err != nil {
···
66
79
})
67
80
68
81
t.Run("should return error when all providers timeout", func(t *testing.T) {
82
+
logger, _ := zap.NewProduction()
83
+
defer logger.Sync()
84
+
sugar := logger.Sugar()
85
+
69
86
spyFirst := &SpyProvider{Timeout: 20 * time.Millisecond, Response: &domain.Provider{Description: "First"}}
70
87
spySecond := &SpyProvider{Timeout: 20 * time.Millisecond, Response: &domain.Provider{Description: "Second"}}
71
88
72
89
payment, _ := domain.NewPayment(1000, "R$", "", "card", domain.PaymentCard{Number: "", HolderName: "", CVV: "", ExpirationDate: "02/2025", Installments: 1})
73
90
74
-
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, 5*time.Millisecond)
91
+
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, sugar, 5*time.Millisecond)
75
92
data, err := useProvider.Payment(context.Background(), payment)
76
93
77
94
if data != nil {
+7
-3
internal/providers/stripe.go
+7
-3
internal/providers/stripe.go
···
34
34
Card StripeChargeCard `json:"card"`
35
35
}
36
36
37
-
func (b StripeProvider) Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error) {
37
+
func (b *StripeProvider) Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error) {
38
38
body := b.createChargeBody(request)
39
39
req, err := http.NewRequestWithContext(ctx, http.MethodPost, b.Url+"/transactions", bytes.NewBuffer(body))
40
40
if err != nil {
···
52
52
return b.responseCharge(response)
53
53
}
54
54
55
-
func (b StripeProvider) createChargeBody(request *domain.Payment) []byte {
55
+
func (b *StripeProvider) createChargeBody(request *domain.Payment) []byte {
56
56
toSend := &StripeCharge{
57
57
Amount: request.Amount,
58
58
Currency: request.Currency,
···
82
82
CardId uuid.UUID `json:"cardId"`
83
83
}
84
84
85
-
func (b StripeProvider) responseCharge(response *http.Response) (*domain.Provider, error) {
85
+
func (b *StripeProvider) responseCharge(response *http.Response) (*domain.Provider, error) {
86
86
var data StripeChargeResponse
87
87
if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
88
88
return nil, err
···
111
111
}
112
112
return providerResponse, nil
113
113
}
114
+
115
+
func (s *StripeProvider) GetName() string {
116
+
return "Stripe provider"
117
+
}
+4
-1
internal/service/payment_service.go
+4
-1
internal/service/payment_service.go
···
2
2
3
3
import (
4
4
"context"
5
+
"fmt"
5
6
6
7
"github.com/Tulkdan/payment-gateway/internal/domain"
7
8
"github.com/Tulkdan/payment-gateway/internal/dto"
···
22
23
return nil, err
23
24
}
24
25
25
-
_, err = p.providers.Payment(ctx, payment)
26
+
data, err := p.providers.Payment(ctx, payment)
26
27
if err != nil {
27
28
return nil, err
28
29
}
30
+
31
+
fmt.Println(data)
29
32
30
33
return &dto.PaymentOutput{Message: "Processed successfully"}, nil
31
34
}
+13
-1
internal/web/handler/payments_handler.go
+13
-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.SugaredLogger
13
16
}
14
17
15
-
func NewPaymentsHandler(paymentsService *service.PaymentService) *PaymentsHandler {
18
+
func NewPaymentsHandler(paymentsService *service.PaymentService, logger *zap.SugaredLogger) *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.Errorw("Body incomplete",
29
+
"error", err.Error(),
30
+
"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.Errorw("Failed to create payment",
39
+
"error", err.Error(),
40
+
"requestId", r.Context().Value("request-id").(string))
41
+
30
42
http.Error(w, err.Error(), http.StatusBadRequest)
31
43
return
32
44
}
+5
-2
internal/web/server.go
+5
-2
internal/web/server.go
···
9
9
"github.com/Tulkdan/payment-gateway/internal/service"
10
10
"github.com/Tulkdan/payment-gateway/internal/web/handler"
11
11
"github.com/Tulkdan/payment-gateway/internal/web/middleware"
12
+
"go.uber.org/zap"
12
13
)
13
14
14
15
type Server struct {
15
16
port string
16
17
router *http.ServeMux
17
18
server *http.Server
19
+
logger *zap.SugaredLogger
18
20
19
21
paymentsService *service.PaymentService
20
22
}
21
23
22
-
func NewServer(paymentsService *service.PaymentService, port string) *Server {
24
+
func NewServer(paymentsService *service.PaymentService, port string, logger *zap.SugaredLogger) *Server {
23
25
return &Server{
24
26
port: port,
25
27
paymentsService: paymentsService,
28
+
logger: logger,
26
29
}
27
30
}
28
31
29
32
func (s *Server) ConfigureRouter() {
30
33
mux := http.NewServeMux()
31
34
32
-
paymentsHandler := handler.NewPaymentsHandler(s.paymentsService)
35
+
paymentsHandler := handler.NewPaymentsHandler(s.paymentsService, s.logger)
33
36
34
37
mux.HandleFunc("POST /payments", middleware.WithRequestId(paymentsHandler.Create))
35
38
// r.HandleFunc("POST /refunds", func(http.ResponseWriter, *http.Request) {})