this repo has no description

Compare changes

Choose any two refs to compare.

+5
.dockerignore
··· 1 + bin/ 2 + *.hurl 3 + .git/ 4 + .jj/ 5 + .direnv/
+5
.envrc
··· 1 + use flake 2 + 3 + export BRAINTREE_URL=http://localhost:8001 4 + export STRIPE_URL=http://localhost:8001 5 + export PORT=8000
+1 -1
.gitignore
··· 27 27 28 28 # End of https://www.toptal.com/developers/gitignore/api/go 29 29 30 - *.hurl 30 + .direnv/
+17
Dockerfile
··· 1 + FROM golang:1.24 AS base 2 + 3 + WORKDIR /build 4 + 5 + COPY go.mod go.sum ./ 6 + 7 + RUN go mod download 8 + 9 + COPY . . 10 + 11 + RUN go build -o bin/gateway cmd/app/main.go 12 + 13 + ENV PORT=8000 14 + EXPOSE 8000 15 + 16 + # Start the application 17 + CMD ["/build/bin/gateway"]
+7
Dockerfile-stubby
··· 1 + FROM node:24 2 + 3 + RUN npm install -g stubby 4 + 5 + WORKDIR /app 6 + 7 + CMD ["stubby", "-d", "mock.yml"]
+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
··· 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
··· 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
··· 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
··· 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
··· 3 3 go 1.24.4 4 4 5 5 require github.com/google/uuid v1.6.0 6 + 7 + require ( 8 + go.uber.org/multierr v1.11.0 // indirect 9 + go.uber.org/zap v1.27.0 // indirect 10 + )
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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