5 min report fix + arifpay integration
This commit is contained in:
parent
ae56d253c2
commit
d8de92e7d6
28
cmd/main.go
28
cmd/main.go
|
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ import (
|
|||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||
|
||||
// "github.com/SamuelTariku/FortuneBet-Backend/internal/router"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/arifpay"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bonus"
|
||||
|
|
@ -38,6 +40,7 @@ import (
|
|||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
|
||||
issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/kafka"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
|
||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||
|
|
@ -119,6 +122,7 @@ func main() {
|
|||
// userStore,
|
||||
notificationSvc,
|
||||
logger,
|
||||
kafka.NewProducer([]string{"localhost:9092"}, "wallet-events"),
|
||||
)
|
||||
|
||||
branchSvc := branch.NewService(store)
|
||||
|
|
@ -209,32 +213,18 @@ func main() {
|
|||
httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc)
|
||||
httpserver.StartTicketCrons(*ticketSvc)
|
||||
|
||||
// Fetch companies and branches for live wallet metrics update
|
||||
// ctx := context.Background()
|
||||
|
||||
// companies := []domain.GetCompany{
|
||||
// {ID: 1, Name: "Company A", WalletBalance: 1000.0},
|
||||
// }
|
||||
|
||||
// branches := []domain.BranchWallet{
|
||||
// {ID: 10, Name: "Branch Z", CompanyID: 1, Balance: 500.0},
|
||||
// }
|
||||
|
||||
// notificationSvc.UpdateLiveWalletMetrics(ctx, companies, branches)
|
||||
// if err != nil {
|
||||
// log.Println("Failed to update live metrics:", err)
|
||||
// } else {
|
||||
// log.Println("Live metrics broadcasted successfully")
|
||||
// }
|
||||
|
||||
issueReportingRepo := repository.NewReportedIssueRepository(store)
|
||||
|
||||
issueReportingSvc := issuereporting.New(issueReportingRepo)
|
||||
|
||||
// go httpserver.SetupReportCronJob(reportWorker)
|
||||
transferStore := wallet.TransferStore(store)
|
||||
|
||||
arifpaySvc := arifpay.NewArifpayService(cfg, transferStore, &http.Client{
|
||||
Timeout: 30 * time.Second})
|
||||
|
||||
// Initialize and start HTTP server
|
||||
app := httpserver.NewApp(
|
||||
arifpaySvc,
|
||||
issueReportingSvc,
|
||||
instSvc,
|
||||
currSvc,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
-- Settings Initial Data
|
||||
INSERT INTO settings (key, value)
|
||||
VALUES ('max_number_of_outcomes', '30'),
|
||||
VALUES
|
||||
('max_number_of_outcomes', '30'),
|
||||
('bet_amount_limit', '100000'),
|
||||
('daily_ticket_limit', '50'),
|
||||
('total_winnings_limit', '1000000'),
|
||||
('amount_for_bet_referral', '1000000')
|
||||
('amount_for_bet_referral', '1000000'),
|
||||
('cashback_amount_cap', '1000')
|
||||
ON CONFLICT (key) DO
|
||||
UPDATE
|
||||
SET value = EXCLUDED.value;
|
||||
ON CONFLICT (key)
|
||||
DO UPDATE SET value = EXCLUDED.value;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ services:
|
|||
|
||||
mongo:
|
||||
container_name: fortunebet-mongo
|
||||
image: mongo:7.0
|
||||
image: mongo:7.0.11
|
||||
restart: always
|
||||
ports:
|
||||
- "27017:27017"
|
||||
|
|
@ -89,8 +89,8 @@ services:
|
|||
networks:
|
||||
- app
|
||||
command: ["/app/bin/web"]
|
||||
volumes:
|
||||
- "C:/Users/User/Desktop:/host-desktop"
|
||||
# volumes:
|
||||
# - "C:/Users/User/Desktop:/host-desktop"
|
||||
|
||||
test:
|
||||
build:
|
||||
|
|
|
|||
1743
docs/docs.go
1743
docs/docs.go
File diff suppressed because it is too large
Load Diff
1743
docs/swagger.json
1743
docs/swagger.json
File diff suppressed because it is too large
Load Diff
1448
docs/swagger.yaml
1448
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
29
go.mod
29
go.mod
|
|
@ -16,15 +16,15 @@ require (
|
|||
// github.com/stretchr/testify v1.10.0
|
||||
github.com/swaggo/fiber-swagger v1.3.0
|
||||
github.com/swaggo/swag v1.16.4
|
||||
github.com/valyala/fasthttp v1.59.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
github.com/valyala/fasthttp v1.63.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
)
|
||||
|
||||
require (
|
||||
// github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
// github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
// github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
|
|
@ -53,11 +53,11 @@ require (
|
|||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.3
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
|
@ -77,10 +77,8 @@ require (
|
|||
go.uber.org/multierr v1.10.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
// github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/twilio/twilio-go v1.26.3
|
||||
)
|
||||
// github.com/go-resty/resty/v2 v2.16.5
|
||||
require github.com/twilio/twilio-go v1.26.3
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
|
|
@ -90,3 +88,10 @@ require (
|
|||
github.com/redis/go-redis/v9 v9.10.0 // direct
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/pierrec/lz4/v4 v4.1.15 // indirect
|
||||
github.com/segmentio/kafka-go v0.4.48 // direct
|
||||
)
|
||||
|
||||
require github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f // direct
|
||||
|
|
|
|||
67
go.sum
67
go.sum
|
|
@ -1,3 +1,5 @@
|
|||
github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f h1:UOp9At84RG8OT2Nw2TQidYWaoW1rAfTqChOJLdhYcm8=
|
||||
github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f/go.mod h1:N2NQ6ad3i+oLQU+MlPci2f7mx6Mtg+wUcvzCv77KCl8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
|
|
@ -7,9 +9,13 @@ github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaW
|
|||
github.com/amanuelabay/afrosms-go v1.0.6 h1:/B9upckMqzr5/de7dbXPqIfmJm4utbQq0QUQePxmXtk=
|
||||
github.com/amanuelabay/afrosms-go v1.0.6/go.mod h1:5mzzZtWSCDdvQsA0OyYf5CtbdGpl9lXyrcpl8/DyBj0=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
|
|
@ -54,8 +60,6 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY=
|
||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
|
|
@ -87,6 +91,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
|||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
|
|
@ -124,6 +129,8 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ
|
|||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
|
||||
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
|
@ -143,6 +150,8 @@ github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4
|
|||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/segmentio/kafka-go v0.4.48 h1:9jyu9CWK4W5W+SroCe8EffbrRZVqAOkuaLd/ApID4Vs=
|
||||
github.com/segmentio/kafka-go v0.4.48/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
|
|
@ -174,8 +183,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
|||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.35.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||
github.com/valyala/fasthttp v1.36.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
|
||||
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
|
||||
github.com/valyala/fasthttp v1.63.0 h1:DisIL8OjB7ul2d7cBaMRcKTQDYnrGy56R4FCiuDP0Ns=
|
||||
github.com/valyala/fasthttp v1.63.0/go.mod h1:REc4IeW+cAEyLrRPa5A81MIjvz0QE1laoTX2EaPHKJM=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
|
|
@ -206,12 +215,14 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
|
@ -222,13 +233,17 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -242,28 +257,36 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
|||
|
|
@ -58,6 +58,14 @@ type VeliConfig struct {
|
|||
Enabled bool `mapstructure:"Enabled"`
|
||||
}
|
||||
|
||||
type ARIFPAYConfig struct {
|
||||
APIKey string `mapstructure:"VELI_API_KEY"`
|
||||
CancelUrl string `mapstructure:"VELI_BASE_URL"`
|
||||
ErrorUrl string `mapstructure:"VELI_SECRET_KEY"`
|
||||
NotifyUrl string `mapstructure:"VELI_OPERATOR_ID"`
|
||||
SuccessUrl string `mapstructure:"VELI_BRAND_ID"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
FIXER_API_KEY string
|
||||
FIXER_BASE_URL string
|
||||
|
|
@ -87,6 +95,7 @@ type Config struct {
|
|||
PopOK domain.PopOKConfig
|
||||
AleaPlay AleaPlayConfig `mapstructure:"alea_play"`
|
||||
VeliGames VeliConfig `mapstructure:"veli_games"`
|
||||
ARIFPAY ARIFPAYConfig `mapstructure:"arifpay_config"`
|
||||
ResendApiKey string
|
||||
ResendSenderEmail string
|
||||
TwilioAccountSid string
|
||||
|
|
@ -189,6 +198,12 @@ func (c *Config) loadEnv() error {
|
|||
c.CHAPA_CALLBACK_URL = os.Getenv("CHAPA_CALLBACK_URL")
|
||||
c.CHAPA_RETURN_URL = os.Getenv("CHAPA_RETURN_URL")
|
||||
|
||||
c.ARIFPAY.APIKey = os.Getenv("ARIFPAY_API_KEY")
|
||||
c.ARIFPAY.CancelUrl = os.Getenv("ARIFPAY_CANCEL_URL")
|
||||
c.ARIFPAY.ErrorUrl = os.Getenv("ARIFPAY_ERROR_URL")
|
||||
c.ARIFPAY.NotifyUrl = os.Getenv("ARIFPAY_NOTIFY_URL")
|
||||
c.ARIFPAY.SuccessUrl = os.Getenv("ARIFPAY_SUCCESS_URL")
|
||||
|
||||
//Alea Play
|
||||
aleaEnabled := os.Getenv("ALEA_ENABLED")
|
||||
if aleaEnabled == "" {
|
||||
|
|
|
|||
53
internal/domain/arifpay.go
Normal file
53
internal/domain/arifpay.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type Item struct {
|
||||
Name string `json:"name"`
|
||||
Quantity int `json:"quantity"`
|
||||
Price float64 `json:"price"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
type Beneficiary struct {
|
||||
AccountNumber string `json:"accountNumber"`
|
||||
Bank string `json:"bank"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
type CreateCheckoutSessionRequest struct {
|
||||
CancelUrl string `json:"cancelUrl"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
Nonce string `json:"nonce"`
|
||||
ErrorUrl string `json:"errorUrl"`
|
||||
NotifyUrl string `json:"notifyUrl"`
|
||||
SuccessUrl string `json:"successUrl"`
|
||||
PaymentMethods []string `json:"paymentMethods"`
|
||||
ExpireDate time.Time `json:"expireDate"`
|
||||
Items []Item `json:"items"`
|
||||
Beneficiaries []Beneficiary `json:"beneficiaries"`
|
||||
Lang string `json:"lang"`
|
||||
}
|
||||
|
||||
type ArifPayCheckoutResponse struct {
|
||||
Error bool `json:"error"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
SessionID string `json:"sessionId"`
|
||||
PaymentURL string `json:"paymentUrl"`
|
||||
CancelURL string `json:"cancelUrl"`
|
||||
TotalAmount float64 `json:"totalAmount"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type ArifPayB2CRequest struct {
|
||||
SessionID string `json:"Sessionid"`
|
||||
PhoneNumber string `json:"Phonenumber"`
|
||||
}
|
||||
|
||||
type ArifpayVerifyByTransactionIDRequest struct {
|
||||
TransactionID string `json:"transactionId"`
|
||||
PaymentType int `json:"paymentType"`
|
||||
}
|
||||
18
internal/event/wallet_event.go
Normal file
18
internal/event/wallet_event.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package event
|
||||
|
||||
import "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
|
||||
type WalletEventType string
|
||||
|
||||
const (
|
||||
WalletBalanceUpdated WalletEventType = "wallet.balance.updated"
|
||||
WalletLowAlert WalletEventType = "wallet.alert.low_balance"
|
||||
)
|
||||
|
||||
type WalletEvent struct {
|
||||
EventType WalletEventType `json:"event_type"`
|
||||
WalletID int64 `json:"wallet_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Balance domain.Currency `json:"balance"`
|
||||
Trigger string `json:"trigger"` // e.g. "AddToWallet", "DeductFromWallet"
|
||||
}
|
||||
192
internal/services/arifpay/service.go
Normal file
192
internal/services/arifpay/service.go
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
package arifpay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/AnaniyaBelew/ArifpayGoPlugin"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ArifpayService struct {
|
||||
cfg *config.Config
|
||||
transferStore wallet.TransferStore
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewArifpayService(cfg *config.Config, transferStore wallet.TransferStore, httpClient *http.Client) *ArifpayService {
|
||||
return &ArifpayService{
|
||||
cfg: cfg,
|
||||
transferStore: transferStore,
|
||||
httpClient: httpClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ArifpayService) CreateCheckoutSession(req domain.CreateCheckoutSessionRequest) (string, error) {
|
||||
// Create SDK-compatible payload
|
||||
paymentPayload := ArifpayGoPlugin.PaymentRequest{
|
||||
CancelUrl: s.cfg.ARIFPAY.CancelUrl,
|
||||
Phone: req.Phone,
|
||||
Email: req.Email,
|
||||
Nonce: req.Nonce,
|
||||
ErrorUrl: s.cfg.ARIFPAY.ErrorUrl,
|
||||
NotifyUrl: s.cfg.ARIFPAY.NotifyUrl,
|
||||
SuccessUrl: s.cfg.ARIFPAY.SuccessUrl,
|
||||
PaymentMethods: req.PaymentMethods,
|
||||
Lang: req.Lang,
|
||||
}
|
||||
|
||||
// Convert items
|
||||
for _, item := range req.Items {
|
||||
paymentPayload.Items = append(paymentPayload.Items, domain.Item{
|
||||
Name: item.Name,
|
||||
Quantity: item.Quantity,
|
||||
Price: item.Price,
|
||||
Description: item.Description,
|
||||
Image: item.Image,
|
||||
})
|
||||
}
|
||||
|
||||
// Convert beneficiaries
|
||||
for _, b := range req.Beneficiaries {
|
||||
paymentPayload.Beneficiaries = append(paymentPayload.Beneficiaries, domain.Beneficiary{
|
||||
AccountNumber: b.AccountNumber,
|
||||
Bank: b.Bank,
|
||||
Amount: b.Amount,
|
||||
})
|
||||
}
|
||||
|
||||
// Instantiate payment client
|
||||
expireDate := time.Now().AddDate(2, 0, 0) // 2 months from now
|
||||
paymentClient := ArifpayGoPlugin.NewPayment(s.cfg.ARIFPAY.APIKey, expireDate)
|
||||
|
||||
// Create checkout session
|
||||
response, err := paymentClient.MakePayment(paymentPayload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
transfer := domain.CreateTransfer{
|
||||
Amount: domain.Currency(req.Beneficiaries[0].Amount),
|
||||
Verified: false,
|
||||
Type: domain.DEPOSIT,
|
||||
ReferenceNumber: uuid.NewString(),
|
||||
Status: string(domain.PaymentStatusPending),
|
||||
}
|
||||
|
||||
if _, err := s.transferStore.CreateTransfer(context.Background(), transfer); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *ArifpayService) B2CTransfer(ctx context.Context, req domain.ArifPayB2CRequest, endpoint string) (*map[string]interface{}, error) {
|
||||
// endpoint := c.baseURL + "/api/Telebirr/b2c/transfer"
|
||||
|
||||
payloadBytes, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request payload: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(payloadBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Accept", "application/json")
|
||||
httpReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
|
||||
|
||||
resp, err := s.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request to Telebirr B2C failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Telebirr API returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response body: %w", err)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func (s *ArifpayService) VerifyByTransactionID(transactionID string, paymentType int) ([]byte, error) {
|
||||
url := "https://gateway.arifpay.org/api/checkout/getSessionByTransactionId"
|
||||
|
||||
var payload domain.ArifpayVerifyByTransactionIDRequest
|
||||
|
||||
bodyBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("non-200 response from Arifpay: %s", string(respBody))
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
func (s *ArifpayService) VerifyBySessionID(sessionID string) ([]byte, error) {
|
||||
url := "https://gateway.arifpay.org/api/ms/transaction/status/" + sessionID
|
||||
|
||||
// Create GET request without body
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("non-200 response from Arifpay: %s", string(respBody))
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
80
internal/services/kafka/consumer.go
Normal file
80
internal/services/kafka/consumer.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package kafka
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/event"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws"
|
||||
"github.com/segmentio/kafka-go"
|
||||
)
|
||||
|
||||
type WalletConsumer struct {
|
||||
reader *kafka.Reader
|
||||
hub *ws.NotificationHub
|
||||
topic string
|
||||
groupID string
|
||||
brokers []string
|
||||
}
|
||||
|
||||
func NewWalletConsumer(brokers []string, topic, groupID string, hub *ws.NotificationHub) *WalletConsumer {
|
||||
return &WalletConsumer{
|
||||
brokers: brokers,
|
||||
topic: topic,
|
||||
groupID: groupID,
|
||||
hub: hub,
|
||||
reader: kafka.NewReader(kafka.ReaderConfig{
|
||||
Brokers: brokers,
|
||||
GroupID: groupID,
|
||||
Topic: topic,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *WalletConsumer) Start(ctx context.Context) {
|
||||
go func() {
|
||||
for {
|
||||
m, err := c.reader.ReadMessage(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Error reading wallet Kafka message: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var evt event.WalletEvent
|
||||
if err := json.Unmarshal(m.Value, &evt); err != nil {
|
||||
log.Printf("Failed to unmarshal wallet event: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"type": evt.EventType,
|
||||
"wallet_id": evt.WalletID,
|
||||
"user_id": evt.UserID,
|
||||
"balance": evt.Balance,
|
||||
"trigger": evt.Trigger,
|
||||
"recipient_id": evt.UserID,
|
||||
}
|
||||
|
||||
// Broadcast to appropriate WebSocket clients
|
||||
c.hub.Broadcast <- payload
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// func (c *WalletConsumer) Shutdown() error {
|
||||
// return c.reader.Close()
|
||||
// }
|
||||
|
||||
// func (h *ws.NotificationHub) Shutdown() {
|
||||
// close(h.Register)
|
||||
// close(h.Unregister)
|
||||
// close(h.Broadcast)
|
||||
|
||||
// h.mu.Lock()
|
||||
// defer h.mu.Unlock()
|
||||
// for client := range h.Clients {
|
||||
// client.Conn.Close()
|
||||
// delete(h.Clients, client)
|
||||
// }
|
||||
// }
|
||||
36
internal/services/kafka/producer.go
Normal file
36
internal/services/kafka/producer.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package kafka
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/segmentio/kafka-go"
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
writer *kafka.Writer
|
||||
}
|
||||
|
||||
func NewProducer(brokers []string, topic string) *Producer {
|
||||
return &Producer{
|
||||
writer: &kafka.Writer{
|
||||
Addr: kafka.TCP(brokers...),
|
||||
Topic: topic,
|
||||
Balancer: &kafka.LeastBytes{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Producer) Publish(ctx context.Context, key string, event any) error {
|
||||
msgBytes, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.writer.WriteMessages(ctx, kafka.Message{
|
||||
Key: []byte(key),
|
||||
Value: msgBytes,
|
||||
Time: time.Now(),
|
||||
})
|
||||
}
|
||||
|
|
@ -394,7 +394,7 @@ func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error)
|
|||
return metric, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateLiveWalletMetricForWallet(ctx context.Context, wallet domain.Wallet) {
|
||||
func (s *Service) UpdateLiveMetricForWallet(ctx context.Context, wallet domain.Wallet) {
|
||||
var (
|
||||
payload domain.LiveWalletMetrics
|
||||
event map[string]interface{}
|
||||
|
|
|
|||
|
|
@ -779,164 +779,6 @@ func getTimeRange(period string) (time.Time, time.Time) {
|
|||
}
|
||||
}
|
||||
|
||||
// func (s *Service) GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error) {
|
||||
// // Get company bet activity
|
||||
// companyBets, err := s.betStore.GetCompanyBetActivity(ctx, filter)
|
||||
// if err != nil {
|
||||
// s.logger.Error("failed to get company bet activity", "error", err)
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// // Get company details
|
||||
// companyDetails, err := s.branchStore.GetCompanyDetails(ctx, filter)
|
||||
// if err != nil {
|
||||
// s.logger.Error("failed to get company details", "error", err)
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// // Get company branches
|
||||
// companyBranches, err := s.branchStore.GetCompanyBranchCounts(ctx, filter)
|
||||
// if err != nil {
|
||||
// s.logger.Error("failed to get company branch counts", "error", err)
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// // Combine data into performance report
|
||||
// var performances []domain.CompanyPerformance
|
||||
// for _, bet := range companyBets {
|
||||
// performance := domain.CompanyPerformance{
|
||||
// CompanyID: bet.CompanyID,
|
||||
// TotalBets: bet.TotalBets,
|
||||
// TotalStakes: bet.TotalStakes,
|
||||
// TotalWins: bet.TotalWins,
|
||||
// TotalPayouts: bet.TotalPayouts,
|
||||
// Profit: bet.TotalStakes - bet.TotalPayouts,
|
||||
// }
|
||||
|
||||
// // Add company details
|
||||
// if details, ok := companyDetails[bet.CompanyID]; ok {
|
||||
// performance.CompanyName = details.Name
|
||||
// performance.ContactEmail = details.ContactEmail
|
||||
// }
|
||||
|
||||
// // Add branch counts
|
||||
// if branches, ok := companyBranches[bet.CompanyID]; ok {
|
||||
// performance.TotalBranches = branches.Total
|
||||
// performance.ActiveBranches = branches.Active
|
||||
// }
|
||||
|
||||
// // Calculate metrics
|
||||
// if bet.TotalBets > 0 {
|
||||
// performance.WinRate = float64(bet.TotalWins) / float64(bet.TotalBets) * 100
|
||||
// performance.AverageStake = bet.TotalStakes / domain.Currency(bet.TotalBets)
|
||||
// }
|
||||
|
||||
// performances = append(performances, performance)
|
||||
// }
|
||||
|
||||
// // Sort by profit (descending)
|
||||
// sort.Slice(performances, func(i, j int) bool {
|
||||
// return performances[i].Profit > performances[j].Profit
|
||||
// })
|
||||
|
||||
// return performances, nil
|
||||
// }
|
||||
|
||||
// GetCashierPerformance returns cashier performance report
|
||||
// func (s *Service) GetCashierPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CashierPerformance, error) {
|
||||
// // Get cashier bet activity
|
||||
// cashierBets, err := s.betStore.GetCashierBetActivity(ctx, filter)
|
||||
// if err != nil {
|
||||
// s.logger.Error("failed to get cashier bet activity", "error", err)
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// // Get cashier details
|
||||
// cashierDetails, err := s.userStore.GetCashierDetails(ctx, filter)
|
||||
// if err != nil {
|
||||
// s.logger.Error("failed to get cashier details", "error", err)
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// // Get cashier transactions
|
||||
// cashierTransactions, err := s.transactionStore.GetCashierTransactionTotals(ctx, filter)
|
||||
// if err != nil {
|
||||
// s.logger.Error("failed to get cashier transactions", "error", err)
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// // Combine data into performance report
|
||||
// var performances []domain.CashierPerformance
|
||||
// for _, bet := range cashierBets {
|
||||
// performance := domain.CashierPerformance{
|
||||
// CashierID: bet.CashierID,
|
||||
// TotalBets: bet.TotalBets,
|
||||
// TotalStakes: bet.TotalStakes,
|
||||
// TotalWins: bet.TotalWins,
|
||||
// TotalPayouts: bet.TotalPayouts,
|
||||
// Profit: bet.TotalStakes - bet.TotalPayouts,
|
||||
// }
|
||||
|
||||
// // Add cashier details
|
||||
// if details, ok := cashierDetails[bet.CashierID]; ok {
|
||||
// performance.CashierName = details.Name
|
||||
// performance.BranchID = details.BranchID
|
||||
// performance.BranchName = details.BranchName
|
||||
// }
|
||||
|
||||
// // Add transactions
|
||||
// if transactions, ok := cashierTransactions[bet.CashierID]; ok {
|
||||
// performance.Deposits = transactions.Deposits
|
||||
// performance.Withdrawals = transactions.Withdrawals
|
||||
// }
|
||||
|
||||
// // Calculate metrics
|
||||
// if bet.TotalBets > 0 {
|
||||
// performance.WinRate = float64(bet.TotalWins) / float64(bet.TotalBets) * 100
|
||||
// performance.AverageStake = bet.TotalStakes / domain.Currency(bet.TotalBets)
|
||||
// }
|
||||
|
||||
// performances = append(performances, performance)
|
||||
// }
|
||||
|
||||
// // Sort by total stakes (descending)
|
||||
// sort.Slice(performances, func(i, j int) bool {
|
||||
// return performances[i].TotalStakes > performances[j].TotalStakes
|
||||
// })
|
||||
|
||||
// return performances, nil
|
||||
// }
|
||||
|
||||
// GetNotificationReport returns notification statistics report
|
||||
// func (s *Service) GetNotificationReport(ctx context.Context, filter domain.ReportFilter) (domain.NotificationReport, error) {
|
||||
// // Get notification counts by type
|
||||
// countsByType, err := s.notificationStore.GetNotificationCountsByType(ctx, filter)
|
||||
// if err != nil {
|
||||
// s.logger.Error("failed to get notification counts by type", "error", err)
|
||||
// return domain.NotificationReport{}, err
|
||||
// }
|
||||
|
||||
// // Get notification delivery stats
|
||||
// deliveryStats, err := s.notificationStore.GetNotificationDeliveryStats(ctx, filter)
|
||||
// if err != nil {
|
||||
// s.logger.Error("failed to get notification delivery stats", "error", err)
|
||||
// return domain.NotificationReport{}, err
|
||||
// }
|
||||
|
||||
// // Get most active notification recipients
|
||||
// activeRecipients, err := s.notificationStore.GetMostActiveNotificationRecipients(ctx, filter)
|
||||
// if err != nil {
|
||||
// s.logger.Error("failed to get active notification recipients", "error", err)
|
||||
// return domain.NotificationReport{}, err
|
||||
// }
|
||||
|
||||
// return domain.NotificationReport{
|
||||
// CountsByType: countsByType,
|
||||
// DeliveryStats: deliveryStats,
|
||||
// ActiveRecipients: activeRecipients,
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
// Helper functions
|
||||
func validateTimeRange(filter domain.ReportFilter) error {
|
||||
if filter.StartTime.Valid && filter.EndTime.Valid {
|
||||
|
|
|
|||
|
|
@ -141,7 +141,8 @@ func (s *Service) GetBranchByRole(ctx context.Context, branchID *int64, role dom
|
|||
// var branchID int64
|
||||
// var companyID int64
|
||||
|
||||
if role == domain.RoleAdmin || role == domain.RoleBranchManager || role == domain.RoleSuperAdmin {
|
||||
switch role {
|
||||
case domain.RoleAdmin, domain.RoleBranchManager, domain.RoleSuperAdmin:
|
||||
if branchID == nil {
|
||||
// h.logger.Error("CashoutReq Branch ID is required for this user role")
|
||||
return nil, nil, ErrBranchRequiredForRole
|
||||
|
|
@ -166,14 +167,14 @@ func (s *Service) GetBranchByRole(ctx context.Context, branchID *int64, role dom
|
|||
}
|
||||
|
||||
return &branch.ID, &branch.CompanyID, nil
|
||||
} else if role == domain.RoleCashier {
|
||||
case domain.RoleCashier:
|
||||
branch, err := s.branchSvc.GetBranchByCashier(ctx, userID)
|
||||
if err != nil {
|
||||
// h.logger.Error("CashoutReq failed, branch id invalid")
|
||||
return nil, nil, ErrInvalidBranchID
|
||||
}
|
||||
return &branch.ID, &branch.CompanyID, nil
|
||||
} else {
|
||||
default:
|
||||
return nil, nil, ErrCustomerRoleNotAuthorized
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ func (s *Service) CreateShopDeposit(ctx context.Context, userID int64, role doma
|
|||
return domain.ShopDeposit{}, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return domain.ShopDeposit{}, err
|
||||
}
|
||||
// if err != nil {
|
||||
// return domain.ShopDeposit{}, err
|
||||
// }
|
||||
|
||||
newTransaction, err := s.CreateShopTransaction(ctx, domain.CreateShopTransaction{
|
||||
Amount: domain.Currency(req.Amount),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package wallet
|
|||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/kafka"
|
||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
||||
)
|
||||
|
||||
|
|
@ -13,10 +14,10 @@ type Service struct {
|
|||
notificationStore notificationservice.NotificationStore
|
||||
notificationSvc *notificationservice.Service
|
||||
logger *slog.Logger
|
||||
// userStore user.UserStore
|
||||
kafkaProducer *kafka.Producer
|
||||
}
|
||||
|
||||
func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, notificationSvc *notificationservice.Service, logger *slog.Logger) *Service {
|
||||
func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, notificationSvc *notificationservice.Service, logger *slog.Logger, kafkaProducer *kafka.Producer) *Service {
|
||||
return &Service{
|
||||
walletStore: walletStore,
|
||||
transferStore: transferStore,
|
||||
|
|
@ -24,7 +25,6 @@ func NewService(walletStore WalletStore, transferStore TransferStore, notificati
|
|||
notificationStore: notificationStore,
|
||||
notificationSvc: notificationSvc,
|
||||
logger: logger,
|
||||
// userStore: userStore,
|
||||
// userStore users
|
||||
kafkaProducer: kafkaProducer,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/event"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -76,12 +77,21 @@ func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Cu
|
|||
return err
|
||||
}
|
||||
|
||||
wallet, err := s.GetWalletByID(ctx, id)
|
||||
wallet, err := s.walletStore.GetWalletByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
|
||||
go func() {
|
||||
s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.ID), event.WalletEvent{
|
||||
EventType: event.WalletBalanceUpdated,
|
||||
WalletID: wallet.ID,
|
||||
UserID: wallet.UserID,
|
||||
Balance: balance,
|
||||
Trigger: "UpdateBalance",
|
||||
})
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +107,15 @@ func (s *Service) AddToWallet(
|
|||
return domain.Transfer{}, err
|
||||
}
|
||||
|
||||
// go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
|
||||
go func() {
|
||||
s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.ID), event.WalletEvent{
|
||||
EventType: event.WalletBalanceUpdated,
|
||||
WalletID: wallet.ID,
|
||||
UserID: wallet.UserID,
|
||||
Balance: wallet.Balance + amount,
|
||||
Trigger: "AddToWallet",
|
||||
})
|
||||
}()
|
||||
|
||||
// Log the transfer here for reference
|
||||
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
|
||||
|
|
@ -137,7 +155,15 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
|
|||
return domain.Transfer{}, nil
|
||||
}
|
||||
|
||||
// go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
|
||||
go func() {
|
||||
s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.ID), event.WalletEvent{
|
||||
EventType: event.WalletBalanceUpdated,
|
||||
WalletID: wallet.ID,
|
||||
UserID: wallet.UserID,
|
||||
Balance: wallet.Balance - amount,
|
||||
Trigger: "DeductFromWallet",
|
||||
})
|
||||
}()
|
||||
|
||||
// Log the transfer here for reference
|
||||
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"log/slog"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/arifpay"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bonus"
|
||||
|
|
@ -40,6 +41,7 @@ import (
|
|||
)
|
||||
|
||||
type App struct {
|
||||
arifpaySvc *arifpay.ArifpayService
|
||||
issueReportingSvc *issuereporting.Service
|
||||
instSvc *institutions.Service
|
||||
currSvc *currency.Service
|
||||
|
|
@ -76,6 +78,7 @@ type App struct {
|
|||
}
|
||||
|
||||
func NewApp(
|
||||
arifpaySvc *arifpay.ArifpayService,
|
||||
issueReportingSvc *issuereporting.Service,
|
||||
instSvc *institutions.Service,
|
||||
currSvc *currency.Service,
|
||||
|
|
@ -122,6 +125,7 @@ func NewApp(
|
|||
}))
|
||||
|
||||
s := &App{
|
||||
arifpaySvc: arifpaySvc,
|
||||
issueReportingSvc: issueReportingSvc,
|
||||
instSvc: instSvc,
|
||||
currSvc: currSvc,
|
||||
|
|
|
|||
|
|
@ -114,10 +114,10 @@ func SetupReportCronJobs(ctx context.Context, reportService *report.Service) {
|
|||
spec string
|
||||
period string
|
||||
}{
|
||||
{
|
||||
spec: "*/300 * * * * *", // Every 5 minutes (300 seconds)
|
||||
period: "5min",
|
||||
},
|
||||
// {
|
||||
// spec: "*/300 * * * * *", // Every 5 minutes (300 seconds)
|
||||
// period: "5min",
|
||||
// },
|
||||
{
|
||||
spec: "0 0 0 * * *", // Daily at midnight
|
||||
period: "daily",
|
||||
|
|
|
|||
179
internal/web_server/handlers/arifpay.go
Normal file
179
internal/web_server/handlers/arifpay.go
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// CreateCheckoutSessionHandler initializes a checkout session with Arifpay.
|
||||
//
|
||||
// @Summary Create Arifpay Checkout Session
|
||||
// @Description Creates a payment session using Arifpay and returns a redirect URL.
|
||||
// @Tags Arifpay
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.CreateCheckoutSessionRequest true "Checkout session request payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/arifpay/checkout [post]
|
||||
func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error {
|
||||
var req domain.CreateCheckoutSessionRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Error: err.Error(),
|
||||
Message: "Failed to process your request",
|
||||
})
|
||||
}
|
||||
|
||||
paymentURL, err := h.arifpaySvc.CreateCheckoutSession(req)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Error: err.Error(),
|
||||
Message: "Failed to process your request",
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Checkout session created successfully",
|
||||
Data: paymentURL,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// B2CTransferHandler handles Arifpay B2C transfers based on the transfer_mode.
|
||||
//
|
||||
// @Summary Initiate B2C Transfer
|
||||
// @Description Initiates a B2C transfer via Telebirr, CBE, or MPESA through Arifpay
|
||||
// @Tags Arifpay
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param transfer_mode query string true "Transfer mode (Telebirr, CBE, MPESA)"
|
||||
// @Param request body domain.ArifPayB2CRequest true "Transfer request payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 502 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/arifpay/b2c/transfer [post]
|
||||
func (h *Handler) B2CTransferHandler(c *fiber.Ctx) error {
|
||||
transferMode := c.Query("transfer_mode")
|
||||
|
||||
var endpoint string
|
||||
switch transferMode {
|
||||
case "Telebirr":
|
||||
endpoint = "https://telebirr-b2c.arifpay.net/api/Telebirr/b2c/transfer"
|
||||
case "CBE":
|
||||
endpoint = "https://cbe-b2c.arifpay.net/api/Cbebirr/b2c/transfer"
|
||||
case "MPESA":
|
||||
endpoint = "https://mpesa-b2c.arifpay.net/api/Mpesa/b2c/transfer"
|
||||
default:
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Error: "invalid transfer_mode. Allowed values: Telebirr, CBE, MPESA",
|
||||
Message: "Failed to process your request",
|
||||
})
|
||||
}
|
||||
|
||||
var req domain.ArifPayB2CRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Error: err.Error(),
|
||||
Message: "Failed to process your request",
|
||||
})
|
||||
}
|
||||
|
||||
resp, err := h.arifpaySvc.B2CTransfer(c.Context(), req, endpoint)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
|
||||
Error: err.Error(),
|
||||
Message: "Failed to process your request",
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Transfer initiated successfully",
|
||||
Data: resp,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// ArifpayVerifyByTransactionIDHandler godoc
|
||||
// @Summary Verify Arifpay Transaction
|
||||
// @Description Verifies a transaction using transaction ID and payment type
|
||||
// @Tags Arifpay
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.ArifpayVerifyByTransactionIDRequest true "Transaction verification payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 502 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/arifpay/transaction-id/verify-transaction [post]
|
||||
func (h *Handler) ArifpayVerifyByTransactionIDHandler(c *fiber.Ctx) error {
|
||||
var req domain.ArifpayVerifyByTransactionIDRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Error: err.Error(),
|
||||
Message: "Failed to parse request body",
|
||||
})
|
||||
}
|
||||
|
||||
if req.TransactionID == "" || req.PaymentType == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Error: "missing transactionId or paymentType",
|
||||
Message: "transactionId and paymentType are required fields",
|
||||
})
|
||||
}
|
||||
|
||||
resp, err := h.arifpaySvc.VerifyByTransactionID(req.TransactionID, req.PaymentType)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
|
||||
Error: err.Error(),
|
||||
Message: "Failed to verify transaction",
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Transaction verified successfully",
|
||||
Data: json.RawMessage(resp),
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// ArifpayVerifyBySessionIDHandler godoc
|
||||
// @Summary Verify Arifpay Transaction by Session ID
|
||||
// @Description Verifies an Arifpay transaction using a session ID
|
||||
// @Tags Arifpay
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param session_id query string true "Arifpay Session ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 502 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/arifpay/session-id/verify-transaction/{session_id} [get]
|
||||
func (h *Handler) ArifpayVerifyBySessionIDHandler(c *fiber.Ctx) error {
|
||||
sessionID := c.Query("session_id")
|
||||
if sessionID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Error: "missing session_id",
|
||||
Message: "session_id query parameter is required",
|
||||
})
|
||||
}
|
||||
|
||||
resp, err := h.arifpaySvc.VerifyBySessionID(sessionID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
|
||||
Error: err.Error(),
|
||||
Message: "Failed to verify session",
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Session verified successfully",
|
||||
Data: json.RawMessage(resp),
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"log/slog"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/arifpay"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bonus"
|
||||
|
|
@ -35,6 +36,7 @@ import (
|
|||
)
|
||||
|
||||
type Handler struct {
|
||||
arifpaySvc *arifpay.ArifpayService
|
||||
issueReportingSvc *issuereporting.Service
|
||||
instSvc *institutions.Service
|
||||
currSvc *currency.Service
|
||||
|
|
@ -68,6 +70,7 @@ type Handler struct {
|
|||
}
|
||||
|
||||
func New(
|
||||
arifpaySvc *arifpay.ArifpayService,
|
||||
issueReportingSvc *issuereporting.Service,
|
||||
instSvc *institutions.Service,
|
||||
currSvc *currency.Service,
|
||||
|
|
@ -100,6 +103,7 @@ func New(
|
|||
mongoLoggerSvc *zap.Logger,
|
||||
) *Handler {
|
||||
return &Handler{
|
||||
arifpaySvc: arifpaySvc,
|
||||
issueReportingSvc: issueReportingSvc,
|
||||
instSvc: instSvc,
|
||||
currSvc: currSvc,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
|
||||
func (a *App) initAppRoutes() {
|
||||
h := handlers.New(
|
||||
a.arifpaySvc,
|
||||
a.issueReportingSvc,
|
||||
a.instSvc,
|
||||
a.currSvc,
|
||||
|
|
@ -58,6 +59,7 @@ func (a *App) initAppRoutes() {
|
|||
"version": "1.0dev10",
|
||||
})
|
||||
})
|
||||
|
||||
// Swagger
|
||||
a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler())
|
||||
|
||||
|
|
@ -68,6 +70,7 @@ func (a *App) initAppRoutes() {
|
|||
"version": "1.0dev10",
|
||||
})
|
||||
})
|
||||
|
||||
// Auth Routes
|
||||
groupV1.Post("/auth/login", h.LoginCustomer)
|
||||
groupV1.Post("/auth/refresh", h.RefreshToken)
|
||||
|
|
@ -98,6 +101,12 @@ func (a *App) initAppRoutes() {
|
|||
return c.SendString("Test endpoint")
|
||||
})
|
||||
|
||||
//Arifpay
|
||||
groupV1.Post("/arifpay/checkout", a.authMiddleware, h.CreateCheckoutSessionHandler)
|
||||
groupV1.Post("/arifpay/b2c/transfer", a.authMiddleware, h.B2CTransferHandler)
|
||||
groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler)
|
||||
groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler)
|
||||
|
||||
// User Routes
|
||||
groupV1.Post("/user/resetPassword", h.ResetPassword)
|
||||
groupV1.Post("/user/sendResetCode", h.SendResetCode)
|
||||
|
|
@ -225,11 +234,11 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet)
|
||||
|
||||
//Chapa Routes
|
||||
a.fiber.Post("/chapa/payments/webhook/verify", h.WebhookCallback)
|
||||
a.fiber.Get("/chapa/payments/manual/verify/:tx_ref", h.ManualVerifyTransaction)
|
||||
a.fiber.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit)
|
||||
a.fiber.Post("/chapa/payments/withdraw", a.authMiddleware, h.InitiateWithdrawal)
|
||||
a.fiber.Get("/chapa/banks", h.GetSupportedBanks)
|
||||
groupV1.Post("/chapa/payments/webhook/verify", h.WebhookCallback)
|
||||
groupV1.Get("/chapa/payments/manual/verify/:tx_ref", h.ManualVerifyTransaction)
|
||||
groupV1.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit)
|
||||
groupV1.Post("/chapa/payments/withdraw", a.authMiddleware, h.InitiateWithdrawal)
|
||||
groupV1.Get("/chapa/banks", h.GetSupportedBanks)
|
||||
|
||||
// Currencies
|
||||
groupV1.Get("/currencies", h.GetSupportedCurrencies)
|
||||
|
|
@ -240,26 +249,6 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Get("/report-files/download/:filename", a.authMiddleware, a.OnlyAdminAndAbove, h.DownloadReportFile)
|
||||
groupV1.Get("/report-files/list", a.authMiddleware, a.OnlyAdminAndAbove, h.ListReportFiles)
|
||||
|
||||
//Wallet Monitor Service
|
||||
// group.Get("/debug/wallet-monitor/status", func(c *fiber.Ctx) error {
|
||||
// return c.JSON(fiber.Map{
|
||||
// "running": monitor.IsRunning(),
|
||||
// "last_check": walletMonitorSvc.LastCheckTime(),
|
||||
// })
|
||||
// })
|
||||
|
||||
// group.Post("/debug/wallet-monitor/trigger", func(c *fiber.Ctx) error {
|
||||
// walletMonitorSvc.ForceCheck()
|
||||
// return c.SendStatus(fiber.StatusOK)
|
||||
// })
|
||||
|
||||
// group.Post("/chapa/payments/initialize", h.InitializePayment)
|
||||
// group.Get("/chapa/payments/verify/:tx_ref", h.VerifyTransaction)
|
||||
// group.Post("/chapa/payments/callback", h.ReceiveWebhook)
|
||||
// group.Get("/chapa/banks", h.GetBanks)
|
||||
// group.Post("/chapa/transfers", h.CreateTransfer)
|
||||
// group.Get("/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer)
|
||||
|
||||
//Alea Play Virtual Game Routes
|
||||
groupV1.Get("/alea-play/launch", a.authMiddleware, h.LaunchAleaGame)
|
||||
groupV1.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback)
|
||||
|
|
@ -270,14 +259,10 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Post("/veli/start-game", a.authMiddleware, h.StartGame)
|
||||
groupV1.Post("/veli/start-demo-game", a.authMiddleware, h.StartDemoGame)
|
||||
a.fiber.Post("/balance", h.GetBalance)
|
||||
// a.fiber.Post("/bet", h.PlaceBet)
|
||||
// a.fiber.Post("/win", h.RegisterWin)
|
||||
// a.fiber.Post("/cancel", h.CancelTransaction)
|
||||
groupV1.Post("/veli/gaming-activity", h.GetGamingActivity)
|
||||
|
||||
//mongoDB logs
|
||||
ctx := context.Background()
|
||||
groupV1.Get("/logs", a.authMiddleware, a.SuperAdminOnly, handlers.GetLogsHandler(ctx))
|
||||
groupV1.Get("/logs", a.authMiddleware, a.SuperAdminOnly, handlers.GetLogsHandler(context.Background()))
|
||||
|
||||
// Recommendation Routes
|
||||
// group.Get("/virtual-games/recommendations/:userID", h.GetRecommendations)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,14 @@ package ws
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/event"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/valyala/fasthttp/fasthttpadaptor"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
|
|
@ -36,7 +41,6 @@ func (h *NotificationHub) Run() {
|
|||
h.mu.Lock()
|
||||
h.Clients[client] = true
|
||||
h.mu.Unlock()
|
||||
// log.Printf("Client registered: %d", client.RecipientID)
|
||||
case client := <-h.Unregister:
|
||||
h.mu.Lock()
|
||||
if _, ok := h.Clients[client]; ok {
|
||||
|
|
@ -44,7 +48,6 @@ func (h *NotificationHub) Run() {
|
|||
client.Conn.Close()
|
||||
}
|
||||
h.mu.Unlock()
|
||||
// log.Printf("Client unregistered: %d", client.RecipientID)
|
||||
case message := <-h.Broadcast:
|
||||
h.mu.Lock()
|
||||
for client := range h.Clients {
|
||||
|
|
@ -70,3 +73,102 @@ var Upgrader = websocket.Upgrader{
|
|||
return true
|
||||
},
|
||||
}
|
||||
|
||||
type responseWriterWrapper struct {
|
||||
ctx *fasthttp.RequestCtx
|
||||
header http.Header
|
||||
wrote bool
|
||||
}
|
||||
|
||||
func newResponseWriterWrapper(ctx *fasthttp.RequestCtx) *responseWriterWrapper {
|
||||
return &responseWriterWrapper{
|
||||
ctx: ctx,
|
||||
header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *responseWriterWrapper) Header() http.Header {
|
||||
return rw.header
|
||||
}
|
||||
|
||||
func (rw *responseWriterWrapper) Write(b []byte) (int, error) {
|
||||
rw.writeHeaderIfNeeded(http.StatusOK)
|
||||
return rw.ctx.Write(b)
|
||||
}
|
||||
|
||||
func (rw *responseWriterWrapper) WriteHeader(statusCode int) {
|
||||
if rw.wrote {
|
||||
return
|
||||
}
|
||||
rw.writeHeaderIfNeeded(statusCode)
|
||||
}
|
||||
|
||||
func (rw *responseWriterWrapper) writeHeaderIfNeeded(statusCode int) {
|
||||
if rw.wrote {
|
||||
return
|
||||
}
|
||||
rw.wrote = true
|
||||
|
||||
// Copy headers from rw.header to fasthttp.Response.Header
|
||||
for k, vv := range rw.header {
|
||||
for _, v := range vv {
|
||||
rw.ctx.Response.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
rw.ctx.SetStatusCode(statusCode)
|
||||
}
|
||||
|
||||
// ✅ WebSocketHandler integrates Gorilla WebSocket with Fiber
|
||||
func (h *NotificationHub) WebSocketHandler() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
userIDStr := c.Params("user_id")
|
||||
userID, err := strconv.ParseInt(userIDStr, 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).SendString("Invalid user ID")
|
||||
}
|
||||
|
||||
// Use your custom responseWriterWrapper here
|
||||
rw := newResponseWriterWrapper(c.Context())
|
||||
|
||||
stdReq := new(http.Request)
|
||||
if err := fasthttpadaptor.ConvertRequest(c.Context(), stdReq, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := Upgrader.Upgrade(rw, stdReq, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
Conn: conn,
|
||||
RecipientID: userID,
|
||||
}
|
||||
|
||||
h.Register <- client
|
||||
defer func() {
|
||||
h.Unregister <- client
|
||||
}()
|
||||
|
||||
for {
|
||||
if _, _, err := conn.ReadMessage(); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *NotificationHub) BroadcastWalletUpdate(userID int64, event event.WalletEvent) {
|
||||
payload := map[string]interface{}{
|
||||
"type": event.EventType,
|
||||
"wallet_id": event.WalletID,
|
||||
"user_id": event.UserID,
|
||||
"balance": event.Balance,
|
||||
"trigger": event.Trigger,
|
||||
"recipient_id": userID,
|
||||
}
|
||||
h.Broadcast <- payload
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user