From 3834565b568f024387773d73f3874d7e3b210c8e Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 5 Jun 2025 15:48:20 +0530 Subject: [PATCH 01/68] create contribution fetching for all users in the system from bigquery service (in future this will be a cron job that will be scheduled to run daily) --- .gitignore | 4 +- cmd/main.go | 13 ++- go.mod | 44 +++++++- go.sum | 87 ++++++++++++++++ internal/app/bigquery/domain.go | 15 +++ internal/app/bigquery/service.go | 87 ++++++++++++++++ internal/app/contribution/domain.go | 40 ++++++++ internal/app/contribution/handler.go | 35 +++++++ internal/app/contribution/service.go | 148 +++++++++++++++++++++++++++ internal/app/dependencies.go | 36 +++++-- internal/app/repository/domain.go | 30 ++++++ internal/app/repository/service.go | 91 ++++++++++++++++ internal/app/router.go | 1 + internal/config/app.go | 18 ++-- internal/config/bigquery.go | 23 +++++ internal/pkg/apperrors/errors.go | 17 +-- internal/repository/base.go | 3 +- internal/repository/contribution.go | 73 +++++++++++++ internal/repository/domain.go | 26 +++++ internal/repository/repository.go | 111 ++++++++++++++++++++ internal/repository/user.go | 25 +++++ 21 files changed, 897 insertions(+), 30 deletions(-) create mode 100644 internal/app/bigquery/domain.go create mode 100644 internal/app/bigquery/service.go create mode 100644 internal/app/contribution/domain.go create mode 100644 internal/app/contribution/handler.go create mode 100644 internal/app/contribution/service.go create mode 100644 internal/app/repository/domain.go create mode 100644 internal/app/repository/service.go create mode 100644 internal/config/bigquery.go create mode 100644 internal/repository/contribution.go create mode 100644 internal/repository/repository.go diff --git a/.gitignore b/.gitignore index 9890de4..6f3ff51 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -local.yaml \ No newline at end of file +local.yaml + +*.ps1 \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 35ae97a..fd6692d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,7 +6,7 @@ import ( "log/slog" "net/http" "os" - + "os/signal" "syscall" "time" @@ -18,13 +18,12 @@ import ( func main() { ctx := context.Background() - cfg,err := config.LoadAppConfig() + cfg, err := config.LoadAppConfig() if err != nil { slog.Error("error loading app config", "error", err) return } - db, err := config.InitDataStore(cfg) if err != nil { slog.Error("error initializing database", "error", err) @@ -32,7 +31,13 @@ func main() { } defer db.Close() - dependencies := app.InitDependencies(db,cfg) + bigqueryInstance, err := config.BigqueryInit(ctx, cfg) + if err != nil { + slog.Error("error initializing bigquery", "error", err) + return + } + + dependencies := app.InitDependencies(db, cfg, bigqueryInstance) router := app.NewRouter(dependencies) diff --git a/go.mod b/go.mod index 79cfc35..93d5a95 100644 --- a/go.mod +++ b/go.mod @@ -11,10 +11,52 @@ require ( ) require ( + cloud.google.com/go v0.121.0 // indirect + cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/bigquery v1.68.0 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect github.com/BurntSushi/toml v1.2.1 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/apache/arrow/go/v15 v15.0.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/flatbuffers v23.5.26+incompatible // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/kr/pretty v0.3.1 // indirect + github.com/pierrec/lz4/v4 v4.1.18 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.30.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/api v0.231.0 // indirect + google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect + google.golang.org/grpc v1.72.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect diff --git a/go.sum b/go.sum index b892459..2bbeb64 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,58 @@ +cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg= +cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/bigquery v1.68.0 h1:F+CPqdcMxZGUDBACzGtOJ1E6E0MWSYcKeFthxnhpYIU= +cloud.google.com/go/bigquery v1.68.0/go.mod h1:1UAksG8IFXJomQV38xUsRB+2m2c1H9U0etvoGHgyhDk= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= +github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= +github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -26,11 +64,60 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= +github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q= +google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= +google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE= +google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/app/bigquery/domain.go b/internal/app/bigquery/domain.go new file mode 100644 index 0000000..3a8575a --- /dev/null +++ b/internal/app/bigquery/domain.go @@ -0,0 +1,15 @@ +package bigquery + +import "time" + +type ContributionResponse struct { + ID string `bigquery:"id"` + Type string `bigquery:"type"` + ActorID int `bigquery:"actor_id"` + ActorLogin string `bigquery:"actor_login"` + RepoID int `bigquery:"repo_id"` + RepoName string `bigquery:"repo_name"` + RepoUrl string `bigquery:"repo_url"` + Payload string `bigquery:"payload"` + CreatedAt time.Time `bigquery:"created_at"` +} diff --git a/internal/app/bigquery/service.go b/internal/app/bigquery/service.go new file mode 100644 index 0000000..5b623e4 --- /dev/null +++ b/internal/app/bigquery/service.go @@ -0,0 +1,87 @@ +package bigquery + +import ( + "context" + "fmt" + "log/slog" + "strings" + "time" + + bq "cloud.google.com/go/bigquery" + "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + bigqueryInstance config.Bigquery + userRepository repository.UserRepository +} + +type Service interface { + FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) +} + +func NewService(bigqueryInstance config.Bigquery) Service { + return &service{ + bigqueryInstance: bigqueryInstance, + } +} + +func (s *service) FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) { + YesterdayDate := time.Now().AddDate(0, 0, -1) + YesterdayYearMonthDay := YesterdayDate.Format("20030101") + + usersNamesList, err := s.userRepository.GetAllUsersGithubUsernames(ctx, nil) + if err != nil { + slog.Error("error fetching users github usernames") + return nil, apperrors.ErrInternalServer + } + + var quotedUsernamesList []string + for _, username := range usersNamesList { + quotedUsernamesList = append(quotedUsernamesList, fmt.Sprintf("'%s'", username)) + } + + githubUsernames := strings.Join(quotedUsernamesList, ",") + fetchDailyContributionsQuery := fmt.Sprintf(` +SELECT + id, + type, + public, + actor.id AS actor_id, + actor.login AS actor_login, + actor.gravatar_id AS actor_gravatar_id, + actor.url AS actor_url, + actor.avatar_url AS actor_avatar_url, + repo.id AS repo_id, + repo.name AS repo_name, + repo.url AS repo_url, + payload, + created_at, + other +FROM + githubarchive.day.%s +WHERE + type IN ( + 'IssuesEvent', + 'PullRequestEvent', + 'PullRequestReviewEvent', + 'IssueCommentEvent', + 'PullRequestReviewCommentEvent' + ) + AND ( + actor.login IN (%s) OR + JSON_EXTRACT_SCALAR(payload, "$.pull_request.user.login") IN (%s) + ) +`, YesterdayYearMonthDay, githubUsernames, githubUsernames) + + bigqueryQuery := s.bigqueryInstance.Client.Query(fetchDailyContributionsQuery) + contributionRows, err := bigqueryQuery.Read(ctx) + if err != nil { + slog.Error("error fetching contributions", "error", err) + return nil, err + } + + return contributionRows, err +} diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go new file mode 100644 index 0000000..2017962 --- /dev/null +++ b/internal/app/contribution/domain.go @@ -0,0 +1,40 @@ +package contribution + +import "time" + +type ContributionResponse struct { + ID string `bigquery:"id"` + Type string `bigquery:"type"` + ActorID int `bigquery:"actor_id"` + ActorLogin string `bigquery:"actor_login"` + RepoID int `bigquery:"repo_id"` + RepoName string `bigquery:"repo_name"` + RepoUrl string `bigquery:"repo_url"` + Payload string `bigquery:"payload"` + CreatedAt time.Time `bigquery:"created_at"` +} + +type Repository struct { + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + CreatedAt int64 + UpdatedAt int64 +} + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + CreatedAt int64 + UpdatedAt int64 +} diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go new file mode 100644 index 0000000..b680d18 --- /dev/null +++ b/internal/app/contribution/handler.go @@ -0,0 +1,35 @@ +package contribution + +import ( + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" +) + +type handler struct { + contributionService Service +} + +type Handler interface { + FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) +} + +func NewHandler(contributionService Service) Handler { + return &handler{ + contributionService: contributionService, + } +} + +func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + err := h.contributionService.ProcessFetchedContributions(ctx) + if err != nil { + slog.Error("error fetching latest contributions") + return + } + + response.WriteJson(w, http.StatusOK, "contribution fetched successfully", nil) +} diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go new file mode 100644 index 0000000..62ff3c0 --- /dev/null +++ b/internal/app/contribution/service.go @@ -0,0 +1,148 @@ +package contribution + +import ( + "context" + "encoding/json" + "log/slog" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" + repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" + "google.golang.org/api/iterator" +) + +type service struct { + bigqueryService bigquery.Service + contributionRepository repository.ContributionRepository + repositoryService repoService.Service +} + +type Service interface { + ProcessFetchedContributions(ctx context.Context) error + CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int) (Contribution, error) +} + +func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service) Service { + return &service{ + bigqueryService: bigqueryService, + contributionRepository: contributionRepository, + repositoryService: repositoryService, + } +} + +func (s *service) ProcessFetchedContributions(ctx context.Context) error { + contributions, err := s.bigqueryService.FetchDailyContributions(ctx) + if err != nil { + slog.Error("error fetching daily contributions", "error", err) + return err + } + + for { + var contribution ContributionResponse + if err := contributions.Next(&contribution); err == iterator.Done { + break + } else if err != nil { + slog.Error("error iterating contribution rows", "error", err) + break + } + + var contributionPayload map[string]interface{} + err := json.Unmarshal([]byte(contribution.Payload), &contributionPayload) + if err != nil { + slog.Warn("invalid payload", "error", err) + continue + } + + var action string + if actionVal, ok := contributionPayload["action"]; ok { + action = actionVal.(string) + } + + var pullRequest map[string]interface{} + var isMerged bool + if pullRequestPayload, ok := contributionPayload["pull_request"]; ok { + pullRequest = pullRequestPayload.(map[string]interface{}) + isMerged = pullRequest["merged"].(bool) + } + + var issue map[string]interface{} + var stateReason string + if issuePayload, ok := contributionPayload["issue"]; ok { + issue = issuePayload.(map[string]interface{}) + stateReason = issue["state_reason"].(string) + } + + var contributionType string + switch contribution.Type { + case "PullRequestEvent": + if action == "closed" && isMerged { + contributionType = "PullRequestMerged" + } else if action == "opened" { + contributionType = "PullRequestOpened" + } + + case "IssuesEvent": + if action == "opened" { + contributionType = "IssueOpened" + } else if action == "closed" && stateReason == "not_planned" { + contributionType = "IssueClosed" + } else if action == "closed" && stateReason == "completed" { + contributionType = "IssueResolved" + } + + case "PushEvent": + contributionType = "PullRequestUpdated" + + case "IssueCommentEvent": + contributionType = "IssueComment" + + case "PullRequestComment ": + contributionType = "PullRequestComment" + } + + repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) + repositoryId := repoFetched.Id + if err != nil { + repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) + if err != nil { + slog.Error("error fetching repository details") + return err + } + + repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, repo) + if err != nil { + slog.Error("error creating repository", "error", err) + return err + } + + repositoryId = repositoryCreated.Id + } + + _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId) + if err != nil { + slog.Error("error creating contribution", "error", err) + return err + } + } + return nil +} + +func (s *service) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int) (Contribution, error) { + contribution := Contribution{ + UserId: contributionDetails.ActorID, + RepositoryId: repositoryId, + ContributionType: contributionType, + //get id and balance from contribution_score_id table by sending it contribution_type (hardcoded for now) + ContributionScoreId: 1, + BalanceChange: 10, + ContributedAt: contributionDetails.CreatedAt, + } + + contributionResponse, err := s.contributionRepository.CreateContribution(ctx, nil, repository.Contribution(contribution)) + if err != nil { + slog.Error("error creating contribution", "error", err) + return Contribution{}, err + } + + return Contribution(contributionResponse), nil +} diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index fa49370..57a0660 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -3,33 +3,47 @@ package app import ( "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/app/auth" + "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" + "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" + repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) type Dependencies struct { - AuthService auth.Service - UserService user.Service - AuthHandler auth.Handler - UserHandler user.Handler - AppCfg config.AppConfig + AuthService auth.Service + UserService user.Service + AuthHandler auth.Handler + UserHandler user.Handler + ContributionHandler contribution.Handler + AppCfg config.AppConfig + Client config.Bigquery } -func InitDependencies(db *sqlx.DB, appCfg config.AppConfig) Dependencies { +func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigquery) Dependencies { userRepository := repository.NewUserRepository(db) + contributionRepository := repository.NewContributionRepository(db) + repositoryRepository := repository.NewRepositoryRepository(db) userService := user.NewService(userRepository) authService := auth.NewService(userService, appCfg) + bigqueryService := bigquery.NewService(client) + repositoryService := repoService.NewService(repositoryRepository, appCfg) + contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService) authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) + contributionHandler := contribution.NewHandler(contributionService) return Dependencies{ - AuthService: authService, - UserService: userService, - AuthHandler: authHandler, - UserHandler: userHandler, - AppCfg: appCfg, + AuthService: authService, + UserService: userService, + AuthHandler: authHandler, + UserHandler: userHandler, + ContributionHandler: contributionHandler, + AppCfg: appCfg, + Client: client, } } diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go new file mode 100644 index 0000000..b2da319 --- /dev/null +++ b/internal/app/repository/domain.go @@ -0,0 +1,30 @@ +package repository + +import "time" + +type RepoOWner struct { + Login string `json:"login"` +} + +type FetchRepositoryDetailsResponse struct { + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` + RepoOwnerName RepoOWner `json:"owner"` + RepoUrl string `json:"html_url"` +} + +type Repository struct { + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + CreatedAt int64 + UpdatedAt int64 +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go new file mode 100644 index 0000000..6b449d8 --- /dev/null +++ b/internal/app/repository/service.go @@ -0,0 +1,91 @@ +package repository + +import ( + "context" + "encoding/json" + "io" + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + repositoryRepository repository.RepositoryRepository + appCfg config.AppConfig +} + +type Service interface { + GetRepoByGithubId(ctx context.Context, githubRepoId int) (Repository, error) + FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) + CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) +} + +func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { + return &service{ + repositoryRepository: repositoryRepository, + appCfg: appCfg, + } +} + +func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repository, error) { + repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) + if err != nil { + slog.Error("failed to get repository by github id") + return Repository{}, err + } + + return Repository(repoDetails), nil +} + +func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { + req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error freading body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + var repoDetails FetchRepositoryDetailsResponse + err = json.Unmarshal(body, &repoDetails) + if err != nil { + slog.Error("error unmarshalling fetch repository details body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + return repoDetails, nil +} + +func (s *service) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) { + createRepo := Repository{ + GithubRepoId: repoGithubId, + RepoName: repo.Name, + RepoUrl: repo.RepoUrl, + Description: repo.Description, + LanguagesUrl: repo.LanguagesURL, + OwnerName: repo.RepoOwnerName.Login, + UpdateDate: repo.UpdateDate, + } + repositoryCreated, err := s.repositoryRepository.CreateRepository(ctx, nil, repository.Repository(createRepo)) + if err != nil { + slog.Error("failed to create repository", "error", err) + return Repository{}, err + } + + return Repository(repositoryCreated), nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 072a53a..d844c3c 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -20,5 +20,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/recent", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/config/app.go b/internal/config/app.go index c4715f5..4070c0f 100644 --- a/internal/config/app.go +++ b/internal/config/app.go @@ -25,13 +25,19 @@ type GithubOauth struct { RedirectURL string `yaml:"redirect_url" required:"true"` } +type BigqueryProject struct { + ProjectID string `yaml:"project_id" required:"true"` +} + type AppConfig struct { - IsProduction bool `yaml:"is_production"` - HTTPServer HTTPServer `yaml:"http_server"` - Database Database `yaml:"database"` - JWTSecret string `yaml:"jwt_secret"` - ClientURL string `yaml:"client_url"` - GithubOauth GithubOauth `yaml:"github_oauth"` + IsProduction bool `yaml:"is_production"` + HTTPServer HTTPServer `yaml:"http_server"` + Database Database `yaml:"database"` + JWTSecret string `yaml:"jwt_secret"` + ClientURL string `yaml:"client_url"` + GithubOauth GithubOauth `yaml:"github_oauth"` + BigqueryProject BigqueryProject `yaml:"bigquery_project"` + GithubPersonalAccessToken string `yaml:"github_personal_access_token"` } func LoadAppConfig() (AppConfig, error) { diff --git a/internal/config/bigquery.go b/internal/config/bigquery.go new file mode 100644 index 0000000..30294c5 --- /dev/null +++ b/internal/config/bigquery.go @@ -0,0 +1,23 @@ +package config + +import ( + "context" + + "cloud.google.com/go/bigquery" +) + +type Bigquery struct { + Client *bigquery.Client +} + +func BigqueryInit(ctx context.Context, appCfg AppConfig) (Bigquery, error) { + client, err := bigquery.NewClient(ctx, appCfg.BigqueryProject.ProjectID) + if err != nil { + return Bigquery{}, err + } + + bigqueryInstance := Bigquery{ + Client: client, + } + return bigqueryInstance, nil +} diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 5c7244d..beeba07 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -20,16 +20,21 @@ var ( ErrNoAppConfigPath = errors.New("no config path provided") ErrFailedToLoadAppConfig = errors.New("failed to load environment configuration") - ErrLoginWithGithubFailed = errors.New("failed to login with Github") + ErrLoginWithGithubFailed = errors.New("failed to login with Github") ErrGithubTokenExchangeFailed = errors.New("failed to exchange Github token") - ErrFailedToGetGithubUser = errors.New("failed to get Github user info") - ErrFailedToGetUserEmail = errors.New("failed to get user email from Github") + ErrFailedToGetGithubUser = errors.New("failed to get Github user info") + ErrFailedToGetUserEmail = errors.New("failed to get user email from Github") - ErrUserNotFound = errors.New("user not found") + ErrUserNotFound = errors.New("user not found") ErrUserCreationFailed = errors.New("failed to create user") - ErrJWTCreationFailed = errors.New("failed to create jwt token") - ErrAuthorizationFailed=errors.New("failed to authorize user") + ErrJWTCreationFailed = errors.New("failed to create jwt token") + ErrAuthorizationFailed = errors.New("failed to authorize user") + + ErrRepoNotFound = errors.New("repository not found") + ErrRepoCreationFailed = errors.New("failed to create repo for user") + + ErrContributionCreationFailed = errors.New("failed to create contrbitution") ) func MapError(err error) (statusCode int, errMessage string) { diff --git a/internal/repository/base.go b/internal/repository/base.go index 5516997..a38e9ba 100644 --- a/internal/repository/base.go +++ b/internal/repository/base.go @@ -23,6 +23,7 @@ type RepositoryTransaction interface { type QueryExecuter interface { QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) } func (b *BaseRepository) BeginTx(ctx context.Context) (*sqlx.Tx, error) { @@ -61,7 +62,7 @@ func (b *BaseRepository) HandleTransaction(ctx context.Context, tx *sqlx.Tx, inc } return nil } - + err := tx.Commit() if err != nil { slog.Error("error occurred while committing database transaction", "error", err) diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go new file mode 100644 index 0000000..8b9f9ff --- /dev/null +++ b/internal/repository/contribution.go @@ -0,0 +1,73 @@ +package repository + +import ( + "context" + "log/slog" + "time" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +type contributionRepository struct { + BaseRepository +} + +type ContributionRepository interface { + RepositoryTransaction + CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) +} + +func NewContributionRepository(db *sqlx.DB) ContributionRepository { + return &contributionRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + createContributionQuery = ` + INSERT INTO contributions ( + user_id, + repository_id, + contribution_score_id, + contribution_type, + balance_change, + contributed_at, + created_at, + updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING *` +) + +func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contribution Contribution + err := executer.QueryRowContext(ctx, createContributionQuery, + contributionInfo.UserId, + contributionInfo.RepositoryId, + contributionInfo.ContributionScoreId, + contributionInfo.ContributionType, + contributionInfo.BalanceChange, + contributionInfo.ContributedAt, + time.Now().Unix(), + time.Now().Unix(), + ).Scan( + &contribution.Id, + &contribution.UserId, + &contribution.RepositoryId, + &contribution.ContributionScoreId, + &contribution.ContributionType, + &contribution.BalanceChange, + &contribution.ContributedAt, + &contribution.CreatedAt, + &contribution.UpdatedAt, + ) + if err != nil { + slog.Error("error occured while inserting contributions", "error", err) + return Contribution{}, apperrors.ErrContributionCreationFailed + } + + return contribution, err +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index b8f6e57..90b2066 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -2,6 +2,7 @@ package repository import ( "database/sql" + "time" ) type User struct { @@ -28,3 +29,28 @@ type CreateUserRequestBody struct { Email string IsAdmin bool } + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + CreatedAt int64 + UpdatedAt int64 +} + +type Repository struct { + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + CreatedAt int64 + UpdatedAt int64 +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go new file mode 100644 index 0000000..1ed9e2f --- /dev/null +++ b/internal/repository/repository.go @@ -0,0 +1,111 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "log/slog" + "time" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +type repositoryRepository struct { + BaseRepository +} + +type RepositoryRepository interface { + RepositoryTransaction + GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) + CreateRepository(ctx context.Context, tx *sqlx.Tx, repository Repository) (Repository, error) +} + +func NewRepositoryRepository(db *sqlx.DB) RepositoryRepository { + return &repositoryRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + getRepoByGithubIdQuery = `SELECT * from repositories where github_repo_id=$1` + + createRepositoryQuery = ` + INSERT INTO repositories ( + github_repo_id, + repo_name, + description, + languages_url, + repo_url, + owner_name, + update_date, + created_at, + updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING *` +) + +func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.QueryRowContext(ctx, getRepoByGithubIdQuery, repoGithubId).Scan( + &repository.Id, + &repository.GithubRepoId, + &repository.RepoName, + &repository.Description, + &repository.LanguagesUrl, + &repository.RepoUrl, + &repository.OwnerName, + &repository.UpdateDate, + &repository.CreatedAt, + &repository.UpdatedAt, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("repository not found", "error", err) + return Repository{}, apperrors.ErrRepoNotFound + } + slog.Error("error occurred while getting repository by id", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil + +} + +func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.Tx, repositoryInfo Repository) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.QueryRowContext(ctx, createRepositoryQuery, + repositoryInfo.GithubRepoId, + repositoryInfo.RepoName, + repositoryInfo.Description, + repositoryInfo.LanguagesUrl, + repositoryInfo.RepoUrl, + repositoryInfo.OwnerName, + repositoryInfo.UpdateDate, + time.Now().Unix(), + time.Now().Unix(), + ).Scan( + &repository.Id, + &repository.GithubRepoId, + &repository.RepoName, + &repository.Description, + &repository.LanguagesUrl, + &repository.RepoUrl, + &repository.OwnerName, + &repository.UpdateDate, + &repository.CreatedAt, + &repository.UpdatedAt, + ) + if err != nil { + slog.Error("error occured while creating repository", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil + +} diff --git a/internal/repository/user.go b/internal/repository/user.go index c7fb093..bf2a713 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -21,6 +21,7 @@ type UserRepository interface { GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error + GetAllUsersGithubUsernames(ctx context.Context, tx *sqlx.Tx) ([]string, error) } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -47,6 +48,8 @@ const ( RETURNING *` updateEmailQuery = "UPDATE users SET email=$1 where id=$2" + + getAllUsersGithubUsernamesQuery = "SELECT github_username from users" ) func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { @@ -160,3 +163,25 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, Id i return nil } + +func (ur *userRepository) GetAllUsersGithubUsernames(ctx context.Context, tx *sqlx.Tx) ([]string, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + rows, err := executer.QueryContext(ctx, getAllUsersGithubUsernamesQuery) + if err != nil { + slog.Error("failed to get github usernames", "error", err) + return nil, apperrors.ErrInternalServer + } + defer rows.Close() + + var githubUsernames []string + for rows.Next() { + var username string + if err := rows.Scan(&username); err != nil { + return nil, err + } + githubUsernames = append(githubUsernames, username) + } + + return githubUsernames, nil +} From d3d91a4dfa87e38b91536d9c863cebff0ab08ba7 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 9 Jun 2025 21:18:19 +0530 Subject: [PATCH 02/68] use userid while adding contribution --- internal/app/contribution/domain.go | 8 ++++---- internal/app/contribution/service.go | 21 +++++++++++++++------ internal/app/dependencies.go | 4 ++-- internal/app/repository/domain.go | 4 ++-- internal/app/repository/service.go | 4 ++-- internal/repository/contribution.go | 9 ++------- internal/repository/domain.go | 8 ++++---- internal/repository/repository.go | 9 ++------- internal/repository/user.go | 1 - 9 files changed, 33 insertions(+), 35 deletions(-) diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index 2017962..e1eb0ea 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -23,8 +23,8 @@ type Repository struct { RepoUrl string OwnerName string UpdateDate time.Time - CreatedAt int64 - UpdatedAt int64 + CreatedAt time.Time + UpdatedAt time.Time } type Contribution struct { @@ -35,6 +35,6 @@ type Contribution struct { ContributionType string BalanceChange int ContributedAt time.Time - CreatedAt int64 - UpdatedAt int64 + CreatedAt time.Time + UpdatedAt time.Time } diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 62ff3c0..0756582 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -7,6 +7,7 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" + "github.com/joshsoftware/code-curiosity-2025/internal/app/user" "github.com/joshsoftware/code-curiosity-2025/internal/repository" "google.golang.org/api/iterator" ) @@ -15,18 +16,20 @@ type service struct { bigqueryService bigquery.Service contributionRepository repository.ContributionRepository repositoryService repoService.Service + userService user.Service } type Service interface { ProcessFetchedContributions(ctx context.Context) error - CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int) (Contribution, error) + CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) } -func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service) Service { +func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service) Service { return &service{ bigqueryService: bigqueryService, contributionRepository: contributionRepository, repositoryService: repositoryService, + userService: userService, } } @@ -100,7 +103,7 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { contributionType = "PullRequestComment" } - repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) + repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) repositoryId := repoFetched.Id if err != nil { repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) @@ -118,7 +121,13 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { repositoryId = repositoryCreated.Id } - _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId) + user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) + if err != nil { + slog.Error("error getting user id", "error", err) + return err + } + + _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) if err != nil { slog.Error("error creating contribution", "error", err) return err @@ -127,9 +136,9 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { return nil } -func (s *service) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int) (Contribution, error) { +func (s *service) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) { contribution := Contribution{ - UserId: contributionDetails.ActorID, + UserId: userId, RepositoryId: repositoryId, ContributionType: contributionType, //get id and balance from contribution_score_id table by sending it contribution_type (hardcoded for now) diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 57a0660..7f56523 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -29,9 +29,9 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque userService := user.NewService(userRepository) authService := auth.NewService(userService, appCfg) - bigqueryService := bigquery.NewService(client) + bigqueryService := bigquery.NewService(client, userRepository) repositoryService := repoService.NewService(repositoryRepository, appCfg) - contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService) + contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService) authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index b2da319..3c0a80c 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -25,6 +25,6 @@ type Repository struct { RepoUrl string OwnerName string UpdateDate time.Time - CreatedAt int64 - UpdatedAt int64 + CreatedAt time.Time + UpdatedAt time.Time } diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 6b449d8..b598f1b 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -17,7 +17,7 @@ type service struct { } type Service interface { - GetRepoByGithubId(ctx context.Context, githubRepoId int) (Repository, error) + GetRepoByRepoId(ctx context.Context, githubRepoId int) (Repository, error) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) } @@ -29,7 +29,7 @@ func NewService(repositoryRepository repository.RepositoryRepository, appCfg con } } -func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repository, error) { +func (s *service) GetRepoByRepoId(ctx context.Context, repoGithubId int) (Repository, error) { repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) if err != nil { slog.Error("failed to get repository by github id") diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 8b9f9ff..807c646 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -3,7 +3,6 @@ package repository import ( "context" "log/slog" - "time" "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" @@ -32,11 +31,9 @@ const ( contribution_score_id, contribution_type, balance_change, - contributed_at, - created_at, - updated_at + contributed_at ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING *` ) @@ -51,8 +48,6 @@ func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sq contributionInfo.ContributionType, contributionInfo.BalanceChange, contributionInfo.ContributedAt, - time.Now().Unix(), - time.Now().Unix(), ).Scan( &contribution.Id, &contribution.UserId, diff --git a/internal/repository/domain.go b/internal/repository/domain.go index f817e75..5dcab55 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -38,8 +38,8 @@ type Contribution struct { ContributionType string BalanceChange int ContributedAt time.Time - CreatedAt int64 - UpdatedAt int64 + CreatedAt time.Time + UpdatedAt time.Time } type Repository struct { @@ -51,6 +51,6 @@ type Repository struct { RepoUrl string OwnerName string UpdateDate time.Time - CreatedAt int64 - UpdatedAt int64 + CreatedAt time.Time + UpdatedAt time.Time } diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 1ed9e2f..32a77b6 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -5,7 +5,6 @@ import ( "database/sql" "errors" "log/slog" - "time" "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" @@ -38,11 +37,9 @@ const ( languages_url, repo_url, owner_name, - update_date, - created_at, - updated_at + update_date ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *` ) @@ -87,8 +84,6 @@ func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.T repositoryInfo.RepoUrl, repositoryInfo.OwnerName, repositoryInfo.UpdateDate, - time.Now().Unix(), - time.Now().Unix(), ).Scan( &repository.Id, &repository.GithubRepoId, diff --git a/internal/repository/user.go b/internal/repository/user.go index 3d77131..c504048 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -162,7 +162,6 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user func (ur *userRepository) GetAllUsersGithubUsernames(ctx context.Context, tx *sqlx.Tx) ([]string, error) { executer := ur.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, getAllUsersGithubUsernamesQuery) if err != nil { slog.Error("failed to get github usernames", "error", err) From 1c1135f2cf2764ab5e8f4fdbe1706f1a66c40712 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 9 Jun 2025 21:52:52 +0530 Subject: [PATCH 03/68] get id and score from contribution_score --- internal/app/bigquery/service.go | 8 ++++---- internal/app/contribution/domain.go | 9 +++++++++ internal/app/contribution/service.go | 25 +++++++++++++++++++++---- internal/repository/contribution.go | 23 +++++++++++++++++++++++ internal/repository/domain.go | 9 +++++++++ 5 files changed, 66 insertions(+), 8 deletions(-) diff --git a/internal/app/bigquery/service.go b/internal/app/bigquery/service.go index 5b623e4..9d00a48 100644 --- a/internal/app/bigquery/service.go +++ b/internal/app/bigquery/service.go @@ -22,9 +22,10 @@ type Service interface { FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) } -func NewService(bigqueryInstance config.Bigquery) Service { +func NewService(bigqueryInstance config.Bigquery, userRepository repository.UserRepository) Service { return &service{ bigqueryInstance: bigqueryInstance, + userRepository: userRepository, } } @@ -71,10 +72,9 @@ WHERE 'PullRequestReviewCommentEvent' ) AND ( - actor.login IN (%s) OR - JSON_EXTRACT_SCALAR(payload, "$.pull_request.user.login") IN (%s) + actor.login IN (%s) ) -`, YesterdayYearMonthDay, githubUsernames, githubUsernames) +`, YesterdayYearMonthDay, githubUsernames) bigqueryQuery := s.bigqueryInstance.Client.Query(fetchDailyContributionsQuery) contributionRows, err := bigqueryQuery.Read(ctx) diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index e1eb0ea..b736d91 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -38,3 +38,12 @@ type Contribution struct { CreatedAt time.Time UpdatedAt time.Time } + +type ContributionScore struct { + Id int + AdminId int + ContributionType string + Score int + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 0756582..fbd025d 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -22,6 +22,7 @@ type service struct { type Service interface { ProcessFetchedContributions(ctx context.Context) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) + GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service) Service { @@ -137,16 +138,23 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { } func (s *service) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) { + contribution := Contribution{ UserId: userId, RepositoryId: repositoryId, ContributionType: contributionType, - //get id and balance from contribution_score_id table by sending it contribution_type (hardcoded for now) - ContributionScoreId: 1, - BalanceChange: 10, - ContributedAt: contributionDetails.CreatedAt, + ContributedAt: contributionDetails.CreatedAt, + } + + contributionScoreDetails, err := s.GetContributionScoreDetailsByContributionType(ctx, contributionType) + if err != nil { + slog.Error("error occured while getting contribution score details", "error", err) + return Contribution{}, err } + contribution.ContributionScoreId = contributionScoreDetails.Id + contribution.BalanceChange = contributionScoreDetails.Score + contributionResponse, err := s.contributionRepository.CreateContribution(ctx, nil, repository.Contribution(contribution)) if err != nil { slog.Error("error creating contribution", "error", err) @@ -155,3 +163,12 @@ func (s *service) CreateContribution(ctx context.Context, contributionType strin return Contribution(contributionResponse), nil } + +func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) { + contributionScoreDetails, err := s.contributionRepository.GetContributionScoreDetailsByContributionType(ctx, nil, contributionType) + if err != nil { + slog.Error("error occured while getting contribution score details", "error", err) + return ContributionScore{}, err + } + return ContributionScore(contributionScoreDetails), nil +} diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 807c646..7105069 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -15,6 +15,7 @@ type contributionRepository struct { type ContributionRepository interface { RepositoryTransaction CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) + GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -35,6 +36,8 @@ const ( ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *` + + getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -66,3 +69,23 @@ func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sq return contribution, err } + +func (cr *contributionRepository) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contributionScoreDetails ContributionScore + err := executer.QueryRowContext(ctx, getContributionScoreDetailsByContributionTypeQuery, contributionType).Scan( + &contributionScoreDetails.Id, + &contributionScoreDetails.AdminId, + &contributionScoreDetails.ContributionType, + &contributionScoreDetails.Score, + &contributionScoreDetails.CreatedAt, + &contributionScoreDetails.UpdatedAt, + ) + if err != nil { + slog.Error("error occured while getting contribution score details", "error", err) + return ContributionScore{}, err + } + + return contributionScoreDetails, nil +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 5dcab55..13cedc5 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -54,3 +54,12 @@ type Repository struct { CreatedAt time.Time UpdatedAt time.Time } + +type ContributionScore struct { + Id int + AdminId int + ContributionType string + Score int + CreatedAt time.Time + UpdatedAt time.Time +} \ No newline at end of file From f94000e1954463a5a1f4920e0ce35067a6f81ee4 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 9 Jun 2025 22:00:32 +0530 Subject: [PATCH 04/68] fix date format --- internal/app/bigquery/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/bigquery/service.go b/internal/app/bigquery/service.go index 9d00a48..7a15d7c 100644 --- a/internal/app/bigquery/service.go +++ b/internal/app/bigquery/service.go @@ -31,7 +31,7 @@ func NewService(bigqueryInstance config.Bigquery, userRepository repository.User func (s *service) FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) { YesterdayDate := time.Now().AddDate(0, 0, -1) - YesterdayYearMonthDay := YesterdayDate.Format("20030101") + YesterdayYearMonthDay := YesterdayDate.Format("20060102") usersNamesList, err := s.userRepository.GetAllUsersGithubUsernames(ctx, nil) if err != nil { From c865d7add70a49e5c1bf7424f0e6d190e9b55f77 Mon Sep 17 00:00:00 2001 From: Ashok Choudhary Date: Thu, 12 Jun 2025 18:12:55 +0530 Subject: [PATCH 05/68] removed readme.md file changes --- cmd/main.go | 11 +++-- go.mod | 1 + go.sum | 2 + internal/app/auth/service.go | 11 +++++ internal/app/router.go | 2 + internal/app/user/domain.go | 4 +- internal/app/user/handler.go | 20 +++++++++ internal/app/user/service.go | 22 ++++++++++ internal/pkg/jobs/cleanUp.go | 30 ++++++++++++++ internal/repository/user.go | 78 ++++++++++++++++++++++++++++++++++++ 10 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 internal/pkg/jobs/cleanUp.go diff --git a/cmd/main.go b/cmd/main.go index 35ae97a..9bfc734 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,25 +6,25 @@ import ( "log/slog" "net/http" "os" - + "os/signal" "syscall" "time" "github.com/joshsoftware/code-curiosity-2025/internal/app" "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/jobs" ) func main() { ctx := context.Background() - cfg,err := config.LoadAppConfig() + cfg, err := config.LoadAppConfig() if err != nil { slog.Error("error loading app config", "error", err) return } - db, err := config.InitDataStore(cfg) if err != nil { slog.Error("error initializing database", "error", err) @@ -32,7 +32,7 @@ func main() { } defer db.Close() - dependencies := app.InitDependencies(db,cfg) + dependencies := app.InitDependencies(db, cfg) router := app.NewRouter(dependencies) @@ -41,6 +41,9 @@ func main() { Handler: router, } + // backround job start + jobs.PermanentDeleteJob(db) + serverRunning := make(chan os.Signal, 1) signal.Notify( diff --git a/go.mod b/go.mod index e0eeada..c7a9903 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/kr/pretty v0.3.1 // indirect + github.com/robfig/cron/v3 v3.0.0 // indirect go.uber.org/atomic v1.11.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index ddd7ccb..a4fa16a 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= +github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= diff --git a/internal/app/auth/service.go b/internal/app/auth/service.go index 3bca6c0..2aeeb91 100644 --- a/internal/app/auth/service.go +++ b/internal/app/auth/service.go @@ -3,6 +3,7 @@ package auth import ( "context" "encoding/json" + "fmt" "log/slog" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" @@ -83,6 +84,16 @@ func (s *service) GithubOAuthLoginCallback(ctx context.Context, code string) (st return "", apperrors.ErrInternalServer } + // soft delete checker + err = s.userService.RecoverAccountInGracePeriod(ctx, userData.Id) + if err != nil { + slog.Error("error in recovering account in grace period during login", "error", err) + return "", apperrors.ErrInternalServer + } + // token print + + fmt.Println(jwtToken) + return jwtToken, nil } diff --git a/internal/app/router.go b/internal/app/router.go index 072a53a..a104288 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -20,5 +20,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) + router.HandleFunc("DELETE /api/user/delete", middleware.Authentication(deps.UserHandler.DeleteUser, deps.AppCfg)) + return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/app/user/domain.go b/internal/app/user/domain.go index e2d9e6c..d27f5a0 100644 --- a/internal/app/user/domain.go +++ b/internal/app/user/domain.go @@ -18,8 +18,8 @@ type User struct { Password string `json:"password"` IsDeleted bool `json:"is_deleted"` DeletedAt sql.NullTime `json:"deleted_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type CreateUserRequestBody struct { diff --git a/internal/app/user/handler.go b/internal/app/user/handler.go index 00bcd51..191b4fe 100644 --- a/internal/app/user/handler.go +++ b/internal/app/user/handler.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) @@ -15,6 +16,7 @@ type handler struct { type Handler interface { UpdateUserEmail(w http.ResponseWriter, r *http.Request) + DeleteUser(w http.ResponseWriter, r *http.Request) } func NewHandler(userService Service) Handler { @@ -44,3 +46,21 @@ func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "email updated successfully", nil) } + +func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + val := ctx.Value(middleware.UserIdKey) + + userID := val.(int) + + user, err := h.userService.SoftDeleteUser(ctx, userID) + if err != nil { + slog.Error("failed to softdelete user", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "user scheduled for deletion", user) + +} diff --git a/internal/app/user/service.go b/internal/app/user/service.go index 93b8572..6a0fbd2 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -3,6 +3,7 @@ package user import ( "context" "log/slog" + "time" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" @@ -18,6 +19,8 @@ type Service interface { GetUserByGithubId(ctx context.Context, githubId int) (User, error) CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, email string) error + SoftDeleteUser(ctx context.Context, userID int) (User, error) + RecoverAccountInGracePeriod(ctx context.Context, userID int) error } func NewService(userRepository repository.UserRepository) Service { @@ -74,3 +77,22 @@ func (s *service) UpdateUserEmail(ctx context.Context, email string) error { return nil } + +func (s *service) SoftDeleteUser(ctx context.Context, userID int) (User, error) { + now := time.Now() + user, err := s.userRepository.MarkUserAsDeleted(ctx, nil, userID, now) + if err != nil { + slog.Error("unable to softdelete user", "error", err) + return User{}, apperrors.ErrInternalServer + } + return User(user), nil +} + +func (s *service) RecoverAccountInGracePeriod(ctx context.Context, userID int) error { + err := s.userRepository.AccountScheduledForDelete(ctx, nil, userID) + if err != nil { + slog.Error("failed to recover account in grace period", "error", err) + return err + } + return nil +} diff --git a/internal/pkg/jobs/cleanUp.go b/internal/pkg/jobs/cleanUp.go new file mode 100644 index 0000000..50f639e --- /dev/null +++ b/internal/pkg/jobs/cleanUp.go @@ -0,0 +1,30 @@ +package jobs + +import ( + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" + "github.com/robfig/cron/v3" +) + +func PermanentDeleteJob(db *sqlx.DB) { + slog.Info("entering into the cleanup job") + c := cron.New() + _, err := c.AddFunc("36 00 * * *", func() { + slog.Info("Job scheduled for user cleanup from database") + ur := repository.NewUserRepository(db) // pass in *sql.DB or whatever is needed + err := ur.DeleteUser(nil) + if err != nil { + slog.Error("Cleanup job error", "error", err) + } else { + slog.Info("User cleanup Job completed.") + } + }) + + if err != nil { + slog.Error("failed to start user delete job ", "error", err) + } + + c.Start() +} diff --git a/internal/repository/user.go b/internal/repository/user.go index 284ce27..67d7455 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -21,6 +21,9 @@ type UserRepository interface { GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error + MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) (User, error) + AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error + DeleteUser(tx *sqlx.Tx) error } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -120,6 +123,8 @@ func (ur *userRepository) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo userInfo.GithubUsername, userInfo.Email, userInfo.AvatarUrl, + time.Now(), + time.Now(), ).Scan( &user.Id, &user.GithubId, @@ -156,3 +161,76 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user return nil } + +func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) (User, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + _, err := executer.ExecContext(ctx, `UPDATE users SET is_deleted = TRUE, deleted_at=$1 WHERE id = $2`, deletedAt, userID) + if err != nil { + slog.Error("unable to mark user as deleted", "error", err) + return User{}, apperrors.ErrInternalServer + } + var user User + err = executer.QueryRowContext(ctx, getUserByIdQuery, userID).Scan( + &user.Id, + &user.GithubId, + &user.GithubUsername, + &user.AvatarUrl, + &user.Email, + &user.CurrentActiveGoalId, + &user.CurrentBalance, + &user.IsBlocked, + &user.IsAdmin, + &user.Password, + &user.IsDeleted, + &user.DeletedAt, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("user not found", "error", err) + return User{}, apperrors.ErrUserNotFound + } + slog.Error("error occurred while getting user by id", "error", err) + return User{}, apperrors.ErrInternalServer + } + return user, nil +} + +func (ur *userRepository) AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error { + var deleteGracePeriod = 90 * 24 * time.Hour + user, err := ur.GetUserById(ctx, tx, userID) + + if err != nil { + slog.Error("unable to fetch user by ID ", "error", err) + return apperrors.ErrInternalServer + } + + if user.IsDeleted { + var dlt_at time.Time + if !user.DeletedAt.Valid { + return errors.New("invalid deletion state") + } else { + dlt_at = user.DeletedAt.Time + } + + if time.Since(dlt_at) >= deleteGracePeriod { + slog.Error("user is permanentaly deleted ", "error", err) + return apperrors.ErrInternalServer + } else { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + _, err := executer.ExecContext(ctx, `UPDATE users SET is_deleted = false, deleted_at = NULL WHERE id = $1`, userID) + slog.Error("unable to reverse the soft delete ", "error", err) + return apperrors.ErrInternalServer + } + } + return nil +} + +func (ur *userRepository) DeleteUser(tx *sqlx.Tx) error { + threshold := time.Now().Add(-90 * 1 * time.Second) + executer := ur.BaseRepository.initiateQueryExecuter(tx) + ctx := context.Background() + _, err := executer.ExecContext(ctx, `DELETE FROM users WHERE is_deleted = TRUE AND deleted_at <= $1 `, threshold) + return err +} From a17215cf33717feb000de2d8813cbe823eaece0b Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 16 Jun 2025 15:50:07 +0530 Subject: [PATCH 06/68] refactor processfetchedcontribution function and update http client handling while fetching repositoryDetails --- internal/app/contribution/handler.go | 3 +- internal/app/contribution/service.go | 125 +++++++++++++++------------ internal/app/repository/service.go | 5 +- 3 files changed, 74 insertions(+), 59 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index b680d18..195debb 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -25,7 +25,8 @@ func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Re ctx := r.Context() - err := h.contributionService.ProcessFetchedContributions(ctx) + client := &http.Client{} + err := h.contributionService.ProcessFetchedContributions(ctx, client) if err != nil { slog.Error("error fetching latest contributions") return diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index fbd025d..b38a151 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "log/slog" + "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" @@ -20,7 +21,7 @@ type service struct { } type Service interface { - ProcessFetchedContributions(ctx context.Context) error + ProcessFetchedContributions(ctx context.Context, client *http.Client) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) } @@ -34,7 +35,7 @@ func NewService(bigqueryService bigquery.Service, contributionRepository reposit } } -func (s *service) ProcessFetchedContributions(ctx context.Context) error { +func (s *service) ProcessFetchedContributions(ctx context.Context, client *http.Client) error { contributions, err := s.bigqueryService.FetchDailyContributions(ctx) if err != nil { slog.Error("error fetching daily contributions", "error", err) @@ -50,64 +51,16 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { break } - var contributionPayload map[string]interface{} - err := json.Unmarshal([]byte(contribution.Payload), &contributionPayload) + contributionType, err := s.GetContributionType(ctx, contribution) if err != nil { - slog.Warn("invalid payload", "error", err) - continue - } - - var action string - if actionVal, ok := contributionPayload["action"]; ok { - action = actionVal.(string) - } - - var pullRequest map[string]interface{} - var isMerged bool - if pullRequestPayload, ok := contributionPayload["pull_request"]; ok { - pullRequest = pullRequestPayload.(map[string]interface{}) - isMerged = pullRequest["merged"].(bool) - } - - var issue map[string]interface{} - var stateReason string - if issuePayload, ok := contributionPayload["issue"]; ok { - issue = issuePayload.(map[string]interface{}) - stateReason = issue["state_reason"].(string) - } - - var contributionType string - switch contribution.Type { - case "PullRequestEvent": - if action == "closed" && isMerged { - contributionType = "PullRequestMerged" - } else if action == "opened" { - contributionType = "PullRequestOpened" - } - - case "IssuesEvent": - if action == "opened" { - contributionType = "IssueOpened" - } else if action == "closed" && stateReason == "not_planned" { - contributionType = "IssueClosed" - } else if action == "closed" && stateReason == "completed" { - contributionType = "IssueResolved" - } - - case "PushEvent": - contributionType = "PullRequestUpdated" - - case "IssueCommentEvent": - contributionType = "IssueComment" - - case "PullRequestComment ": - contributionType = "PullRequestComment" + slog.Error("error getting contribution type") + return err } + var repositoryId int repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) - repositoryId := repoFetched.Id if err != nil { - repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) + repo, err := s.repositoryService.FetchRepositoryDetails(ctx, client, contribution.RepoUrl) if err != nil { slog.Error("error fetching repository details") return err @@ -120,6 +73,8 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { } repositoryId = repositoryCreated.Id + } else { + repositoryId = repoFetched.Id } user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) @@ -134,9 +89,68 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { return err } } + return nil } +func (s *service) GetContributionType(ctx context.Context, contribution ContributionResponse) (string, error) { + var contributionPayload map[string]interface{} + err := json.Unmarshal([]byte(contribution.Payload), &contributionPayload) + if err != nil { + slog.Warn("invalid payload", "error", err) + return "", err + } + + var action string + if actionVal, ok := contributionPayload["action"]; ok { + action = actionVal.(string) + } + + var pullRequest map[string]interface{} + var isMerged bool + if pullRequestPayload, ok := contributionPayload["pull_request"]; ok { + pullRequest = pullRequestPayload.(map[string]interface{}) + isMerged = pullRequest["merged"].(bool) + } + + var issue map[string]interface{} + var stateReason string + if issuePayload, ok := contributionPayload["issue"]; ok { + issue = issuePayload.(map[string]interface{}) + stateReason = issue["state_reason"].(string) + } + + var contributionType string + switch contribution.Type { + case "PullRequestEvent": + if action == "closed" && isMerged { + contributionType = "PullRequestMerged" + } else if action == "opened" { + contributionType = "PullRequestOpened" + } + + case "IssuesEvent": + if action == "opened" { + contributionType = "IssueOpened" + } else if action == "closed" && stateReason == "not_planned" { + contributionType = "IssueClosed" + } else if action == "closed" && stateReason == "completed" { + contributionType = "IssueResolved" + } + + case "PushEvent": + contributionType = "PullRequestUpdated" + + case "IssueCommentEvent": + contributionType = "IssueComment" + + case "PullRequestComment ": + contributionType = "PullRequestComment" + } + + return contributionType, nil +} + func (s *service) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) { contribution := Contribution{ @@ -170,5 +184,6 @@ func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Cont slog.Error("error occured while getting contribution score details", "error", err) return ContributionScore{}, err } + return ContributionScore(contributionScoreDetails), nil } diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index b598f1b..1643745 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -18,7 +18,7 @@ type service struct { type Service interface { GetRepoByRepoId(ctx context.Context, githubRepoId int) (Repository, error) - FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) + FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) } @@ -39,7 +39,7 @@ func (s *service) GetRepoByRepoId(ctx context.Context, repoGithubId int) (Reposi return Repository(repoDetails), nil } -func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { +func (s *service) FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) if err != nil { slog.Error("error fetching user repositories details", "error", err) @@ -48,7 +48,6 @@ func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetails req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) - client := &http.Client{} resp, err := client.Do(req) if err != nil { slog.Error("error fetching user repositories details", "error", err) From de13e7a2dd998c554b3b4e5e713200b563ca4297 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 17 Jun 2025 11:40:49 +0530 Subject: [PATCH 07/68] implement users five recent contributions --- internal/app/contribution/handler.go | 19 ++++++++++++ internal/app/contribution/service.go | 16 ++++++++++ internal/app/router.go | 3 +- internal/pkg/apperrors/errors.go | 3 +- internal/repository/contribution.go | 45 +++++++++++++++++++++++++++- 5 files changed, 83 insertions(+), 3 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 195debb..62b2dbe 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -4,6 +4,7 @@ import ( "log/slog" "net/http" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) @@ -13,6 +14,7 @@ type handler struct { type Handler interface { FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) + FetchUsersFiveRecentContributions(w http.ResponseWriter, r *http.Request) } func NewHandler(contributionService Service) Handler { @@ -29,8 +31,25 @@ func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Re err := h.contributionService.ProcessFetchedContributions(ctx, client) if err != nil { slog.Error("error fetching latest contributions") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) return } response.WriteJson(w, http.StatusOK, "contribution fetched successfully", nil) } + +func (h *handler) FetchUsersFiveRecentContributions(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + usersFiveRecentContributions, err := h.contributionService.FetchUsersFiveRecentContributions(ctx) + if err != nil { + slog.Error("error fetching users five recent contributions") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "users five recent contributions fetched successfully", usersFiveRecentContributions) +} diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index b38a151..15377f3 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -24,6 +24,7 @@ type Service interface { ProcessFetchedContributions(ctx context.Context, client *http.Client) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) + FetchUsersFiveRecentContributions(ctx context.Context) ([]Contribution, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service) Service { @@ -187,3 +188,18 @@ func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Cont return ContributionScore(contributionScoreDetails), nil } + +func (s *service) FetchUsersFiveRecentContributions(ctx context.Context) ([]Contribution, error) { + usersFiveRecentContributions, err := s.contributionRepository.FetchUsersFiveRecentContributions(ctx, nil) + if err != nil { + slog.Error("error occured while fetching users five recent contributions", "error", err) + return nil, err + } + + serviceContributions := make([]Contribution, len(usersFiveRecentContributions)) + for i, c := range usersFiveRecentContributions { + serviceContributions[i] = Contribution((c)) + } + + return serviceContributions, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index d844c3c..3d55723 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -20,6 +20,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/contributions/recent", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/latest", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/recent", middleware.Authentication(deps.ContributionHandler.FetchUsersFiveRecentContributions, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index beeba07..34d97ca 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -34,7 +34,8 @@ var ( ErrRepoNotFound = errors.New("repository not found") ErrRepoCreationFailed = errors.New("failed to create repo for user") - ErrContributionCreationFailed = errors.New("failed to create contrbitution") + ErrContributionCreationFailed = errors.New("failed to create contrbitution") + ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") ) func MapError(err error) (statusCode int, errMessage string) { diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 7105069..020ccf5 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -6,6 +6,7 @@ import ( "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" ) type contributionRepository struct { @@ -16,6 +17,7 @@ type ContributionRepository interface { RepositoryTransaction CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) + FetchUsersFiveRecentContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -38,6 +40,8 @@ const ( RETURNING *` getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` + + fetchUsersFiveRecentContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc limit 5` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -81,7 +85,7 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( &contributionScoreDetails.Score, &contributionScoreDetails.CreatedAt, &contributionScoreDetails.UpdatedAt, - ) + ) if err != nil { slog.Error("error occured while getting contribution score details", "error", err) return ContributionScore{}, err @@ -89,3 +93,42 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( return contributionScoreDetails, nil } + +func (cr *contributionRepository) FetchUsersFiveRecentContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return nil, apperrors.ErrInternalServer + } + + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + rows, err := executer.QueryContext(ctx, fetchUsersFiveRecentContributionsQuery, userId) + if err != nil { + slog.Error("error fetching users five recent contributions") + return nil, apperrors.ErrFetchingRecentContributions + } + defer rows.Close() + + var usersFiveRecentContributions []Contribution + for rows.Next() { + var recentContribution Contribution + if err = rows.Scan( + &recentContribution.Id, + &recentContribution.UserId, + &recentContribution.RepositoryId, + &recentContribution.ContributionScoreId, + &recentContribution.ContributionType, + &recentContribution.BalanceChange, + &recentContribution.ContributedAt, + &recentContribution.CreatedAt, &recentContribution.UpdatedAt); err != nil { + return nil, err + } + + usersFiveRecentContributions = append(usersFiveRecentContributions, recentContribution) + } + + return usersFiveRecentContributions, nil +} From f7e8fa9833097885ce2cbb74d360a4a3a1df9252 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 17 Jun 2025 11:59:22 +0530 Subject: [PATCH 08/68] fetch all contributions for the user --- internal/app/contribution/handler.go | 16 +++++++++++ internal/app/contribution/service.go | 18 +++++++++++- internal/app/router.go | 1 + internal/pkg/apperrors/errors.go | 1 + internal/repository/contribution.go | 42 ++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 62b2dbe..cbcaaaa 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -15,6 +15,7 @@ type handler struct { type Handler interface { FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) FetchUsersFiveRecentContributions(w http.ResponseWriter, r *http.Request) + FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) } func NewHandler(contributionService Service) Handler { @@ -53,3 +54,18 @@ func (h *handler) FetchUsersFiveRecentContributions(w http.ResponseWriter, r *ht response.WriteJson(w, http.StatusOK, "users five recent contributions fetched successfully", usersFiveRecentContributions) } + +func (h *handler) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + usersAllContributions, err := h.contributionService.FetchUsersAllContributions(ctx) + if err != nil { + slog.Error("error fetching all contributions for user") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "all contributions for user fetched successfully", usersAllContributions) +} diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 15377f3..1bb6e30 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -25,6 +25,7 @@ type Service interface { CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUsersFiveRecentContributions(ctx context.Context) ([]Contribution, error) + FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service) Service { @@ -195,7 +196,7 @@ func (s *service) FetchUsersFiveRecentContributions(ctx context.Context) ([]Cont slog.Error("error occured while fetching users five recent contributions", "error", err) return nil, err } - + serviceContributions := make([]Contribution, len(usersFiveRecentContributions)) for i, c := range usersFiveRecentContributions { serviceContributions[i] = Contribution((c)) @@ -203,3 +204,18 @@ func (s *service) FetchUsersFiveRecentContributions(ctx context.Context) ([]Cont return serviceContributions, nil } + +func (s *service) FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) { + usersAllContributions, err := s.contributionRepository.FetchUsersAllContributions(ctx, nil) + if err != nil { + slog.Error("error occured while fetching all contributions for user", "error", err) + return nil, err + } + + serviceContributions := make([]Contribution, len(usersAllContributions)) + for i, c := range usersAllContributions { + serviceContributions[i] = Contribution((c)) + } + + return serviceContributions, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 3d55723..fe2c51d 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -22,5 +22,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/contributions/latest", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/recent", middleware.Authentication(deps.ContributionHandler.FetchUsersFiveRecentContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 34d97ca..c9c7f2d 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -36,6 +36,7 @@ var ( ErrContributionCreationFailed = errors.New("failed to create contrbitution") ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") + ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") ) func MapError(err error) (statusCode int, errMessage string) { diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 020ccf5..1f6f2b3 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -18,6 +18,7 @@ type ContributionRepository interface { CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) FetchUsersFiveRecentContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) + FetchUsersAllContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -42,6 +43,8 @@ const ( getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` fetchUsersFiveRecentContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc limit 5` + + fetchUsersAllContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -132,3 +135,42 @@ func (cr *contributionRepository) FetchUsersFiveRecentContributions(ctx context. return usersFiveRecentContributions, nil } + +func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return nil, apperrors.ErrInternalServer + } + + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + rows, err := executer.QueryContext(ctx, fetchUsersAllContributionsQuery, userId) + if err != nil { + slog.Error("error fetching all contributions for user") + return nil, apperrors.ErrFetchingAllContributions + } + defer rows.Close() + + var usersAllContributions []Contribution + for rows.Next() { + var currentContribution Contribution + if err = rows.Scan( + ¤tContribution.Id, + ¤tContribution.UserId, + ¤tContribution.RepositoryId, + ¤tContribution.ContributionScoreId, + ¤tContribution.ContributionType, + ¤tContribution.BalanceChange, + ¤tContribution.ContributedAt, + ¤tContribution.CreatedAt, ¤tContribution.UpdatedAt); err != nil { + return nil, err + } + + usersAllContributions = append(usersAllContributions, currentContribution) + } + + return usersAllContributions, nil +} From 887faa4de4ed537ca3a6148a9b2a665031970183 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 17 Jun 2025 18:17:21 +0530 Subject: [PATCH 09/68] remove unnecessary lines --- internal/app/contribution/handler.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index cbcaaaa..d8b9f92 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -25,7 +25,6 @@ func NewHandler(contributionService Service) Handler { } func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() client := &http.Client{} @@ -41,7 +40,6 @@ func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Re } func (h *handler) FetchUsersFiveRecentContributions(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() usersFiveRecentContributions, err := h.contributionService.FetchUsersFiveRecentContributions(ctx) @@ -56,7 +54,6 @@ func (h *handler) FetchUsersFiveRecentContributions(w http.ResponseWriter, r *ht } func (h *handler) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() usersAllContributions, err := h.contributionService.FetchUsersAllContributions(ctx) From f59627d00ebf9715c4572643e858636998c22c52 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 12:08:10 +0530 Subject: [PATCH 10/68] implement fetch all contributed repositories of user (my contributions section) --- internal/app/dependencies.go | 28 +++-- internal/app/repository/domain.go | 41 +++++++ internal/app/repository/handler.go | 39 +++++++ internal/app/repository/service.go | 157 +++++++++++++++++++++++++ internal/app/router.go | 5 + internal/pkg/apperrors/errors.go | 15 ++- internal/repository/repository.go | 176 +++++++++++++++++++++++++++++ 7 files changed, 448 insertions(+), 13 deletions(-) create mode 100644 internal/app/repository/domain.go create mode 100644 internal/app/repository/handler.go create mode 100644 internal/app/repository/service.go create mode 100644 internal/repository/repository.go diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index fa49370..3f6448a 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -9,11 +9,14 @@ import ( ) type Dependencies struct { - AuthService auth.Service - UserService user.Service - AuthHandler auth.Handler - UserHandler user.Handler - AppCfg config.AppConfig + AuthService auth.Service + UserService user.Service + AuthHandler auth.Handler + UserHandler user.Handler + ContributionHandler contribution.Handler + RepositoryHandler repoService.Handler + AppCfg config.AppConfig + Client config.Bigquery } func InitDependencies(db *sqlx.DB, appCfg config.AppConfig) Dependencies { @@ -24,12 +27,17 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig) Dependencies { authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) + repositoryHandler := repoService.NewHandler(repositoryService) + contributionHandler := contribution.NewHandler(contributionService) return Dependencies{ - AuthService: authService, - UserService: userService, - AuthHandler: authHandler, - UserHandler: userHandler, - AppCfg: appCfg, + AuthService: authService, + UserService: userService, + AuthHandler: authHandler, + UserHandler: userHandler, + RepositoryHandler: repositoryHandler, + ContributionHandler: contributionHandler, + AppCfg: appCfg, + Client: client, } } diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go new file mode 100644 index 0000000..fdb1204 --- /dev/null +++ b/internal/app/repository/domain.go @@ -0,0 +1,41 @@ +package repository + +import "time" + +type RepoOWner struct { + Login string `json:"login"` +} + +type FetchRepositoryDetailsResponse struct { + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` + RepoOwnerName RepoOWner `json:"owner"` + RepoUrl string `json:"html_url"` +} + +type Repository struct { + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + + +type RepoLanguages map[string]int + +type FetchUsersContributedReposResponse struct { + RepoName string + Description string + Languages []string + UpdateDate time.Time + TotalCoinsEarned int +} diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go new file mode 100644 index 0000000..6868bdb --- /dev/null +++ b/internal/app/repository/handler.go @@ -0,0 +1,39 @@ +package repository + +import ( + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" +) + +type handler struct { + repositoryService Service +} + +type Handler interface { + FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) +} + +func NewHandler(repositoryService Service) Handler { + return &handler{ + repositoryService: repositoryService, + } +} + +func (h *handler) FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + client := &http.Client{} + + usersContributedRepos, err := h.repositoryService.FetchUsersContributedRepos(ctx, client) + if err != nil { + slog.Error("error fetching users conributed repos") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "users contributed repositories fetched successfully", usersContributedRepos) +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go new file mode 100644 index 0000000..988e5d7 --- /dev/null +++ b/internal/app/repository/service.go @@ -0,0 +1,157 @@ +package repository + +import ( + "context" + "encoding/json" + "io" + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + repositoryRepository repository.RepositoryRepository + appCfg config.AppConfig +} + +type Service interface { + GetRepoByRepoId(ctx context.Context, githubRepoId int) (Repository, error) + FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) + CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) + FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) + FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) +} + +func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { + return &service{ + repositoryRepository: repositoryRepository, + appCfg: appCfg, + } +} + +func (s *service) GetRepoByRepoId(ctx context.Context, repoGithubId int) (Repository, error) { + repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) + if err != nil { + slog.Error("failed to get repository by github id") + return Repository{}, err + } + + return Repository(repoDetails), nil +} + +func (s *service) FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { + req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) + + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + var repoDetails FetchRepositoryDetailsResponse + err = json.Unmarshal(body, &repoDetails) + if err != nil { + slog.Error("error unmarshalling fetch repository details body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + return repoDetails, nil +} + +func (s *service) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) { + createRepo := Repository{ + GithubRepoId: repoGithubId, + RepoName: repo.Name, + RepoUrl: repo.RepoUrl, + Description: repo.Description, + LanguagesUrl: repo.LanguagesURL, + OwnerName: repo.RepoOwnerName.Login, + UpdateDate: repo.UpdateDate, + } + repositoryCreated, err := s.repositoryRepository.CreateRepository(ctx, nil, repository.Repository(createRepo)) + if err != nil { + slog.Error("failed to create repository", "error", err) + return Repository{}, err + } + + return Repository(repositoryCreated), nil +} + +func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) { + req, err := http.NewRequest("GET", getRepoLanguagesURL, nil) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return RepoLanguages{}, err + } + + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return RepoLanguages{}, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return RepoLanguages{}, err + } + + var repoLanguages RepoLanguages + err = json.Unmarshal(body, &repoLanguages) + if err != nil { + slog.Error("error unmarshalling fetch repository languages body", "error", err) + return RepoLanguages{}, err + } + + return repoLanguages, nil +} + +func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) { + usersContributedRepos, err := s.repositoryRepository.FetchUsersContributedRepos(ctx, nil) + if err != nil { + slog.Error("error fetching users conributed repos") + return nil, err + } + + fetchUsersContributedReposResponse := make([]FetchUsersContributedReposResponse, len(usersContributedRepos)) + + for i, usersContributedRepo := range usersContributedRepos { + fetchUsersContributedReposResponse[i].RepoName = usersContributedRepo.RepoName + fetchUsersContributedReposResponse[i].Description = usersContributedRepo.Description + fetchUsersContributedReposResponse[i].UpdateDate = usersContributedRepo.UpdateDate + + contributedRepoLanguages, err := s.FetchRepositoryLanguages(ctx, client, usersContributedRepo.LanguagesUrl) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return nil, err + } + + for language := range contributedRepoLanguages { + fetchUsersContributedReposResponse[i].Languages = append(fetchUsersContributedReposResponse[i].Languages, language) + } + + userRepoTotalCoins, err := s.repositoryRepository.GetUserRepoTotalCoins(ctx, nil, usersContributedRepo.Id) + if err != nil { + slog.Error("error calculating total coins earned by user for the repository") + return nil, err + } + + fetchUsersContributedReposResponse[i].TotalCoinsEarned = userRepoTotalCoins + } + + return fetchUsersContributedReposResponse, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 072a53a..ad5d19b 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -20,5 +20,10 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/latest", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/recent", middleware.Authentication(deps.ContributionHandler.FetchUsersFiveRecentContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) + + router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 5c7244d..878bbaf 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -28,8 +28,17 @@ var ( ErrUserNotFound = errors.New("user not found") ErrUserCreationFailed = errors.New("failed to create user") - ErrJWTCreationFailed = errors.New("failed to create jwt token") - ErrAuthorizationFailed=errors.New("failed to authorize user") + ErrJWTCreationFailed = errors.New("failed to create jwt token") + ErrAuthorizationFailed = errors.New("failed to authorize user") + + ErrRepoNotFound = errors.New("repository not found") + ErrRepoCreationFailed = errors.New("failed to create repo for user") + ErrCalculatingUserRepoTotalCoins = errors.New("error calculating total coins earned by user for the repository") + ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") + + ErrContributionCreationFailed = errors.New("failed to create contrbitution") + ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") + ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") ) func MapError(err error) (statusCode int, errMessage string) { @@ -40,7 +49,7 @@ func MapError(err error) (statusCode int, errMessage string) { return http.StatusUnauthorized, err.Error() case ErrAccessForbidden: return http.StatusForbidden, err.Error() - case ErrUserNotFound: + case ErrUserNotFound, ErrRepoNotFound: return http.StatusNotFound, err.Error() case ErrInvalidToken: return http.StatusUnprocessableEntity, err.Error() diff --git a/internal/repository/repository.go b/internal/repository/repository.go new file mode 100644 index 0000000..21b5b10 --- /dev/null +++ b/internal/repository/repository.go @@ -0,0 +1,176 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" +) + +type repositoryRepository struct { + BaseRepository +} + +type RepositoryRepository interface { + RepositoryTransaction + GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) + CreateRepository(ctx context.Context, tx *sqlx.Tx, repository Repository) (Repository, error) + GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) + FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) +} + +func NewRepositoryRepository(db *sqlx.DB) RepositoryRepository { + return &repositoryRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + getRepoByGithubIdQuery = `SELECT * from repositories where github_repo_id=$1` + + createRepositoryQuery = ` + INSERT INTO repositories ( + github_repo_id, + repo_name, + description, + languages_url, + repo_url, + owner_name, + update_date + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING *` + + getUserRepoTotalCoinsQuery = `SELECT sum(balance_change) from contributions where user_id = $1 and repository_id = $2;` + + fetchUsersContributedReposQuery = `SELECT * from repositories where id in (SELECT repository_id from contributions where user_id=$1);` +) + +func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.QueryRowContext(ctx, getRepoByGithubIdQuery, repoGithubId).Scan( + &repository.Id, + &repository.GithubRepoId, + &repository.RepoName, + &repository.Description, + &repository.LanguagesUrl, + &repository.RepoUrl, + &repository.OwnerName, + &repository.UpdateDate, + &repository.CreatedAt, + &repository.UpdatedAt, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("repository not found", "error", err) + return Repository{}, apperrors.ErrRepoNotFound + } + slog.Error("error occurred while getting repository by id", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil + +} + +func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.Tx, repositoryInfo Repository) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.QueryRowContext(ctx, createRepositoryQuery, + repositoryInfo.GithubRepoId, + repositoryInfo.RepoName, + repositoryInfo.Description, + repositoryInfo.LanguagesUrl, + repositoryInfo.RepoUrl, + repositoryInfo.OwnerName, + repositoryInfo.UpdateDate, + ).Scan( + &repository.Id, + &repository.GithubRepoId, + &repository.RepoName, + &repository.Description, + &repository.LanguagesUrl, + &repository.RepoUrl, + &repository.OwnerName, + &repository.UpdateDate, + &repository.CreatedAt, + &repository.UpdatedAt, + ) + if err != nil { + slog.Error("error occured while creating repository", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil + +} + +func (r *repositoryRepository) GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return 0, apperrors.ErrInternalServer + } + + executer := r.BaseRepository.initiateQueryExecuter(tx) + + var totalCoins int + + err := executer.QueryRowContext(ctx, getUserRepoTotalCoinsQuery, userId, repoId).Scan(&totalCoins) + if err != nil { + slog.Error("error calculating total coins earned by user for the repository") + return 0, apperrors.ErrCalculatingUserRepoTotalCoins + } + + return totalCoins, nil +} + +func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return nil, apperrors.ErrInternalServer + } + + executer := r.BaseRepository.initiateQueryExecuter(tx) + + rows, err := executer.QueryContext(ctx, fetchUsersContributedReposQuery, userId) + if err != nil { + slog.Error("error fetching users contributed repositories") + return nil, apperrors.ErrFetchingUsersContributedRepos + } + defer rows.Close() + + var usersContributedRepos []Repository + for rows.Next() { + var usersContributedRepo Repository + if err = rows.Scan( + &usersContributedRepo.Id, + &usersContributedRepo.GithubRepoId, + &usersContributedRepo.RepoName, + &usersContributedRepo.Description, + &usersContributedRepo.LanguagesUrl, + &usersContributedRepo.RepoUrl, + &usersContributedRepo.OwnerName, + &usersContributedRepo.UpdateDate, + &usersContributedRepo.CreatedAt, + &usersContributedRepo.UpdatedAt); err != nil { + return nil, err + } + + usersContributedRepos = append(usersContributedRepos, usersContributedRepo) + } + + return usersContributedRepos, nil +} From 82afbddd58fb8db5f81ed543719e442aa0b571a3 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 15:31:07 +0530 Subject: [PATCH 11/68] send all details for the repository --- internal/app/repository/domain.go | 5 +---- internal/app/repository/service.go | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index fdb1204..11dc558 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -29,13 +29,10 @@ type Repository struct { UpdatedAt time.Time } - type RepoLanguages map[string]int type FetchUsersContributedReposResponse struct { - RepoName string - Description string + Repository Languages []string - UpdateDate time.Time TotalCoinsEarned int } diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 988e5d7..53ef3b8 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -130,9 +130,7 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C fetchUsersContributedReposResponse := make([]FetchUsersContributedReposResponse, len(usersContributedRepos)) for i, usersContributedRepo := range usersContributedRepos { - fetchUsersContributedReposResponse[i].RepoName = usersContributedRepo.RepoName - fetchUsersContributedReposResponse[i].Description = usersContributedRepo.Description - fetchUsersContributedReposResponse[i].UpdateDate = usersContributedRepo.UpdateDate + fetchUsersContributedReposResponse[i].Repository = Repository(usersContributedRepo) contributedRepoLanguages, err := s.FetchRepositoryLanguages(ctx, client, usersContributedRepo.LanguagesUrl) if err != nil { From c8da5f1295b12a9bde34970b1923c52cbd6861fb Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 15:50:12 +0530 Subject: [PATCH 12/68] fetch particular repository details --- internal/app/repository/handler.go | 24 ++++++++++++++++++++++++ internal/app/router.go | 2 ++ 2 files changed, 26 insertions(+) diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index 6868bdb..dbfd979 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -3,6 +3,7 @@ package repository import ( "log/slog" "net/http" + "strconv" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" @@ -14,6 +15,7 @@ type handler struct { type Handler interface { FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) + FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) } func NewHandler(repositoryService Service) Handler { @@ -37,3 +39,25 @@ func (h *handler) FetchUsersContributedRepos(w http.ResponseWriter, r *http.Requ response.WriteJson(w, http.StatusOK, "users contributed repositories fetched successfully", usersContributedRepos) } + +func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) + if err != nil { + slog.Error("error fetching particular repo details") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "repository details fetched successfully", repoDetails) +} diff --git a/internal/app/router.go b/internal/app/router.go index ad5d19b..1117988 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -25,5 +25,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) + return middleware.CorsMiddleware(router, deps.AppCfg) } From 121d59c07a50fec4a7b7a9d1eecea9285461c1f1 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 17:35:53 +0530 Subject: [PATCH 13/68] implement fetch particular repo contributors --- internal/app/repository/domain.go | 49 +++++++++++++++++++----------- internal/app/repository/handler.go | 33 ++++++++++++++++++++ internal/app/repository/service.go | 30 ++++++++++++++++++ internal/repository/domain.go | 23 +++++++------- 4 files changed, 107 insertions(+), 28 deletions(-) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 11dc558..1b6f91e 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -7,26 +7,28 @@ type RepoOWner struct { } type FetchRepositoryDetailsResponse struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - LanguagesURL string `json:"languages_url"` - UpdateDate time.Time `json:"updated_at"` - RepoOwnerName RepoOWner `json:"owner"` - RepoUrl string `json:"html_url"` + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` + RepoOwnerName RepoOWner `json:"owner"` + RepoUrl string `json:"html_url"` + ContributorsUrl string `json:"contributors_url"` } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + ContributorsUrl string + OwnerName string + UpdateDate time.Time + CreatedAt time.Time + UpdatedAt time.Time } type RepoLanguages map[string]int @@ -36,3 +38,16 @@ type FetchUsersContributedReposResponse struct { Languages []string TotalCoinsEarned int } + +type FetchRepoContributorsResponse struct { + Id int `json:"id"` + Name string `json:"login"` + AvatarUrl string `json:"avatar_url"` + GithubUrl string `json:"html_url"` + Contributions int `json:"contributions"` +} + +type FetchParticularRepoDetails struct { + Repository + Contributors []FetchRepoContributorsResponse +} diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index dbfd979..13bceb2 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -61,3 +61,36 @@ func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Requ response.WriteJson(w, http.StatusOK, "repository details fetched successfully", repoDetails) } + +func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + client := &http.Client{} + + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) + if err != nil { + slog.Error("error fetching particular repo details") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoContributors, err := h.repositoryService.FetchRepositoryContributors(ctx, client, repoDetails.ContributorsUrl) + if err != nil { + slog.Error("error fetching repo contributors") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "contributors for repo fetched successfully", repoContributors) +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 53ef3b8..b4c5d8c 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -22,6 +22,7 @@ type Service interface { CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) + FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) } func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { @@ -153,3 +154,32 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C return fetchUsersContributedReposResponse, nil } + +func (s *service) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { + req, err := http.NewRequest("GET", getRepoContributorsURl, nil) + if err != nil { + slog.Error("error fetching contributors for repository", "error", err) + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching contributors for repository", "error", err) + return nil, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return nil, err + } + + var repoContributors []FetchRepoContributorsResponse + err = json.Unmarshal(body, &repoContributors) + if err != nil { + slog.Error("error unmarshalling fetch contributors body", "error", err) + return nil, err + } + + return repoContributors, nil +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 13cedc5..4a6966b 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -43,16 +43,17 @@ type Contribution struct { } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + ContributorsUrl string + OwnerName string + UpdateDate time.Time + CreatedAt time.Time + UpdatedAt time.Time } type ContributionScore struct { @@ -62,4 +63,4 @@ type ContributionScore struct { Score int CreatedAt time.Time UpdatedAt time.Time -} \ No newline at end of file +} From 465fcd4666e1aff463c800678668b74f7b30333b Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 18:27:14 +0530 Subject: [PATCH 14/68] implement fetch contributions of user in a particular repository --- internal/app/repository/domain.go | 12 +++++++++ internal/app/repository/handler.go | 24 +++++++++++++++++ internal/app/repository/service.go | 16 +++++++++++ internal/app/router.go | 2 +- internal/pkg/apperrors/errors.go | 9 ++++--- internal/repository/repository.go | 43 ++++++++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 5 deletions(-) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 1b6f91e..0dcfa99 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -51,3 +51,15 @@ type FetchParticularRepoDetails struct { Repository Contributors []FetchRepoContributorsResponse } + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index 13bceb2..2567e8c 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -16,6 +16,7 @@ type handler struct { type Handler interface { FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) + FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Request) } func NewHandler(repositoryService Service) Handler { @@ -94,3 +95,26 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http response.WriteJson(w, http.StatusOK, "contributors for repo fetched successfully", repoContributors) } + +func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + usersContributionsInRepo, err := h.repositoryService.FetchUserContributionsInRepo(ctx, repoId) + if err != nil { + slog.Error("error fetching users contribution in repository") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "users contribution for repository fetched successfully", usersContributionsInRepo) +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index b4c5d8c..bb2bf17 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -23,6 +23,7 @@ type Service interface { FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) + FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) } func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { @@ -183,3 +184,18 @@ func (s *service) FetchRepositoryContributors(ctx context.Context, client *http. return repoContributors, nil } + +func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) { + userContributionsInRepo, err := s.repositoryRepository.FetchUserContributionsInRepo(ctx, nil, githubRepoId) + if err != nil { + slog.Error("error fetching users contribution in repository") + return nil, err + } + + serviceUserContributionsInRepo := make([]Contribution, len(userContributionsInRepo)) + for i, c := range userContributionsInRepo { + serviceUserContributionsInRepo[i] = Contribution(c) + } + + return serviceUserContributionsInRepo, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 1117988..6e77e6c 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -26,6 +26,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) - + router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchUserContributionsInRepo, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 8703f38..743e37a 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -31,10 +31,11 @@ var ( ErrJWTCreationFailed = errors.New("failed to create jwt token") ErrAuthorizationFailed = errors.New("failed to authorize user") - ErrRepoNotFound = errors.New("repository not found") - ErrRepoCreationFailed = errors.New("failed to create repo for user") - ErrCalculatingUserRepoTotalCoins = errors.New("error calculating total coins earned by user for the repository") - ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") + ErrRepoNotFound = errors.New("repository not found") + ErrRepoCreationFailed = errors.New("failed to create repo for user") + ErrCalculatingUserRepoTotalCoins = errors.New("error calculating total coins earned by user for the repository") + ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") + ErrFetchingUserContributionsInRepo = errors.New("error fetching users contribution in repository") ErrContributionCreationFailed = errors.New("failed to create contrbitution") ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 21b5b10..3bf0d81 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -21,6 +21,7 @@ type RepositoryRepository interface { CreateRepository(ctx context.Context, tx *sqlx.Tx, repository Repository) (Repository, error) GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) + FetchUserContributionsInRepo(ctx context.Context, tx *sqlx.Tx, repoGithubId int) ([]Contribution, error) } func NewRepositoryRepository(db *sqlx.DB) RepositoryRepository { @@ -48,6 +49,8 @@ const ( getUserRepoTotalCoinsQuery = `SELECT sum(balance_change) from contributions where user_id = $1 and repository_id = $2;` fetchUsersContributedReposQuery = `SELECT * from repositories where id in (SELECT repository_id from contributions where user_id=$1);` + + fetchUserContributionsInRepoQuery = `SELECT * from contributions where repository_id in (SELECT id from repositories where github_repo_id=$1) and user_id=$2;` ) func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) { @@ -174,3 +177,43 @@ func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, t return usersContributedRepos, nil } + +func (r *repositoryRepository) FetchUserContributionsInRepo(ctx context.Context, tx *sqlx.Tx, repoGithubId int) ([]Contribution, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return nil, apperrors.ErrInternalServer + } + + executer := r.BaseRepository.initiateQueryExecuter(tx) + + rows, err := executer.QueryContext(ctx, fetchUserContributionsInRepoQuery, repoGithubId, userId) + if err != nil { + slog.Error("error fetching users contribution in repository") + return nil, apperrors.ErrFetchingUserContributionsInRepo + } + defer rows.Close() + + var userContributionsInRepo []Contribution + for rows.Next() { + var userContributionInRepo Contribution + if err = rows.Scan( + &userContributionInRepo.Id, + &userContributionInRepo.UserId, + &userContributionInRepo.RepositoryId, + &userContributionInRepo.ContributionScoreId, + &userContributionInRepo.ContributionType, + &userContributionInRepo.BalanceChange, + &userContributionInRepo.ContributedAt, + &userContributionInRepo.CreatedAt, + &userContributionInRepo.UpdatedAt); err != nil { + return nil, err + } + + userContributionsInRepo = append(userContributionsInRepo, userContributionInRepo) + } + + return userContributionsInRepo, nil +} From f85a010a4f7ab3cfae652f7a9478c7384c56bcb8 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 19 Jun 2025 13:39:23 +0530 Subject: [PATCH 15/68] fetch particular repository language percentage --- internal/app/repository/domain.go | 6 +++++ internal/app/repository/handler.go | 42 ++++++++++++++++++++++++++++++ internal/app/repository/service.go | 22 ++++++++++++++++ internal/app/router.go | 1 + 4 files changed, 71 insertions(+) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 0dcfa99..45305de 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -63,3 +63,9 @@ type Contribution struct { CreatedAt time.Time UpdatedAt time.Time } + +type LanguagePercent struct { + Name string + Bytes int + Percentage float64 +} diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index 2567e8c..e8f65f8 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -17,6 +17,7 @@ type Handler interface { FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Request) + FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Request) } func NewHandler(repositoryService Service) Handler { @@ -118,3 +119,44 @@ func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Re response.WriteJson(w, http.StatusOK, "users contribution for repository fetched successfully", usersContributionsInRepo) } + +func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + client := &http.Client{} + + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) + if err != nil { + slog.Error("error fetching particular repo details") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoLanguages, err := h.repositoryService.FetchRepositoryLanguages(ctx, client, repoDetails.LanguagesUrl) + if err != nil { + slog.Error("error fetching particular repo languages") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + langPercent, err := h.repositoryService.CalculateLanguagePercentInRepo(ctx, repoLanguages) + if err != nil { + slog.Error("error fetching particular repo languages") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "language percentages for repo fetched successfully", langPercent) +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index bb2bf17..0eddc1e 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "io" "log/slog" + "math" "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/config" @@ -24,6 +25,7 @@ type Service interface { FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) + CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) } func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { @@ -199,3 +201,23 @@ func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId return serviceUserContributionsInRepo, nil } + +func (s *service) CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) { + var total int + for _, bytes := range repoLanguages { + total += bytes + } + + var langPercent []LanguagePercent + + for lang, bytes := range repoLanguages { + percentage := (float64(bytes) / float64(total)) * 100 + langPercent = append(langPercent, LanguagePercent{ + Name: lang, + Bytes: bytes, + Percentage: math.Round(percentage*10) / 10, + }) + } + + return langPercent, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 6e77e6c..3e3a1b5 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -27,5 +27,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchUserContributionsInRepo, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories/languages/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchLanguagePercentInRepo, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } From bb68dfaa87dc624273ccc677e0cb9b5d2c0720bc Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 19 Jun 2025 15:41:51 +0530 Subject: [PATCH 16/68] pass http client in service struct --- cmd/main.go | 4 +++- internal/app/contribution/handler.go | 3 +-- internal/app/contribution/service.go | 12 +++++++----- internal/app/dependencies.go | 8 +++++--- internal/app/repository/service.go | 10 ++++++---- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index fd6692d..947cd10 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,7 +37,9 @@ func main() { return } - dependencies := app.InitDependencies(db, cfg, bigqueryInstance) + httpClient := &http.Client{} + + dependencies := app.InitDependencies(db, cfg, bigqueryInstance, httpClient) router := app.NewRouter(dependencies) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index d8b9f92..fc1416d 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -27,8 +27,7 @@ func NewHandler(contributionService Service) Handler { func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - client := &http.Client{} - err := h.contributionService.ProcessFetchedContributions(ctx, client) + err := h.contributionService.ProcessFetchedContributions(ctx) if err != nil { slog.Error("error fetching latest contributions") status, errorMessage := apperrors.MapError(err) diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 1bb6e30..e6b1e25 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -18,26 +18,28 @@ type service struct { contributionRepository repository.ContributionRepository repositoryService repoService.Service userService user.Service + httpClient *http.Client } type Service interface { - ProcessFetchedContributions(ctx context.Context, client *http.Client) error + ProcessFetchedContributions(ctx context.Context) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUsersFiveRecentContributions(ctx context.Context) ([]Contribution, error) FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) } -func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service) Service { +func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, httpClient *http.Client) Service { return &service{ bigqueryService: bigqueryService, contributionRepository: contributionRepository, repositoryService: repositoryService, userService: userService, + httpClient: httpClient, } } -func (s *service) ProcessFetchedContributions(ctx context.Context, client *http.Client) error { +func (s *service) ProcessFetchedContributions(ctx context.Context) error { contributions, err := s.bigqueryService.FetchDailyContributions(ctx) if err != nil { slog.Error("error fetching daily contributions", "error", err) @@ -60,9 +62,9 @@ func (s *service) ProcessFetchedContributions(ctx context.Context, client *http. } var repositoryId int - repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) + repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) //err no rows if err != nil { - repo, err := s.repositoryService.FetchRepositoryDetails(ctx, client, contribution.RepoUrl) + repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) if err != nil { slog.Error("error fetching repository details") return err diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 7f56523..616e377 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -1,6 +1,8 @@ package app import ( + "net/http" + "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/app/auth" "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" @@ -22,7 +24,7 @@ type Dependencies struct { Client config.Bigquery } -func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigquery) Dependencies { +func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigquery, httpClient *http.Client) Dependencies { userRepository := repository.NewUserRepository(db) contributionRepository := repository.NewContributionRepository(db) repositoryRepository := repository.NewRepositoryRepository(db) @@ -30,8 +32,8 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque userService := user.NewService(userRepository) authService := auth.NewService(userService, appCfg) bigqueryService := bigquery.NewService(client, userRepository) - repositoryService := repoService.NewService(repositoryRepository, appCfg) - contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService) + repositoryService := repoService.NewService(repositoryRepository, appCfg, httpClient) + contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, httpClient) authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 1643745..b46a33c 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -14,18 +14,20 @@ import ( type service struct { repositoryRepository repository.RepositoryRepository appCfg config.AppConfig + httpClient *http.Client } type Service interface { GetRepoByRepoId(ctx context.Context, githubRepoId int) (Repository, error) - FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) + FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) } -func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { +func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig, httpClient *http.Client) Service { return &service{ repositoryRepository: repositoryRepository, appCfg: appCfg, + httpClient: httpClient, } } @@ -39,7 +41,7 @@ func (s *service) GetRepoByRepoId(ctx context.Context, repoGithubId int) (Reposi return Repository(repoDetails), nil } -func (s *service) FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { +func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) if err != nil { slog.Error("error fetching user repositories details", "error", err) @@ -48,7 +50,7 @@ func (s *service) FetchRepositoryDetails(ctx context.Context, client *http.Clien req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) - resp, err := client.Do(req) + resp, err := s.httpClient.Do(req) if err != nil { slog.Error("error fetching user repositories details", "error", err) return FetchRepositoryDetailsResponse{}, err From e5a4d0bf24b030f810d62a4a61884949e34113f8 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 19 Jun 2025 18:05:29 +0530 Subject: [PATCH 17/68] add contributors_url in repository table --- .../1750328591_add_column_contributors_url.down.sql | 1 + .../migrations/1750328591_add_column_contributors_url.up.sql | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 internal/db/migrations/1750328591_add_column_contributors_url.down.sql create mode 100644 internal/db/migrations/1750328591_add_column_contributors_url.up.sql diff --git a/internal/db/migrations/1750328591_add_column_contributors_url.down.sql b/internal/db/migrations/1750328591_add_column_contributors_url.down.sql new file mode 100644 index 0000000..1ed0731 --- /dev/null +++ b/internal/db/migrations/1750328591_add_column_contributors_url.down.sql @@ -0,0 +1 @@ +ALTER TABLE repositories DROP COLUMN contributors_url; \ No newline at end of file diff --git a/internal/db/migrations/1750328591_add_column_contributors_url.up.sql b/internal/db/migrations/1750328591_add_column_contributors_url.up.sql new file mode 100644 index 0000000..cc05125 --- /dev/null +++ b/internal/db/migrations/1750328591_add_column_contributors_url.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE repositories ADD COLUMN contributors_url VARCHAR(255); + +UPDATE repositories SET contributors_url = '' WHERE contributors_url IS NULL; + +ALTER TABLE repositories ALTER COLUMN contributors_url SET NOT NULL; From 577324577e1e46ec4c8f5d6832dd0b07d93c5324 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 20 Jun 2025 12:14:57 +0530 Subject: [PATCH 18/68] fetch and contributors url for repository --- internal/app/contribution/domain.go | 21 +++++++++-------- internal/app/repository/domain.go | 36 +++++++++++++++-------------- internal/app/repository/service.go | 15 ++++++------ internal/repository/domain.go | 21 +++++++++-------- internal/repository/repository.go | 8 +++++-- 5 files changed, 55 insertions(+), 46 deletions(-) diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index b736d91..5fecd14 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -15,16 +15,17 @@ type ContributionResponse struct { } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + ContributorsUrl string + CreatedAt time.Time + UpdatedAt time.Time } type Contribution struct { diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 3c0a80c..90101b5 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -7,24 +7,26 @@ type RepoOWner struct { } type FetchRepositoryDetailsResponse struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - LanguagesURL string `json:"languages_url"` - UpdateDate time.Time `json:"updated_at"` - RepoOwnerName RepoOWner `json:"owner"` - RepoUrl string `json:"html_url"` + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` + RepoOwnerName RepoOWner `json:"owner"` + ContributorsUrl string `json:"contributors_url"` + RepoUrl string `json:"html_url"` } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + ContributorsUrl string + CreatedAt time.Time + UpdatedAt time.Time } diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index b46a33c..92c51e0 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -74,13 +74,14 @@ func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetails func (s *service) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) { createRepo := Repository{ - GithubRepoId: repoGithubId, - RepoName: repo.Name, - RepoUrl: repo.RepoUrl, - Description: repo.Description, - LanguagesUrl: repo.LanguagesURL, - OwnerName: repo.RepoOwnerName.Login, - UpdateDate: repo.UpdateDate, + GithubRepoId: repoGithubId, + RepoName: repo.Name, + RepoUrl: repo.RepoUrl, + Description: repo.Description, + LanguagesUrl: repo.LanguagesURL, + OwnerName: repo.RepoOwnerName.Login, + UpdateDate: repo.UpdateDate, + ContributorsUrl: repo.ContributorsUrl, } repositoryCreated, err := s.repositoryRepository.CreateRepository(ctx, nil, repository.Repository(createRepo)) if err != nil { diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 13cedc5..d56a615 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -43,16 +43,17 @@ type Contribution struct { } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + ContributorsUrl string + CreatedAt time.Time + UpdatedAt time.Time } type ContributionScore struct { diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 32a77b6..9a2fbf7 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -37,9 +37,10 @@ const ( languages_url, repo_url, owner_name, - update_date + update_date, + contributors_url ) - VALUES ($1, $2, $3, $4, $5, $6, $7) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *` ) @@ -58,6 +59,7 @@ func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx. &repository.UpdateDate, &repository.CreatedAt, &repository.UpdatedAt, + &repository.ContributorsUrl, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -84,6 +86,7 @@ func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.T repositoryInfo.RepoUrl, repositoryInfo.OwnerName, repositoryInfo.UpdateDate, + repositoryInfo.ContributorsUrl, ).Scan( &repository.Id, &repository.GithubRepoId, @@ -95,6 +98,7 @@ func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.T &repository.UpdateDate, &repository.CreatedAt, &repository.UpdatedAt, + &repository.ContributorsUrl, ) if err != nil { slog.Error("error occured while creating repository", "error", err) From f365171d3831b234d221a8d9d19aa0a2a9096739 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 20 Jun 2025 12:20:25 +0530 Subject: [PATCH 19/68] remove fetch five recent contributions for the user- this could be handled in frontend --- internal/app/contribution/handler.go | 15 ---------- internal/app/contribution/service.go | 16 ----------- internal/app/router.go | 1 - internal/repository/contribution.go | 42 ---------------------------- 4 files changed, 74 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index fc1416d..19b9d2e 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -14,7 +14,6 @@ type handler struct { type Handler interface { FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) - FetchUsersFiveRecentContributions(w http.ResponseWriter, r *http.Request) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) } @@ -38,20 +37,6 @@ func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Re response.WriteJson(w, http.StatusOK, "contribution fetched successfully", nil) } -func (h *handler) FetchUsersFiveRecentContributions(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - usersFiveRecentContributions, err := h.contributionService.FetchUsersFiveRecentContributions(ctx) - if err != nil { - slog.Error("error fetching users five recent contributions") - status, errorMessage := apperrors.MapError(err) - response.WriteJson(w, status, errorMessage, nil) - return - } - - response.WriteJson(w, http.StatusOK, "users five recent contributions fetched successfully", usersFiveRecentContributions) -} - func (h *handler) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index e6b1e25..046e617 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -25,7 +25,6 @@ type Service interface { ProcessFetchedContributions(ctx context.Context) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) - FetchUsersFiveRecentContributions(ctx context.Context) ([]Contribution, error) FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) } @@ -192,21 +191,6 @@ func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Cont return ContributionScore(contributionScoreDetails), nil } -func (s *service) FetchUsersFiveRecentContributions(ctx context.Context) ([]Contribution, error) { - usersFiveRecentContributions, err := s.contributionRepository.FetchUsersFiveRecentContributions(ctx, nil) - if err != nil { - slog.Error("error occured while fetching users five recent contributions", "error", err) - return nil, err - } - - serviceContributions := make([]Contribution, len(usersFiveRecentContributions)) - for i, c := range usersFiveRecentContributions { - serviceContributions[i] = Contribution((c)) - } - - return serviceContributions, nil -} - func (s *service) FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) { usersAllContributions, err := s.contributionRepository.FetchUsersAllContributions(ctx, nil) if err != nil { diff --git a/internal/app/router.go b/internal/app/router.go index fe2c51d..6ab919a 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -21,7 +21,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/latest", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/contributions/recent", middleware.Authentication(deps.ContributionHandler.FetchUsersFiveRecentContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 1f6f2b3..436dc11 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -17,7 +17,6 @@ type ContributionRepository interface { RepositoryTransaction CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) - FetchUsersFiveRecentContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) FetchUsersAllContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) } @@ -42,8 +41,6 @@ const ( getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` - fetchUsersFiveRecentContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc limit 5` - fetchUsersAllContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc` ) @@ -97,45 +94,6 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( return contributionScoreDetails, nil } -func (cr *contributionRepository) FetchUsersFiveRecentContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return nil, apperrors.ErrInternalServer - } - - executer := cr.BaseRepository.initiateQueryExecuter(tx) - - rows, err := executer.QueryContext(ctx, fetchUsersFiveRecentContributionsQuery, userId) - if err != nil { - slog.Error("error fetching users five recent contributions") - return nil, apperrors.ErrFetchingRecentContributions - } - defer rows.Close() - - var usersFiveRecentContributions []Contribution - for rows.Next() { - var recentContribution Contribution - if err = rows.Scan( - &recentContribution.Id, - &recentContribution.UserId, - &recentContribution.RepositoryId, - &recentContribution.ContributionScoreId, - &recentContribution.ContributionType, - &recentContribution.BalanceChange, - &recentContribution.ContributedAt, - &recentContribution.CreatedAt, &recentContribution.UpdatedAt); err != nil { - return nil, err - } - - usersFiveRecentContributions = append(usersFiveRecentContributions, recentContribution) - } - - return usersFiveRecentContributions, nil -} - func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { userIdValue := ctx.Value(middleware.UserIdKey) From 966d2a23d5b5292ab63445b2e6e272ba32fe0902 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 20 Jun 2025 13:34:23 +0530 Subject: [PATCH 20/68] Fix: Restore correct merge changes for internal/app/repository/domain.go --- internal/app/repository/domain.go | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 90101b5..7788e8c 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -30,3 +30,42 @@ type Repository struct { CreatedAt time.Time UpdatedAt time.Time } + +type RepoLanguages map[string]int + +type FetchUsersContributedReposResponse struct { + Repository + Languages []string + TotalCoinsEarned int +} + +type FetchRepoContributorsResponse struct { + Id int `json:"id"` + Name string `json:"login"` + AvatarUrl string `json:"avatar_url"` + GithubUrl string `json:"html_url"` + Contributions int `json:"contributions"` +} + +type FetchParticularRepoDetails struct { + Repository + Contributors []FetchRepoContributorsResponse +} + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type LanguagePercent struct { + Name string + Bytes int + Percentage float64 +} \ No newline at end of file From 671c8ac8ad0762d90ec0bc35a0b99e70b123ee3e Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 20 Jun 2025 13:39:47 +0530 Subject: [PATCH 21/68] add contributors url when fetching users contributes repositories --- internal/repository/repository.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 084099a..75d61a8 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -172,7 +172,8 @@ func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, t &usersContributedRepo.OwnerName, &usersContributedRepo.UpdateDate, &usersContributedRepo.CreatedAt, - &usersContributedRepo.UpdatedAt); err != nil { + &usersContributedRepo.UpdatedAt, + &usersContributedRepo.ContributorsUrl); err != nil { return nil, err } From 67ab318b49c635b0b5dc6e8486d765b775660fa3 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 20 Jun 2025 13:59:08 +0530 Subject: [PATCH 22/68] seperate logic for fetching repo by github id and repository table id --- internal/app/repository/service.go | 17 ++++++++++++--- internal/repository/repository.go | 34 +++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 23ab6f3..9a6774f 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -19,7 +19,8 @@ type service struct { } type Service interface { - GetRepoByRepoId(ctx context.Context, githubRepoId int) (Repository, error) + GetRepoByGithubId(ctx context.Context, githubRepoId int) (Repository, error) + GetRepoByRepoId(ctx context.Context, repoId int) (Repository, error) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) @@ -37,10 +38,20 @@ func NewService(repositoryRepository repository.RepositoryRepository, appCfg con } } -func (s *service) GetRepoByRepoId(ctx context.Context, repoGithubId int) (Repository, error) { +func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repository, error) { repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) if err != nil { - slog.Error("failed to get repository by github id") + slog.Error("failed to get repository by repo github id") + return Repository{}, err + } + + return Repository(repoDetails), nil +} + +func (s *service) GetRepoByRepoId(ctx context.Context, repobId int) (Repository, error) { + repoDetails, err := s.repositoryRepository.GetRepoByRepoId(ctx, nil, repobId) + if err != nil { + slog.Error("failed to get repository by repo id") return Repository{}, err } diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 75d61a8..453947b 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -18,6 +18,7 @@ type repositoryRepository struct { type RepositoryRepository interface { RepositoryTransaction GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) + GetRepoByRepoId(ctx context.Context, tx *sqlx.Tx, repoId int) (Repository, error) CreateRepository(ctx context.Context, tx *sqlx.Tx, repository Repository) (Repository, error) GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) @@ -33,6 +34,8 @@ func NewRepositoryRepository(db *sqlx.DB) RepositoryRepository { const ( getRepoByGithubIdQuery = `SELECT * from repositories where github_repo_id=$1` + getrepoByRepoIdQuery = `SELECT * from repositories where id=$1` + createRepositoryQuery = ` INSERT INTO repositories ( github_repo_id, @@ -76,7 +79,7 @@ func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx. slog.Error("repository not found", "error", err) return Repository{}, apperrors.ErrRepoNotFound } - slog.Error("error occurred while getting repository by id", "error", err) + slog.Error("error occurred while getting repository by repo github id", "error", err) return Repository{}, apperrors.ErrInternalServer } @@ -84,6 +87,35 @@ func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx. } +func (rr *repositoryRepository) GetRepoByRepoId(ctx context.Context, tx *sqlx.Tx, repoId int) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.QueryRowContext(ctx, getrepoByRepoIdQuery, repoId).Scan( + &repository.Id, + &repository.GithubRepoId, + &repository.RepoName, + &repository.Description, + &repository.LanguagesUrl, + &repository.RepoUrl, + &repository.OwnerName, + &repository.UpdateDate, + &repository.CreatedAt, + &repository.UpdatedAt, + &repository.ContributorsUrl, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("repository not found", "error", err) + return Repository{}, apperrors.ErrRepoNotFound + } + slog.Error("error occurred while getting repository by id", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil +} + func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.Tx, repositoryInfo Repository) (Repository, error) { executer := rr.BaseRepository.initiateQueryExecuter(tx) From 778c359fc5361f2556316e34fa950753c3bd7d1e Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 12:38:43 +0530 Subject: [PATCH 23/68] use repo id for fetching contributions of a particular repo of a user --- internal/repository/repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 453947b..fcda6b2 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -54,7 +54,7 @@ const ( fetchUsersContributedReposQuery = `SELECT * from repositories where id in (SELECT repository_id from contributions where user_id=$1);` - fetchUserContributionsInRepoQuery = `SELECT * from contributions where repository_id in (SELECT id from repositories where github_repo_id=$1) and user_id=$2;` + fetchUserContributionsInRepoQuery = `SELECT * from contributions where repository_id=$1 and user_id=$2;` ) func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) { From cbafcc09738f627db01c9ce7f2222e54a8f95f69 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 13:06:35 +0530 Subject: [PATCH 24/68] use sqlx methods in user.go --- internal/repository/base.go | 2 + internal/repository/domain.go | 38 +++++++++---------- internal/repository/user.go | 69 ++++------------------------------- 3 files changed, 29 insertions(+), 80 deletions(-) diff --git a/internal/repository/base.go b/internal/repository/base.go index a38e9ba..e4b0795 100644 --- a/internal/repository/base.go +++ b/internal/repository/base.go @@ -24,6 +24,8 @@ type QueryExecuter interface { QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error + SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error } func (b *BaseRepository) BeginTx(ctx context.Context) (*sqlx.Tx, error) { diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 29450f0..8febe1f 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -6,28 +6,28 @@ import ( ) type User struct { - Id int - GithubId int - GithubUsername string - Email string - AvatarUrl string - CurrentBalance int - CurrentActiveGoalId sql.NullInt64 - IsBlocked bool - IsAdmin bool - Password string - IsDeleted bool - DeletedAt sql.NullTime - CreatedAt time.Time - UpdatedAt time.Time + Id int `db:"id"` + GithubId int `db:"github_id"` + GithubUsername string `db:"github_username"` + Email string `db:"email"` + AvatarUrl string `db:"avatar_url"` + CurrentBalance int `db:"current_balance"` + CurrentActiveGoalId sql.NullInt64 `db:"current_active_goal_id"` + IsBlocked bool `db:"is_blocked"` + IsAdmin bool `db:"is_admin"` + Password string `db:"password"` + IsDeleted bool `db:"is_deleted"` + DeletedAt sql.NullTime `db:"deleted_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } type CreateUserRequestBody struct { - GithubId int - GithubUsername string - AvatarUrl string - Email string - IsAdmin bool + GithubId int `db:"github_id"` + GithubUsername string `db:"github_username"` + AvatarUrl string `db:"avatar_url"` + Email string `db:"email"` + IsAdmin bool `db:"is_admin"` } type Contribution struct { diff --git a/internal/repository/user.go b/internal/repository/user.go index c504048..806124a 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -54,22 +54,7 @@ func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId i executer := ur.BaseRepository.initiateQueryExecuter(tx) var user User - err := executer.QueryRowContext(ctx, getUserByIdQuery, userId).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) + err := executer.GetContext(ctx, &user, getUserByIdQuery, userId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("user not found", "error", err) @@ -86,22 +71,7 @@ func (ur *userRepository) GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, gi executer := ur.BaseRepository.initiateQueryExecuter(tx) var user User - err := executer.QueryRowContext(ctx, getUserByGithubIdQuery, githubId).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) + err := executer.GetContext(ctx, &user, getUserByGithubIdQuery, githubId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("user not found", "error", err) @@ -118,27 +88,12 @@ func (ur *userRepository) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo executer := ur.BaseRepository.initiateQueryExecuter(tx) var user User - err := executer.QueryRowContext(ctx, createUserQuery, + err := executer.GetContext(ctx, &user, createUserQuery, userInfo.GithubId, userInfo.GithubUsername, userInfo.Email, - userInfo.AvatarUrl, - ).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) + userInfo.AvatarUrl) + if err != nil { slog.Error("error occurred while creating user", "error", err) return User{}, apperrors.ErrUserCreationFailed @@ -162,21 +117,13 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user func (ur *userRepository) GetAllUsersGithubUsernames(ctx context.Context, tx *sqlx.Tx) ([]string, error) { executer := ur.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, getAllUsersGithubUsernamesQuery) + + var githubUsernames []string + err := executer.SelectContext(ctx, &githubUsernames, getAllUsersGithubUsernamesQuery) if err != nil { slog.Error("failed to get github usernames", "error", err) return nil, apperrors.ErrInternalServer } - defer rows.Close() - - var githubUsernames []string - for rows.Next() { - var username string - if err := rows.Scan(&username); err != nil { - return nil, err - } - githubUsernames = append(githubUsernames, username) - } return githubUsernames, nil } From f36069f9577c22510d74bb05e6aa8581719881ce Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 13:27:09 +0530 Subject: [PATCH 25/68] use sqlx methods in repository.go --- internal/repository/domain.go | 40 +++++++------- internal/repository/repository.go | 92 +++---------------------------- 2 files changed, 28 insertions(+), 104 deletions(-) diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 8febe1f..7a00a25 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -31,29 +31,29 @@ type CreateUserRequestBody struct { } type Contribution struct { - Id int - UserId int - RepositoryId int - ContributionScoreId int - ContributionType string - BalanceChange int - ContributedAt time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int `db:"id"` + UserId int `db:"user_id"` + RepositoryId int `db:"repository_id"` + ContributionScoreId int `db:"contribution_score_id"` + ContributionType string `db:"contribution_type"` + BalanceChange int `db:"balance_change"` + ContributedAt time.Time `db:"contributed_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - ContributorsUrl string - CreatedAt time.Time - UpdatedAt time.Time + Id int `db:"id"` + GithubRepoId int `db:"github_repo_id"` + RepoName string `db:"repo_name"` + Description string `db:"description"` + LanguagesUrl string `db:"languages_url"` + RepoUrl string `db:"repo_url"` + OwnerName string `db:"owner_name"` + UpdateDate time.Time `db:"update_date"` + ContributorsUrl string `db:"contributors_url"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } type ContributionScore struct { diff --git a/internal/repository/repository.go b/internal/repository/repository.go index fcda6b2..c1b03ce 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -61,19 +61,7 @@ func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx. executer := rr.BaseRepository.initiateQueryExecuter(tx) var repository Repository - err := executer.QueryRowContext(ctx, getRepoByGithubIdQuery, repoGithubId).Scan( - &repository.Id, - &repository.GithubRepoId, - &repository.RepoName, - &repository.Description, - &repository.LanguagesUrl, - &repository.RepoUrl, - &repository.OwnerName, - &repository.UpdateDate, - &repository.CreatedAt, - &repository.UpdatedAt, - &repository.ContributorsUrl, - ) + err := executer.GetContext(ctx, &repository, getRepoByGithubIdQuery, repoGithubId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("repository not found", "error", err) @@ -91,19 +79,7 @@ func (rr *repositoryRepository) GetRepoByRepoId(ctx context.Context, tx *sqlx.Tx executer := rr.BaseRepository.initiateQueryExecuter(tx) var repository Repository - err := executer.QueryRowContext(ctx, getrepoByRepoIdQuery, repoId).Scan( - &repository.Id, - &repository.GithubRepoId, - &repository.RepoName, - &repository.Description, - &repository.LanguagesUrl, - &repository.RepoUrl, - &repository.OwnerName, - &repository.UpdateDate, - &repository.CreatedAt, - &repository.UpdatedAt, - &repository.ContributorsUrl, - ) + err := executer.GetContext(ctx, &repository, getrepoByRepoIdQuery, repoId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("repository not found", "error", err) @@ -120,7 +96,7 @@ func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.T executer := rr.BaseRepository.initiateQueryExecuter(tx) var repository Repository - err := executer.QueryRowContext(ctx, createRepositoryQuery, + err := executer.GetContext(ctx, &repository, createRepositoryQuery, repositoryInfo.GithubRepoId, repositoryInfo.RepoName, repositoryInfo.Description, @@ -129,18 +105,6 @@ func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.T repositoryInfo.OwnerName, repositoryInfo.UpdateDate, repositoryInfo.ContributorsUrl, - ).Scan( - &repository.Id, - &repository.GithubRepoId, - &repository.RepoName, - &repository.Description, - &repository.LanguagesUrl, - &repository.RepoUrl, - &repository.OwnerName, - &repository.UpdateDate, - &repository.CreatedAt, - &repository.UpdatedAt, - &repository.ContributorsUrl, ) if err != nil { slog.Error("error occured while creating repository", "error", err) @@ -164,7 +128,7 @@ func (r *repositoryRepository) GetUserRepoTotalCoins(ctx context.Context, tx *sq var totalCoins int - err := executer.QueryRowContext(ctx, getUserRepoTotalCoinsQuery, userId, repoId).Scan(&totalCoins) + err := executer.GetContext(ctx, &totalCoins, getUserRepoTotalCoinsQuery, userId, repoId) if err != nil { slog.Error("error calculating total coins earned by user for the repository") return 0, apperrors.ErrCalculatingUserRepoTotalCoins @@ -184,33 +148,12 @@ func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, t executer := r.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, fetchUsersContributedReposQuery, userId) + var usersContributedRepos []Repository + err := executer.SelectContext(ctx, &usersContributedRepos, fetchUsersContributedReposQuery, userId) if err != nil { slog.Error("error fetching users contributed repositories") return nil, apperrors.ErrFetchingUsersContributedRepos } - defer rows.Close() - - var usersContributedRepos []Repository - for rows.Next() { - var usersContributedRepo Repository - if err = rows.Scan( - &usersContributedRepo.Id, - &usersContributedRepo.GithubRepoId, - &usersContributedRepo.RepoName, - &usersContributedRepo.Description, - &usersContributedRepo.LanguagesUrl, - &usersContributedRepo.RepoUrl, - &usersContributedRepo.OwnerName, - &usersContributedRepo.UpdateDate, - &usersContributedRepo.CreatedAt, - &usersContributedRepo.UpdatedAt, - &usersContributedRepo.ContributorsUrl); err != nil { - return nil, err - } - - usersContributedRepos = append(usersContributedRepos, usersContributedRepo) - } return usersContributedRepos, nil } @@ -226,31 +169,12 @@ func (r *repositoryRepository) FetchUserContributionsInRepo(ctx context.Context, executer := r.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, fetchUserContributionsInRepoQuery, repoGithubId, userId) + var userContributionsInRepo []Contribution + err := executer.SelectContext(ctx, &userContributionsInRepo, fetchUserContributionsInRepoQuery, repoGithubId, userId) if err != nil { slog.Error("error fetching users contribution in repository") return nil, apperrors.ErrFetchingUserContributionsInRepo } - defer rows.Close() - - var userContributionsInRepo []Contribution - for rows.Next() { - var userContributionInRepo Contribution - if err = rows.Scan( - &userContributionInRepo.Id, - &userContributionInRepo.UserId, - &userContributionInRepo.RepositoryId, - &userContributionInRepo.ContributionScoreId, - &userContributionInRepo.ContributionType, - &userContributionInRepo.BalanceChange, - &userContributionInRepo.ContributedAt, - &userContributionInRepo.CreatedAt, - &userContributionInRepo.UpdatedAt); err != nil { - return nil, err - } - - userContributionsInRepo = append(userContributionsInRepo, userContributionInRepo) - } return userContributionsInRepo, nil } From 050487e1b12dd66fbaa45f6d401347173a111767 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 13:38:26 +0530 Subject: [PATCH 26/68] use sqlx methods in contribution.go --- internal/repository/contribution.go | 43 +++-------------------------- internal/repository/domain.go | 12 ++++---- 2 files changed, 10 insertions(+), 45 deletions(-) diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 436dc11..54081ab 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -48,23 +48,13 @@ func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sq executer := cr.BaseRepository.initiateQueryExecuter(tx) var contribution Contribution - err := executer.QueryRowContext(ctx, createContributionQuery, + err := executer.GetContext(ctx, &contribution, createContributionQuery, contributionInfo.UserId, contributionInfo.RepositoryId, contributionInfo.ContributionScoreId, contributionInfo.ContributionType, contributionInfo.BalanceChange, contributionInfo.ContributedAt, - ).Scan( - &contribution.Id, - &contribution.UserId, - &contribution.RepositoryId, - &contribution.ContributionScoreId, - &contribution.ContributionType, - &contribution.BalanceChange, - &contribution.ContributedAt, - &contribution.CreatedAt, - &contribution.UpdatedAt, ) if err != nil { slog.Error("error occured while inserting contributions", "error", err) @@ -78,14 +68,7 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( executer := cr.BaseRepository.initiateQueryExecuter(tx) var contributionScoreDetails ContributionScore - err := executer.QueryRowContext(ctx, getContributionScoreDetailsByContributionTypeQuery, contributionType).Scan( - &contributionScoreDetails.Id, - &contributionScoreDetails.AdminId, - &contributionScoreDetails.ContributionType, - &contributionScoreDetails.Score, - &contributionScoreDetails.CreatedAt, - &contributionScoreDetails.UpdatedAt, - ) + err := executer.GetContext(ctx, &contributionScoreDetails, getContributionScoreDetailsByContributionTypeQuery, contributionType) if err != nil { slog.Error("error occured while getting contribution score details", "error", err) return ContributionScore{}, err @@ -105,30 +88,12 @@ func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context executer := cr.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, fetchUsersAllContributionsQuery, userId) + var usersAllContributions []Contribution + err := executer.SelectContext(ctx, &usersAllContributions, fetchUsersAllContributionsQuery, userId) if err != nil { slog.Error("error fetching all contributions for user") return nil, apperrors.ErrFetchingAllContributions } - defer rows.Close() - - var usersAllContributions []Contribution - for rows.Next() { - var currentContribution Contribution - if err = rows.Scan( - ¤tContribution.Id, - ¤tContribution.UserId, - ¤tContribution.RepositoryId, - ¤tContribution.ContributionScoreId, - ¤tContribution.ContributionType, - ¤tContribution.BalanceChange, - ¤tContribution.ContributedAt, - ¤tContribution.CreatedAt, ¤tContribution.UpdatedAt); err != nil { - return nil, err - } - - usersAllContributions = append(usersAllContributions, currentContribution) - } return usersAllContributions, nil } diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 7a00a25..148c2e3 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -57,10 +57,10 @@ type Repository struct { } type ContributionScore struct { - Id int - AdminId int - ContributionType string - Score int - CreatedAt time.Time - UpdatedAt time.Time + Id int `db:"id"` + AdminId int `db:"admin_id"` + ContributionType string `db:"contribution_type"` + Score int `db:"score"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } From f5af0ac6f607a8923b24f971b1a1e6bc52af7987 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 13:44:08 +0530 Subject: [PATCH 27/68] log error in slog --- internal/app/contribution/handler.go | 4 ++-- internal/app/contribution/service.go | 4 ++-- internal/app/repository/handler.go | 24 ++++++++++++------------ internal/app/repository/service.go | 10 +++++----- internal/repository/contribution.go | 2 +- internal/repository/repository.go | 6 +++--- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 19b9d2e..b4b8a4e 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -28,7 +28,7 @@ func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Re err := h.contributionService.ProcessFetchedContributions(ctx) if err != nil { - slog.Error("error fetching latest contributions") + slog.Error("error fetching latest contributions", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -42,7 +42,7 @@ func (h *handler) FetchUsersAllContributions(w http.ResponseWriter, r *http.Requ usersAllContributions, err := h.contributionService.FetchUsersAllContributions(ctx) if err != nil { - slog.Error("error fetching all contributions for user") + slog.Error("error fetching all contributions for user", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 046e617..085c269 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -56,7 +56,7 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { contributionType, err := s.GetContributionType(ctx, contribution) if err != nil { - slog.Error("error getting contribution type") + slog.Error("error getting contribution type", "error", err) return err } @@ -65,7 +65,7 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { if err != nil { repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) if err != nil { - slog.Error("error fetching repository details") + slog.Error("error fetching repository details", "error", err) return err } diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index e8f65f8..083d14c 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -33,7 +33,7 @@ func (h *handler) FetchUsersContributedRepos(w http.ResponseWriter, r *http.Requ usersContributedRepos, err := h.repositoryService.FetchUsersContributedRepos(ctx, client) if err != nil { - slog.Error("error fetching users conributed repos") + slog.Error("error fetching users conributed repos", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -47,7 +47,7 @@ func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Requ repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { - slog.Error("error getting repo id from request url") + slog.Error("error getting repo id from request url", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -55,7 +55,7 @@ func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Requ repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) if err != nil { - slog.Error("error fetching particular repo details") + slog.Error("error fetching particular repo details", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -72,7 +72,7 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { - slog.Error("error getting repo id from request url") + slog.Error("error getting repo id from request url", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -80,7 +80,7 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) if err != nil { - slog.Error("error fetching particular repo details") + slog.Error("error fetching particular repo details", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -88,7 +88,7 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http repoContributors, err := h.repositoryService.FetchRepositoryContributors(ctx, client, repoDetails.ContributorsUrl) if err != nil { - slog.Error("error fetching repo contributors") + slog.Error("error fetching repo contributors", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -103,7 +103,7 @@ func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Re repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { - slog.Error("error getting repo id from request url") + slog.Error("error getting repo id from request url", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -111,7 +111,7 @@ func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Re usersContributionsInRepo, err := h.repositoryService.FetchUserContributionsInRepo(ctx, repoId) if err != nil { - slog.Error("error fetching users contribution in repository") + slog.Error("error fetching users contribution in repository", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -128,7 +128,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { - slog.Error("error getting repo id from request url") + slog.Error("error getting repo id from request url", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -136,7 +136,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) if err != nil { - slog.Error("error fetching particular repo details") + slog.Error("error fetching particular repo details", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -144,7 +144,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ repoLanguages, err := h.repositoryService.FetchRepositoryLanguages(ctx, client, repoDetails.LanguagesUrl) if err != nil { - slog.Error("error fetching particular repo languages") + slog.Error("error fetching particular repo languages", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -152,7 +152,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ langPercent, err := h.repositoryService.CalculateLanguagePercentInRepo(ctx, repoLanguages) if err != nil { - slog.Error("error fetching particular repo languages") + slog.Error("error fetching particular repo languages", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 9a6774f..a7499f6 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -41,7 +41,7 @@ func NewService(repositoryRepository repository.RepositoryRepository, appCfg con func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repository, error) { repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) if err != nil { - slog.Error("failed to get repository by repo github id") + slog.Error("failed to get repository by repo github id", "error", err) return Repository{}, err } @@ -51,7 +51,7 @@ func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repo func (s *service) GetRepoByRepoId(ctx context.Context, repobId int) (Repository, error) { repoDetails, err := s.repositoryRepository.GetRepoByRepoId(ctx, nil, repobId) if err != nil { - slog.Error("failed to get repository by repo id") + slog.Error("failed to get repository by repo id", "error", err) return Repository{}, err } @@ -141,7 +141,7 @@ func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Cli func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) { usersContributedRepos, err := s.repositoryRepository.FetchUsersContributedRepos(ctx, nil) if err != nil { - slog.Error("error fetching users conributed repos") + slog.Error("error fetching users conributed repos", "error", err) return nil, err } @@ -162,7 +162,7 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C userRepoTotalCoins, err := s.repositoryRepository.GetUserRepoTotalCoins(ctx, nil, usersContributedRepo.Id) if err != nil { - slog.Error("error calculating total coins earned by user for the repository") + slog.Error("error calculating total coins earned by user for the repository", "error", err) return nil, err } @@ -204,7 +204,7 @@ func (s *service) FetchRepositoryContributors(ctx context.Context, client *http. func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) { userContributionsInRepo, err := s.repositoryRepository.FetchUserContributionsInRepo(ctx, nil, githubRepoId) if err != nil { - slog.Error("error fetching users contribution in repository") + slog.Error("error fetching users contribution in repository", "error", err) return nil, err } diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 436dc11..94b7e13 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -107,7 +107,7 @@ func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context rows, err := executer.QueryContext(ctx, fetchUsersAllContributionsQuery, userId) if err != nil { - slog.Error("error fetching all contributions for user") + slog.Error("error fetching all contributions for user", "error", err) return nil, apperrors.ErrFetchingAllContributions } defer rows.Close() diff --git a/internal/repository/repository.go b/internal/repository/repository.go index fcda6b2..05e5cd9 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -166,7 +166,7 @@ func (r *repositoryRepository) GetUserRepoTotalCoins(ctx context.Context, tx *sq err := executer.QueryRowContext(ctx, getUserRepoTotalCoinsQuery, userId, repoId).Scan(&totalCoins) if err != nil { - slog.Error("error calculating total coins earned by user for the repository") + slog.Error("error calculating total coins earned by user for the repository", "error", err) return 0, apperrors.ErrCalculatingUserRepoTotalCoins } @@ -186,7 +186,7 @@ func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, t rows, err := executer.QueryContext(ctx, fetchUsersContributedReposQuery, userId) if err != nil { - slog.Error("error fetching users contributed repositories") + slog.Error("error fetching users contributed repositories", "error", err) return nil, apperrors.ErrFetchingUsersContributedRepos } defer rows.Close() @@ -228,7 +228,7 @@ func (r *repositoryRepository) FetchUserContributionsInRepo(ctx context.Context, rows, err := executer.QueryContext(ctx, fetchUserContributionsInRepoQuery, repoGithubId, userId) if err != nil { - slog.Error("error fetching users contribution in repository") + slog.Error("error fetching users contribution in repository", "error", err) return nil, apperrors.ErrFetchingUserContributionsInRepo } defer rows.Close() From 5293cb6dbfca5ad55ab66409941f8fe0627330e6 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 27 Jun 2025 11:47:14 +0530 Subject: [PATCH 28/68] make contribution fetch process from gh-archive dataset on bigquery, a cron job that is scheduled everyday at 1 am IST --- go.mod | 1 + go.sum | 2 ++ internal/app/cronJob/cronjob.go | 26 ++++++++++++++++++++++ internal/app/cronJob/dailyJob.go | 32 +++++++++++++++++++++++++++ internal/app/cronJob/init.go | 38 ++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+) create mode 100644 internal/app/cronJob/cronjob.go create mode 100644 internal/app/cronJob/dailyJob.go create mode 100644 internal/app/cronJob/init.go diff --git a/go.mod b/go.mod index f308cae..e73f2e2 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 + github.com/robfig/cron/v3 v3.0.1 golang.org/x/oauth2 v0.29.0 google.golang.org/api v0.231.0 ) diff --git a/go.sum b/go.sum index d272ace..a599248 100644 --- a/go.sum +++ b/go.sum @@ -133,6 +133,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgm github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= diff --git a/internal/app/cronJob/cronjob.go b/internal/app/cronJob/cronjob.go new file mode 100644 index 0000000..a567985 --- /dev/null +++ b/internal/app/cronJob/cronjob.go @@ -0,0 +1,26 @@ +package cronJob + +import ( + "context" + "log/slog" + "time" +) + +type Job interface { + Schedule(c *CronSchedular) error +} + +type CronJob struct { + Name string +} + +func (c *CronJob) Execute(ctx context.Context, fn func(context.Context)) func() { + return func() { + slog.Info("cron job started at", "time ", time.Now()) + defer func() { + slog.Info("cron job completed") + }() + + fn(ctx) + } +} diff --git a/internal/app/cronJob/dailyJob.go b/internal/app/cronJob/dailyJob.go new file mode 100644 index 0000000..30f225f --- /dev/null +++ b/internal/app/cronJob/dailyJob.go @@ -0,0 +1,32 @@ +package cronJob + +import ( + "context" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" +) + +type DailyJob struct { + CronJob + contributionService contribution.Service +} + +func NewDailyJob(contributionService contribution.Service) *DailyJob { + return &DailyJob{ + contributionService: contributionService, + CronJob: CronJob{Name: "Fetch Contributions Daily"}, + } +} + +func (d *DailyJob) Schedule(s *CronSchedular) error { + _, err := s.cron.AddFunc("0 1 * * *", func() { d.Execute(context.Background(), d.run)() }) + if err != nil { + return err + } + + return nil +} + +func (d *DailyJob) run(ctx context.Context) { + d.contributionService.ProcessFetchedContributions(ctx) +} diff --git a/internal/app/cronJob/init.go b/internal/app/cronJob/init.go new file mode 100644 index 0000000..7fba0f3 --- /dev/null +++ b/internal/app/cronJob/init.go @@ -0,0 +1,38 @@ +package cronJob + +import ( + "log/slog" + "time" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" + "github.com/robfig/cron/v3" +) + +type CronSchedular struct { + cron *cron.Cron +} + +func NewCronSchedular() *CronSchedular { + location, err := time.LoadLocation("Asia/Kolkata") + if err != nil { + slog.Error("failed to load IST timezone", "error", err) + } + + return &CronSchedular{ + cron: cron.New(cron.WithLocation(location)), + } +} + +func (c *CronSchedular) InitCronJobs(contributionService contribution.Service) { + jobs := []Job{ + NewDailyJob(contributionService), + } + + for _, job := range jobs { + if err := job.Schedule(c); err != nil { + slog.Error("failed to execute cron job") + } + } + + c.cron.Start() +} From adb7845dafd5935585dd76eb218fa067eec024c9 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 27 Jun 2025 11:49:40 +0530 Subject: [PATCH 29/68] remove fetch users latest contribution handler, made for testing processfetchcontributions service. As this will be a cronjob --- internal/app/contribution/handler.go | 15 --------------- internal/app/router.go | 1 - 2 files changed, 16 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 19b9d2e..df50396 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -13,7 +13,6 @@ type handler struct { } type Handler interface { - FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) } @@ -23,20 +22,6 @@ func NewHandler(contributionService Service) Handler { } } -func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - err := h.contributionService.ProcessFetchedContributions(ctx) - if err != nil { - slog.Error("error fetching latest contributions") - status, errorMessage := apperrors.MapError(err) - response.WriteJson(w, status, errorMessage, nil) - return - } - - response.WriteJson(w, http.StatusOK, "contribution fetched successfully", nil) -} - func (h *handler) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/internal/app/router.go b/internal/app/router.go index 6ab919a..7c15d9b 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -20,7 +20,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/contributions/latest", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } From 6c3a6b6528d6c66d865a223611e45442454b8e48 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 27 Jun 2025 15:01:55 +0530 Subject: [PATCH 30/68] allow null contribution id for redeemed transactions --- .../1751016438_allow-null-contribution-id.down.sql | 6 ++++++ .../migrations/1751016438_allow-null-contribution-id.up.sql | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 internal/db/migrations/1751016438_allow-null-contribution-id.down.sql create mode 100644 internal/db/migrations/1751016438_allow-null-contribution-id.up.sql diff --git a/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql b/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql new file mode 100644 index 0000000..f5b7718 --- /dev/null +++ b/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql @@ -0,0 +1,6 @@ +UPDATE transactions +SET contribution_id = 0 +WHERE contribution_id IS NULL; + +ALTER TABLE transactions +ALTER COLUMN contribution_id SET NOT NULL; \ No newline at end of file diff --git a/internal/db/migrations/1751016438_allow-null-contribution-id.up.sql b/internal/db/migrations/1751016438_allow-null-contribution-id.up.sql new file mode 100644 index 0000000..8f9e5d0 --- /dev/null +++ b/internal/db/migrations/1751016438_allow-null-contribution-id.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE transactions +ALTER COLUMN contribution_id DROP NOT NULL; From 4a736ceba73320f16def079eb91d7ffd1f9f35d0 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 27 Jun 2025 16:00:00 +0530 Subject: [PATCH 31/68] pass contribution service in dependencies for cron job and remove unnecessary auth and user service from dependencies --- cmd/main.go | 4 ++++ internal/app/dependencies.go | 6 ++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 947cd10..1a5b091 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -12,6 +12,7 @@ import ( "time" "github.com/joshsoftware/code-curiosity-2025/internal/app" + "github.com/joshsoftware/code-curiosity-2025/internal/app/cronJob" "github.com/joshsoftware/code-curiosity-2025/internal/config" ) @@ -43,6 +44,9 @@ func main() { router := app.NewRouter(dependencies) + newCronSchedular := cronJob.NewCronSchedular() + newCronSchedular.InitCronJobs(dependencies.ContributionService) + server := http.Server{ Addr: fmt.Sprintf(":%s", cfg.HTTPServer.Port), Handler: router, diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 616e377..e65fd39 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -15,8 +15,7 @@ import ( ) type Dependencies struct { - AuthService auth.Service - UserService user.Service + ContributionService contribution.Service AuthHandler auth.Handler UserHandler user.Handler ContributionHandler contribution.Handler @@ -40,8 +39,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque contributionHandler := contribution.NewHandler(contributionService) return Dependencies{ - AuthService: authService, - UserService: userService, + ContributionService: contributionService, AuthHandler: authHandler, UserHandler: userHandler, ContributionHandler: contributionHandler, From 71d8a4ab9920919682e9c5da7d1f39f3694a7261 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 30 Jun 2025 12:21:33 +0530 Subject: [PATCH 32/68] add column gh-event-id to use gh-event-id for contributions instead of usernames --- internal/db/migrations/1751028730_add-gh-event-id.down.sql | 1 + internal/db/migrations/1751028730_add-gh-event-id.up.sql | 1 + 2 files changed, 2 insertions(+) create mode 100644 internal/db/migrations/1751028730_add-gh-event-id.down.sql create mode 100644 internal/db/migrations/1751028730_add-gh-event-id.up.sql diff --git a/internal/db/migrations/1751028730_add-gh-event-id.down.sql b/internal/db/migrations/1751028730_add-gh-event-id.down.sql new file mode 100644 index 0000000..f4f9416 --- /dev/null +++ b/internal/db/migrations/1751028730_add-gh-event-id.down.sql @@ -0,0 +1 @@ +ALTER TABLE contributions DROP COLUMN gh_event_id; \ No newline at end of file diff --git a/internal/db/migrations/1751028730_add-gh-event-id.up.sql b/internal/db/migrations/1751028730_add-gh-event-id.up.sql new file mode 100644 index 0000000..4ee0e62 --- /dev/null +++ b/internal/db/migrations/1751028730_add-gh-event-id.up.sql @@ -0,0 +1 @@ +ALTER TABLE contributions ADD COLUMN gh_event_id VARCHAR(255) DEFAULT ''; \ No newline at end of file From 165fec8d271389cd75ac9761e8fdf879c8bb0d99 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 30 Jun 2025 12:27:11 +0530 Subject: [PATCH 33/68] seperate and use one alter table per migrations --- .../1750328591_add_column_contributors_url.up.sql | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/db/migrations/1750328591_add_column_contributors_url.up.sql b/internal/db/migrations/1750328591_add_column_contributors_url.up.sql index cc05125..c05df31 100644 --- a/internal/db/migrations/1750328591_add_column_contributors_url.up.sql +++ b/internal/db/migrations/1750328591_add_column_contributors_url.up.sql @@ -1,5 +1 @@ -ALTER TABLE repositories ADD COLUMN contributors_url VARCHAR(255); - -UPDATE repositories SET contributors_url = '' WHERE contributors_url IS NULL; - -ALTER TABLE repositories ALTER COLUMN contributors_url SET NOT NULL; +ALTER TABLE repositories ADD COLUMN contributors_url VARCHAR(255) DEFAULT ''; \ No newline at end of file From 82fb23aed12da01c2105b5e845e4c6dd0fbf10ff Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 30 Jun 2025 12:29:17 +0530 Subject: [PATCH 34/68] set column contributors as not null in seperate migration file --- .../migrations/1751266661_set-not-null-contributors-url.down.sql | 1 + .../migrations/1751266661_set-not-null-contributors-url.up.sql | 1 + 2 files changed, 2 insertions(+) create mode 100644 internal/db/migrations/1751266661_set-not-null-contributors-url.down.sql create mode 100644 internal/db/migrations/1751266661_set-not-null-contributors-url.up.sql diff --git a/internal/db/migrations/1751266661_set-not-null-contributors-url.down.sql b/internal/db/migrations/1751266661_set-not-null-contributors-url.down.sql new file mode 100644 index 0000000..7645eb9 --- /dev/null +++ b/internal/db/migrations/1751266661_set-not-null-contributors-url.down.sql @@ -0,0 +1 @@ +ALTER TABLE repositories ALTER COLUMN contributors_url DROP NOT NULL; \ No newline at end of file diff --git a/internal/db/migrations/1751266661_set-not-null-contributors-url.up.sql b/internal/db/migrations/1751266661_set-not-null-contributors-url.up.sql new file mode 100644 index 0000000..bba0f67 --- /dev/null +++ b/internal/db/migrations/1751266661_set-not-null-contributors-url.up.sql @@ -0,0 +1 @@ +ALTER TABLE repositories ALTER COLUMN contributors_url SET NOT NULL; \ No newline at end of file From 79804700d71cac5be0bbb85b2a0cab6442546f90 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 30 Jun 2025 12:41:54 +0530 Subject: [PATCH 35/68] remove unnecessary lines of code --- .../1751016438_allow-null-contribution-id.down.sql | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql b/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql index f5b7718..c44d425 100644 --- a/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql +++ b/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql @@ -1,6 +1,2 @@ -UPDATE transactions -SET contribution_id = 0 -WHERE contribution_id IS NULL; - ALTER TABLE transactions -ALTER COLUMN contribution_id SET NOT NULL; \ No newline at end of file +ALTER COLUMN contribution_id SET NOT NULL DEFAULT 0; \ No newline at end of file From 5d8cb59a8ac95b040232701226104c545464c4e5 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 30 Jun 2025 12:54:19 +0530 Subject: [PATCH 36/68] rename gh_event_id to github_event_id to ensure consistency across names --- internal/db/migrations/1751028730_add-gh-event-id.down.sql | 2 +- internal/db/migrations/1751028730_add-gh-event-id.up.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/db/migrations/1751028730_add-gh-event-id.down.sql b/internal/db/migrations/1751028730_add-gh-event-id.down.sql index f4f9416..a63e61f 100644 --- a/internal/db/migrations/1751028730_add-gh-event-id.down.sql +++ b/internal/db/migrations/1751028730_add-gh-event-id.down.sql @@ -1 +1 @@ -ALTER TABLE contributions DROP COLUMN gh_event_id; \ No newline at end of file +ALTER TABLE contributions DROP COLUMN github_event_id; \ No newline at end of file diff --git a/internal/db/migrations/1751028730_add-gh-event-id.up.sql b/internal/db/migrations/1751028730_add-gh-event-id.up.sql index 4ee0e62..334b976 100644 --- a/internal/db/migrations/1751028730_add-gh-event-id.up.sql +++ b/internal/db/migrations/1751028730_add-gh-event-id.up.sql @@ -1 +1 @@ -ALTER TABLE contributions ADD COLUMN gh_event_id VARCHAR(255) DEFAULT ''; \ No newline at end of file +ALTER TABLE contributions ADD COLUMN github_event_id VARCHAR(255) DEFAULT ''; \ No newline at end of file From 230cd4e85535c9b9a4359e9418987d979b5648ec Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 30 Jun 2025 12:56:18 +0530 Subject: [PATCH 37/68] set not null github-event-id --- .../db/migrations/1751268286_set-not-null-gh-event-id.down.sql | 1 + .../db/migrations/1751268286_set-not-null-gh-event-id.up.sql | 1 + 2 files changed, 2 insertions(+) create mode 100644 internal/db/migrations/1751268286_set-not-null-gh-event-id.down.sql create mode 100644 internal/db/migrations/1751268286_set-not-null-gh-event-id.up.sql diff --git a/internal/db/migrations/1751268286_set-not-null-gh-event-id.down.sql b/internal/db/migrations/1751268286_set-not-null-gh-event-id.down.sql new file mode 100644 index 0000000..5828c2e --- /dev/null +++ b/internal/db/migrations/1751268286_set-not-null-gh-event-id.down.sql @@ -0,0 +1 @@ +ALTER TABLE contributions ALTER COLUMN github_event_id DROP NOT NULL; \ No newline at end of file diff --git a/internal/db/migrations/1751268286_set-not-null-gh-event-id.up.sql b/internal/db/migrations/1751268286_set-not-null-gh-event-id.up.sql new file mode 100644 index 0000000..2ac1f91 --- /dev/null +++ b/internal/db/migrations/1751268286_set-not-null-gh-event-id.up.sql @@ -0,0 +1 @@ +ALTER TABLE contributions ALTER COLUMN github_event_id SET NOT NULL; \ No newline at end of file From 3ac004e4c2ad0653db3445a0063026cc968ca170 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 1 Jul 2025 11:52:32 +0530 Subject: [PATCH 38/68] define contribution fetching query from bigquery as a constant --- internal/app/bigquery/domain.go | 29 +++++++++++++++++++++++++ internal/app/bigquery/service.go | 37 ++++---------------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/internal/app/bigquery/domain.go b/internal/app/bigquery/domain.go index 3a8575a..7b15922 100644 --- a/internal/app/bigquery/domain.go +++ b/internal/app/bigquery/domain.go @@ -2,6 +2,35 @@ package bigquery import "time" +const DailyQuery = `SELECT + id, + type, + public, + actor.id AS actor_id, + actor.login AS actor_login, + actor.gravatar_id AS actor_gravatar_id, + actor.url AS actor_url, + actor.avatar_url AS actor_avatar_url, + repo.id AS repo_id, + repo.name AS repo_name, + repo.url AS repo_url, + payload, + created_at, + other +FROM + githubarchive.day.%s +WHERE + type IN ( + 'IssuesEvent', + 'PullRequestEvent', + 'PullRequestReviewEvent', + 'IssueCommentEvent', + 'PullRequestReviewCommentEvent' + ) + AND ( + actor.login IN (%s) + )` + type ContributionResponse struct { ID string `bigquery:"id"` Type string `bigquery:"type"` diff --git a/internal/app/bigquery/service.go b/internal/app/bigquery/service.go index 7a15d7c..5f64ee1 100644 --- a/internal/app/bigquery/service.go +++ b/internal/app/bigquery/service.go @@ -30,9 +30,6 @@ func NewService(bigqueryInstance config.Bigquery, userRepository repository.User } func (s *service) FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) { - YesterdayDate := time.Now().AddDate(0, 0, -1) - YesterdayYearMonthDay := YesterdayDate.Format("20060102") - usersNamesList, err := s.userRepository.GetAllUsersGithubUsernames(ctx, nil) if err != nil { slog.Error("error fetching users github usernames") @@ -44,37 +41,11 @@ func (s *service) FetchDailyContributions(ctx context.Context) (*bq.RowIterator, quotedUsernamesList = append(quotedUsernamesList, fmt.Sprintf("'%s'", username)) } + YesterdayDate := time.Now().AddDate(0, 0, -1) + YesterdayYearMonthDay := YesterdayDate.Format("20060102") + githubUsernames := strings.Join(quotedUsernamesList, ",") - fetchDailyContributionsQuery := fmt.Sprintf(` -SELECT - id, - type, - public, - actor.id AS actor_id, - actor.login AS actor_login, - actor.gravatar_id AS actor_gravatar_id, - actor.url AS actor_url, - actor.avatar_url AS actor_avatar_url, - repo.id AS repo_id, - repo.name AS repo_name, - repo.url AS repo_url, - payload, - created_at, - other -FROM - githubarchive.day.%s -WHERE - type IN ( - 'IssuesEvent', - 'PullRequestEvent', - 'PullRequestReviewEvent', - 'IssueCommentEvent', - 'PullRequestReviewCommentEvent' - ) - AND ( - actor.login IN (%s) - ) -`, YesterdayYearMonthDay, githubUsernames) + fetchDailyContributionsQuery := fmt.Sprintf(DailyQuery, YesterdayYearMonthDay, githubUsernames) bigqueryQuery := s.bigqueryInstance.Client.Query(fetchDailyContributionsQuery) contributionRows, err := bigqueryQuery.Read(ctx) From a3067768c603b5633ceb97188b7ab974260f6f6f Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 1 Jul 2025 11:54:07 +0530 Subject: [PATCH 39/68] rename function FetchUsersAllContributions to FetchUserContributions, define constants for strings --- internal/app/contribution/handler.go | 10 +- internal/app/contribution/service.go | 148 ++++++++++++++++++--------- internal/app/repository/domain.go | 4 +- internal/app/router.go | 2 +- internal/pkg/apperrors/errors.go | 2 + internal/repository/contribution.go | 14 +-- 6 files changed, 117 insertions(+), 63 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 19b9d2e..fe614f8 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -14,7 +14,7 @@ type handler struct { type Handler interface { FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) - FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) + FetchUserContributions(w http.ResponseWriter, r *http.Request) } func NewHandler(contributionService Service) Handler { @@ -37,16 +37,16 @@ func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Re response.WriteJson(w, http.StatusOK, "contribution fetched successfully", nil) } -func (h *handler) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) { +func (h *handler) FetchUserContributions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - usersAllContributions, err := h.contributionService.FetchUsersAllContributions(ctx) + userContributions, err := h.contributionService.FetchUserContributions(ctx) if err != nil { - slog.Error("error fetching all contributions for user") + slog.Error("error fetching user contributions") status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return } - response.WriteJson(w, http.StatusOK, "all contributions for user fetched successfully", usersAllContributions) + response.WriteJson(w, http.StatusOK, "user contributions fetched successfully", userContributions) } diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 046e617..7d0b3cf 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -9,10 +9,44 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/repository" "google.golang.org/api/iterator" ) +// github event names +const ( + pullRequestEvent = "PullRequestEvent" + issuesEvent = "IssuesEvent" + pushEvent = "PushEvent" + issueCommentEvent = "IssueCommentEvent" +) + +// app contribution types +const ( + pullRequestMerged = "PullRequestMerged" + pullRequestOpened = "PullRequestOpened" + issueOpened = "IssueOpened" + issueClosed = "IssueClosed" + issueResolved = "IssueResolved" + pullRequestUpdated = "PullRequestUpdated" + issueComment = "IssueComment" + pullRequestComment = "PullRequestComment" +) + +// payload +const ( + payloadActionKey = "action" + payloadPullRequestKey = "pull_request" + PayloadMergedKey = "merged" + PayloadIssueKey = "issue" + PayloadStateReasonKey = "state_reason" + PayloadClosedKey = "closed" + PayloadOpenedKey = "opened" + PayloadNotPlannedKey = "not_planned" + PayloadCompletedKey = "completed" +) + type service struct { bigqueryService bigquery.Service contributionRepository repository.ContributionRepository @@ -25,7 +59,7 @@ type Service interface { ProcessFetchedContributions(ctx context.Context) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) - FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) + FetchUserContributions(ctx context.Context) ([]Contribution, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, httpClient *http.Client) Service { @@ -33,8 +67,9 @@ func NewService(bigqueryService bigquery.Service, contributionRepository reposit bigqueryService: bigqueryService, contributionRepository: contributionRepository, repositoryService: repositoryService, - userService: userService, - httpClient: httpClient, + + userService: userService, + httpClient: httpClient, } } @@ -42,18 +77,30 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { contributions, err := s.bigqueryService.FetchDailyContributions(ctx) if err != nil { slog.Error("error fetching daily contributions", "error", err) - return err + return apperrors.ErrFetchingFromBigquery } + var fetchedContributions []ContributionResponse + + //using a local copy here to copy contribution so that I can implement retry mechanism in future + //thinking of batch processing to be implemented later on, to handle memory overflow for { var contribution ContributionResponse - if err := contributions.Next(&contribution); err == iterator.Done { - break - } else if err != nil { + err := contributions.Next(&contribution) + if err != nil { + if err == iterator.Done { + break + } + slog.Error("error iterating contribution rows", "error", err) - break + return apperrors.ErrNextContribution } + fetchedContributions = append(fetchedContributions, contribution) + } + + for _, contribution := range fetchedContributions { + contributionType, err := s.GetContributionType(ctx, contribution) if err != nil { slog.Error("error getting contribution type") @@ -61,21 +108,26 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { } var repositoryId int - repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) //err no rows + repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) if err != nil { - repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) - if err != nil { - slog.Error("error fetching repository details") + if err == apperrors.ErrRepoNotFound { + repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) + if err != nil { + slog.Error("error fetching repository details") + return err + } + + repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, repo) + if err != nil { + slog.Error("error creating repository", "error", err) + return err + } + + repositoryId = repositoryCreated.Id + } else { + slog.Error("error fetching repo by repo id", "error", err) return err } - - repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, repo) - if err != nil { - slog.Error("error creating repository", "error", err) - return err - } - - repositoryId = repositoryCreated.Id } else { repositoryId = repoFetched.Id } @@ -105,50 +157,50 @@ func (s *service) GetContributionType(ctx context.Context, contribution Contribu } var action string - if actionVal, ok := contributionPayload["action"]; ok { + if actionVal, ok := contributionPayload[action]; ok { action = actionVal.(string) } var pullRequest map[string]interface{} var isMerged bool - if pullRequestPayload, ok := contributionPayload["pull_request"]; ok { + if pullRequestPayload, ok := contributionPayload[payloadPullRequestKey]; ok { pullRequest = pullRequestPayload.(map[string]interface{}) - isMerged = pullRequest["merged"].(bool) + isMerged = pullRequest[PayloadMergedKey].(bool) } var issue map[string]interface{} var stateReason string - if issuePayload, ok := contributionPayload["issue"]; ok { + if issuePayload, ok := contributionPayload[PayloadIssueKey]; ok { issue = issuePayload.(map[string]interface{}) - stateReason = issue["state_reason"].(string) + stateReason = issue[PayloadStateReasonKey].(string) } var contributionType string switch contribution.Type { - case "PullRequestEvent": - if action == "closed" && isMerged { - contributionType = "PullRequestMerged" - } else if action == "opened" { - contributionType = "PullRequestOpened" + case pullRequestEvent: + if action == PayloadClosedKey && isMerged { + contributionType = pullRequestMerged + } else if action == PayloadOpenedKey { + contributionType = pullRequestOpened } - case "IssuesEvent": - if action == "opened" { - contributionType = "IssueOpened" - } else if action == "closed" && stateReason == "not_planned" { - contributionType = "IssueClosed" - } else if action == "closed" && stateReason == "completed" { - contributionType = "IssueResolved" + case issuesEvent: + if action == PayloadOpenedKey { + contributionType = issueOpened + } else if action == PayloadClosedKey && stateReason == PayloadNotPlannedKey { + contributionType = issueClosed + } else if action == PayloadClosedKey && stateReason == PayloadCompletedKey { + contributionType = issueResolved } - case "PushEvent": - contributionType = "PullRequestUpdated" + case pushEvent: + contributionType = pullRequestUpdated - case "IssueCommentEvent": - contributionType = "IssueComment" + case issueCommentEvent: + contributionType = issueComment - case "PullRequestComment ": - contributionType = "PullRequestComment" + case pullRequestComment: + contributionType = pullRequestComment } return contributionType, nil @@ -191,15 +243,15 @@ func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Cont return ContributionScore(contributionScoreDetails), nil } -func (s *service) FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) { - usersAllContributions, err := s.contributionRepository.FetchUsersAllContributions(ctx, nil) +func (s *service) FetchUserContributions(ctx context.Context) ([]Contribution, error) { + userContributions, err := s.contributionRepository.FetchUserContributions(ctx, nil) if err != nil { - slog.Error("error occured while fetching all contributions for user", "error", err) + slog.Error("error occured while fetching user contributions", "error", err) return nil, err } - serviceContributions := make([]Contribution, len(usersAllContributions)) - for i, c := range usersAllContributions { + serviceContributions := make([]Contribution, len(userContributions)) + for i, c := range userContributions { serviceContributions[i] = Contribution((c)) } diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 90101b5..be5757c 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -2,7 +2,7 @@ package repository import "time" -type RepoOWner struct { +type RepoOwner struct { Login string `json:"login"` } @@ -12,7 +12,7 @@ type FetchRepositoryDetailsResponse struct { Description string `json:"description"` LanguagesURL string `json:"languages_url"` UpdateDate time.Time `json:"updated_at"` - RepoOwnerName RepoOWner `json:"owner"` + RepoOwnerName RepoOwner `json:"owner"` ContributorsUrl string `json:"contributors_url"` RepoUrl string `json:"html_url"` } diff --git a/internal/app/router.go b/internal/app/router.go index 6ab919a..cdcf7c2 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -21,6 +21,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/latest", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index c9c7f2d..a1a79d0 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -34,6 +34,8 @@ var ( ErrRepoNotFound = errors.New("repository not found") ErrRepoCreationFailed = errors.New("failed to create repo for user") + ErrFetchingFromBigquery = errors.New("error fetching contributions from bigquery service") + ErrNextContribution = errors.New("error while loading next bigquery contribution") ErrContributionCreationFailed = errors.New("failed to create contrbitution") ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 436dc11..385b8c6 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -17,7 +17,7 @@ type ContributionRepository interface { RepositoryTransaction CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) - FetchUsersAllContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) + FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -41,7 +41,7 @@ const ( getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` - fetchUsersAllContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc` + fetchUserContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -94,7 +94,7 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( return contributionScoreDetails, nil } -func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { +func (cr *contributionRepository) FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { userIdValue := ctx.Value(middleware.UserIdKey) userId, ok := userIdValue.(int) @@ -105,14 +105,14 @@ func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context executer := cr.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, fetchUsersAllContributionsQuery, userId) + rows, err := executer.QueryContext(ctx, fetchUserContributionsQuery, userId) if err != nil { slog.Error("error fetching all contributions for user") return nil, apperrors.ErrFetchingAllContributions } defer rows.Close() - var usersAllContributions []Contribution + var userContributions []Contribution for rows.Next() { var currentContribution Contribution if err = rows.Scan( @@ -127,8 +127,8 @@ func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context return nil, err } - usersAllContributions = append(usersAllContributions, currentContribution) + userContributions = append(userContributions, currentContribution) } - return usersAllContributions, nil + return userContributions, nil } From 187dc89eeae4d596417d7951e31633cc395440c6 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 1 Jul 2025 12:03:15 +0530 Subject: [PATCH 40/68] handle no row error in GetContributionScoreDetails function --- internal/pkg/apperrors/errors.go | 1 + internal/repository/contribution.go | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 743e37a..7c6a6e4 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -40,6 +40,7 @@ var ( ErrContributionCreationFailed = errors.New("failed to create contrbitution") ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") + ErrContributionScoreNotFound = errors.New("failed to get contributionscore details for given contribution type") ) func MapError(err error) (statusCode int, errMessage string) { diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 674dec6..3293992 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -2,6 +2,8 @@ package repository import ( "context" + "database/sql" + "errors" "log/slog" "github.com/jmoiron/sqlx" @@ -70,6 +72,11 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( var contributionScoreDetails ContributionScore err := executer.GetContext(ctx, &contributionScoreDetails, getContributionScoreDetailsByContributionTypeQuery, contributionType) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Warn("no contribution score details found for contribution type", "contributionType", contributionType) + return ContributionScore{}, apperrors.ErrContributionScoreNotFound + } + slog.Error("error occured while getting contribution score details", "error", err) return ContributionScore{}, err } From a73d29500237642c629ce13a3e27d871dfc22dd9 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 1 Jul 2025 12:07:46 +0530 Subject: [PATCH 41/68] refactor Execute function --- internal/app/cronJob/cronjob.go | 14 ++++++-------- internal/app/cronJob/dailyJob.go | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/app/cronJob/cronjob.go b/internal/app/cronJob/cronjob.go index a567985..f8f3a21 100644 --- a/internal/app/cronJob/cronjob.go +++ b/internal/app/cronJob/cronjob.go @@ -14,13 +14,11 @@ type CronJob struct { Name string } -func (c *CronJob) Execute(ctx context.Context, fn func(context.Context)) func() { - return func() { - slog.Info("cron job started at", "time ", time.Now()) - defer func() { - slog.Info("cron job completed") - }() +func (c *CronJob) Execute(ctx context.Context, fn func(context.Context)) { + slog.Info("cron job started at", "time ", time.Now()) + defer func() { + slog.Info("cron job completed") + }() - fn(ctx) - } + fn(ctx) } diff --git a/internal/app/cronJob/dailyJob.go b/internal/app/cronJob/dailyJob.go index 30f225f..67f3e24 100644 --- a/internal/app/cronJob/dailyJob.go +++ b/internal/app/cronJob/dailyJob.go @@ -19,7 +19,7 @@ func NewDailyJob(contributionService contribution.Service) *DailyJob { } func (d *DailyJob) Schedule(s *CronSchedular) error { - _, err := s.cron.AddFunc("0 1 * * *", func() { d.Execute(context.Background(), d.run)() }) + _, err := s.cron.AddFunc("0 1 * * *", func() { d.Execute(context.Background(), d.run) }) if err != nil { return err } From 5397c3ebaeea07c077caf8a6d53054033360c1c7 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 1 Jul 2025 13:23:31 +0530 Subject: [PATCH 42/68] fix - use actionkey for action string --- internal/app/contribution/service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index c9c74f4..17095d8 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -113,7 +113,7 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { if err == apperrors.ErrRepoNotFound { repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) if err != nil { - slog.Error("error fetching repository details") + slog.Error("error fetching repository details", "error", err) return err } @@ -157,7 +157,7 @@ func (s *service) GetContributionType(ctx context.Context, contribution Contribu } var action string - if actionVal, ok := contributionPayload[action]; ok { + if actionVal, ok := contributionPayload[payloadActionKey]; ok { action = actionVal.(string) } From 6cb482d8b73ec9aca99cb79f248aa87e30824ec4 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 1 Jul 2025 18:29:41 +0530 Subject: [PATCH 43/68] save github event id for each contribution in db --- internal/app/contribution/domain.go | 1 + internal/app/contribution/service.go | 1 + internal/app/repository/domain.go | 3 ++- internal/repository/contribution.go | 8 +++++--- internal/repository/domain.go | 1 + 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index 5fecd14..872f452 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -36,6 +36,7 @@ type Contribution struct { ContributionType string BalanceChange int ContributedAt time.Time + GithubEventId int CreatedAt time.Time UpdatedAt time.Time } diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 17095d8..8273aef 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -213,6 +213,7 @@ func (s *service) CreateContribution(ctx context.Context, contributionType strin RepositoryId: repositoryId, ContributionType: contributionType, ContributedAt: contributionDetails.CreatedAt, + GithubEventId: contributionDetails.ActorID, } contributionScoreDetails, err := s.GetContributionScoreDetailsByContributionType(ctx, contributionType) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index d63c113..940a5f3 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -60,6 +60,7 @@ type Contribution struct { ContributionType string BalanceChange int ContributedAt time.Time + GithubEventId int CreatedAt time.Time UpdatedAt time.Time } @@ -68,4 +69,4 @@ type LanguagePercent struct { Name string Bytes int Percentage float64 -} \ No newline at end of file +} diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 5270983..74887a6 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -36,9 +36,10 @@ const ( contribution_score_id, contribution_type, balance_change, - contributed_at + contributed_at, + github_event_id ) - VALUES ($1, $2, $3, $4, $5, $6) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *` getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` @@ -57,6 +58,7 @@ func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sq contributionInfo.ContributionType, contributionInfo.BalanceChange, contributionInfo.ContributedAt, + contributionInfo.GithubEventId, ) if err != nil { slog.Error("error occured while inserting contributions", "error", err) @@ -101,6 +103,6 @@ func (cr *contributionRepository) FetchUserContributions(ctx context.Context, tx slog.Error("error fetching user contributions", "error", err) return nil, apperrors.ErrFetchingAllContributions } - + return userContributions, nil } diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 148c2e3..7d2066b 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -38,6 +38,7 @@ type Contribution struct { ContributionType string `db:"contribution_type"` BalanceChange int `db:"balance_change"` ContributedAt time.Time `db:"contributed_at"` + GithubEventId int `db:"github_event_id"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } From 521be1adb57d17cbff334c766c70f6d216c38239 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 2 Jul 2025 15:55:34 +0530 Subject: [PATCH 44/68] use user github id to fetch contributions from bigquery and refactor ProcessFetchedContributions function --- internal/app/bigquery/domain.go | 2 +- internal/app/bigquery/service.go | 13 ++++-------- internal/app/contribution/domain.go | 2 +- internal/app/contribution/service.go | 30 +++++++++++++++++++++++----- internal/app/repository/domain.go | 2 +- internal/pkg/apperrors/errors.go | 2 ++ internal/pkg/utils/helper.go | 15 ++++++++++++++ internal/repository/contribution.go | 21 +++++++++++++++++++ internal/repository/domain.go | 2 +- internal/repository/user.go | 12 +++++------ 10 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 internal/pkg/utils/helper.go diff --git a/internal/app/bigquery/domain.go b/internal/app/bigquery/domain.go index 7b15922..bedd595 100644 --- a/internal/app/bigquery/domain.go +++ b/internal/app/bigquery/domain.go @@ -28,7 +28,7 @@ WHERE 'PullRequestReviewCommentEvent' ) AND ( - actor.login IN (%s) + actor.id IN (%s) )` type ContributionResponse struct { diff --git a/internal/app/bigquery/service.go b/internal/app/bigquery/service.go index 5f64ee1..21a2064 100644 --- a/internal/app/bigquery/service.go +++ b/internal/app/bigquery/service.go @@ -4,12 +4,12 @@ import ( "context" "fmt" "log/slog" - "strings" "time" bq "cloud.google.com/go/bigquery" "github.com/joshsoftware/code-curiosity-2025/internal/config" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/utils" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) @@ -30,22 +30,17 @@ func NewService(bigqueryInstance config.Bigquery, userRepository repository.User } func (s *service) FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) { - usersNamesList, err := s.userRepository.GetAllUsersGithubUsernames(ctx, nil) + usersGithubId, err := s.userRepository.GetAllUsersGithubId(ctx, nil) if err != nil { slog.Error("error fetching users github usernames") return nil, apperrors.ErrInternalServer } - var quotedUsernamesList []string - for _, username := range usersNamesList { - quotedUsernamesList = append(quotedUsernamesList, fmt.Sprintf("'%s'", username)) - } + formattedGithubIds := utils.FormatIntSliceForQuery(usersGithubId) YesterdayDate := time.Now().AddDate(0, 0, -1) YesterdayYearMonthDay := YesterdayDate.Format("20060102") - - githubUsernames := strings.Join(quotedUsernamesList, ",") - fetchDailyContributionsQuery := fmt.Sprintf(DailyQuery, YesterdayYearMonthDay, githubUsernames) + fetchDailyContributionsQuery := fmt.Sprintf(DailyQuery, YesterdayYearMonthDay, formattedGithubIds) bigqueryQuery := s.bigqueryInstance.Client.Query(fetchDailyContributionsQuery) contributionRows, err := bigqueryQuery.Read(ctx) diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index 872f452..295c851 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -36,7 +36,7 @@ type Contribution struct { ContributionType string BalanceChange int ContributedAt time.Time - GithubEventId int + GithubEventId string CreatedAt time.Time UpdatedAt time.Time } diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 8273aef..f54f41b 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -60,6 +60,7 @@ type Service interface { CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context) ([]Contribution, error) + GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, httpClient *http.Client) Service { @@ -100,15 +101,18 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { } for _, contribution := range fetchedContributions { + _, err := s.GetContributionByGithubEventId(ctx, contribution.ID) + if err == nil { + continue + } - contributionType, err := s.GetContributionType(ctx, contribution) - if err != nil { - slog.Error("error getting contribution type", "error", err) + if err != apperrors.ErrContributionNotFound { + slog.Error("error fetching contribution by github event id", "error", err) return err } var repositoryId int - repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) + repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) if err != nil { if err == apperrors.ErrRepoNotFound { repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) @@ -138,6 +142,12 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { return err } + contributionType, err := s.GetContributionType(ctx, contribution) + if err != nil { + slog.Error("error getting contribution type", "error", err) + return err + } + _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) if err != nil { slog.Error("error creating contribution", "error", err) @@ -213,7 +223,7 @@ func (s *service) CreateContribution(ctx context.Context, contributionType strin RepositoryId: repositoryId, ContributionType: contributionType, ContributedAt: contributionDetails.CreatedAt, - GithubEventId: contributionDetails.ActorID, + GithubEventId: contributionDetails.ID, } contributionScoreDetails, err := s.GetContributionScoreDetailsByContributionType(ctx, contributionType) @@ -258,3 +268,13 @@ func (s *service) FetchUserContributions(ctx context.Context) ([]Contribution, e return serviceContributions, nil } + +func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) { + contribution, err := s.contributionRepository.GetContributionByGithubEventId(ctx, nil, githubEventId) + if err != nil { + slog.Error("error fetching contribution by github event id") + return Contribution{}, err + } + + return Contribution(contribution), nil +} diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 940a5f3..6d8303a 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -60,7 +60,7 @@ type Contribution struct { ContributionType string BalanceChange int ContributedAt time.Time - GithubEventId int + GithubEventId string CreatedAt time.Time UpdatedAt time.Time } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index a432f41..4efe8cf 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -43,6 +43,8 @@ var ( ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") ErrContributionScoreNotFound = errors.New("failed to get contributionscore details for given contribution type") + ErrFetchingContribution = errors.New("error fetching contribution by github repo id") + ErrContributionNotFound = errors.New("contribution not found") ) func MapError(err error) (statusCode int, errMessage string) { diff --git a/internal/pkg/utils/helper.go b/internal/pkg/utils/helper.go new file mode 100644 index 0000000..05cca78 --- /dev/null +++ b/internal/pkg/utils/helper.go @@ -0,0 +1,15 @@ +package utils + +import ( + "fmt" + "strings" +) + +func FormatIntSliceForQuery(ids []int) string { + strIDs := make([]string, len(ids)) + for i, id := range ids { + strIDs[i] = fmt.Sprintf("%d", id) + } + + return strings.Join(strIDs, ",") +} diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 74887a6..eb1d1a5 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -20,6 +20,7 @@ type ContributionRepository interface { CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) + GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -45,6 +46,8 @@ const ( getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` fetchUserContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc` + + GetContributionByGithubEventIdQuery = `SELECT * from contributions where github_event_id=$1` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -106,3 +109,21 @@ func (cr *contributionRepository) FetchUserContributions(ctx context.Context, tx return userContributions, nil } + +func (cr *contributionRepository) GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contribution Contribution + err := executer.GetContext(ctx, &contribution, GetContributionByGithubEventIdQuery, githubEventId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("contribution not found", "error", err) + return Contribution{}, apperrors.ErrContributionNotFound + } + slog.Error("error fetching contribution by github event id", "error", err) + return Contribution{}, apperrors.ErrFetchingContribution + } + + return contribution, nil + +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 7d2066b..eb4ba97 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -38,7 +38,7 @@ type Contribution struct { ContributionType string `db:"contribution_type"` BalanceChange int `db:"balance_change"` ContributedAt time.Time `db:"contributed_at"` - GithubEventId int `db:"github_event_id"` + GithubEventId string `db:"github_event_id"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } diff --git a/internal/repository/user.go b/internal/repository/user.go index 806124a..254da1b 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -21,7 +21,7 @@ type UserRepository interface { GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error - GetAllUsersGithubUsernames(ctx context.Context, tx *sqlx.Tx) ([]string, error) + GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -47,7 +47,7 @@ const ( updateEmailQuery = "UPDATE users SET email=$1, updated_at=$2 where id=$3" - getAllUsersGithubUsernamesQuery = "SELECT github_username from users" + getAllUsersGithubIdQuery = "SELECT github_id from users" ) func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { @@ -115,15 +115,15 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user return nil } -func (ur *userRepository) GetAllUsersGithubUsernames(ctx context.Context, tx *sqlx.Tx) ([]string, error) { +func (ur *userRepository) GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) { executer := ur.BaseRepository.initiateQueryExecuter(tx) - var githubUsernames []string - err := executer.SelectContext(ctx, &githubUsernames, getAllUsersGithubUsernamesQuery) + var githubIds []int + err := executer.SelectContext(ctx, &githubIds, getAllUsersGithubIdQuery) if err != nil { slog.Error("failed to get github usernames", "error", err) return nil, apperrors.ErrInternalServer } - return githubUsernames, nil + return githubIds, nil } From 031ba382748b8494817cdd616661fafa2fe04418 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 2 Jul 2025 17:18:32 +0530 Subject: [PATCH 45/68] make separate github service for github api calls --- internal/app/contribution/service.go | 37 ++++----- internal/app/dependencies.go | 4 +- internal/app/github/domain.go | 28 +++++++ internal/app/github/service.go | 118 +++++++++++++++++++++++++++ internal/app/repository/domain.go | 28 ------- internal/app/repository/handler.go | 8 +- internal/app/repository/service.go | 108 ++---------------------- 7 files changed, 177 insertions(+), 154 deletions(-) create mode 100644 internal/app/github/domain.go create mode 100644 internal/app/github/service.go diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index f54f41b..b4f31cb 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -68,9 +68,8 @@ func NewService(bigqueryService bigquery.Service, contributionRepository reposit bigqueryService: bigqueryService, contributionRepository: contributionRepository, repositoryService: repositoryService, - - userService: userService, - httpClient: httpClient, + userService: userService, + httpClient: httpClient, } } @@ -81,10 +80,10 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { return apperrors.ErrFetchingFromBigquery } - var fetchedContributions []ContributionResponse - //using a local copy here to copy contribution so that I can implement retry mechanism in future //thinking of batch processing to be implemented later on, to handle memory overflow + var fetchedContributions []ContributionResponse + for { var contribution ContributionResponse err := contributions.Next(&contribution) @@ -113,27 +112,19 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { var repositoryId int repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) - if err != nil { - if err == apperrors.ErrRepoNotFound { - repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) - if err != nil { - slog.Error("error fetching repository details", "error", err) - return err - } - - repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, repo) - if err != nil { - slog.Error("error creating repository", "error", err) - return err - } - - repositoryId = repositoryCreated.Id - } else { - slog.Error("error fetching repo by repo id", "error", err) + if err == nil { + repositoryId = repoFetched.Id + } else if err == apperrors.ErrRepoNotFound { + repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, contribution.RepoUrl) + if err != nil { + slog.Error("error creating repository", "error", err) return err } + + repositoryId = repositoryCreated.Id } else { - repositoryId = repoFetched.Id + slog.Error("error fetching repo by repo id", "error", err) + return err } user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 9789265..f390342 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -7,6 +7,7 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app/auth" "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" + "github.com/joshsoftware/code-curiosity-2025/internal/app/github" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" "github.com/joshsoftware/code-curiosity-2025/internal/config" @@ -32,7 +33,8 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque userService := user.NewService(userRepository) authService := auth.NewService(userService, appCfg) bigqueryService := bigquery.NewService(client, userRepository) - repositoryService := repoService.NewService(repositoryRepository, appCfg, httpClient) + githubService := github.NewService(appCfg, httpClient) + repositoryService := repoService.NewService(repositoryRepository, githubService) contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, httpClient) authHandler := auth.NewHandler(authService, appCfg) diff --git a/internal/app/github/domain.go b/internal/app/github/domain.go new file mode 100644 index 0000000..af7300f --- /dev/null +++ b/internal/app/github/domain.go @@ -0,0 +1,28 @@ +package github + +import "time" + +type RepoOwner struct { + Login string `json:"login"` +} + +type FetchRepositoryDetailsResponse struct { + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` + RepoOwnerName RepoOwner `json:"owner"` + ContributorsUrl string `json:"contributors_url"` + RepoUrl string `json:"html_url"` +} + +type RepoLanguages map[string]int + +type FetchRepoContributorsResponse struct { + Id int `json:"id"` + Name string `json:"login"` + AvatarUrl string `json:"avatar_url"` + GithubUrl string `json:"html_url"` + Contributions int `json:"contributions"` +} diff --git a/internal/app/github/service.go b/internal/app/github/service.go new file mode 100644 index 0000000..c4982c1 --- /dev/null +++ b/internal/app/github/service.go @@ -0,0 +1,118 @@ +package github + +import ( + "context" + "encoding/json" + "io" + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/config" +) + +type service struct { + appCfg config.AppConfig + httpClient *http.Client +} + +type Service interface { + FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) + FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) + FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) +} + +func NewService(appCfg config.AppConfig, httpClient *http.Client) Service { + return &service{ + appCfg: appCfg, + httpClient: httpClient, + } +} + +func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { + req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) + + resp, err := s.httpClient.Do(req) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + var repoDetails FetchRepositoryDetailsResponse + err = json.Unmarshal(body, &repoDetails) + if err != nil { + slog.Error("error unmarshalling fetch repository details body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + return repoDetails, nil +} + +func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) { + req, err := http.NewRequest("GET", getRepoLanguagesURL, nil) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return RepoLanguages{}, err + } + + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return RepoLanguages{}, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return RepoLanguages{}, err + } + + var repoLanguages RepoLanguages + err = json.Unmarshal(body, &repoLanguages) + if err != nil { + slog.Error("error unmarshalling fetch repository languages body", "error", err) + return RepoLanguages{}, err + } + + return repoLanguages, nil +} + +func (s *service) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { + req, err := http.NewRequest("GET", getRepoContributorsURl, nil) + if err != nil { + slog.Error("error fetching contributors for repository", "error", err) + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching contributors for repository", "error", err) + return nil, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return nil, err + } + + var repoContributors []FetchRepoContributorsResponse + err = json.Unmarshal(body, &repoContributors) + if err != nil { + slog.Error("error unmarshalling fetch contributors body", "error", err) + return nil, err + } + + return repoContributors, nil +} diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 6d8303a..60b7400 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -2,21 +2,6 @@ package repository import "time" -type RepoOwner struct { - Login string `json:"login"` -} - -type FetchRepositoryDetailsResponse struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - LanguagesURL string `json:"languages_url"` - UpdateDate time.Time `json:"updated_at"` - RepoOwnerName RepoOwner `json:"owner"` - ContributorsUrl string `json:"contributors_url"` - RepoUrl string `json:"html_url"` -} - type Repository struct { Id int GithubRepoId int @@ -39,19 +24,6 @@ type FetchUsersContributedReposResponse struct { TotalCoinsEarned int } -type FetchRepoContributorsResponse struct { - Id int `json:"id"` - Name string `json:"login"` - AvatarUrl string `json:"avatar_url"` - GithubUrl string `json:"html_url"` - Contributions int `json:"contributions"` -} - -type FetchParticularRepoDetails struct { - Repository - Contributors []FetchRepoContributorsResponse -} - type Contribution struct { Id int UserId int diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index 083d14c..5edbb29 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -5,12 +5,14 @@ import ( "net/http" "strconv" + "github.com/joshsoftware/code-curiosity-2025/internal/app/github" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) type handler struct { repositoryService Service + githubService github.Service } type Handler interface { @@ -86,7 +88,7 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http return } - repoContributors, err := h.repositoryService.FetchRepositoryContributors(ctx, client, repoDetails.ContributorsUrl) + repoContributors, err := h.githubService.FetchRepositoryContributors(ctx, client, repoDetails.ContributorsUrl) if err != nil { slog.Error("error fetching repo contributors", "error", err) status, errorMessage := apperrors.MapError(err) @@ -142,7 +144,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ return } - repoLanguages, err := h.repositoryService.FetchRepositoryLanguages(ctx, client, repoDetails.LanguagesUrl) + repoLanguages, err := h.githubService.FetchRepositoryLanguages(ctx, client, repoDetails.LanguagesUrl) if err != nil { slog.Error("error fetching particular repo languages", "error", err) status, errorMessage := apperrors.MapError(err) @@ -150,7 +152,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ return } - langPercent, err := h.repositoryService.CalculateLanguagePercentInRepo(ctx, repoLanguages) + langPercent, err := h.repositoryService.CalculateLanguagePercentInRepo(ctx, RepoLanguages(repoLanguages)) if err != nil { slog.Error("error fetching particular repo languages", "error", err) status, errorMessage := apperrors.MapError(err) diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index a7499f6..bbae42d 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -2,39 +2,32 @@ package repository import ( "context" - "encoding/json" - "io" "log/slog" "math" "net/http" - "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/app/github" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) type service struct { repositoryRepository repository.RepositoryRepository - appCfg config.AppConfig - httpClient *http.Client + githubService github.Service } type Service interface { GetRepoByGithubId(ctx context.Context, githubRepoId int) (Repository, error) GetRepoByRepoId(ctx context.Context, repoId int) (Repository, error) - FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) - CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) - FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) + CreateRepository(ctx context.Context, repoGithubId int, ContributionRepoDetailsUrl string) (Repository, error) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) - FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) } -func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig, httpClient *http.Client) Service { +func NewService(repositoryRepository repository.RepositoryRepository, githubService github.Service) Service { return &service{ repositoryRepository: repositoryRepository, - appCfg: appCfg, - httpClient: httpClient, + githubService: githubService, } } @@ -58,38 +51,13 @@ func (s *service) GetRepoByRepoId(ctx context.Context, repobId int) (Repository, return Repository(repoDetails), nil } -func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { - req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) +func (s *service) CreateRepository(ctx context.Context, repoGithubId int, ContributionRepoDetailsUrl string) (Repository, error) { + repo, err := s.githubService.FetchRepositoryDetails(ctx, ContributionRepoDetailsUrl) if err != nil { slog.Error("error fetching user repositories details", "error", err) - return FetchRepositoryDetailsResponse{}, err - } - - req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) - - resp, err := s.httpClient.Do(req) - if err != nil { - slog.Error("error fetching user repositories details", "error", err) - return FetchRepositoryDetailsResponse{}, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - slog.Error("error reading body", "error", err) - return FetchRepositoryDetailsResponse{}, err - } - - var repoDetails FetchRepositoryDetailsResponse - err = json.Unmarshal(body, &repoDetails) - if err != nil { - slog.Error("error unmarshalling fetch repository details body", "error", err) - return FetchRepositoryDetailsResponse{}, err + return Repository{}, err } - return repoDetails, nil -} - -func (s *service) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) { createRepo := Repository{ GithubRepoId: repoGithubId, RepoName: repo.Name, @@ -109,35 +77,6 @@ func (s *service) CreateRepository(ctx context.Context, repoGithubId int, repo F return Repository(repositoryCreated), nil } -func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) { - req, err := http.NewRequest("GET", getRepoLanguagesURL, nil) - if err != nil { - slog.Error("error fetching languages for repository", "error", err) - return RepoLanguages{}, err - } - - resp, err := client.Do(req) - if err != nil { - slog.Error("error fetching languages for repository", "error", err) - return RepoLanguages{}, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - slog.Error("error reading body", "error", err) - return RepoLanguages{}, err - } - - var repoLanguages RepoLanguages - err = json.Unmarshal(body, &repoLanguages) - if err != nil { - slog.Error("error unmarshalling fetch repository languages body", "error", err) - return RepoLanguages{}, err - } - - return repoLanguages, nil -} - func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) { usersContributedRepos, err := s.repositoryRepository.FetchUsersContributedRepos(ctx, nil) if err != nil { @@ -150,7 +89,7 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C for i, usersContributedRepo := range usersContributedRepos { fetchUsersContributedReposResponse[i].Repository = Repository(usersContributedRepo) - contributedRepoLanguages, err := s.FetchRepositoryLanguages(ctx, client, usersContributedRepo.LanguagesUrl) + contributedRepoLanguages, err := s.githubService.FetchRepositoryLanguages(ctx, client, usersContributedRepo.LanguagesUrl) if err != nil { slog.Error("error fetching languages for repository", "error", err) return nil, err @@ -172,35 +111,6 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C return fetchUsersContributedReposResponse, nil } -func (s *service) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { - req, err := http.NewRequest("GET", getRepoContributorsURl, nil) - if err != nil { - slog.Error("error fetching contributors for repository", "error", err) - return nil, err - } - - resp, err := client.Do(req) - if err != nil { - slog.Error("error fetching contributors for repository", "error", err) - return nil, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - slog.Error("error reading body", "error", err) - return nil, err - } - - var repoContributors []FetchRepoContributorsResponse - err = json.Unmarshal(body, &repoContributors) - if err != nil { - slog.Error("error unmarshalling fetch contributors body", "error", err) - return nil, err - } - - return repoContributors, nil -} - func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) { userContributionsInRepo, err := s.repositoryRepository.FetchUserContributionsInRepo(ctx, nil, githubRepoId) if err != nil { From 1e3e4a48b0106183d2fab910d6478a55fd3e3ca6 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 2 Jul 2025 17:34:39 +0530 Subject: [PATCH 46/68] reduce cognitive complexity of ProcessFetchedContributions Function by separating logic for ProcessEachContribution --- internal/app/contribution/service.go | 79 ++++++++++++++++------------ 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index b4f31cb..5be08ec 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -57,6 +57,7 @@ type service struct { type Service interface { ProcessFetchedContributions(ctx context.Context) error + ProcessEachContribution(ctx context.Context, contribution ContributionResponse) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context) ([]Contribution, error) @@ -100,50 +101,60 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { } for _, contribution := range fetchedContributions { - _, err := s.GetContributionByGithubEventId(ctx, contribution.ID) - if err == nil { - continue - } - - if err != apperrors.ErrContributionNotFound { - slog.Error("error fetching contribution by github event id", "error", err) + err := s.ProcessEachContribution(ctx, contribution) + if err != nil { + slog.Error("error processing contribution with github event id", "github event id", "error", contribution.ID, err) return err } + } - var repositoryId int - repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) - if err == nil { - repositoryId = repoFetched.Id - } else if err == apperrors.ErrRepoNotFound { - repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, contribution.RepoUrl) - if err != nil { - slog.Error("error creating repository", "error", err) - return err - } + return nil +} - repositoryId = repositoryCreated.Id - } else { - slog.Error("error fetching repo by repo id", "error", err) - return err - } +func (s *service) ProcessEachContribution(ctx context.Context, contribution ContributionResponse) error { + _, err := s.GetContributionByGithubEventId(ctx, contribution.ID) + if err == nil { + return nil + } - user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) - if err != nil { - slog.Error("error getting user id", "error", err) - return err - } + if err != apperrors.ErrContributionNotFound { + slog.Error("error fetching contribution by github event id", "error", err) + return err + } - contributionType, err := s.GetContributionType(ctx, contribution) + var repositoryId int + repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) + if err == nil { + repositoryId = repoFetched.Id + } else if err == apperrors.ErrRepoNotFound { + repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, contribution.RepoUrl) if err != nil { - slog.Error("error getting contribution type", "error", err) + slog.Error("error creating repository", "error", err) return err } - _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) - if err != nil { - slog.Error("error creating contribution", "error", err) - return err - } + repositoryId = repositoryCreated.Id + } else { + slog.Error("error fetching repo by repo id", "error", err) + return err + } + + user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) + if err != nil { + slog.Error("error getting user id", "error", err) + return err + } + + contributionType, err := s.GetContributionType(ctx, contribution) + if err != nil { + slog.Error("error getting contribution type", "error", err) + return err + } + + _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) + if err != nil { + slog.Error("error creating contribution", "error", err) + return err } return nil From 76df1d2ca2397e113364a19fd4216aa5f3958ef4 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 3 Jul 2025 12:27:24 +0530 Subject: [PATCH 47/68] log error in GetContributionByGithubEventId --- internal/app/contribution/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 5be08ec..b8e832b 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -274,7 +274,7 @@ func (s *service) FetchUserContributions(ctx context.Context) ([]Contribution, e func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) { contribution, err := s.contributionRepository.GetContributionByGithubEventId(ctx, nil, githubEventId) if err != nil { - slog.Error("error fetching contribution by github event id") + slog.Error("error fetching contribution by github event id", "error", err) return Contribution{}, err } From f3c25f37edb9f33c89b961c9dc4b6ef659e6f62b Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 3 Jul 2025 12:27:59 +0530 Subject: [PATCH 48/68] make utility function for GET requests to github API's --- internal/app/github/domain.go | 2 ++ internal/app/github/service.go | 59 ++++++++++------------------------ internal/pkg/utils/helper.go | 30 +++++++++++++++++ 3 files changed, 49 insertions(+), 42 deletions(-) diff --git a/internal/app/github/domain.go b/internal/app/github/domain.go index af7300f..efdd441 100644 --- a/internal/app/github/domain.go +++ b/internal/app/github/domain.go @@ -2,6 +2,8 @@ package github import "time" +const AuthorizationKey = "Authorization" + type RepoOwner struct { Login string `json:"login"` } diff --git a/internal/app/github/service.go b/internal/app/github/service.go index c4982c1..8b81912 100644 --- a/internal/app/github/service.go +++ b/internal/app/github/service.go @@ -3,11 +3,11 @@ package github import ( "context" "encoding/json" - "io" "log/slog" "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/utils" ) type service struct { @@ -16,6 +16,7 @@ type service struct { } type Service interface { + configureGithubApiHeaders() map[string]string FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) @@ -28,24 +29,18 @@ func NewService(appCfg config.AppConfig, httpClient *http.Client) Service { } } -func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { - req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) - if err != nil { - slog.Error("error fetching user repositories details", "error", err) - return FetchRepositoryDetailsResponse{}, err +func (s *service) configureGithubApiHeaders() map[string]string { + return map[string]string{ + AuthorizationKey: s.appCfg.GithubPersonalAccessToken, } +} - req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) - - resp, err := s.httpClient.Do(req) - if err != nil { - slog.Error("error fetching user repositories details", "error", err) - return FetchRepositoryDetailsResponse{}, err - } +func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { + headers := s.configureGithubApiHeaders() - body, err := io.ReadAll(resp.Body) + body, err := utils.DoGet(s.httpClient, getUserRepoDetailsUrl, headers) if err != nil { - slog.Error("error reading body", "error", err) + slog.Error("error making a GET request", "error", err) return FetchRepositoryDetailsResponse{}, err } @@ -60,21 +55,11 @@ func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetails } func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) { - req, err := http.NewRequest("GET", getRepoLanguagesURL, nil) - if err != nil { - slog.Error("error fetching languages for repository", "error", err) - return RepoLanguages{}, err - } + headers := s.configureGithubApiHeaders() - resp, err := client.Do(req) + body, err := utils.DoGet(s.httpClient, getRepoLanguagesURL, headers) if err != nil { - slog.Error("error fetching languages for repository", "error", err) - return RepoLanguages{}, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - slog.Error("error reading body", "error", err) + slog.Error("error making a GET request", "error", err) return RepoLanguages{}, err } @@ -89,22 +74,12 @@ func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Cli } func (s *service) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { - req, err := http.NewRequest("GET", getRepoContributorsURl, nil) - if err != nil { - slog.Error("error fetching contributors for repository", "error", err) - return nil, err - } - - resp, err := client.Do(req) - if err != nil { - slog.Error("error fetching contributors for repository", "error", err) - return nil, err - } + headers := s.configureGithubApiHeaders() - body, err := io.ReadAll(resp.Body) + body, err := utils.DoGet(s.httpClient, getRepoContributorsURl, headers) if err != nil { - slog.Error("error reading body", "error", err) - return nil, err + slog.Error("error making a GET request", "error", err) + return []FetchRepoContributorsResponse{}, err } var repoContributors []FetchRepoContributorsResponse diff --git a/internal/pkg/utils/helper.go b/internal/pkg/utils/helper.go index 05cca78..3fa0492 100644 --- a/internal/pkg/utils/helper.go +++ b/internal/pkg/utils/helper.go @@ -2,6 +2,9 @@ package utils import ( "fmt" + "io" + "log/slog" + "net/http" "strings" ) @@ -13,3 +16,30 @@ func FormatIntSliceForQuery(ids []int) string { return strings.Join(strIDs, ",") } + +func DoGet(httpClient *http.Client, url string, headers map[string]string) ([]byte, error) { + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + slog.Error("failed to create GET request", "error", err) + return nil, err + } + + for key, value := range headers { + req.Header.Add(key, value) + } + + resp, err := httpClient.Do(req) + if err != nil { + slog.Error("failed to send GET request", "error", err) + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return nil, err + } + + return body, nil +} From ee7aa7f67414d046f7ee886642059ee4fd92df02 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 3 Jul 2025 15:08:01 +0530 Subject: [PATCH 49/68] create a transaction entry for each contribution --- internal/app/contribution/domain.go | 12 ++++++ internal/app/contribution/service.go | 32 +++++++++++++++- internal/app/dependencies.go | 5 ++- internal/app/transaction/domain.go | 15 ++++++++ internal/app/transaction/service.go | 32 ++++++++++++++++ internal/pkg/apperrors/errors.go | 4 +- internal/repository/domain.go | 12 ++++++ internal/repository/transaction.go | 57 ++++++++++++++++++++++++++++ 8 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 internal/app/transaction/domain.go create mode 100644 internal/app/transaction/service.go create mode 100644 internal/repository/transaction.go diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index 295c851..3c90423 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -49,3 +49,15 @@ type ContributionScore struct { CreatedAt time.Time UpdatedAt time.Time } + +type Transaction struct { + Id int `db:"id"` + UserId int `db:"user_id"` + ContributionId int `db:"contribution_id"` + IsRedeemed bool `db:"is_redeemed"` + IsGained bool `db:"is_gained"` + TransactedBalance int `db:"transacted_balance"` + TransactedAt time.Time `db:"transacted_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index b8e832b..fecba8e 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -8,6 +8,7 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" + "github.com/joshsoftware/code-curiosity-2025/internal/app/transaction" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/repository" @@ -52,6 +53,7 @@ type service struct { contributionRepository repository.ContributionRepository repositoryService repoService.Service userService user.Service + transactionService transaction.Service httpClient *http.Client } @@ -62,14 +64,16 @@ type Service interface { GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) + CreateContributionTransaction(ctx context.Context, userId int, contributionDetails Contribution) (Transaction, error) } -func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, httpClient *http.Client) Service { +func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service { return &service{ bigqueryService: bigqueryService, contributionRepository: contributionRepository, repositoryService: repositoryService, userService: userService, + transactionService: transactionService, httpClient: httpClient, } } @@ -151,12 +155,18 @@ func (s *service) ProcessEachContribution(ctx context.Context, contribution Cont return err } - _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) + createdContribution, err := s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) if err != nil { slog.Error("error creating contribution", "error", err) return err } + _, err = s.CreateContributionTransaction(ctx, user.Id, createdContribution) + if err != nil { + slog.Error("error creating transaction for current contribution", "error", err) + return err + } + return nil } @@ -280,3 +290,21 @@ func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEven return Contribution(contribution), nil } + +func (s *service) CreateContributionTransaction(ctx context.Context, userId int, contributionDetails Contribution) (Transaction, error) { + transactionInfo := Transaction{ + UserId: userId, + ContributionId: contributionDetails.Id, + IsRedeemed: false, + IsGained: true, + TransactedBalance: contributionDetails.BalanceChange, + TransactedAt: contributionDetails.ContributedAt, + } + transaction, err := s.transactionService.CreateTransaction(ctx, transaction.Transaction(transactionInfo)) + if err != nil { + slog.Error("error creating transaction for current contribution", "error", err) + return Transaction{}, err + } + + return Transaction(transaction), nil +} diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index f390342..0316cea 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -9,6 +9,7 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" "github.com/joshsoftware/code-curiosity-2025/internal/app/github" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" + "github.com/joshsoftware/code-curiosity-2025/internal/app/transaction" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" "github.com/joshsoftware/code-curiosity-2025/internal/config" @@ -29,13 +30,15 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque userRepository := repository.NewUserRepository(db) contributionRepository := repository.NewContributionRepository(db) repositoryRepository := repository.NewRepositoryRepository(db) + transactionRepository := repository.NewTransactionRepository(db) userService := user.NewService(userRepository) authService := auth.NewService(userService, appCfg) bigqueryService := bigquery.NewService(client, userRepository) githubService := github.NewService(appCfg, httpClient) repositoryService := repoService.NewService(repositoryRepository, githubService) - contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, httpClient) + transactionService := transaction.NewService(transactionRepository) + contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, transactionService, httpClient) authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) diff --git a/internal/app/transaction/domain.go b/internal/app/transaction/domain.go new file mode 100644 index 0000000..54a8c95 --- /dev/null +++ b/internal/app/transaction/domain.go @@ -0,0 +1,15 @@ +package transaction + +import "time" + +type Transaction struct { + Id int `db:"id"` + UserId int `db:"user_id"` + ContributionId int `db:"contribution_id"` + IsRedeemed bool `db:"is_redeemed"` + IsGained bool `db:"is_gained"` + TransactedBalance int `db:"transacted_balance"` + TransactedAt time.Time `db:"transacted_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} diff --git a/internal/app/transaction/service.go b/internal/app/transaction/service.go new file mode 100644 index 0000000..5f61572 --- /dev/null +++ b/internal/app/transaction/service.go @@ -0,0 +1,32 @@ +package transaction + +import ( + "context" + "log/slog" + + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + transactionRepository repository.TransactionRepository +} + +type Service interface { + CreateTransaction(ctx context.Context, transactionInfo Transaction) (Transaction, error) +} + +func NewService(transactionRepository repository.TransactionRepository) Service { + return &service{ + transactionRepository: transactionRepository, + } +} + +func (s *service) CreateTransaction(ctx context.Context, transactionInfo Transaction) (Transaction, error) { + transaction, err := s.transactionRepository.CreateTransaction(ctx, nil, repository.Transaction(transactionInfo)) + if err != nil { + slog.Error("error occured while creating transaction", "error", err) + return Transaction{}, err + } + + return Transaction(transaction), nil +} diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 4efe8cf..4d7fe27 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -45,6 +45,8 @@ var ( ErrContributionScoreNotFound = errors.New("failed to get contributionscore details for given contribution type") ErrFetchingContribution = errors.New("error fetching contribution by github repo id") ErrContributionNotFound = errors.New("contribution not found") + + ErrTransactionCreationFailed = errors.New("error failed to create transaction") ) func MapError(err error) (statusCode int, errMessage string) { @@ -55,7 +57,7 @@ func MapError(err error) (statusCode int, errMessage string) { return http.StatusUnauthorized, err.Error() case ErrAccessForbidden: return http.StatusForbidden, err.Error() - case ErrUserNotFound, ErrRepoNotFound: + case ErrUserNotFound, ErrRepoNotFound, ErrContributionNotFound: return http.StatusNotFound, err.Error() case ErrInvalidToken: return http.StatusUnprocessableEntity, err.Error() diff --git a/internal/repository/domain.go b/internal/repository/domain.go index eb4ba97..1a94621 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -65,3 +65,15 @@ type ContributionScore struct { CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + +type Transaction struct { + Id int `db:"id"` + UserId int `db:"user_id"` + ContributionId int `db:"contribution_id"` + IsRedeemed bool `db:"is_redeemed"` + IsGained bool `db:"is_gained"` + TransactedBalance int `db:"transacted_balance"` + TransactedAt time.Time `db:"transacted_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} diff --git a/internal/repository/transaction.go b/internal/repository/transaction.go new file mode 100644 index 0000000..e98a600 --- /dev/null +++ b/internal/repository/transaction.go @@ -0,0 +1,57 @@ +package repository + +import ( + "context" + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +type transactionRepository struct { + BaseRepository +} + +type TransactionRepository interface { + RepositoryTransaction + CreateTransaction(ctx context.Context, tx *sqlx.Tx, transactionInfo Transaction) (Transaction, error) +} + +func NewTransactionRepository(db *sqlx.DB) TransactionRepository { + return &transactionRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + createTransactionQuery = `INSERT INTO transactions ( + user_id, + contribution_id, + is_redeemed, + is_gained, + transacted_balance, + transacted_at + ) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING *` +) + +func (tr *transactionRepository) CreateTransaction(ctx context.Context, tx *sqlx.Tx, transactionInfo Transaction) (Transaction, error) { + executer := tr.BaseRepository.initiateQueryExecuter(tx) + + var transaction Transaction + err := executer.GetContext(ctx, &transaction, createTransactionQuery, + transactionInfo.UserId, + transactionInfo.ContributionId, + transactionInfo.IsRedeemed, + transactionInfo.IsGained, + transactionInfo.TransactedBalance, + transactionInfo.TransactedAt, + ) + if err != nil { + slog.Error("error occured while creating transaction", "error", err) + return Transaction{}, apperrors.ErrTransactionCreationFailed + } + + return transaction, nil +} From 244c44d931bf76b4fac24240e1042c1cbdb4dad6 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 4 Jul 2025 12:09:14 +0530 Subject: [PATCH 50/68] update user balance with each transaction created --- internal/app/dependencies.go | 2 +- internal/app/transaction/service.go | 29 +++++++++++++++++++++++++-- internal/app/user/domain.go | 16 +++++++++++++-- internal/app/user/service.go | 24 ++++++++++++++++++++++ internal/pkg/middleware/middleware.go | 14 +++++++++++++ internal/repository/user.go | 15 ++++++++++++++ 6 files changed, 95 insertions(+), 5 deletions(-) diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 0316cea..9eda099 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -37,7 +37,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque bigqueryService := bigquery.NewService(client, userRepository) githubService := github.NewService(appCfg, httpClient) repositoryService := repoService.NewService(repositoryRepository, githubService) - transactionService := transaction.NewService(transactionRepository) + transactionService := transaction.NewService(transactionRepository, userService) contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, transactionService, httpClient) authHandler := auth.NewHandler(authService, appCfg) diff --git a/internal/app/transaction/service.go b/internal/app/transaction/service.go index 5f61572..3606cce 100644 --- a/internal/app/transaction/service.go +++ b/internal/app/transaction/service.go @@ -4,29 +4,54 @@ import ( "context" "log/slog" + "github.com/joshsoftware/code-curiosity-2025/internal/app/user" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) type service struct { transactionRepository repository.TransactionRepository + userService user.Service } type Service interface { CreateTransaction(ctx context.Context, transactionInfo Transaction) (Transaction, error) } -func NewService(transactionRepository repository.TransactionRepository) Service { +func NewService(transactionRepository repository.TransactionRepository, userService user.Service) Service { return &service{ transactionRepository: transactionRepository, + userService: userService, } } func (s *service) CreateTransaction(ctx context.Context, transactionInfo Transaction) (Transaction, error) { - transaction, err := s.transactionRepository.CreateTransaction(ctx, nil, repository.Transaction(transactionInfo)) + tx, err := s.transactionRepository.BeginTx(ctx) + if err != nil { + slog.Error("failed to start transaction creation") + return Transaction{}, err + } + + ctx = middleware.EmbedTxInContext(ctx, tx) + + defer func() { + if txErr := s.transactionRepository.HandleTransaction(ctx, tx, err); txErr != nil { + slog.Error("failed to handle transaction", "error", txErr) + err = txErr + } + }() + + transaction, err := s.transactionRepository.CreateTransaction(ctx, tx, repository.Transaction(transactionInfo)) if err != nil { slog.Error("error occured while creating transaction", "error", err) return Transaction{}, err } + err = s.userService.UpdateUserCurrentBalance(ctx, user.Transaction(transaction)) + if err != nil { + slog.Error("error occured while updating user current balance", "error", err) + return Transaction{}, err + } + return Transaction(transaction), nil } diff --git a/internal/app/user/domain.go b/internal/app/user/domain.go index e2d9e6c..471f0b1 100644 --- a/internal/app/user/domain.go +++ b/internal/app/user/domain.go @@ -18,8 +18,8 @@ type User struct { Password string `json:"password"` IsDeleted bool `json:"is_deleted"` DeletedAt sql.NullTime `json:"deleted_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type CreateUserRequestBody struct { @@ -33,3 +33,15 @@ type CreateUserRequestBody struct { type Email struct { Email string `json:"email"` } + +type Transaction struct { + Id int `db:"id"` + UserId int `db:"user_id"` + ContributionId int `db:"contribution_id"` + IsRedeemed bool `db:"is_redeemed"` + IsGained bool `db:"is_gained"` + TransactedBalance int `db:"transacted_balance"` + TransactedAt time.Time `db:"transacted_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} diff --git a/internal/app/user/service.go b/internal/app/user/service.go index 93b8572..6dad743 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -18,6 +18,7 @@ type Service interface { GetUserByGithubId(ctx context.Context, githubId int) (User, error) CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, email string) error + UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error } func NewService(userRepository repository.UserRepository) Service { @@ -74,3 +75,26 @@ func (s *service) UpdateUserEmail(ctx context.Context, email string) error { return nil } + +func (s *service) UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error { + user, err := s.GetUserById(ctx, transaction.UserId) + if err != nil { + slog.Error("error obtaining user by id", "error", err) + return err + } + + user.CurrentBalance += transaction.TransactedBalance + + tx, ok := middleware.ExtractTxFromContext(ctx) + if !ok { + slog.Error("error obtaining tx from context") + } + + err = s.userRepository.UpdateUserCurrentBalance(ctx, tx, repository.User(user)) + if err != nil { + slog.Error("error updating user current balance", "error", err) + return err + } + + return nil +} diff --git a/internal/pkg/middleware/middleware.go b/internal/pkg/middleware/middleware.go index 3ece089..4c40789 100644 --- a/internal/pkg/middleware/middleware.go +++ b/internal/pkg/middleware/middleware.go @@ -5,12 +5,17 @@ import ( "net/http" "strings" + "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/config" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/jwt" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) +type txKeyType struct{} + +var txKey = txKeyType{} + type contextKey string const ( @@ -18,6 +23,15 @@ const ( IsAdminKey contextKey = "isAdmin" ) +func EmbedTxInContext(ctx context.Context, tx *sqlx.Tx) context.Context { + return context.WithValue(ctx, txKey, tx) +} + +func ExtractTxFromContext(ctx context.Context) (*sqlx.Tx, bool) { + tx, ok := ctx.Value(txKey).(*sqlx.Tx) + return tx, ok +} + func CorsMiddleware(next http.Handler, appCfg config.AppConfig) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", appCfg.ClientURL) diff --git a/internal/repository/user.go b/internal/repository/user.go index 254da1b..a3a748a 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -22,6 +22,7 @@ type UserRepository interface { CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) + UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -48,6 +49,8 @@ const ( updateEmailQuery = "UPDATE users SET email=$1, updated_at=$2 where id=$3" getAllUsersGithubIdQuery = "SELECT github_id from users" + + updateUserCurrentBalanceQuery = "UPDATE users SET current_balance=$1, updated_at=$2 where id=$3" ) func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { @@ -127,3 +130,15 @@ func (ur *userRepository) GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) return githubIds, nil } + +func (ur *userRepository) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + _, err := executer.ExecContext(ctx, updateUserCurrentBalanceQuery, user.CurrentBalance, time.Now(), user.Id) + if err != nil { + slog.Error("failed to update user balance change", "error", err) + return apperrors.ErrInternalServer + } + + return nil +} From 3a8296612db6677bdb34b862d628b517a03d0554 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 4 Jul 2025 18:25:35 +0530 Subject: [PATCH 51/68] refactor code to reduce cognitive complexity of ProcessEachContribution --- internal/app/contribution/service.go | 103 +++++++++++---------------- internal/app/repository/domain.go | 12 ++++ internal/app/repository/service.go | 22 +++++- internal/app/transaction/domain.go | 13 ++++ internal/app/transaction/service.go | 52 ++++++++++++++ internal/repository/transaction.go | 22 ++++++ 6 files changed, 161 insertions(+), 63 deletions(-) diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index fecba8e..f927207 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -60,11 +60,12 @@ type service struct { type Service interface { ProcessFetchedContributions(ctx context.Context) error ProcessEachContribution(ctx context.Context, contribution ContributionResponse) error + GetContributionType(ctx context.Context, contribution ContributionResponse) (string, error) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) + HandleContributionCreation(ctx context.Context, repositoryID int, contribution ContributionResponse) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) - CreateContributionTransaction(ctx context.Context, userId int, contributionDetails Contribution) (Transaction, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service { @@ -116,54 +117,28 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { } func (s *service) ProcessEachContribution(ctx context.Context, contribution ContributionResponse) error { - _, err := s.GetContributionByGithubEventId(ctx, contribution.ID) - if err == nil { - return nil - } - - if err != apperrors.ErrContributionNotFound { - slog.Error("error fetching contribution by github event id", "error", err) - return err - } - - var repositoryId int - repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) - if err == nil { - repositoryId = repoFetched.Id - } else if err == apperrors.ErrRepoNotFound { - repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, contribution.RepoUrl) - if err != nil { - slog.Error("error creating repository", "error", err) + obtainedContribution, err := s.GetContributionByGithubEventId(ctx, contribution.ID) + if err != nil { + if err == apperrors.ErrContributionNotFound { + obtainedRepository, err := s.repositoryService.HandleRepositoryCreation(ctx, repoService.ContributionResponse(contribution)) + if err != nil { + slog.Error("error handling repository creation", "error", err) + return err + } + obtainedContribution, err = s.HandleContributionCreation(ctx, obtainedRepository.Id, contribution) + if err != nil { + slog.Error("error handling contribution creation", "error", err) + return err + } + } else { + slog.Error("error fetching contribution by github event id", "error", err) return err } - - repositoryId = repositoryCreated.Id - } else { - slog.Error("error fetching repo by repo id", "error", err) - return err - } - - user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) - if err != nil { - slog.Error("error getting user id", "error", err) - return err - } - - contributionType, err := s.GetContributionType(ctx, contribution) - if err != nil { - slog.Error("error getting contribution type", "error", err) - return err - } - - createdContribution, err := s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) - if err != nil { - slog.Error("error creating contribution", "error", err) - return err } - _, err = s.CreateContributionTransaction(ctx, user.Id, createdContribution) + _, err = s.transactionService.HandleTransactionCreation(ctx, transaction.Contribution(obtainedContribution)) if err != nil { - slog.Error("error creating transaction for current contribution", "error", err) + slog.Error("error handling transaction creation", "error", err) return err } @@ -256,6 +231,28 @@ func (s *service) CreateContribution(ctx context.Context, contributionType strin return Contribution(contributionResponse), nil } +func (s *service) HandleContributionCreation(ctx context.Context, repositoryID int, contribution ContributionResponse) (Contribution, error) { + user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) + if err != nil { + slog.Error("error getting user id", "error", err) + return Contribution{}, err + } + + contributionType, err := s.GetContributionType(ctx, contribution) + if err != nil { + slog.Error("error getting contribution type", "error", err) + return Contribution{}, err + } + + obtainedContribution, err := s.CreateContribution(ctx, contributionType, contribution, repositoryID, user.Id) + if err != nil { + slog.Error("error creating contribution", "error", err) + return Contribution{}, err + } + + return obtainedContribution, nil +} + func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) { contributionScoreDetails, err := s.contributionRepository.GetContributionScoreDetailsByContributionType(ctx, nil, contributionType) if err != nil { @@ -290,21 +287,3 @@ func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEven return Contribution(contribution), nil } - -func (s *service) CreateContributionTransaction(ctx context.Context, userId int, contributionDetails Contribution) (Transaction, error) { - transactionInfo := Transaction{ - UserId: userId, - ContributionId: contributionDetails.Id, - IsRedeemed: false, - IsGained: true, - TransactedBalance: contributionDetails.BalanceChange, - TransactedAt: contributionDetails.ContributedAt, - } - transaction, err := s.transactionService.CreateTransaction(ctx, transaction.Transaction(transactionInfo)) - if err != nil { - slog.Error("error creating transaction for current contribution", "error", err) - return Transaction{}, err - } - - return Transaction(transaction), nil -} diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 60b7400..208f297 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -24,6 +24,18 @@ type FetchUsersContributedReposResponse struct { TotalCoinsEarned int } +type ContributionResponse struct { + ID string `bigquery:"id"` + Type string `bigquery:"type"` + ActorID int `bigquery:"actor_id"` + ActorLogin string `bigquery:"actor_login"` + RepoID int `bigquery:"repo_id"` + RepoName string `bigquery:"repo_name"` + RepoUrl string `bigquery:"repo_url"` + Payload string `bigquery:"payload"` + CreatedAt time.Time `bigquery:"created_at"` +} + type Contribution struct { Id int UserId int diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index bbae42d..e73b75f 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/app/github" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) @@ -19,6 +20,7 @@ type Service interface { GetRepoByGithubId(ctx context.Context, githubRepoId int) (Repository, error) GetRepoByRepoId(ctx context.Context, repoId int) (Repository, error) CreateRepository(ctx context.Context, repoGithubId int, ContributionRepoDetailsUrl string) (Repository, error) + HandleRepositoryCreation(ctx context.Context, contribution ContributionResponse) (Repository, error) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) @@ -77,6 +79,24 @@ func (s *service) CreateRepository(ctx context.Context, repoGithubId int, Contri return Repository(repositoryCreated), nil } +func (s *service) HandleRepositoryCreation(ctx context.Context, contribution ContributionResponse) (Repository, error) { + obtainedRepository, err := s.GetRepoByGithubId(ctx, contribution.RepoID) + if err != nil { + if err == apperrors.ErrRepoNotFound { + obtainedRepository, err = s.CreateRepository(ctx, contribution.RepoID, contribution.RepoUrl) + if err != nil { + slog.Error("error creating repository", "error", err) + return Repository{}, err + } + } else { + slog.Error("error fetching repo by repo id", "error", err) + return Repository{}, err + } + } + + return obtainedRepository, nil +} + func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) { usersContributedRepos, err := s.repositoryRepository.FetchUsersContributedRepos(ctx, nil) if err != nil { @@ -89,7 +109,7 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C for i, usersContributedRepo := range usersContributedRepos { fetchUsersContributedReposResponse[i].Repository = Repository(usersContributedRepo) - contributedRepoLanguages, err := s.githubService.FetchRepositoryLanguages(ctx, client, usersContributedRepo.LanguagesUrl) + contributedRepoLanguages, err := s.githubService.FetchRepositoryLanguages(ctx, usersContributedRepo.LanguagesUrl) if err != nil { slog.Error("error fetching languages for repository", "error", err) return nil, err diff --git a/internal/app/transaction/domain.go b/internal/app/transaction/domain.go index 54a8c95..b8989b6 100644 --- a/internal/app/transaction/domain.go +++ b/internal/app/transaction/domain.go @@ -13,3 +13,16 @@ type Transaction struct { CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + GithubEventId string + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/app/transaction/service.go b/internal/app/transaction/service.go index 3606cce..59b4f17 100644 --- a/internal/app/transaction/service.go +++ b/internal/app/transaction/service.go @@ -5,6 +5,7 @@ import ( "log/slog" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) @@ -16,6 +17,9 @@ type service struct { type Service interface { CreateTransaction(ctx context.Context, transactionInfo Transaction) (Transaction, error) + GetTransactionByContributionId(ctx context.Context, contributionId int) (Transaction, error) + CreateTransactionForContribution(ctx context.Context, contribution Contribution) (Transaction, error) + HandleTransactionCreation(ctx context.Context, contribution Contribution) (Transaction, error) } func NewService(transactionRepository repository.TransactionRepository, userService user.Service) Service { @@ -55,3 +59,51 @@ func (s *service) CreateTransaction(ctx context.Context, transactionInfo Transac return Transaction(transaction), nil } + +func (s *service) GetTransactionByContributionId(ctx context.Context, contributionId int) (Transaction, error) { + transaction, err := s.transactionRepository.GetTransactionByContributionId(ctx, nil, contributionId) + if err != nil { + slog.Error("error fetching transaction using contribution id", "error", err) + return Transaction{}, err + } + + return Transaction(transaction), nil +} + +func (s *service) CreateTransactionForContribution(ctx context.Context, contribution Contribution) (Transaction, error) { + transactionInfo := Transaction{ + UserId: contribution.UserId, + ContributionId: contribution.Id, + IsRedeemed: false, + IsGained: true, + TransactedBalance: contribution.BalanceChange, + TransactedAt: contribution.ContributedAt, + } + transaction, err := s.CreateTransaction(ctx, transactionInfo) + if err != nil { + slog.Error("error creating transaction for current contribution", "error", err) + return Transaction{}, err + } + + return transaction, nil +} + +func (s *service) HandleTransactionCreation(ctx context.Context, contribution Contribution) (Transaction, error) { + var transaction Transaction + + transaction, err := s.GetTransactionByContributionId(ctx, contribution.Id) + if err != nil { + if err == apperrors.ErrTransactionNotFound { + transaction, err = s.CreateTransactionForContribution(ctx, contribution) + if err != nil { + slog.Error("error creating transaction for exisiting contribution", "error", err) + return Transaction{}, err + } + } else { + slog.Error("error fetching transaction", "error", err) + return Transaction{}, err + } + } + + return transaction, nil +} diff --git a/internal/repository/transaction.go b/internal/repository/transaction.go index e98a600..b154565 100644 --- a/internal/repository/transaction.go +++ b/internal/repository/transaction.go @@ -2,6 +2,8 @@ package repository import ( "context" + "database/sql" + "errors" "log/slog" "github.com/jmoiron/sqlx" @@ -15,6 +17,7 @@ type transactionRepository struct { type TransactionRepository interface { RepositoryTransaction CreateTransaction(ctx context.Context, tx *sqlx.Tx, transactionInfo Transaction) (Transaction, error) + GetTransactionByContributionId(ctx context.Context, tx *sqlx.Tx, contributionId int) (Transaction, error) } func NewTransactionRepository(db *sqlx.DB) TransactionRepository { @@ -34,6 +37,8 @@ const ( ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *` + + getTransactionByContributionIdQuery = `SELECT * from transactions where contribution_id=$1` ) func (tr *transactionRepository) CreateTransaction(ctx context.Context, tx *sqlx.Tx, transactionInfo Transaction) (Transaction, error) { @@ -55,3 +60,20 @@ func (tr *transactionRepository) CreateTransaction(ctx context.Context, tx *sqlx return transaction, nil } + +func (tr *transactionRepository) GetTransactionByContributionId(ctx context.Context, tx *sqlx.Tx, contributionId int) (Transaction, error) { + executer := tr.BaseRepository.initiateQueryExecuter(tx) + + var transaction Transaction + err := executer.GetContext(ctx, &transaction, getTransactionByContributionIdQuery, contributionId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("transaction for the contribution id does not exist", "error", err) + return Transaction{}, apperrors.ErrTransactionNotFound + } + slog.Error("error fetching transaction using contributionid", "error", err) + return Transaction{}, err + } + + return transaction, nil +} From 65519857deb16d9a8ef0ddb3fa63a8b21534e1ec Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 4 Jul 2025 18:26:22 +0530 Subject: [PATCH 52/68] fix incorrect merge conflict: missing fetchusecontribution router --- internal/app/router.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/app/router.go b/internal/app/router.go index bb97fd7..612efd1 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -20,6 +20,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) From 6c1f24c32a74ca58b09c349df28764cfdfb4c578 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 4 Jul 2025 18:27:28 +0530 Subject: [PATCH 53/68] use client from dependencies instead of new client in github handler --- internal/app/dependencies.go | 2 +- internal/app/github/service.go | 8 ++++---- internal/app/repository/handler.go | 11 ++++------- internal/pkg/apperrors/errors.go | 1 + 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 9eda099..86d4c3d 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -42,7 +42,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) - repositoryHandler := repoService.NewHandler(repositoryService) + repositoryHandler := repoService.NewHandler(repositoryService, githubService) contributionHandler := contribution.NewHandler(contributionService) return Dependencies{ diff --git a/internal/app/github/service.go b/internal/app/github/service.go index 8b81912..16637c0 100644 --- a/internal/app/github/service.go +++ b/internal/app/github/service.go @@ -18,8 +18,8 @@ type service struct { type Service interface { configureGithubApiHeaders() map[string]string FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) - FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) - FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) + FetchRepositoryLanguages(ctx context.Context, getRepoLanguagesURL string) (RepoLanguages, error) + FetchRepositoryContributors(ctx context.Context, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) } func NewService(appCfg config.AppConfig, httpClient *http.Client) Service { @@ -54,7 +54,7 @@ func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetails return repoDetails, nil } -func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) { +func (s *service) FetchRepositoryLanguages(ctx context.Context, getRepoLanguagesURL string) (RepoLanguages, error) { headers := s.configureGithubApiHeaders() body, err := utils.DoGet(s.httpClient, getRepoLanguagesURL, headers) @@ -73,7 +73,7 @@ func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Cli return repoLanguages, nil } -func (s *service) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { +func (s *service) FetchRepositoryContributors(ctx context.Context, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { headers := s.configureGithubApiHeaders() body, err := utils.DoGet(s.httpClient, getRepoContributorsURl, headers) diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index 5edbb29..f040a31 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -22,9 +22,10 @@ type Handler interface { FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Request) } -func NewHandler(repositoryService Service) Handler { +func NewHandler(repositoryService Service, githubService github.Service) Handler { return &handler{ repositoryService: repositoryService, + githubService: githubService, } } @@ -69,8 +70,6 @@ func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Requ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - client := &http.Client{} - repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { @@ -88,7 +87,7 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http return } - repoContributors, err := h.githubService.FetchRepositoryContributors(ctx, client, repoDetails.ContributorsUrl) + repoContributors, err := h.githubService.FetchRepositoryContributors(ctx, repoDetails.ContributorsUrl) if err != nil { slog.Error("error fetching repo contributors", "error", err) status, errorMessage := apperrors.MapError(err) @@ -125,8 +124,6 @@ func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Re func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - client := &http.Client{} - repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { @@ -144,7 +141,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ return } - repoLanguages, err := h.githubService.FetchRepositoryLanguages(ctx, client, repoDetails.LanguagesUrl) + repoLanguages, err := h.githubService.FetchRepositoryLanguages(ctx, repoDetails.LanguagesUrl) if err != nil { slog.Error("error fetching particular repo languages", "error", err) status, errorMessage := apperrors.MapError(err) diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 4d7fe27..31c9e49 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -47,6 +47,7 @@ var ( ErrContributionNotFound = errors.New("contribution not found") ErrTransactionCreationFailed = errors.New("error failed to create transaction") + ErrTransactionNotFound = errors.New("error transaction for the contribution id does not exist") ) func MapError(err error) (statusCode int, errMessage string) { From d01765b7454b44e0331e0a30b71f4f1dec210f23 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 7 Jul 2025 13:34:59 +0530 Subject: [PATCH 54/68] implement leaderboard feature --- internal/app/router.go | 4 +++ internal/app/user/domain.go | 8 +++++ internal/app/user/handler.go | 30 +++++++++++++++++ internal/app/user/service.go | 27 +++++++++++++++ internal/repository/domain.go | 8 +++++ internal/repository/user.go | 62 +++++++++++++++++++++++++++++++++++ 6 files changed, 139 insertions(+) diff --git a/internal/app/router.go b/internal/app/router.go index 612efd1..02f9c7e 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -26,5 +26,9 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchUserContributionsInRepo, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/languages/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchLanguagePercentInRepo, deps.AppCfg)) + + router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.GetAllUsersRank, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg)) + return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/app/user/domain.go b/internal/app/user/domain.go index 471f0b1..cf0e527 100644 --- a/internal/app/user/domain.go +++ b/internal/app/user/domain.go @@ -45,3 +45,11 @@ type Transaction struct { CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + +type LeaderboardUser struct { + Id int `db:"id"` + GithubUsername string `db:"github_username"` + AvatarUrl string `db:"avatar_url"` + CurrentBalance int `db:"current_balance"` + Rank int `db:"rank"` +} diff --git a/internal/app/user/handler.go b/internal/app/user/handler.go index 00bcd51..3f085b8 100644 --- a/internal/app/user/handler.go +++ b/internal/app/user/handler.go @@ -15,6 +15,8 @@ type handler struct { type Handler interface { UpdateUserEmail(w http.ResponseWriter, r *http.Request) + GetAllUsersRank(w http.ResponseWriter, r *http.Request) + GetCurrentUserRank(w http.ResponseWriter, r *http.Request) } func NewHandler(userService Service) Handler { @@ -44,3 +46,31 @@ func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "email updated successfully", nil) } + +func (h *handler) GetAllUsersRank(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + leaderboard, err := h.userService.GetAllUsersRank(ctx) + if err != nil { + slog.Error("failed to get all users rank", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "leaderboard fetched successfully", leaderboard) +} + +func (h *handler) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + currentUserRank, err := h.userService.GetCurrentUserRank(ctx) + if err != nil { + slog.Error("failed to get current user rank", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "current user rank fetched successfully", currentUserRank) +} diff --git a/internal/app/user/service.go b/internal/app/user/service.go index 6dad743..0aed35e 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -19,6 +19,8 @@ type Service interface { CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, email string) error UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error + GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) + GetCurrentUserRank(ctx context.Context) (LeaderboardUser, error) } func NewService(userRepository repository.UserRepository) Service { @@ -98,3 +100,28 @@ func (s *service) UpdateUserCurrentBalance(ctx context.Context, transaction Tran return nil } + +func (s *service) GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) { + leaderboard, err := s.userRepository.GetAllUsersRank(ctx, nil) + if err != nil { + slog.Error("error obtaining all users rank", "error", err) + return nil, err + } + + serviceLeaderboard := make([]LeaderboardUser, len(leaderboard)) + for i, l := range leaderboard { + serviceLeaderboard[i] = LeaderboardUser((l)) + } + + return serviceLeaderboard, nil +} + +func (s *service) GetCurrentUserRank(ctx context.Context) (LeaderboardUser, error) { + currentUserRank, err := s.userRepository.GetCurrentUserRank(ctx, nil) + if err != nil { + slog.Error("error obtaining current user rank", "error", err) + return LeaderboardUser{}, err + } + + return LeaderboardUser(currentUserRank), nil +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 1a94621..d52494b 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -77,3 +77,11 @@ type Transaction struct { CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + +type LeaderboardUser struct { + Id int `db:"id"` + GithubUsername string `db:"github_username"` + AvatarUrl string `db:"avatar_url"` + CurrentBalance int `db:"current_balance"` + Rank int `db:"rank"` +} diff --git a/internal/repository/user.go b/internal/repository/user.go index a3a748a..82f660a 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -9,6 +9,7 @@ import ( "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" ) type userRepository struct { @@ -23,6 +24,8 @@ type UserRepository interface { UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error + GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) + GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx) (LeaderboardUser, error) } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -51,6 +54,31 @@ const ( getAllUsersGithubIdQuery = "SELECT github_id from users" updateUserCurrentBalanceQuery = "UPDATE users SET current_balance=$1, updated_at=$2 where id=$3" + + getAllUsersRankQuery = ` + SELECT + id, + github_username, + avatar_url, + current_balance, + RANK() over (ORDER BY current_balance DESC) AS rank + FROM users + ORDER BY current_balance DESC` + + getCurrentUserRankQuery = ` + SELECT * + FROM + ( + SELECT + id, + github_username, + avatar_url, + current_balance, + RANK() OVER (ORDER BY current_balance DESC) AS rank + FROM users + ) + ranked_users + WHERE id = $1;` ) func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { @@ -142,3 +170,37 @@ func (ur *userRepository) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx return nil } + +func (ur *userRepository) GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + var leaderboard []LeaderboardUser + err := executer.SelectContext(ctx, &leaderboard, getAllUsersRankQuery) + if err != nil { + slog.Error("failed to get users rank", "error", err) + return nil, apperrors.ErrInternalServer + } + + return leaderboard, nil +} + +func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx) (LeaderboardUser, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return LeaderboardUser{}, apperrors.ErrInternalServer + } + + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + var currentUserRank LeaderboardUser + err := executer.GetContext(ctx, ¤tUserRank, getCurrentUserRankQuery, userId) + if err != nil { + slog.Error("failed to get user rank", "error", err) + return LeaderboardUser{}, apperrors.ErrInternalServer + } + + return currentUserRank, nil +} From 91b8c648e14b323bcbeca82a9358300cbdf5f97d Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 8 Jul 2025 15:08:33 +0530 Subject: [PATCH 55/68] implement monthly overview feature --- internal/app/contribution/domain.go | 7 ++++ internal/app/contribution/handler.go | 17 ++++++++ internal/app/contribution/service.go | 35 ++++++++++++++++ internal/app/router.go | 1 + internal/pkg/apperrors/errors.go | 18 +++++---- internal/repository/contribution.go | 60 +++++++++++++++++++++++++++- internal/repository/domain.go | 7 ++++ 7 files changed, 135 insertions(+), 10 deletions(-) diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index 3c90423..9316048 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -61,3 +61,10 @@ type Transaction struct { CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + +type ContributionTypeSummary struct { + ContributionType string `db:"contribution_type"` + ContributionCount int `db:"contribution_count"` + TotalCoins int `db:"total_coins"` + Month time.Time `db:"month"` +} diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 2c2045d..85c5e7d 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -14,6 +14,7 @@ type handler struct { type Handler interface { FetchUserContributions(w http.ResponseWriter, r *http.Request) + GetContributionTypeSummaryForMonth(w http.ResponseWriter, r *http.Request) } func NewHandler(contributionService Service) Handler { @@ -35,3 +36,19 @@ func (h *handler) FetchUserContributions(w http.ResponseWriter, r *http.Request) response.WriteJson(w, http.StatusOK, "user contributions fetched successfully", userContributions) } + +func (h *handler) GetContributionTypeSummaryForMonth(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + month := r.URL.Query().Get("month") + + contributionTypeSummaryForMonth, err := h.contributionService.GetContributionTypeSummaryForMonth(ctx, month) + if err != nil { + slog.Error("error fetching contribution type summary for month") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "contribution type overview for month fetched successfully", contributionTypeSummaryForMonth) +} diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index f927207..88d4d77 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -3,8 +3,10 @@ package contribution import ( "context" "encoding/json" + "errors" "log/slog" "net/http" + "time" "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" @@ -66,6 +68,7 @@ type Service interface { GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) + GetContributionTypeSummaryForMonth(ctx context.Context, monthParam string) ([]ContributionTypeSummary, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service { @@ -287,3 +290,35 @@ func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEven return Contribution(contribution), nil } + +func (s *service) GetContributionTypeSummaryForMonth(ctx context.Context, monthParam string) ([]ContributionTypeSummary, error) { + month, err := time.Parse("2006-01", monthParam) + if err != nil { + slog.Error("error parsing month query parameter", "error", err) + return nil, err + } + + contributionTypes, err := s.contributionRepository.GetAllContributionTypes(ctx, nil) + if err != nil { + slog.Error("error fetching contribution types", "error", err) + return nil, err + } + + var contributionTypeSummaryForMonth []ContributionTypeSummary + + for _, contributionType := range contributionTypes { + contributionTypeSummary, err := s.contributionRepository.GetContributionTypeSummaryForMonth(ctx, nil, contributionType.ContributionType, month) + if err != nil { + if errors.Is(err, apperrors.ErrNoContributionForContributionType) { + contributionTypeSummaryForMonth = append(contributionTypeSummaryForMonth, ContributionTypeSummary{ContributionType: contributionType.ContributionType}) + continue + } + slog.Error("error fetching contribution type summary", "error", err) + return nil, err + } + + contributionTypeSummaryForMonth = append(contributionTypeSummaryForMonth, ContributionTypeSummary(contributionTypeSummary)) + } + + return contributionTypeSummaryForMonth, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 02f9c7e..c61e717 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -21,6 +21,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/monthlyoverview", middleware.Authentication(deps.ContributionHandler.GetContributionTypeSummaryForMonth, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 31c9e49..930ce96 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -37,14 +37,16 @@ var ( ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") ErrFetchingUserContributionsInRepo = errors.New("error fetching users contribution in repository") - ErrFetchingFromBigquery = errors.New("error fetching contributions from bigquery service") - ErrNextContribution = errors.New("error while loading next bigquery contribution") - ErrContributionCreationFailed = errors.New("failed to create contrbitution") - ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") - ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") - ErrContributionScoreNotFound = errors.New("failed to get contributionscore details for given contribution type") - ErrFetchingContribution = errors.New("error fetching contribution by github repo id") - ErrContributionNotFound = errors.New("contribution not found") + ErrFetchingFromBigquery = errors.New("error fetching contributions from bigquery service") + ErrNextContribution = errors.New("error while loading next bigquery contribution") + ErrContributionCreationFailed = errors.New("failed to create contrbitution") + ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") + ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") + ErrContributionScoreNotFound = errors.New("failed to get contributionscore details for given contribution type") + ErrFetchingContribution = errors.New("error fetching contribution by github repo id") + ErrContributionNotFound = errors.New("contribution not found") + ErrFetchingContributionTypes = errors.New("failed to fetch all contribution types") + ErrNoContributionForContributionType = errors.New("contribution for contribution type does not exist") ErrTransactionCreationFailed = errors.New("error failed to create transaction") ErrTransactionNotFound = errors.New("error transaction for the contribution id does not exist") diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index eb1d1a5..697028b 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "log/slog" + "time" "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" @@ -21,6 +22,8 @@ type ContributionRepository interface { GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) + GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) + GetContributionTypeSummaryForMonth(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (ContributionTypeSummary, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -47,7 +50,23 @@ const ( fetchUserContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc` - GetContributionByGithubEventIdQuery = `SELECT * from contributions where github_event_id=$1` + getContributionByGithubEventIdQuery = `SELECT * from contributions where github_event_id=$1` + + getAllContributionTypesQuery = `SELECT * from contribution_score` + + GetContributionTypeSummaryForMonthQuery = ` + SELECT + DATE_TRUNC('month', contributed_at) AS month, + contribution_type, + COUNT(*) AS contribution_count, + SUM(balance_change) AS total_coins + FROM contributions + WHERE user_id = $1 + AND contribution_type = $2 + AND contributed_at >= DATE_TRUNC('month', $3::timestamptz) + AND contributed_at < DATE_TRUNC('month', $3::timestamptz) + INTERVAL '1 month' + GROUP BY + month, contribution_type;` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -114,7 +133,7 @@ func (cr *contributionRepository) GetContributionByGithubEventId(ctx context.Con executer := cr.BaseRepository.initiateQueryExecuter(tx) var contribution Contribution - err := executer.GetContext(ctx, &contribution, GetContributionByGithubEventIdQuery, githubEventId) + err := executer.GetContext(ctx, &contribution, getContributionByGithubEventIdQuery, githubEventId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("contribution not found", "error", err) @@ -127,3 +146,40 @@ func (cr *contributionRepository) GetContributionByGithubEventId(ctx context.Con return contribution, nil } + +func (cr *contributionRepository) GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contributionTypes []ContributionScore + err := executer.SelectContext(ctx, &contributionTypes, getAllContributionTypesQuery) + if err != nil { + slog.Error("error fetching all contribution types", "error", err) + return nil, apperrors.ErrFetchingContributionTypes + } + + return contributionTypes, nil +} + +func (cr *contributionRepository) GetContributionTypeSummaryForMonth(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (ContributionTypeSummary, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return ContributionTypeSummary{}, apperrors.ErrInternalServer + } + + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contributionTypeSummary ContributionTypeSummary + err := executer.GetContext(ctx, &contributionTypeSummary, GetContributionTypeSummaryForMonthQuery, userId, contributionType, month) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return ContributionTypeSummary{}, apperrors.ErrNoContributionForContributionType + } + slog.Error("error fetching contribution summary for contribution type", "error", err) + return ContributionTypeSummary{}, apperrors.ErrInternalServer + } + + return contributionTypeSummary, nil +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index d52494b..9d75663 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -85,3 +85,10 @@ type LeaderboardUser struct { CurrentBalance int `db:"current_balance"` Rank int `db:"rank"` } + +type ContributionTypeSummary struct { + ContributionType string `db:"contribution_type"` + ContributionCount int `db:"contribution_count"` + TotalCoins int `db:"total_coins"` + Month time.Time `db:"month"` +} From 6ee49c93e5ad06f3ff69f6b6c965e1b4a230be47 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 14 Jul 2025 12:44:57 +0530 Subject: [PATCH 56/68] rename GetUserRanks to ListUserRank and get value from context in handler itself --- internal/app/router.go | 2 +- internal/app/user/handler.go | 17 ++++++++++++++--- internal/app/user/service.go | 16 ++++++++-------- internal/pkg/apperrors/errors.go | 3 ++- internal/repository/user.go | 12 ++---------- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/internal/app/router.go b/internal/app/router.go index 02f9c7e..f7047ab 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -27,7 +27,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchUserContributionsInRepo, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/languages/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchLanguagePercentInRepo, deps.AppCfg)) - router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.GetAllUsersRank, deps.AppCfg)) + router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.ListUserRanks, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) diff --git a/internal/app/user/handler.go b/internal/app/user/handler.go index 3f085b8..e1000ad 100644 --- a/internal/app/user/handler.go +++ b/internal/app/user/handler.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) @@ -15,7 +16,7 @@ type handler struct { type Handler interface { UpdateUserEmail(w http.ResponseWriter, r *http.Request) - GetAllUsersRank(w http.ResponseWriter, r *http.Request) + ListUserRanks(w http.ResponseWriter, r *http.Request) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) } @@ -47,7 +48,7 @@ func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "email updated successfully", nil) } -func (h *handler) GetAllUsersRank(w http.ResponseWriter, r *http.Request) { +func (h *handler) ListUserRanks(w http.ResponseWriter, r *http.Request) { ctx := r.Context() leaderboard, err := h.userService.GetAllUsersRank(ctx) @@ -64,7 +65,17 @@ func (h *handler) GetAllUsersRank(w http.ResponseWriter, r *http.Request) { func (h *handler) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - currentUserRank, err := h.userService.GetCurrentUserRank(ctx) + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + currentUserRank, err := h.userService.GetCurrentUserRank(ctx, userId) if err != nil { slog.Error("failed to get current user rank", "error", err) status, errorMessage := apperrors.MapError(err) diff --git a/internal/app/user/service.go b/internal/app/user/service.go index 0aed35e..24a0c2d 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -20,7 +20,7 @@ type Service interface { UpdateUserEmail(ctx context.Context, email string) error UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) - GetCurrentUserRank(ctx context.Context) (LeaderboardUser, error) + GetCurrentUserRank(ctx context.Context, userId int) (LeaderboardUser, error) } func NewService(userRepository repository.UserRepository) Service { @@ -102,22 +102,22 @@ func (s *service) UpdateUserCurrentBalance(ctx context.Context, transaction Tran } func (s *service) GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) { - leaderboard, err := s.userRepository.GetAllUsersRank(ctx, nil) + userRanks, err := s.userRepository.GetAllUsersRank(ctx, nil) if err != nil { slog.Error("error obtaining all users rank", "error", err) return nil, err } - serviceLeaderboard := make([]LeaderboardUser, len(leaderboard)) - for i, l := range leaderboard { - serviceLeaderboard[i] = LeaderboardUser((l)) + Leaderboard := make([]LeaderboardUser, len(userRanks)) + for i, l := range userRanks { + Leaderboard[i] = LeaderboardUser(l) } - return serviceLeaderboard, nil + return Leaderboard, nil } -func (s *service) GetCurrentUserRank(ctx context.Context) (LeaderboardUser, error) { - currentUserRank, err := s.userRepository.GetCurrentUserRank(ctx, nil) +func (s *service) GetCurrentUserRank(ctx context.Context, userId int) (LeaderboardUser, error) { + currentUserRank, err := s.userRepository.GetCurrentUserRank(ctx, nil, userId) if err != nil { slog.Error("error obtaining current user rank", "error", err) return LeaderboardUser{}, err diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 31c9e49..1ebee57 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -6,6 +6,7 @@ import ( ) var ( + ErrContextValue = errors.New("error obtaining value from context") ErrInternalServer = errors.New("internal server error") ErrInvalidRequestBody = errors.New("invalid or missing parameters in the request body") @@ -52,7 +53,7 @@ var ( func MapError(err error) (statusCode int, errMessage string) { switch err { - case ErrInvalidRequestBody, ErrInvalidQueryParams: + case ErrInvalidRequestBody, ErrInvalidQueryParams, ErrContextValue: return http.StatusBadRequest, err.Error() case ErrUnauthorizedAccess: return http.StatusUnauthorized, err.Error() diff --git a/internal/repository/user.go b/internal/repository/user.go index 82f660a..0a80cbe 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -9,7 +9,6 @@ import ( "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" ) type userRepository struct { @@ -25,7 +24,7 @@ type UserRepository interface { GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) - GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx) (LeaderboardUser, error) + GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -184,14 +183,7 @@ func (ur *userRepository) GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]L return leaderboard, nil } -func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx) (LeaderboardUser, error) { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return LeaderboardUser{}, apperrors.ErrInternalServer - } +func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) { executer := ur.BaseRepository.initiateQueryExecuter(tx) From ecc55c15e30b69ca6ea32d0aecdeb95565346474 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 14 Jul 2025 12:45:36 +0530 Subject: [PATCH 57/68] create index for users_current_balance --- .../1752476063_create-index-users-current-balance.down.sql | 1 + .../1752476063_create-index-users-current-balance.up.sql | 1 + 2 files changed, 2 insertions(+) create mode 100644 internal/db/migrations/1752476063_create-index-users-current-balance.down.sql create mode 100644 internal/db/migrations/1752476063_create-index-users-current-balance.up.sql diff --git a/internal/db/migrations/1752476063_create-index-users-current-balance.down.sql b/internal/db/migrations/1752476063_create-index-users-current-balance.down.sql new file mode 100644 index 0000000..a987013 --- /dev/null +++ b/internal/db/migrations/1752476063_create-index-users-current-balance.down.sql @@ -0,0 +1 @@ +drop index idx_users_current_balance \ No newline at end of file diff --git a/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql b/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql new file mode 100644 index 0000000..a934bf1 --- /dev/null +++ b/internal/db/migrations/1752476063_create-index-users-current-balance.up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_users_current_balance ON users(current_balance DESC); \ No newline at end of file From 0d0fe6b5534a11864a1882a9fe410240a7d7f385 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 14 Jul 2025 13:09:59 +0530 Subject: [PATCH 58/68] refactor : update namings --- internal/app/contribution/domain.go | 28 ++++++++++++++-------------- internal/app/contribution/handler.go | 6 +++--- internal/app/contribution/service.go | 14 +++++++------- internal/app/router.go | 2 +- internal/repository/contribution.go | 16 ++++++++-------- internal/repository/domain.go | 10 +++++----- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index 9316048..238f5dc 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -51,20 +51,20 @@ type ContributionScore struct { } type Transaction struct { - Id int `db:"id"` - UserId int `db:"user_id"` - ContributionId int `db:"contribution_id"` - IsRedeemed bool `db:"is_redeemed"` - IsGained bool `db:"is_gained"` - TransactedBalance int `db:"transacted_balance"` - TransactedAt time.Time `db:"transacted_at"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + Id int + UserId int + ContributionId int + IsRedeemed bool + IsGained bool + TransactedBalance int + TransactedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time } -type ContributionTypeSummary struct { - ContributionType string `db:"contribution_type"` - ContributionCount int `db:"contribution_count"` - TotalCoins int `db:"total_coins"` - Month time.Time `db:"month"` +type MonthlyContributionSummary struct { + Type string + Count int + TotalCoins int + Month time.Time } diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 85c5e7d..34c08d3 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -14,7 +14,7 @@ type handler struct { type Handler interface { FetchUserContributions(w http.ResponseWriter, r *http.Request) - GetContributionTypeSummaryForMonth(w http.ResponseWriter, r *http.Request) + ListMonthlyContributionSummary(w http.ResponseWriter, r *http.Request) } func NewHandler(contributionService Service) Handler { @@ -37,12 +37,12 @@ func (h *handler) FetchUserContributions(w http.ResponseWriter, r *http.Request) response.WriteJson(w, http.StatusOK, "user contributions fetched successfully", userContributions) } -func (h *handler) GetContributionTypeSummaryForMonth(w http.ResponseWriter, r *http.Request) { +func (h *handler) ListMonthlyContributionSummary(w http.ResponseWriter, r *http.Request) { ctx := r.Context() month := r.URL.Query().Get("month") - contributionTypeSummaryForMonth, err := h.contributionService.GetContributionTypeSummaryForMonth(ctx, month) + contributionTypeSummaryForMonth, err := h.contributionService.GetMonthlyContributionSummary(ctx, month) if err != nil { slog.Error("error fetching contribution type summary for month") status, errorMessage := apperrors.MapError(err) diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 88d4d77..082ccbe 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -68,7 +68,7 @@ type Service interface { GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) - GetContributionTypeSummaryForMonth(ctx context.Context, monthParam string) ([]ContributionTypeSummary, error) + GetMonthlyContributionSummary(ctx context.Context, monthParam string) ([]MonthlyContributionSummary, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service { @@ -291,7 +291,7 @@ func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEven return Contribution(contribution), nil } -func (s *service) GetContributionTypeSummaryForMonth(ctx context.Context, monthParam string) ([]ContributionTypeSummary, error) { +func (s *service) GetMonthlyContributionSummary(ctx context.Context, monthParam string) ([]MonthlyContributionSummary, error) { month, err := time.Parse("2006-01", monthParam) if err != nil { slog.Error("error parsing month query parameter", "error", err) @@ -304,21 +304,21 @@ func (s *service) GetContributionTypeSummaryForMonth(ctx context.Context, monthP return nil, err } - var contributionTypeSummaryForMonth []ContributionTypeSummary + var monthlyContributionSummary []MonthlyContributionSummary for _, contributionType := range contributionTypes { - contributionTypeSummary, err := s.contributionRepository.GetContributionTypeSummaryForMonth(ctx, nil, contributionType.ContributionType, month) + contributionTypeSummary, err := s.contributionRepository.GetMonthlyContributionTypeSummary(ctx, nil, contributionType.ContributionType, month) if err != nil { if errors.Is(err, apperrors.ErrNoContributionForContributionType) { - contributionTypeSummaryForMonth = append(contributionTypeSummaryForMonth, ContributionTypeSummary{ContributionType: contributionType.ContributionType}) + monthlyContributionSummary = append(monthlyContributionSummary, MonthlyContributionSummary{Type: contributionType.ContributionType}) continue } slog.Error("error fetching contribution type summary", "error", err) return nil, err } - contributionTypeSummaryForMonth = append(contributionTypeSummaryForMonth, ContributionTypeSummary(contributionTypeSummary)) + monthlyContributionSummary = append(monthlyContributionSummary, MonthlyContributionSummary(contributionTypeSummary)) } - return contributionTypeSummaryForMonth, nil + return monthlyContributionSummary, nil } diff --git a/internal/app/router.go b/internal/app/router.go index c61e717..6ace31f 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -21,7 +21,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/monthlyoverview", middleware.Authentication(deps.ContributionHandler.GetContributionTypeSummaryForMonth, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/monthlyoverview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 697028b..79c9f54 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -23,7 +23,7 @@ type ContributionRepository interface { FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) - GetContributionTypeSummaryForMonth(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (ContributionTypeSummary, error) + GetMonthlyContributionTypeSummary(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (MonthlyContributionSummary, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -54,7 +54,7 @@ const ( getAllContributionTypesQuery = `SELECT * from contribution_score` - GetContributionTypeSummaryForMonthQuery = ` + GetMonthlyContributionTypeSummaryQuery = ` SELECT DATE_TRUNC('month', contributed_at) AS month, contribution_type, @@ -160,25 +160,25 @@ func (cr *contributionRepository) GetAllContributionTypes(ctx context.Context, t return contributionTypes, nil } -func (cr *contributionRepository) GetContributionTypeSummaryForMonth(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (ContributionTypeSummary, error) { +func (cr *contributionRepository) GetMonthlyContributionTypeSummary(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (MonthlyContributionSummary, error) { userIdValue := ctx.Value(middleware.UserIdKey) userId, ok := userIdValue.(int) if !ok { slog.Error("error obtaining user id from context") - return ContributionTypeSummary{}, apperrors.ErrInternalServer + return MonthlyContributionSummary{}, apperrors.ErrInternalServer } executer := cr.BaseRepository.initiateQueryExecuter(tx) - var contributionTypeSummary ContributionTypeSummary - err := executer.GetContext(ctx, &contributionTypeSummary, GetContributionTypeSummaryForMonthQuery, userId, contributionType, month) + var contributionTypeSummary MonthlyContributionSummary + err := executer.GetContext(ctx, &contributionTypeSummary, GetMonthlyContributionTypeSummaryQuery, userId, contributionType, month) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return ContributionTypeSummary{}, apperrors.ErrNoContributionForContributionType + return MonthlyContributionSummary{}, apperrors.ErrNoContributionForContributionType } slog.Error("error fetching contribution summary for contribution type", "error", err) - return ContributionTypeSummary{}, apperrors.ErrInternalServer + return MonthlyContributionSummary{}, apperrors.ErrInternalServer } return contributionTypeSummary, nil diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 9d75663..4ac6312 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -86,9 +86,9 @@ type LeaderboardUser struct { Rank int `db:"rank"` } -type ContributionTypeSummary struct { - ContributionType string `db:"contribution_type"` - ContributionCount int `db:"contribution_count"` - TotalCoins int `db:"total_coins"` - Month time.Time `db:"month"` +type MonthlyContributionSummary struct { + Type string `db:"contribution_type"` + Count int `db:"contribution_count"` + TotalCoins int `db:"total_coins"` + Month time.Time `db:"month"` } From 63706a87086e8852e9df44d49eec4723faeb02e3 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 14 Jul 2025 16:53:06 +0530 Subject: [PATCH 59/68] refactor ListMonthlyContributionSummary function --- internal/app/contribution/handler.go | 37 ++++++++++++++++++++++++---- internal/app/contribution/service.go | 33 ++++++------------------- internal/app/router.go | 2 +- internal/pkg/utils/helper.go | 34 +++++++++++++++++++++++++ internal/repository/contribution.go | 32 +++++++----------------- 5 files changed, 84 insertions(+), 54 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 34c08d3..8d604ad 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -5,7 +5,9 @@ import ( "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/utils" ) type handler struct { @@ -28,7 +30,7 @@ func (h *handler) FetchUserContributions(w http.ResponseWriter, r *http.Request) userContributions, err := h.contributionService.FetchUserContributions(ctx) if err != nil { - slog.Error("error fetching user contributions") + slog.Error("error fetching user contributions", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -40,15 +42,40 @@ func (h *handler) FetchUserContributions(w http.ResponseWriter, r *http.Request) func (h *handler) ListMonthlyContributionSummary(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - month := r.URL.Query().Get("month") + userIdValue := ctx.Value(middleware.UserIdKey) + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + yearVal := r.URL.Query().Get("year") + year, err := utils.ValidateYearQueryParam(yearVal) + if err != nil { + slog.Error("error converting year value to integer", "error", err) + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + monthVal := r.URL.Query().Get("month") + month, err := utils.ValidateMonthQueryParam(monthVal) + if err != nil { + slog.Error("error converting month value to integer", "error", err) + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } - contributionTypeSummaryForMonth, err := h.contributionService.GetMonthlyContributionSummary(ctx, month) + monthlyContributionSummary, err := h.contributionService.ListMonthlyContributionSummary(ctx, year, month, userId) if err != nil { - slog.Error("error fetching contribution type summary for month") + slog.Error("error fetching contribution type summary for month", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return } - response.WriteJson(w, http.StatusOK, "contribution type overview for month fetched successfully", contributionTypeSummaryForMonth) + response.WriteJson(w, http.StatusOK, "contribution type overview for month fetched successfully", monthlyContributionSummary) } diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 082ccbe..610918a 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -3,10 +3,8 @@ package contribution import ( "context" "encoding/json" - "errors" "log/slog" "net/http" - "time" "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" @@ -68,7 +66,7 @@ type Service interface { GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUserContributions(ctx context.Context) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, githubEventId string) (Contribution, error) - GetMonthlyContributionSummary(ctx context.Context, monthParam string) ([]MonthlyContributionSummary, error) + ListMonthlyContributionSummary(ctx context.Context, year int, monthParam int, userId int) ([]MonthlyContributionSummary, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, transactionService transaction.Service, httpClient *http.Client) Service { @@ -291,34 +289,19 @@ func (s *service) GetContributionByGithubEventId(ctx context.Context, githubEven return Contribution(contribution), nil } -func (s *service) GetMonthlyContributionSummary(ctx context.Context, monthParam string) ([]MonthlyContributionSummary, error) { - month, err := time.Parse("2006-01", monthParam) - if err != nil { - slog.Error("error parsing month query parameter", "error", err) - return nil, err - } +func (s *service) ListMonthlyContributionSummary(ctx context.Context, year int, month int, userId int) ([]MonthlyContributionSummary, error) { - contributionTypes, err := s.contributionRepository.GetAllContributionTypes(ctx, nil) + MonthlyContributionSummaries, err := s.contributionRepository.ListMonthlyContributionSummary(ctx, nil, year, month, userId) if err != nil { - slog.Error("error fetching contribution types", "error", err) + slog.Error("error fetching monthly contribution summary", "error", err) return nil, err } - var monthlyContributionSummary []MonthlyContributionSummary - - for _, contributionType := range contributionTypes { - contributionTypeSummary, err := s.contributionRepository.GetMonthlyContributionTypeSummary(ctx, nil, contributionType.ContributionType, month) - if err != nil { - if errors.Is(err, apperrors.ErrNoContributionForContributionType) { - monthlyContributionSummary = append(monthlyContributionSummary, MonthlyContributionSummary{Type: contributionType.ContributionType}) - continue - } - slog.Error("error fetching contribution type summary", "error", err) - return nil, err - } + serviceMonthlyContributionSummaries := make([]MonthlyContributionSummary, len(MonthlyContributionSummaries)) - monthlyContributionSummary = append(monthlyContributionSummary, MonthlyContributionSummary(contributionTypeSummary)) + for i, c := range MonthlyContributionSummaries { + serviceMonthlyContributionSummaries[i] = MonthlyContributionSummary(c) } - return monthlyContributionSummary, nil + return serviceMonthlyContributionSummaries, nil } diff --git a/internal/app/router.go b/internal/app/router.go index 0791715..f2f9242 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -21,7 +21,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/monthlyoverview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/overview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) diff --git a/internal/pkg/utils/helper.go b/internal/pkg/utils/helper.go index 3fa0492..e324c8d 100644 --- a/internal/pkg/utils/helper.go +++ b/internal/pkg/utils/helper.go @@ -5,7 +5,11 @@ import ( "io" "log/slog" "net/http" + "strconv" "strings" + "time" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" ) func FormatIntSliceForQuery(ids []int) string { @@ -43,3 +47,33 @@ func DoGet(httpClient *http.Client, url string, headers map[string]string) ([]by return body, nil } + +func ValidateYearQueryParam(yearVal string) (int, error) { + year, err := strconv.Atoi(yearVal) + if err != nil { + slog.Error("error converting year string value to int") + return 0, err + } + + if year < 2025 || year > time.Now().Year() { + slog.Error("invalid year value") + return 0, apperrors.ErrInvalidQueryParams + } + + return year, nil +} + +func ValidateMonthQueryParam(monthVal string) (int, error) { + month, err := strconv.Atoi(monthVal) + if err != nil { + slog.Error("error converting month string value to int") + return 0, err + } + + if month < 0 || month > 12 { + slog.Error("invalid month value") + return 0, apperrors.ErrInvalidQueryParams + } + + return month, nil +} diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 79c9f54..da5163a 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -5,7 +5,6 @@ import ( "database/sql" "errors" "log/slog" - "time" "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" @@ -23,7 +22,7 @@ type ContributionRepository interface { FetchUserContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) - GetMonthlyContributionTypeSummary(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (MonthlyContributionSummary, error) + ListMonthlyContributionSummary(ctx context.Context, tx *sqlx.Tx, year int, month int, userId int) ([]MonthlyContributionSummary, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -54,17 +53,15 @@ const ( getAllContributionTypesQuery = `SELECT * from contribution_score` - GetMonthlyContributionTypeSummaryQuery = ` + getMonthlyContributionSummaryQuery = ` SELECT DATE_TRUNC('month', contributed_at) AS month, - contribution_type, + contribution_type, COUNT(*) AS contribution_count, SUM(balance_change) AS total_coins FROM contributions WHERE user_id = $1 - AND contribution_type = $2 - AND contributed_at >= DATE_TRUNC('month', $3::timestamptz) - AND contributed_at < DATE_TRUNC('month', $3::timestamptz) + INTERVAL '1 month' + AND DATE_TRUNC('month', contributed_at) = MAKE_DATE($2, $3, 1)::timestamptz GROUP BY month, contribution_type;` ) @@ -160,25 +157,14 @@ func (cr *contributionRepository) GetAllContributionTypes(ctx context.Context, t return contributionTypes, nil } -func (cr *contributionRepository) GetMonthlyContributionTypeSummary(ctx context.Context, tx *sqlx.Tx, contributionType string, month time.Time) (MonthlyContributionSummary, error) { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return MonthlyContributionSummary{}, apperrors.ErrInternalServer - } - +func (cr *contributionRepository) ListMonthlyContributionSummary(ctx context.Context, tx *sqlx.Tx, year int, month int, userId int) ([]MonthlyContributionSummary, error) { executer := cr.BaseRepository.initiateQueryExecuter(tx) - var contributionTypeSummary MonthlyContributionSummary - err := executer.GetContext(ctx, &contributionTypeSummary, GetMonthlyContributionTypeSummaryQuery, userId, contributionType, month) + var contributionTypeSummary []MonthlyContributionSummary + err := executer.SelectContext(ctx, &contributionTypeSummary, getMonthlyContributionSummaryQuery, userId, month) if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return MonthlyContributionSummary{}, apperrors.ErrNoContributionForContributionType - } - slog.Error("error fetching contribution summary for contribution type", "error", err) - return MonthlyContributionSummary{}, apperrors.ErrInternalServer + slog.Error("error fetching monthly contribution summary for user", "error", err) + return nil, apperrors.ErrInternalServer } return contributionTypeSummary, nil From 7fc91d090d8d87611fd1d49a4566dd9e00907502 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 14 Jul 2025 18:36:48 +0530 Subject: [PATCH 60/68] refactor soft delete user --- go.mod | 1 - go.sum | 2 -- internal/app/router.go | 2 +- internal/app/user/handler.go | 21 +++++++++++-------- internal/app/user/service.go | 10 ++++----- internal/repository/user.go | 39 +++++++++--------------------------- 6 files changed, 28 insertions(+), 47 deletions(-) diff --git a/go.mod b/go.mod index 5e33a45..e73f2e2 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,6 @@ require ( go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect - github.com/robfig/cron/v3 v3.0.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect diff --git a/go.sum b/go.sum index 4aadde7..a599248 100644 --- a/go.sum +++ b/go.sum @@ -133,8 +133,6 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgm github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= -github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= diff --git a/internal/app/router.go b/internal/app/router.go index cdf5fc4..76759a1 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -19,7 +19,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/auth/user", middleware.Authentication(deps.AuthHandler.GetLoggedInUser, deps.AppCfg)) router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) - router.HandleFunc("DELETE /api/user/delete", middleware.Authentication(deps.UserHandler.DeleteUser, deps.AppCfg)) + router.HandleFunc("DELETE /api/v1/user/delete/{user_id}", middleware.Authentication(deps.UserHandler.SoftDeleteUser, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/overview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) diff --git a/internal/app/user/handler.go b/internal/app/user/handler.go index 801102c..1a0dc22 100644 --- a/internal/app/user/handler.go +++ b/internal/app/user/handler.go @@ -7,7 +7,6 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) @@ -17,7 +16,7 @@ type handler struct { type Handler interface { UpdateUserEmail(w http.ResponseWriter, r *http.Request) - DeleteUser(w http.ResponseWriter, r *http.Request) + SoftDeleteUser(w http.ResponseWriter, r *http.Request) ListUserRanks(w http.ResponseWriter, r *http.Request) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) } @@ -50,13 +49,20 @@ func (h *handler) UpdateUserEmail(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "email updated successfully", nil) } -func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) { +func (h *handler) SoftDeleteUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - val := ctx.Value(middleware.UserIdKey) - userID := val.(int) + userIdValue := ctx.Value(middleware.UserIdKey) - user, err := h.userService.SoftDeleteUser(ctx, userID) + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + err := h.userService.SoftDeleteUser(ctx, userId) if err != nil { slog.Error("failed to softdelete user", "error", err) status, errorMessage := apperrors.MapError(err) @@ -64,8 +70,7 @@ func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) { return } - response.WriteJson(w, http.StatusOK, "user scheduled for deletion", user) - + response.WriteJson(w, http.StatusOK, "user scheduled for deletion", nil) } func (h *handler) ListUserRanks(w http.ResponseWriter, r *http.Request) { diff --git a/internal/app/user/service.go b/internal/app/user/service.go index ff137a5..0fe83ec 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -19,7 +19,7 @@ type Service interface { GetUserByGithubId(ctx context.Context, githubId int) (User, error) CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, email string) error - SoftDeleteUser(ctx context.Context, userID int) (User, error) + SoftDeleteUser(ctx context.Context, userID int) error RecoverAccountInGracePeriod(ctx context.Context, userID int) error UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) @@ -81,14 +81,14 @@ func (s *service) UpdateUserEmail(ctx context.Context, email string) error { return nil } -func (s *service) SoftDeleteUser(ctx context.Context, userID int) (User, error) { +func (s *service) SoftDeleteUser(ctx context.Context, userID int) error { now := time.Now() - user, err := s.userRepository.MarkUserAsDeleted(ctx, nil, userID, now) + err := s.userRepository.MarkUserAsDeleted(ctx, nil, userID, now) if err != nil { slog.Error("unable to softdelete user", "error", err) - return User{}, apperrors.ErrInternalServer + return apperrors.ErrInternalServer } - return User(user), nil + return nil } func (s *service) RecoverAccountInGracePeriod(ctx context.Context, userID int) error { diff --git a/internal/repository/user.go b/internal/repository/user.go index e5d2129..f1ad788 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -21,7 +21,7 @@ type UserRepository interface { GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error - MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) (User, error) + MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) error AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error DeleteUser(tx *sqlx.Tx) error GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) @@ -53,6 +53,8 @@ const ( updateEmailQuery = "UPDATE users SET email=$1, updated_at=$2 where id=$3" + markUserAsDeletedQuery = "UPDATE users SET is_deleted = TRUE, deleted_at=$1 WHERE id = $2" + getAllUsersGithubIdQuery = "SELECT github_id from users" updateUserCurrentBalanceQuery = "UPDATE users SET current_balance=$1, updated_at=$2 where id=$3" @@ -148,39 +150,16 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user return nil } -func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) (User, error) { +func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) error { executer := ur.BaseRepository.initiateQueryExecuter(tx) - _, err := executer.ExecContext(ctx, `UPDATE users SET is_deleted = TRUE, deleted_at=$1 WHERE id = $2`, deletedAt, userID) + + _, err := executer.ExecContext(ctx, markUserAsDeletedQuery, deletedAt, userID) if err != nil { slog.Error("unable to mark user as deleted", "error", err) - return User{}, apperrors.ErrInternalServer - } - var user User - err = executer.QueryRowContext(ctx, getUserByIdQuery, userID).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - slog.Error("user not found", "error", err) - return User{}, apperrors.ErrUserNotFound - } - slog.Error("error occurred while getting user by id", "error", err) - return User{}, apperrors.ErrInternalServer + return apperrors.ErrInternalServer } - return user, nil + + return nil } func (ur *userRepository) AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error { From 29054b1297c596c26648bb3d76f4c4ef27c67a29 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 15 Jul 2025 15:43:53 +0530 Subject: [PATCH 61/68] refactor user deletion feature --- cmd/main.go | 6 +-- internal/app/auth/service.go | 15 +++--- internal/app/cronJob/cleanupJob.go | 32 +++++++++++++ internal/app/cronJob/init.go | 4 +- internal/app/dependencies.go | 2 + internal/app/user/service.go | 15 +++++- internal/pkg/jobs/cleanUp.go | 30 ------------ internal/repository/user.go | 77 +++++++++++++++++++----------- 8 files changed, 105 insertions(+), 76 deletions(-) create mode 100644 internal/app/cronJob/cleanupJob.go delete mode 100644 internal/pkg/jobs/cleanUp.go diff --git a/cmd/main.go b/cmd/main.go index 24ea96c..c0fc99e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -14,7 +14,6 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app" "github.com/joshsoftware/code-curiosity-2025/internal/app/cronJob" "github.com/joshsoftware/code-curiosity-2025/internal/config" - "github.com/joshsoftware/code-curiosity-2025/internal/pkg/jobs" ) func main() { @@ -46,16 +45,13 @@ func main() { router := app.NewRouter(dependencies) newCronSchedular := cronJob.NewCronSchedular() - newCronSchedular.InitCronJobs(dependencies.ContributionService) + newCronSchedular.InitCronJobs(dependencies.ContributionService, dependencies.UserService) server := http.Server{ Addr: fmt.Sprintf(":%s", cfg.HTTPServer.Port), Handler: router, } - // backround job start - jobs.PermanentDeleteJob(db) - serverRunning := make(chan os.Signal, 1) signal.Notify( diff --git a/internal/app/auth/service.go b/internal/app/auth/service.go index 2aeeb91..c0cbeb4 100644 --- a/internal/app/auth/service.go +++ b/internal/app/auth/service.go @@ -3,7 +3,6 @@ package auth import ( "context" "encoding/json" - "fmt" "log/slog" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" @@ -84,15 +83,13 @@ func (s *service) GithubOAuthLoginCallback(ctx context.Context, code string) (st return "", apperrors.ErrInternalServer } - // soft delete checker - err = s.userService.RecoverAccountInGracePeriod(ctx, userData.Id) - if err != nil { - slog.Error("error in recovering account in grace period during login", "error", err) - return "", apperrors.ErrInternalServer + if userData.IsDeleted { + err = s.userService.RecoverAccountInGracePeriod(ctx, userData.Id) + if err != nil { + slog.Error("error in recovering account in grace period during login", "error", err) + return "", apperrors.ErrInternalServer + } } - // token print - - fmt.Println(jwtToken) return jwtToken, nil } diff --git a/internal/app/cronJob/cleanupJob.go b/internal/app/cronJob/cleanupJob.go new file mode 100644 index 0000000..8bb1574 --- /dev/null +++ b/internal/app/cronJob/cleanupJob.go @@ -0,0 +1,32 @@ +package cronJob + +import ( + "context" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/user" +) + +type CleanupJob struct { + CronJob + userService user.Service +} + +func NewCleanupJob(userService user.Service) *CleanupJob { + return &CleanupJob{ + userService: userService, + CronJob: CronJob{Name: "User Cleanup Job Daily"}, + } +} + +func (c *CleanupJob) Schedule(s *CronSchedular) error { + _, err := s.cron.AddFunc("15 13 * * *", func() { c.Execute(context.Background(), c.run) }) + if err != nil { + return err + } + + return nil +} + +func (c *CleanupJob) run(ctx context.Context) { + c.userService.HardDeleteUsers(ctx) +} diff --git a/internal/app/cronJob/init.go b/internal/app/cronJob/init.go index 7fba0f3..77d9f78 100644 --- a/internal/app/cronJob/init.go +++ b/internal/app/cronJob/init.go @@ -5,6 +5,7 @@ import ( "time" "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" + "github.com/joshsoftware/code-curiosity-2025/internal/app/user" "github.com/robfig/cron/v3" ) @@ -23,9 +24,10 @@ func NewCronSchedular() *CronSchedular { } } -func (c *CronSchedular) InitCronJobs(contributionService contribution.Service) { +func (c *CronSchedular) InitCronJobs(contributionService contribution.Service, userService user.Service) { jobs := []Job{ NewDailyJob(contributionService), + NewCleanupJob(userService), } for _, job := range jobs { diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 86d4c3d..cb95a53 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -18,6 +18,7 @@ import ( type Dependencies struct { ContributionService contribution.Service + UserService user.Service AuthHandler auth.Handler UserHandler user.Handler ContributionHandler contribution.Handler @@ -47,6 +48,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque return Dependencies{ ContributionService: contributionService, + UserService: userService, AuthHandler: authHandler, UserHandler: userHandler, RepositoryHandler: repositoryHandler, diff --git a/internal/app/user/service.go b/internal/app/user/service.go index 0fe83ec..fa37e18 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -19,7 +19,8 @@ type Service interface { GetUserByGithubId(ctx context.Context, githubId int) (User, error) CreateUser(ctx context.Context, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, email string) error - SoftDeleteUser(ctx context.Context, userID int) error + SoftDeleteUser(ctx context.Context, userId int) error + HardDeleteUsers(ctx context.Context) error RecoverAccountInGracePeriod(ctx context.Context, userID int) error UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) @@ -91,8 +92,18 @@ func (s *service) SoftDeleteUser(ctx context.Context, userID int) error { return nil } +func (s *service) HardDeleteUsers(ctx context.Context) error { + err := s.userRepository.HardDeleteUsers(ctx, nil) + if err != nil { + slog.Error("error deleting users that are soft deleted for more than three months", "error", err) + return err + } + + return nil +} + func (s *service) RecoverAccountInGracePeriod(ctx context.Context, userID int) error { - err := s.userRepository.AccountScheduledForDelete(ctx, nil, userID) + err := s.userRepository.RecoverAccountInGracePeriod(ctx, nil, userID) if err != nil { slog.Error("failed to recover account in grace period", "error", err) return err diff --git a/internal/pkg/jobs/cleanUp.go b/internal/pkg/jobs/cleanUp.go deleted file mode 100644 index 50f639e..0000000 --- a/internal/pkg/jobs/cleanUp.go +++ /dev/null @@ -1,30 +0,0 @@ -package jobs - -import ( - "log/slog" - - "github.com/jmoiron/sqlx" - "github.com/joshsoftware/code-curiosity-2025/internal/repository" - "github.com/robfig/cron/v3" -) - -func PermanentDeleteJob(db *sqlx.DB) { - slog.Info("entering into the cleanup job") - c := cron.New() - _, err := c.AddFunc("36 00 * * *", func() { - slog.Info("Job scheduled for user cleanup from database") - ur := repository.NewUserRepository(db) // pass in *sql.DB or whatever is needed - err := ur.DeleteUser(nil) - if err != nil { - slog.Error("Cleanup job error", "error", err) - } else { - slog.Info("User cleanup Job completed.") - } - }) - - if err != nil { - slog.Error("failed to start user delete job ", "error", err) - } - - c.Start() -} diff --git a/internal/repository/user.go b/internal/repository/user.go index f1ad788..72968af 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -22,8 +22,8 @@ type UserRepository interface { CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, userID int, deletedAt time.Time) error - AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error - DeleteUser(tx *sqlx.Tx) error + RecoverAccountInGracePeriod(ctx context.Context, tx *sqlx.Tx, userID int) error + HardDeleteUsers(ctx context.Context, tx *sqlx.Tx) error GetAllUsersGithubId(ctx context.Context, tx *sqlx.Tx) ([]int, error) UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) @@ -53,7 +53,11 @@ const ( updateEmailQuery = "UPDATE users SET email=$1, updated_at=$2 where id=$3" - markUserAsDeletedQuery = "UPDATE users SET is_deleted = TRUE, deleted_at=$1 WHERE id = $2" + markUserAsDeletedQuery = "UPDATE users SET is_deleted = TRUE, deleted_at=$1 where id = $2" + + recoverAccountInGracePeriodQuery = "UPDATE users SET is_deleted = false, deleted_at = NULL where id = $1" + + hardDeleteUsersQuery = "DELETE FROM users WHERE is_deleted = TRUE AND deleted_at <= $1" getAllUsersGithubIdQuery = "SELECT github_id from users" @@ -162,41 +166,29 @@ func (ur *userRepository) MarkUserAsDeleted(ctx context.Context, tx *sqlx.Tx, us return nil } -func (ur *userRepository) AccountScheduledForDelete(ctx context.Context, tx *sqlx.Tx, userID int) error { - var deleteGracePeriod = 90 * 24 * time.Hour - user, err := ur.GetUserById(ctx, tx, userID) +func (ur *userRepository) RecoverAccountInGracePeriod(ctx context.Context, tx *sqlx.Tx, userID int) error { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + _, err := executer.ExecContext(ctx, recoverAccountInGracePeriodQuery, userID) if err != nil { - slog.Error("unable to fetch user by ID ", "error", err) + slog.Error("unable to reverse the soft delete ", "error", err) return apperrors.ErrInternalServer } - if user.IsDeleted { - var dlt_at time.Time - if !user.DeletedAt.Valid { - return errors.New("invalid deletion state") - } else { - dlt_at = user.DeletedAt.Time - } - - if time.Since(dlt_at) >= deleteGracePeriod { - slog.Error("user is permanentaly deleted ", "error", err) - return apperrors.ErrInternalServer - } else { - executer := ur.BaseRepository.initiateQueryExecuter(tx) - _, err := executer.ExecContext(ctx, `UPDATE users SET is_deleted = false, deleted_at = NULL WHERE id = $1`, userID) - slog.Error("unable to reverse the soft delete ", "error", err) - return apperrors.ErrInternalServer - } - } return nil } -func (ur *userRepository) DeleteUser(tx *sqlx.Tx) error { - threshold := time.Now().Add(-90 * 1 * time.Second) +func (ur *userRepository) HardDeleteUsers(ctx context.Context, tx *sqlx.Tx) error { executer := ur.BaseRepository.initiateQueryExecuter(tx) - ctx := context.Background() - _, err := executer.ExecContext(ctx, `DELETE FROM users WHERE is_deleted = TRUE AND deleted_at <= $1 `, threshold) + + threshold := time.Now().Add(-90 * 1 * time.Second) + + _, err := executer.ExecContext(ctx, hardDeleteUsersQuery, threshold) + if err != nil { + slog.Error("error deleting users that are soft deleted for more than three months", "error", err) + return apperrors.ErrInternalServer + } + return err } @@ -251,3 +243,30 @@ func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, u return currentUserRank, nil } + +// DELETE FROM leaderboard_hourly WHERE user_id = 1; +// DELETE FROM badges WHERE user_id = 1; +// DELETE FROM goal_contribution WHERE set_by_user_id = 1; +// DELETE FROM contribution_score WHERE admin_id = 1; + +// WITH user_contributions AS ( +// SELECT id FROM contributions WHERE user_id = 1 +// ) + +// DELETE FROM transactions +// WHERE contribution_id IN (SELECT id FROM user_contributions); + +// DELETE FROM transactions WHERE user_id = 1; + +// DELETE FROM summary WHERE user_id = 1; + +// DELETE FROM summary +// WHERE contribution_id IN (SELECT id FROM user_contributions); + +// select id as idstodelete from repositories where id in (select repository_id as repos from contributions where user_id=1) + +// delete from repositories where id in idstodelete and id not in (select repository_id from contributions where user_id=1) + +// DELETE FROM contributions WHERE user_id = 1; + +// DELETE FROM users WHERE id = 1; From 7390be297a04ae5fc2235d2d8785af922fbda020 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 16 Jul 2025 12:42:12 +0530 Subject: [PATCH 62/68] Merge branch 'main' of https://github.com/joshsoftware/code-curiosity-2025 into feat/Delete --- internal/app/cronJob/cleanupJob.go | 2 +- internal/db/migrate.go | 18 +++++++++++------- internal/db/migrations/1748862201_init.up.sql | 14 +++++++------- ...1016438_allow-null-contribution-id.down.sql | 2 +- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/internal/app/cronJob/cleanupJob.go b/internal/app/cronJob/cleanupJob.go index 8bb1574..d87b3c9 100644 --- a/internal/app/cronJob/cleanupJob.go +++ b/internal/app/cronJob/cleanupJob.go @@ -19,7 +19,7 @@ func NewCleanupJob(userService user.Service) *CleanupJob { } func (c *CleanupJob) Schedule(s *CronSchedular) error { - _, err := s.cron.AddFunc("15 13 * * *", func() { c.Execute(context.Background(), c.run) }) + _, err := s.cron.AddFunc("00 18 * * *", func() { c.Execute(context.Background(), c.run) }) if err != nil { return err } diff --git a/internal/db/migrate.go b/internal/db/migrate.go index 634e51a..6b9a159 100644 --- a/internal/db/migrate.go +++ b/internal/db/migrate.go @@ -42,7 +42,7 @@ func InitMainDBMigrations(config config.AppConfig) (migration Migration, er erro return } -func (migration Migration) MigrationsUpAll(){ +func (migration Migration) MigrationsUpAll() { err := migration.m.Up() if err != nil { if err == migrate.ErrNoChange { @@ -56,7 +56,7 @@ func (migration Migration) MigrationsUpAll(){ slog.Info("Migration up completed") } -func (migration Migration) MigrationsUpWithSteps(steps int){ +func (migration Migration) MigrationsUpWithSteps(steps int) { if err := migration.m.Steps(steps); err != nil { if err == migrate.ErrNoChange { slog.Error("No new migrations to apply") @@ -65,7 +65,7 @@ func (migration Migration) MigrationsUpWithSteps(steps int){ slog.Error("An error occurred while making migrations up", "error", err) return - } + } slog.Info("Current migration version:", "version", migration.MigrationVersion()) slog.Info("Migration up completed") @@ -110,7 +110,7 @@ func (migration Migration) MigrationsDownWithSteps(steps int) { slog.Error("An error occurred while making migrations down", "error", err) return - } + } slog.Info("Current migration version:", "version", migration.MigrationVersion()) slog.Info("Migration down completed") @@ -206,13 +206,17 @@ func main() { } action := os.Args[1] + var steps string + if len(os.Args) > 2 { + steps = os.Args[2] + } switch action { case "up": - migration.MigrationsUp(os.Args[2]) + migration.MigrationsUp(steps) case "down": - migration.MigrationsDown(os.Args[2]) + migration.MigrationsDown(steps) case "create": - migration.CreateMigrationFile(os.Args[2]) + migration.CreateMigrationFile(steps) default: slog.Info("Invalid action. Use 'up' or 'down' or 'create'.") } diff --git a/internal/db/migrations/1748862201_init.up.sql b/internal/db/migrations/1748862201_init.up.sql index 476693a..c06fb1d 100644 --- a/internal/db/migrations/1748862201_init.up.sql +++ b/internal/db/migrations/1748862201_init.up.sql @@ -113,28 +113,28 @@ CREATE TABLE "goal_contribution"( ); ALTER TABLE - "goal_contribution" ADD CONSTRAINT "goal_contribution_set_by_user_id_foreign" FOREIGN KEY("set_by_user_id") REFERENCES "users"("id"); + "goal_contribution" ADD CONSTRAINT "goal_contribution_set_by_user_id_foreign" FOREIGN KEY("set_by_user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "goal_contribution" ADD CONSTRAINT "goal_contribution_contribution_score_id_foreign" FOREIGN KEY("contribution_score_id") REFERENCES "contribution_score"("id"); ALTER TABLE - "contribution_score" ADD CONSTRAINT "contribution_score_admin_id_foreign" FOREIGN KEY("admin_id") REFERENCES "users"("id"); + "contribution_score" ADD CONSTRAINT "contribution_score_admin_id_foreign" FOREIGN KEY("admin_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE - "summary" ADD CONSTRAINT "summary_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "summary" ADD CONSTRAINT "summary_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE - "transactions" ADD CONSTRAINT "transactions_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "transactions" ADD CONSTRAINT "transactions_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "contributions" ADD CONSTRAINT "contributions_contribution_score_id_foreign" FOREIGN KEY("contribution_score_id") REFERENCES "contribution_score"("id"); ALTER TABLE - "badges" ADD CONSTRAINT "badges_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "badges" ADD CONSTRAINT "badges_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "goal_contribution" ADD CONSTRAINT "goal_contribution_goal_id_foreign" FOREIGN KEY("goal_id") REFERENCES "goal"("id"); ALTER TABLE "transactions" ADD CONSTRAINT "transactions_contribution_id_foreign" FOREIGN KEY("contribution_id") REFERENCES "contributions"("id"); ALTER TABLE - "contributions" ADD CONSTRAINT "contributions_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "contributions" ADD CONSTRAINT "contributions_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "contributions" ADD CONSTRAINT "contributions_repository_id_foreign" FOREIGN KEY("repository_id") REFERENCES "repositories"("id"); ALTER TABLE - "leaderboard_hourly" ADD CONSTRAINT "leaderboard_hourly_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id"); + "leaderboard_hourly" ADD CONSTRAINT "leaderboard_hourly_user_id_foreign" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE; ALTER TABLE "summary" ADD CONSTRAINT "summary_contribution_id_foreign" FOREIGN KEY("contribution_id") REFERENCES "contributions"("id"); \ No newline at end of file diff --git a/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql b/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql index c44d425..ff2f133 100644 --- a/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql +++ b/internal/db/migrations/1751016438_allow-null-contribution-id.down.sql @@ -1,2 +1,2 @@ ALTER TABLE transactions -ALTER COLUMN contribution_id SET NOT NULL DEFAULT 0; \ No newline at end of file +ALTER COLUMN contribution_id SET NOT NULL; \ No newline at end of file From 8612d285c6031230b37523ca443775cdb2867f19 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 16 Jul 2025 16:32:14 +0530 Subject: [PATCH 63/68] remove unnecesssary comments --- internal/repository/user.go | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/internal/repository/user.go b/internal/repository/user.go index 72968af..d9a6cc9 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -242,31 +242,4 @@ func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, u } return currentUserRank, nil -} - -// DELETE FROM leaderboard_hourly WHERE user_id = 1; -// DELETE FROM badges WHERE user_id = 1; -// DELETE FROM goal_contribution WHERE set_by_user_id = 1; -// DELETE FROM contribution_score WHERE admin_id = 1; - -// WITH user_contributions AS ( -// SELECT id FROM contributions WHERE user_id = 1 -// ) - -// DELETE FROM transactions -// WHERE contribution_id IN (SELECT id FROM user_contributions); - -// DELETE FROM transactions WHERE user_id = 1; - -// DELETE FROM summary WHERE user_id = 1; - -// DELETE FROM summary -// WHERE contribution_id IN (SELECT id FROM user_contributions); - -// select id as idstodelete from repositories where id in (select repository_id as repos from contributions where user_id=1) - -// delete from repositories where id in idstodelete and id not in (select repository_id from contributions where user_id=1) - -// DELETE FROM contributions WHERE user_id = 1; - -// DELETE FROM users WHERE id = 1; +} \ No newline at end of file From a1b4689d250c16b690ffd58b1317598fc8f6143c Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 17 Jul 2025 12:43:29 +0530 Subject: [PATCH 64/68] implement goal service --- internal/app/dependencies.go | 8 ++- internal/app/goal/domain.go | 26 +++++++ internal/app/goal/handler.go | 94 ++++++++++++++++++++++++ internal/app/goal/service.go | 103 ++++++++++++++++++++++++++ internal/app/router.go | 5 ++ internal/app/user/domain.go | 4 ++ internal/app/user/handler.go | 32 +++++++++ internal/app/user/service.go | 24 ++++++- internal/pkg/apperrors/errors.go | 6 +- internal/repository/domain.go | 18 +++++ internal/repository/goal.go | 120 +++++++++++++++++++++++++++++++ internal/repository/user.go | 17 ++++- 12 files changed, 453 insertions(+), 4 deletions(-) create mode 100644 internal/app/goal/domain.go create mode 100644 internal/app/goal/handler.go create mode 100644 internal/app/goal/service.go create mode 100644 internal/repository/goal.go diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index cb95a53..5c3ac80 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -8,6 +8,7 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" "github.com/joshsoftware/code-curiosity-2025/internal/app/github" + "github.com/joshsoftware/code-curiosity-2025/internal/app/goal" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" "github.com/joshsoftware/code-curiosity-2025/internal/app/transaction" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" @@ -23,17 +24,20 @@ type Dependencies struct { UserHandler user.Handler ContributionHandler contribution.Handler RepositoryHandler repoService.Handler + GoalHandler goal.Handler AppCfg config.AppConfig Client config.Bigquery } func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigquery, httpClient *http.Client) Dependencies { + goalRepository := repository.NewGoalRepository(db) userRepository := repository.NewUserRepository(db) contributionRepository := repository.NewContributionRepository(db) repositoryRepository := repository.NewRepositoryRepository(db) transactionRepository := repository.NewTransactionRepository(db) - userService := user.NewService(userRepository) + goalService := goal.NewService(goalRepository, contributionRepository) + userService := user.NewService(userRepository, goalService) authService := auth.NewService(userService, appCfg) bigqueryService := bigquery.NewService(client, userRepository) githubService := github.NewService(appCfg, httpClient) @@ -45,6 +49,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque userHandler := user.NewHandler(userService) repositoryHandler := repoService.NewHandler(repositoryService, githubService) contributionHandler := contribution.NewHandler(contributionService) + goalHandler := goal.NewHandler(goalService) return Dependencies{ ContributionService: contributionService, @@ -53,6 +58,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque UserHandler: userHandler, RepositoryHandler: repositoryHandler, ContributionHandler: contributionHandler, + GoalHandler: goalHandler, AppCfg: appCfg, Client: client, } diff --git a/internal/app/goal/domain.go b/internal/app/goal/domain.go new file mode 100644 index 0000000..163d615 --- /dev/null +++ b/internal/app/goal/domain.go @@ -0,0 +1,26 @@ +package goal + +import "time" + +type Goal struct { + Id int + Level string + CreatedAt time.Time + UpdatedAt time.Time +} + +type GoalContribution struct { + Id int + GoalId int + ContributionScoreId int + TargetCount int + IsCustom bool + SetByUserId int + CreatedAt time.Time + UpdatedAt time.Time +} + +type CustomGoalLevelTarget struct { + ContributionType string `json:"contribution_type"` + Target int `json:"target"` +} diff --git a/internal/app/goal/handler.go b/internal/app/goal/handler.go new file mode 100644 index 0000000..33f21e3 --- /dev/null +++ b/internal/app/goal/handler.go @@ -0,0 +1,94 @@ +package goal + +import ( + "encoding/json" + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" +) + +type handler struct { + goalService Service +} + +type Handler interface { + ListGoalLevels(w http.ResponseWriter, r *http.Request) + ListGoalLevelTargets(w http.ResponseWriter, r *http.Request) + CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Request) +} + +func NewHandler(goalService Service) Handler { + return &handler{ + goalService: goalService, + } +} + +func (h *handler) ListGoalLevels(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + gaols, err := h.goalService.ListGoalLevels(ctx) + if err != nil { + slog.Error("error fetching users conributed repos", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "goal levels fetched successfully", gaols) +} + +func (h *handler) ListGoalLevelTargets(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdCtxVal := ctx.Value(middleware.UserIdKey) + userId, ok := userIdCtxVal.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + goalLevelTargets, err := h.goalService.ListGoalLevelTargetDetail(ctx, userId) + if err != nil { + slog.Error("error fetching goal level targets", "error", err) + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "goal level targets fetched successfully", goalLevelTargets) +} + +func (h *handler) CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdCtxVal := ctx.Value(middleware.UserIdKey) + userId, ok := userIdCtxVal.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + var customGoalLevelTarget []CustomGoalLevelTarget + err := json.NewDecoder(r.Body).Decode(&customGoalLevelTarget) + if err != nil { + slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) + response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) + return + } + + createdCustomGoalLevelTargets, err := h.goalService.CreateCustomGoalLevelTarget(ctx, userId, customGoalLevelTarget) + if err != nil { + slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) + response.WriteJson(w, http.StatusBadRequest, err.Error(), nil) + return + } + + response.WriteJson(w, http.StatusOK, "custom goal level targets created successfully", createdCustomGoalLevelTargets) +} diff --git a/internal/app/goal/service.go b/internal/app/goal/service.go new file mode 100644 index 0000000..da0a917 --- /dev/null +++ b/internal/app/goal/service.go @@ -0,0 +1,103 @@ +package goal + +import ( + "context" + "log/slog" + + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + goalRepository repository.GoalRepository + contributionRepository repository.ContributionRepository +} + +type Service interface { + ListGoalLevels(ctx context.Context) ([]Goal, error) + GetGoalIdByGoalLevel(ctx context.Context, level string) (int, error) + ListGoalLevelTargetDetail(ctx context.Context, userId int) ([]GoalContribution, error) + CreateCustomGoalLevelTarget(ctx context.Context, userID int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) +} + +func NewService(goalRepository repository.GoalRepository, contributionRepository repository.ContributionRepository) Service { + return &service{ + goalRepository: goalRepository, + contributionRepository: contributionRepository, + } +} + +func (s *service) ListGoalLevels(ctx context.Context) ([]Goal, error) { + goals, err := s.goalRepository.ListGoalLevels(ctx, nil) + if err != nil { + slog.Error("error fetching goal levels", "error", err) + return nil, err + } + + serviceGoals := make([]Goal, len(goals)) + + for i, g := range goals { + serviceGoals[i] = Goal(g) + } + + return serviceGoals, nil +} + +func (s *service) GetGoalIdByGoalLevel(ctx context.Context, level string) (int, error) { + goalId, err := s.goalRepository.GetGoalIdByGoalLevel(ctx, nil, level) + + if err != nil { + slog.Error("failed to get goal id by goal level", "error", err) + return 0, err + } + + return goalId, err +} + +func (s *service) ListGoalLevelTargetDetail(ctx context.Context, userId int) ([]GoalContribution, error) { + goalLevelTargets, err := s.goalRepository.ListUserGoalLevelTargets(ctx, nil, userId) + if err != nil { + slog.Error("error fetching goal level targets", "error", err) + return nil, err + } + + serviceGoalLevelTargets := make([]GoalContribution, len(goalLevelTargets)) + for i, g := range goalLevelTargets { + serviceGoalLevelTargets[i] = GoalContribution(g) + } + + return serviceGoalLevelTargets, nil +} + +func (s *service) CreateCustomGoalLevelTarget(ctx context.Context, userID int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) { + customGoalLevelId, err := s.GetGoalIdByGoalLevel(ctx, "Custom") + if err != nil { + slog.Error("error fetching custom goal level id", "error", err) + return nil, err + } + var goalContributions []GoalContribution + + goalContributionInfo := make([]GoalContribution, len(customGoalLevelTarget)) + for i, c := range customGoalLevelTarget { + goalContributionInfo[i].GoalId = customGoalLevelId + + contributionScoreDetails, err := s.contributionRepository.GetContributionScoreDetailsByContributionType(ctx, nil, c.ContributionType) + if err != nil { + slog.Error("error fetching contribution score details by type", "error", err) + return nil, err + } + + goalContributionInfo[i].ContributionScoreId = contributionScoreDetails.Id + goalContributionInfo[i].TargetCount = c.Target + goalContributionInfo[i].SetByUserId = userID + + goalContribution, err := s.goalRepository.CreateCustomGoalLevelTarget(ctx, nil, repository.GoalContribution(goalContributionInfo[i])) + if err != nil { + slog.Error("error creating custom goal level target", "error", err) + return nil, err + } + + goalContributions = append(goalContributions, GoalContribution(goalContribution)) + } + + return goalContributions, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 76759a1..5f99a71 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -20,6 +20,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) router.HandleFunc("DELETE /api/v1/user/delete/{user_id}", middleware.Authentication(deps.UserHandler.SoftDeleteUser, deps.AppCfg)) + router.HandleFunc("PATCH /api/v1/user/goal/level", middleware.Authentication(deps.UserHandler.UpdateCurrentActiveGoalId, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUserContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/overview", middleware.Authentication(deps.ContributionHandler.ListMonthlyContributionSummary, deps.AppCfg)) @@ -32,5 +33,9 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/leaderboard", middleware.Authentication(deps.UserHandler.ListUserRanks, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/leaderboard", middleware.Authentication(deps.UserHandler.GetCurrentUserRank, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/goal/level", middleware.Authentication(deps.GoalHandler.ListGoalLevels, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/goal/level/targets", middleware.Authentication(deps.GoalHandler.ListGoalLevelTargets, deps.AppCfg)) + router.HandleFunc("POST /api/v1/user/goal/level/custom/targets", middleware.Authentication(deps.GoalHandler.CreateCustomGoalLevelTarget, deps.AppCfg)) + return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/app/user/domain.go b/internal/app/user/domain.go index cf0e527..236102d 100644 --- a/internal/app/user/domain.go +++ b/internal/app/user/domain.go @@ -53,3 +53,7 @@ type LeaderboardUser struct { CurrentBalance int `db:"current_balance"` Rank int `db:"rank"` } + +type GoalLevel struct { + Level string `json:"level"` +} diff --git a/internal/app/user/handler.go b/internal/app/user/handler.go index 1a0dc22..2d1bfcd 100644 --- a/internal/app/user/handler.go +++ b/internal/app/user/handler.go @@ -19,6 +19,7 @@ type Handler interface { SoftDeleteUser(w http.ResponseWriter, r *http.Request) ListUserRanks(w http.ResponseWriter, r *http.Request) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) + UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Request) } func NewHandler(userService Service) Handler { @@ -110,3 +111,34 @@ func (h *handler) GetCurrentUserRank(w http.ResponseWriter, r *http.Request) { response.WriteJson(w, http.StatusOK, "current user rank fetched successfully", currentUserRank) } + +func (h *handler) UpdateCurrentActiveGoalId(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdCtxVal := ctx.Value(middleware.UserIdKey) + userId, ok := userIdCtxVal.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + var goal GoalLevel + err := json.NewDecoder(r.Body).Decode(&goal) + if err != nil { + slog.Error(apperrors.ErrFailedMarshal.Error(), "error", err) + response.WriteJson(w, http.StatusBadRequest, apperrors.ErrInvalidRequestBody.Error(), nil) + return + } + + goalId, err := h.userService.UpdateCurrentActiveGoalId(ctx, userId, goal.Level) + if err != nil { + slog.Error("failed to update current active goal id", "error", err) + status, errMsg := apperrors.MapError(err) + response.WriteJson(w, status, errMsg, nil) + return + } + + response.WriteJson(w, http.StatusOK, "Goal updated successfully", goalId) +} diff --git a/internal/app/user/service.go b/internal/app/user/service.go index fa37e18..2b01e82 100644 --- a/internal/app/user/service.go +++ b/internal/app/user/service.go @@ -5,6 +5,7 @@ import ( "log/slog" "time" + "github.com/joshsoftware/code-curiosity-2025/internal/app/goal" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" "github.com/joshsoftware/code-curiosity-2025/internal/repository" @@ -12,6 +13,7 @@ import ( type service struct { userRepository repository.UserRepository + goalService goal.Service } type Service interface { @@ -25,11 +27,13 @@ type Service interface { UpdateUserCurrentBalance(ctx context.Context, transaction Transaction) error GetAllUsersRank(ctx context.Context) ([]LeaderboardUser, error) GetCurrentUserRank(ctx context.Context, userId int) (LeaderboardUser, error) + UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) } -func NewService(userRepository repository.UserRepository) Service { +func NewService(userRepository repository.UserRepository, goalService goal.Service) Service { return &service{ userRepository: userRepository, + goalService: goalService, } } @@ -158,3 +162,21 @@ func (s *service) GetCurrentUserRank(ctx context.Context, userId int) (Leaderboa return LeaderboardUser(currentUserRank), nil } + +func (s *service) UpdateCurrentActiveGoalId(ctx context.Context, userId int, level string) (int, error) { + + goalId, err := s.goalService.GetGoalIdByGoalLevel(ctx, level) + + if err != nil { + slog.Error("error occured while fetching goal id by goal level") + return 0, err + } + + goalId, err = s.userRepository.UpdateCurrentActiveGoalId(ctx, nil, userId, goalId) + + if err != nil { + slog.Error("failed to update current active goal id", "error", err) + } + + return goalId, err +} diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 4c0430c..2ddfbaa 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -51,6 +51,10 @@ var ( ErrTransactionCreationFailed = errors.New("error failed to create transaction") ErrTransactionNotFound = errors.New("error transaction for the contribution id does not exist") + + ErrFetchingGoals = errors.New("error fetching goal levels ") + ErrGoalNotFound = errors.New("goal not found") + ErrCustomGoalTargetCreationFailed = errors.New("failed to create targets for custom goal level") ) func MapError(err error) (statusCode int, errMessage string) { @@ -61,7 +65,7 @@ func MapError(err error) (statusCode int, errMessage string) { return http.StatusUnauthorized, err.Error() case ErrAccessForbidden: return http.StatusForbidden, err.Error() - case ErrUserNotFound, ErrRepoNotFound, ErrContributionNotFound: + case ErrUserNotFound, ErrRepoNotFound, ErrContributionNotFound, ErrGoalNotFound: return http.StatusNotFound, err.Error() case ErrInvalidToken: return http.StatusUnprocessableEntity, err.Error() diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 4ac6312..037ffc3 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -92,3 +92,21 @@ type MonthlyContributionSummary struct { TotalCoins int `db:"total_coins"` Month time.Time `db:"month"` } + +type Goal struct { + Id int `db:"id"` + Level string `db:"level"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type GoalContribution struct { + Id int `db:"id"` + GoalId int `db:"goal_id"` + ContributionScoreId int `db:"contribution_score_id"` + TargetCount int `db:"target_count"` + IsCustom bool `db:"is_custom"` + SetByUserId int `db:"set_by_user_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} diff --git a/internal/repository/goal.go b/internal/repository/goal.go new file mode 100644 index 0000000..1e7c1d3 --- /dev/null +++ b/internal/repository/goal.go @@ -0,0 +1,120 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +type goalRepository struct { + BaseRepository +} + +type GoalRepository interface { + RepositoryTransaction + ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]Goal, error) + GetGoalIdByGoalLevel(ctx context.Context, tx *sqlx.Tx, level string) (int, error) + ListUserGoalLevelTargets(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalContribution, error) + CreateCustomGoalLevelTarget(ctx context.Context, tx *sqlx.Tx, customGoalContributionInfo GoalContribution) (GoalContribution, error) +} + +func NewGoalRepository(db *sqlx.DB) GoalRepository { + return &goalRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + getGoalLevelQuery = "SELECT * from goal;" + + fetchGoalIdByGoalNameQuery = "SELECT id from goal where level=$1" + + getGoalContributionDetailQuery = ` + SELECT * from goal_contribution + where goal_id + IN + (SELECT current_active_goal_id from users where id=$1)` + + createCustomGoalLevelTargetQuery = ` + INSERT INTO goal_contribution( + goal_id, + contribution_score_id, + target_count, + is_custom, + set_by_user_id + ) + VALUES + ($1, $2, $3, $4, $5) + RETURNING *` +) + +func (gr *goalRepository) ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]Goal, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var goals []Goal + err := executer.SelectContext(ctx, &goals, getGoalLevelQuery) + if err != nil { + slog.Error("error fetching goal levels", "error", err) + return nil, apperrors.ErrFetchingGoals + } + + return goals, nil +} + +func (gr *goalRepository) GetGoalIdByGoalLevel(ctx context.Context, tx *sqlx.Tx, level string) (int, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var goalId int + err := executer.GetContext(ctx, &goalId, fetchGoalIdByGoalNameQuery, level) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("error goal not found", "error", err) + return 0, apperrors.ErrGoalNotFound + } + + slog.Error("error occured while getting goal id by goal level", "error", err) + return 0, apperrors.ErrInternalServer + } + + return goalId, nil +} + +func (gr *goalRepository) ListUserGoalLevelTargets(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalContribution, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var goalLevelTargets []GoalContribution + err := executer.SelectContext(ctx, &goalLevelTargets, getGoalContributionDetailQuery, userId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("error goal not found", "error", err) + return nil, apperrors.ErrInternalServer + } + + slog.Error("error occured while getting goal id by goal level", "error", err) + return nil, apperrors.ErrInternalServer + } + + return goalLevelTargets, nil +} + +func (gr *goalRepository) CreateCustomGoalLevelTarget(ctx context.Context, tx *sqlx.Tx, customGoalContributionInfo GoalContribution) (GoalContribution, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var customGoalContribution GoalContribution + err := executer.GetContext(ctx, &customGoalContribution, createCustomGoalLevelTargetQuery, + customGoalContributionInfo.GoalId, + customGoalContributionInfo.ContributionScoreId, + customGoalContributionInfo.TargetCount, + true, + customGoalContributionInfo.SetByUserId) + if err != nil { + slog.Error("error creating custom goal level targets", "error", err) + return GoalContribution{}, apperrors.ErrCustomGoalTargetCreationFailed + } + + return customGoalContribution, nil +} diff --git a/internal/repository/user.go b/internal/repository/user.go index d9a6cc9..15b1f6a 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -28,6 +28,7 @@ type UserRepository interface { UpdateUserCurrentBalance(ctx context.Context, tx *sqlx.Tx, user User) error GetAllUsersRank(ctx context.Context, tx *sqlx.Tx) ([]LeaderboardUser, error) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, userId int) (LeaderboardUser, error) + UpdateCurrentActiveGoalId(ctx context.Context, tx *sqlx.Tx, userId int, goalId int) (int, error) } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -87,6 +88,8 @@ const ( ) ranked_users WHERE id = $1;` + + updateCurrentActiveGoalIdQuery = "UPDATE users SET current_active_goal_id=$1 where id=$2" ) func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { @@ -242,4 +245,16 @@ func (ur *userRepository) GetCurrentUserRank(ctx context.Context, tx *sqlx.Tx, u } return currentUserRank, nil -} \ No newline at end of file +} + +func (ur *userRepository) UpdateCurrentActiveGoalId(ctx context.Context, tx *sqlx.Tx, userId int, goalId int) (int, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + _, err := executer.ExecContext(ctx, updateCurrentActiveGoalIdQuery, goalId, userId) + if err != nil { + slog.Error("failed to update current active goal id", "error", err) + return 0, apperrors.ErrInternalServer + } + + return goalId, nil +} From 7e434ebc68bbdd0b99796a4c7b6515b905fd055e Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 17 Jul 2025 12:45:56 +0530 Subject: [PATCH 65/68] remove unwanted db tags --- internal/app/transaction/domain.go | 18 +++++++++--------- internal/app/user/domain.go | 28 ++++++++++++++-------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/internal/app/transaction/domain.go b/internal/app/transaction/domain.go index b8989b6..6a02f13 100644 --- a/internal/app/transaction/domain.go +++ b/internal/app/transaction/domain.go @@ -3,15 +3,15 @@ package transaction import "time" type Transaction struct { - Id int `db:"id"` - UserId int `db:"user_id"` - ContributionId int `db:"contribution_id"` - IsRedeemed bool `db:"is_redeemed"` - IsGained bool `db:"is_gained"` - TransactedBalance int `db:"transacted_balance"` - TransactedAt time.Time `db:"transacted_at"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + Id int + UserId int + ContributionId int + IsRedeemed bool + IsGained bool + TransactedBalance int + TransactedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time } type Contribution struct { diff --git a/internal/app/user/domain.go b/internal/app/user/domain.go index 236102d..087f25f 100644 --- a/internal/app/user/domain.go +++ b/internal/app/user/domain.go @@ -35,23 +35,23 @@ type Email struct { } type Transaction struct { - Id int `db:"id"` - UserId int `db:"user_id"` - ContributionId int `db:"contribution_id"` - IsRedeemed bool `db:"is_redeemed"` - IsGained bool `db:"is_gained"` - TransactedBalance int `db:"transacted_balance"` - TransactedAt time.Time `db:"transacted_at"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + Id int + UserId int + ContributionId int + IsRedeemed bool + IsGained bool + TransactedBalance int + TransactedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time } type LeaderboardUser struct { - Id int `db:"id"` - GithubUsername string `db:"github_username"` - AvatarUrl string `db:"avatar_url"` - CurrentBalance int `db:"current_balance"` - Rank int `db:"rank"` + Id int + GithubUsername string + AvatarUrl string + CurrentBalance int + Rank int } type GoalLevel struct { From 06a2716e56426ca51f0b07ba711846287f3faa93 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 17 Jul 2025 13:22:48 +0530 Subject: [PATCH 66/68] rename query const according to function names --- internal/repository/goal.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/repository/goal.go b/internal/repository/goal.go index 1e7c1d3..87f84de 100644 --- a/internal/repository/goal.go +++ b/internal/repository/goal.go @@ -29,11 +29,11 @@ func NewGoalRepository(db *sqlx.DB) GoalRepository { } const ( - getGoalLevelQuery = "SELECT * from goal;" + listGoalLevelQuery = "SELECT * from goal;" - fetchGoalIdByGoalNameQuery = "SELECT id from goal where level=$1" + getGoalIdByGoalLevelQuery = "SELECT id from goal where level=$1" - getGoalContributionDetailQuery = ` + listUserGoalLevelTargetsQuery = ` SELECT * from goal_contribution where goal_id IN @@ -56,7 +56,7 @@ func (gr *goalRepository) ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]Go executer := gr.BaseRepository.initiateQueryExecuter(tx) var goals []Goal - err := executer.SelectContext(ctx, &goals, getGoalLevelQuery) + err := executer.SelectContext(ctx, &goals, listGoalLevelQuery) if err != nil { slog.Error("error fetching goal levels", "error", err) return nil, apperrors.ErrFetchingGoals @@ -69,7 +69,7 @@ func (gr *goalRepository) GetGoalIdByGoalLevel(ctx context.Context, tx *sqlx.Tx, executer := gr.BaseRepository.initiateQueryExecuter(tx) var goalId int - err := executer.GetContext(ctx, &goalId, fetchGoalIdByGoalNameQuery, level) + err := executer.GetContext(ctx, &goalId, getGoalIdByGoalLevelQuery, level) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("error goal not found", "error", err) @@ -87,7 +87,7 @@ func (gr *goalRepository) ListUserGoalLevelTargets(ctx context.Context, tx *sqlx executer := gr.BaseRepository.initiateQueryExecuter(tx) var goalLevelTargets []GoalContribution - err := executer.SelectContext(ctx, &goalLevelTargets, getGoalContributionDetailQuery, userId) + err := executer.SelectContext(ctx, &goalLevelTargets, listUserGoalLevelTargetsQuery, userId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("error goal not found", "error", err) From 4db322e61c04c24547c0b432131453d39216ad82 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 17 Jul 2025 15:24:11 +0530 Subject: [PATCH 67/68] implement goal level achieved target --- internal/app/goal/handler.go | 23 +++++++++++++ internal/app/goal/service.go | 51 ++++++++++++++++++++++++++++- internal/app/router.go | 1 + internal/repository/contribution.go | 18 +++++++++- 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/internal/app/goal/handler.go b/internal/app/goal/handler.go index 33f21e3..4fb1227 100644 --- a/internal/app/goal/handler.go +++ b/internal/app/goal/handler.go @@ -18,6 +18,7 @@ type Handler interface { ListGoalLevels(w http.ResponseWriter, r *http.Request) ListGoalLevelTargets(w http.ResponseWriter, r *http.Request) CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Request) + ListGoalLevelAchievedTarget(w http.ResponseWriter, r *http.Request) } func NewHandler(goalService Service) Handler { @@ -92,3 +93,25 @@ func (h *handler) CreateCustomGoalLevelTarget(w http.ResponseWriter, r *http.Req response.WriteJson(w, http.StatusOK, "custom goal level targets created successfully", createdCustomGoalLevelTargets) } + +func (h *handler) ListGoalLevelAchievedTarget(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdCtxVal := ctx.Value(middleware.UserIdKey) + userId, ok := userIdCtxVal.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMessage := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMessage, nil) + return + } + + goalLevelAchievedTarget, err := h.goalService.ListGoalLevelAchievedTarget(ctx, userId) + if err != nil { + slog.Error("error failed to list goal level achieved targets", "error", err) + response.WriteJson(w, http.StatusBadRequest, err.Error(), nil) + return + } + + response.WriteJson(w, http.StatusOK, "goal level achieved targets fetched successfully", goalLevelAchievedTarget) +} diff --git a/internal/app/goal/service.go b/internal/app/goal/service.go index da0a917..3c50125 100644 --- a/internal/app/goal/service.go +++ b/internal/app/goal/service.go @@ -2,7 +2,9 @@ package goal import ( "context" + "fmt" "log/slog" + "time" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) @@ -16,7 +18,8 @@ type Service interface { ListGoalLevels(ctx context.Context) ([]Goal, error) GetGoalIdByGoalLevel(ctx context.Context, level string) (int, error) ListGoalLevelTargetDetail(ctx context.Context, userId int) ([]GoalContribution, error) - CreateCustomGoalLevelTarget(ctx context.Context, userID int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) + CreateCustomGoalLevelTarget(ctx context.Context, userId int, customGoalLevelTarget []CustomGoalLevelTarget) ([]GoalContribution, error) + ListGoalLevelAchievedTarget(ctx context.Context, userId int) (map[string]int, error) } func NewService(goalRepository repository.GoalRepository, contributionRepository repository.ContributionRepository) Service { @@ -101,3 +104,49 @@ func (s *service) CreateCustomGoalLevelTarget(ctx context.Context, userID int, c return goalContributions, nil } + +func (s *service) ListGoalLevelAchievedTarget(ctx context.Context, userId int) (map[string]int, error) { + goalLevelSetTargets, err := s.goalRepository.ListUserGoalLevelTargets(ctx, nil, userId) + if err != nil { + slog.Error("error fetching goal level targets", "error", err) + return nil, err + } + + contributionTypes := make([]CustomGoalLevelTarget, len(goalLevelSetTargets)) + for i, g := range goalLevelSetTargets { + contributionTypes[i].ContributionType, err = s.contributionRepository.GetContributionTypeByContributionScoreId(ctx, nil, g.ContributionScoreId) + if err != nil { + slog.Error("error fetching contribution type by contribution score id", "error", err) + return nil, err + } + + contributionTypes[i].Target = g.TargetCount + } + + year := int(time.Now().Year()) + month := int(time.Now().Month()) + monthlyContributionCount, err := s.contributionRepository.ListMonthlyContributionSummary(ctx, nil, year, month, userId) + if err != nil { + slog.Error("error fetching monthly contribution count", "error", err) + return nil, err + } + + contributionsAchievedTarget := make(map[string]int, len(monthlyContributionCount)) + + for _, m := range monthlyContributionCount { + contributionsAchievedTarget[m.Type] = m.Count + } + + var completedTarget int + for _, c := range contributionTypes { + if c.Target == contributionsAchievedTarget[c.ContributionType] { + completedTarget += 1 + } + } + + if completedTarget == len(goalLevelSetTargets) { + fmt.Println("assign badge") + } + + return contributionsAchievedTarget, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 5f99a71..4122522 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -36,6 +36,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/goal/level", middleware.Authentication(deps.GoalHandler.ListGoalLevels, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/goal/level/targets", middleware.Authentication(deps.GoalHandler.ListGoalLevelTargets, deps.AppCfg)) router.HandleFunc("POST /api/v1/user/goal/level/custom/targets", middleware.Authentication(deps.GoalHandler.CreateCustomGoalLevelTarget, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/goal/level/targets/achieved", middleware.Authentication(deps.GoalHandler.ListGoalLevelAchievedTarget, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index da5163a..7febf7f 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -23,6 +23,7 @@ type ContributionRepository interface { GetContributionByGithubEventId(ctx context.Context, tx *sqlx.Tx, githubEventId string) (Contribution, error) GetAllContributionTypes(ctx context.Context, tx *sqlx.Tx) ([]ContributionScore, error) ListMonthlyContributionSummary(ctx context.Context, tx *sqlx.Tx, year int, month int, userId int) ([]MonthlyContributionSummary, error) + GetContributionTypeByContributionScoreId(ctx context.Context, tx *sqlx.Tx, contributionScoreId int) (string, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -64,6 +65,8 @@ const ( AND DATE_TRUNC('month', contributed_at) = MAKE_DATE($2, $3, 1)::timestamptz GROUP BY month, contribution_type;` + + getContributionTypeByContributionScoreIdQuery = `SELECT contribution_type from contribution_score where id=$1` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -161,7 +164,7 @@ func (cr *contributionRepository) ListMonthlyContributionSummary(ctx context.Con executer := cr.BaseRepository.initiateQueryExecuter(tx) var contributionTypeSummary []MonthlyContributionSummary - err := executer.SelectContext(ctx, &contributionTypeSummary, getMonthlyContributionSummaryQuery, userId, month) + err := executer.SelectContext(ctx, &contributionTypeSummary, getMonthlyContributionSummaryQuery, userId, year, month) if err != nil { slog.Error("error fetching monthly contribution summary for user", "error", err) return nil, apperrors.ErrInternalServer @@ -169,3 +172,16 @@ func (cr *contributionRepository) ListMonthlyContributionSummary(ctx context.Con return contributionTypeSummary, nil } + +func (cr *contributionRepository) GetContributionTypeByContributionScoreId(ctx context.Context, tx *sqlx.Tx, contributionScoreId int) (string, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contributionType string + err := executer.GetContext(ctx, &contributionType, getContributionTypeByContributionScoreIdQuery, contributionScoreId) + if err != nil { + slog.Error("error occured while getting contribution type by contribution score id", "error", err) + return contributionType, err + } + + return contributionType, nil +} From 347eb31f180fedfc3cf262e7a95bfe1ea115fa56 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 18 Jul 2025 12:10:28 +0530 Subject: [PATCH 68/68] implement badge assigning service --- internal/app/badge/domain.go | 12 +++++ internal/app/badge/handler.go | 48 ++++++++++++++++++ internal/app/badge/service.go | 59 ++++++++++++++++++++++ internal/app/dependencies.go | 8 ++- internal/app/goal/service.go | 18 +++++-- internal/app/router.go | 2 + internal/pkg/apperrors/errors.go | 2 + internal/repository/badge.go | 86 ++++++++++++++++++++++++++++++++ internal/repository/domain.go | 9 ++++ internal/repository/goal.go | 19 +++++++ 10 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 internal/app/badge/domain.go create mode 100644 internal/app/badge/handler.go create mode 100644 internal/app/badge/service.go create mode 100644 internal/repository/badge.go diff --git a/internal/app/badge/domain.go b/internal/app/badge/domain.go new file mode 100644 index 0000000..d2c9797 --- /dev/null +++ b/internal/app/badge/domain.go @@ -0,0 +1,12 @@ +package badge + +import "time" + +type Badge struct { + Id int + UserId int + BadgeType string + EarnedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/app/badge/handler.go b/internal/app/badge/handler.go new file mode 100644 index 0000000..0cd456d --- /dev/null +++ b/internal/app/badge/handler.go @@ -0,0 +1,48 @@ +package badge + +import ( + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" +) + +type handler struct { + badgeService Service +} + +type Handler interface { + GetBadgeDetailsOfUser(w http.ResponseWriter, r *http.Request) +} + +func NewHandler(badgeService Service) Handler { + return &handler{ + badgeService: badgeService, + } +} + +func (h *handler) GetBadgeDetailsOfUser(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + userIdValue := ctx.Value(middleware.UserIdKey) + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + status, errorMsg := apperrors.MapError(apperrors.ErrContextValue) + response.WriteJson(w, status, errorMsg, nil) + return + } + + badges, err := h.badgeService.GetBadgeDetailsOfUser(ctx, userId) + + if err != nil { + slog.Error("failed to get badge details of user", "Error", err) + status, errorMsg := apperrors.MapError(err) + response.WriteJson(w, status, errorMsg, nil) + return + } + + response.WriteJson(w, http.StatusOK, "badges fetched successfully", badges) +} diff --git a/internal/app/badge/service.go b/internal/app/badge/service.go new file mode 100644 index 0000000..817ca28 --- /dev/null +++ b/internal/app/badge/service.go @@ -0,0 +1,59 @@ +package badge + +import ( + "context" + "database/sql" + "errors" + "log/slog" + + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + badgeRepository repository.BadgeRepository +} + +type Service interface { + HandleBadgeCreation(ctx context.Context, userId int, badgeType string) (Badge, error) + GetBadgeDetailsOfUser(ctx context.Context, userId int) ([]Badge, error) +} + +func NewService(badgeRepository repository.BadgeRepository) Service { + return &service{ + badgeRepository: badgeRepository, + } +} + +func (s *service) HandleBadgeCreation(ctx context.Context, userId int, badgeType string) (Badge, error) { + badge, err := s.badgeRepository.GetUserCurrentMonthBadge(ctx, nil, userId) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + badge, err = s.badgeRepository.CreateBadge(ctx, nil, userId, badgeType) + if err != nil { + slog.Error("error creating badge for user", "error", err) + return Badge{}, err + } + } + slog.Error("error fetching current month badge for user", "error", err) + return Badge{}, err + } + + return Badge(badge), nil +} + +func (s *service) GetBadgeDetailsOfUser(ctx context.Context, userId int) ([]Badge, error) { + badges, err := s.badgeRepository.GetBadgeDetailsOfUser(ctx, nil, userId) + + if err != nil { + slog.Error("(service) Failed to get the badge details", "error", err) + return nil, err + } + + serviceBadge := make([]Badge, len(badges)) + + for i, b := range badges { + serviceBadge[i] = Badge(b) + } + + return serviceBadge, nil +} diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 5c3ac80..bc4fa57 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -5,6 +5,7 @@ import ( "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/app/auth" + "github.com/joshsoftware/code-curiosity-2025/internal/app/badge" "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" "github.com/joshsoftware/code-curiosity-2025/internal/app/github" @@ -25,18 +26,21 @@ type Dependencies struct { ContributionHandler contribution.Handler RepositoryHandler repoService.Handler GoalHandler goal.Handler + BadgeHandler badge.Handler AppCfg config.AppConfig Client config.Bigquery } func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigquery, httpClient *http.Client) Dependencies { + badgeRepository := repository.NewBadgeRepository(db) goalRepository := repository.NewGoalRepository(db) userRepository := repository.NewUserRepository(db) contributionRepository := repository.NewContributionRepository(db) repositoryRepository := repository.NewRepositoryRepository(db) transactionRepository := repository.NewTransactionRepository(db) - goalService := goal.NewService(goalRepository, contributionRepository) + badgeService := badge.NewService(badgeRepository) + goalService := goal.NewService(goalRepository, contributionRepository, badgeService) userService := user.NewService(userRepository, goalService) authService := auth.NewService(userService, appCfg) bigqueryService := bigquery.NewService(client, userRepository) @@ -50,6 +54,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque repositoryHandler := repoService.NewHandler(repositoryService, githubService) contributionHandler := contribution.NewHandler(contributionService) goalHandler := goal.NewHandler(goalService) + badgeHandler := badge.NewHandler(badgeService) return Dependencies{ ContributionService: contributionService, @@ -59,6 +64,7 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque RepositoryHandler: repositoryHandler, ContributionHandler: contributionHandler, GoalHandler: goalHandler, + BadgeHandler: badgeHandler, AppCfg: appCfg, Client: client, } diff --git a/internal/app/goal/service.go b/internal/app/goal/service.go index 3c50125..34b38d1 100644 --- a/internal/app/goal/service.go +++ b/internal/app/goal/service.go @@ -2,16 +2,17 @@ package goal import ( "context" - "fmt" "log/slog" "time" + "github.com/joshsoftware/code-curiosity-2025/internal/app/badge" "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) type service struct { goalRepository repository.GoalRepository contributionRepository repository.ContributionRepository + badgeService badge.Service } type Service interface { @@ -22,10 +23,11 @@ type Service interface { ListGoalLevelAchievedTarget(ctx context.Context, userId int) (map[string]int, error) } -func NewService(goalRepository repository.GoalRepository, contributionRepository repository.ContributionRepository) Service { +func NewService(goalRepository repository.GoalRepository, contributionRepository repository.ContributionRepository, badgeService badge.Service) Service { return &service{ goalRepository: goalRepository, contributionRepository: contributionRepository, + badgeService: badgeService, } } @@ -145,7 +147,17 @@ func (s *service) ListGoalLevelAchievedTarget(ctx context.Context, userId int) ( } if completedTarget == len(goalLevelSetTargets) { - fmt.Println("assign badge") + userGoalLevel, err := s.goalRepository.GetUserActiveGoalLevel(ctx, nil, userId) + if err != nil { + slog.Error("error fetching user active gaol level", "error", err) + return nil, err + } + + _, err = s.badgeService.HandleBadgeCreation(ctx, userId, userGoalLevel) + if err != nil { + slog.Error("error handling user badge creation", "error", err) + return nil, err + } } return contributionsAchievedTarget, nil diff --git a/internal/app/router.go b/internal/app/router.go index 4122522..c1bf564 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -38,5 +38,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("POST /api/v1/user/goal/level/custom/targets", middleware.Authentication(deps.GoalHandler.CreateCustomGoalLevelTarget, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/goal/level/targets/achieved", middleware.Authentication(deps.GoalHandler.ListGoalLevelAchievedTarget, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/badges", middleware.Authentication(deps.BadgeHandler.GetBadgeDetailsOfUser, deps.AppCfg)) + return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 2ddfbaa..26069af 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -55,6 +55,8 @@ var ( ErrFetchingGoals = errors.New("error fetching goal levels ") ErrGoalNotFound = errors.New("goal not found") ErrCustomGoalTargetCreationFailed = errors.New("failed to create targets for custom goal level") + + ErrBadgeCreationFailed = errors.New("failed to create badge for user") ) func MapError(err error) (statusCode int, errMessage string) { diff --git a/internal/repository/badge.go b/internal/repository/badge.go new file mode 100644 index 0000000..3298c5e --- /dev/null +++ b/internal/repository/badge.go @@ -0,0 +1,86 @@ +package repository + +import ( + "context" + "log/slog" + "time" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +type badgeRepository struct { + BaseRepository +} + +type BadgeRepository interface { + RepositoryTransaction + GetUserCurrentMonthBadge(ctx context.Context, tx *sqlx.Tx, userId int) (Badge, error) + CreateBadge(ctx context.Context, tx *sqlx.Tx, userId int, badgeType string) (Badge, error) + GetBadgeDetailsOfUser(ctx context.Context, tx *sqlx.Tx, userId int) ([]Badge, error) +} + +func NewBadgeRepository(db *sqlx.DB) BadgeRepository { + return &badgeRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + createBadgeQuery = ` + INSERT INTO badges( + user_id, + badge_type, + earned_at + ) + VALUES($1, $2, $3) + RETURNING *` + + getBadgeDetailsOfUserQuery = "SELECT * FROM badges WHERE user_id = $1 ORDER BY earned_at DESC" + + getUserCurrentMonthBadgeQuery = ` + SELECT * FROM badges + WHERE user_id = $1 + AND earned_at >= DATE_TRUNC('month', CURRENT_DATE) + AND earned_at < DATE_TRUNC('month', CURRENT_DATE + INTERVAL '1 month')` +) + +func (br *badgeRepository) GetUserCurrentMonthBadge(ctx context.Context, tx *sqlx.Tx, userId int) (Badge, error) { + executer := br.BaseRepository.initiateQueryExecuter(tx) + + var badge Badge + err := executer.GetContext(ctx, &badge, getUserCurrentMonthBadgeQuery, userId) + if err != nil { + slog.Error("error fetching current month earned badge for user", "error", err) + return Badge{}, apperrors.ErrBadgeCreationFailed + } + + return badge, nil + +} + +func (br *badgeRepository) CreateBadge(ctx context.Context, tx *sqlx.Tx, userId int, badgeType string) (Badge, error) { + executer := br.BaseRepository.initiateQueryExecuter(tx) + + var createdBadge Badge + err := executer.GetContext(ctx, &createdBadge, createBadgeQuery, userId, badgeType, time.Now()) + if err != nil { + slog.Error("error creating badge for user", "error", err) + return Badge{}, apperrors.ErrBadgeCreationFailed + } + + return createdBadge, nil +} + +func (br *badgeRepository) GetBadgeDetailsOfUser(ctx context.Context, tx *sqlx.Tx, userId int) ([]Badge, error) { + executer := br.BaseRepository.initiateQueryExecuter(tx) + + var badges []Badge + + err := executer.SelectContext(ctx, &badges, getBadgeDetailsOfUserQuery, userId) + if err != nil { + return nil, err + } + + return badges, nil +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 037ffc3..c3faf14 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -110,3 +110,12 @@ type GoalContribution struct { CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } + +type Badge struct { + Id int `db:"id"` + UserId int `db:"user_id"` + BadgeType string `db:"badge_type"` + EarnedAt time.Time `db:"earned_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} diff --git a/internal/repository/goal.go b/internal/repository/goal.go index 87f84de..f56bc5e 100644 --- a/internal/repository/goal.go +++ b/internal/repository/goal.go @@ -20,6 +20,7 @@ type GoalRepository interface { GetGoalIdByGoalLevel(ctx context.Context, tx *sqlx.Tx, level string) (int, error) ListUserGoalLevelTargets(ctx context.Context, tx *sqlx.Tx, userId int) ([]GoalContribution, error) CreateCustomGoalLevelTarget(ctx context.Context, tx *sqlx.Tx, customGoalContributionInfo GoalContribution) (GoalContribution, error) + GetUserActiveGoalLevel(ctx context.Context, tx *sqlx.Tx, userId int) (string, error) } func NewGoalRepository(db *sqlx.DB) GoalRepository { @@ -50,6 +51,11 @@ const ( VALUES ($1, $2, $3, $4, $5) RETURNING *` + + getUserActiveGoalLevelQuery = ` + SELECT level from goal + where id IN + (SELECT current_active_goal_id from users where id=$1)` ) func (gr *goalRepository) ListGoalLevels(ctx context.Context, tx *sqlx.Tx) ([]Goal, error) { @@ -118,3 +124,16 @@ func (gr *goalRepository) CreateCustomGoalLevelTarget(ctx context.Context, tx *s return customGoalContribution, nil } + +func (gr *goalRepository) GetUserActiveGoalLevel(ctx context.Context, tx *sqlx.Tx, userId int) (string, error) { + executer := gr.BaseRepository.initiateQueryExecuter(tx) + + var userActiveGoalLevel string + err := executer.GetContext(ctx, &userActiveGoalLevel, getUserActiveGoalLevelQuery, userId) + if err != nil { + slog.Error("error getting users current active goal level name", "error", err) + return userActiveGoalLevel, apperrors.ErrInternalServer + } + + return userActiveGoalLevel, nil +}