this repo has no description

Compare changes

Choose any two refs to compare.

+1
.dockerignore
··· 2 2 *.hurl 3 3 .git/ 4 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/
+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
+3 -2
cmd/app/main.go
··· 26 26 defer logger.Sync() 27 27 28 28 providers := providers.NewUseProviders([]providers.Provider{ 29 - providers.NewBraintreeProvider(getEnv("BRAINTREE_URL", "http://localhost:8001")), 30 - providers.NewStripeProvider(getEnv("STRIPE_URL", "http://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 ··· 37 37 38 38 srvErr := make(chan error, 1) 39 39 go func() { 40 + logger.Info("Starting server", zap.String("port", port)) 40 41 srvErr <- server.Start(ctx) 41 42 }() 42 43
-17
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 18 5 ports: 19 6 - "8002:8882" 20 - environment: 21 - PORT: 8882 22 7 volumes: 23 8 - $PWD/mocks/stripe.yml:/app/mock.yml 24 9 ··· 27 12 dockerfile: $PWD/Dockerfile-stubby 28 13 ports: 29 14 - "8001:8882" 30 - environment: 31 - PORT: 8882 32 15 volumes: 33 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 + }
+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 }
+12 -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 { ··· 52 59 if err != nil { 53 60 return nil, err 54 61 } 62 + defer response.Body.Close() 63 + 55 64 return b.responseCharge(response) 56 65 } 57 66
+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 {
+24 -10
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 { 41 + requestCtx, cancel := context.WithTimeout(ctx, p.timeout) 42 + defer cancel() 43 + 44 + dataCh, errCh := p.charge(requestCtx, payment, provider) 42 45 select { 43 - case data := <-p.charge(ctx, payment, provider): 46 + case data := <-dataCh: 44 47 p.logger.Debug("[Payment] Received request successfully", 45 - zap.String("provider", provider.GetName()), 46 - zap.Int("attempt", attempts)) 48 + zap.String("provider", provider.GetName())) 47 49 48 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 49 58 case <-time.After(p.timeout): 50 59 p.logger.Error("[Payment] Timeout for provider to respond", 51 - zap.String("provider", provider.GetName()), 52 - zap.Int("attempt", attempts)) 60 + zap.String("provider", provider.GetName())) 61 + 62 + cancel() 53 63 54 64 err = errors.New("Timeout") 55 65 continue 66 + case <-ctx.Done(): 67 + cancel() 68 + return nil, ctx.Err() 56 69 } 57 70 } 58 71 59 72 return nil, err 60 73 } 61 74 62 - func (p *UseProviders) charge(ctx context.Context, 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) { 63 76 ch := make(chan *domain.Provider) 77 + chError := make(chan error) 64 78 65 79 go func() { 66 80 response, err := provider.Charge(ctx, charge) 67 81 if err != nil { 68 - close(ch) 82 + chError <- err 69 83 return 70 84 } 71 85 ch <- response 72 86 }() 73 87 74 - return ch 88 + return ch, chError 75 89 }
+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"}}
+17 -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 } ··· 48 55 if err != nil { 49 56 return nil, err 50 57 } 58 + defer response.Body.Close() 51 59 52 - return b.responseCharge(response) 60 + return s.responseCharge(response) 53 61 } 54 62 55 - func (b *StripeProvider) createChargeBody(request *domain.Payment) []byte { 63 + func (s *StripeProvider) createChargeBody(request *domain.Payment) []byte { 56 64 toSend := &StripeCharge{ 57 65 Amount: request.Amount, 58 66 Currency: request.Currency, ··· 82 90 CardId uuid.UUID `json:"cardId"` 83 91 } 84 92 85 - func (b *StripeProvider) responseCharge(response *http.Response) (*domain.Provider, error) { 93 + func (s *StripeProvider) responseCharge(response *http.Response) (*domain.Provider, error) { 86 94 var data StripeChargeResponse 87 95 if err := json.NewDecoder(response.Body).Decode(&data); err != nil { 88 96 return nil, err
+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
··· 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 }
+1
request.hurl
··· 13 13 "installments": 1 14 14 } 15 15 } 16 + HTTP 200