Merge branch 'main' into ticket-bet

This commit is contained in:
Samuel Tariku 2025-06-12 19:11:35 +03:00
commit 2a6e892f5e
49 changed files with 2523 additions and 2230 deletions

View File

@ -5,6 +5,7 @@ import (
// "context" // "context"
"fmt" "fmt"
"log"
"log/slog" "log/slog"
"os" "os"
"time" "time"
@ -15,6 +16,7 @@ import (
// "github.com/gofiber/fiber/v2" // "github.com/gofiber/fiber/v2"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger" customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
"github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger" "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
@ -82,8 +84,13 @@ func main() {
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel) logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
mongoLogger.Init() domain.MongoDBLogger, err = mongoLogger.InitLogger()
mongoDBLogger := zap.L() if err != nil {
log.Fatalf("Logger initialization failed: %v", err)
}
defer domain.MongoDBLogger.Sync()
zap.ReplaceGlobals(domain.MongoDBLogger)
// client := mongoLogger.InitDB() // client := mongoLogger.InitDB()
// defer func() { // defer func() {
@ -119,6 +126,7 @@ func main() {
oddsSvc := odds.New(store, cfg, logger) oddsSvc := odds.New(store, cfg, logger)
ticketSvc := ticket.NewService(store) ticketSvc := ticket.NewService(store)
notificationRepo := repository.NewNotificationRepository(store) notificationRepo := repository.NewNotificationRepository(store)
virtuaGamesRepo := repository.NewVirtualGameRepository(store)
notificationSvc := notificationservice.New(notificationRepo, logger, cfg) notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
@ -140,7 +148,7 @@ func main() {
branchSvc := branch.NewService(store) branchSvc := branch.NewService(store)
companySvc := company.NewService(store) companySvc := company.NewService(store)
leagueSvc := league.New(store) leagueSvc := league.New(store)
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, mongoDBLogger) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger)
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc) resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc)
referalRepo := repository.NewReferralRepository(store) referalRepo := repository.NewReferralRepository(store)
vitualGameRepo := repository.NewVirtualGameRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store)
@ -164,13 +172,10 @@ func main() {
chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY) chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY)
chapaSvc := chapa.NewService( chapaSvc := chapa.NewService(
transaction.TransactionStore(store), wallet.TransferStore(store),
wallet.WalletStore(store), wallet.WalletStore(store),
user.UserStore(store), user.UserStore(store),
referalSvc,
branch.BranchStore(store),
chapaClient, chapaClient,
store,
) )
reportSvc := report.NewService( reportSvc := report.NewService(
@ -179,6 +184,9 @@ func main() {
transaction.TransactionStore(store), transaction.TransactionStore(store),
branch.BranchStore(store), branch.BranchStore(store),
user.UserStore(store), user.UserStore(store),
company.CompanyStore(store),
virtuaGamesRepo,
notificationRepo,
logger, logger,
) )

View File

@ -129,6 +129,7 @@ CREATE TABLE IF NOT EXISTS wallet_transfer (
sender_wallet_id BIGINT, sender_wallet_id BIGINT,
cashier_id BIGINT, cashier_id BIGINT,
verified BOOLEAN NOT NULL DEFAULT false, verified BOOLEAN NOT NULL DEFAULT false,
reference_number VARCHAR(255) NOT NULL,
payment_method VARCHAR(255) NOT NULL, payment_method VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

View File

@ -1,3 +1,19 @@
CREATE TABLE IF NOT EXISTS virtual_games (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
provider VARCHAR(255) NOT NULL,
category VARCHAR(100),
min_bet NUMERIC(10, 2) NOT NULL,
max_bet NUMERIC(10, 2) NOT NULL,
volatility VARCHAR(50),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
rtp NUMERIC(5, 2) CHECK (rtp >= 0 AND rtp <= 100),
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
popularity_score INT DEFAULT 0,
thumbnail_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE TABLE virtual_game_sessions ( CREATE TABLE virtual_game_sessions (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id), user_id BIGINT NOT NULL REFERENCES users(id),

View File

@ -69,3 +69,10 @@ LIMIT $1;
SELECT recipient_id SELECT recipient_id
FROM notifications FROM notifications
WHERE reciever = $1; WHERE reciever = $1;
-- name: GetNotificationCounts :many
SELECT
COUNT(*) as total,
COUNT(CASE WHEN is_read = true THEN 1 END) as read,
COUNT(CASE WHEN is_read = false THEN 1 END) as unread
FROM notifications;

View File

@ -6,9 +6,10 @@ INSERT INTO wallet_transfer (
sender_wallet_id, sender_wallet_id,
cashier_id, cashier_id,
verified, verified,
reference_number,
payment_method payment_method
) )
VALUES ($1, $2, $3, $4, $5, $6, $7) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *; RETURNING *;
-- name: GetAllTransfers :many -- name: GetAllTransfers :many
SELECT * SELECT *
@ -22,6 +23,10 @@ WHERE receiver_wallet_id = $1
SELECT * SELECT *
FROM wallet_transfer FROM wallet_transfer
WHERE id = $1; WHERE id = $1;
-- name: GetTransferByReference :one
SELECT *
FROM wallet_transfer
WHERE reference_number = $1;
-- name: UpdateTransferVerification :exec -- name: UpdateTransferVerification :exec
UPDATE wallet_transfer UPDATE wallet_transfer
SET verified = $1, SET verified = $1,

View File

@ -304,8 +304,9 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/chapa/banks": { "/api/v1/chapa/payments/deposit": {
"get": { "post": {
"description": "Starts a new deposit process using Chapa payment gateway",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -315,50 +316,43 @@ const docTemplate = `{
"tags": [ "tags": [
"Chapa" "Chapa"
], ],
"summary": "fetches chapa supported banks", "summary": "Initiate a deposit",
"parameters": [
{
"description": "Deposit request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaDepositRequestPayload"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaSupportedBanksResponseWrapper" "$ref": "#/definitions/domain.ChapaDepositResponse"
} }
}, },
"400": { "400": {
"description": "Bad Request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/domain.Response"
} }
}, },
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
} }
}, },
"/api/v1/chapa/payments/deposit": { "/api/v1/chapa/payments/manual/verify/{tx_ref}": {
"post": { "get": {
"description": "Deposits money into user wallet from user account using Chapa", "description": "Manually verify a payment using Chapa's API",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -368,48 +362,41 @@ const docTemplate = `{
"tags": [ "tags": [
"Chapa" "Chapa"
], ],
"summary": "Deposit money into user wallet using Chapa", "summary": "Verify a payment manually",
"parameters": [ "parameters": [
{ {
"description": "Deposit request payload", "type": "string",
"name": "payload", "description": "Transaction Reference",
"in": "body", "name": "tx_ref",
"required": true, "in": "path",
"schema": { "required": true
"$ref": "#/definitions/domain.ChapaDepositRequest"
}
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaPaymentUrlResponseWrapper" "$ref": "#/definitions/domain.ChapaVerificationResponse"
} }
}, },
"400": { "400": {
"description": "Invalid request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.ErrorResponse"
}
},
"422": {
"description": "Validation error",
"schema": {
"$ref": "#/definitions/domain.Response"
} }
}, },
"500": { "500": {
"description": "Internal server error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
} }
}, },
"/api/v1/chapa/payments/verify": { "/api/v1/chapa/payments/webhook/verify": {
"post": { "post": {
"description": "Handles payment notifications from Chapa",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -419,93 +406,36 @@ const docTemplate = `{
"tags": [ "tags": [
"Chapa" "Chapa"
], ],
"summary": "Verifies Chapa webhook transaction", "summary": "Chapa payment webhook callback (used by Chapa)",
"parameters": [ "parameters": [
{ {
"description": "Webhook Payload", "description": "Webhook payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaTransactionType"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/payments/withdraw": {
"post": {
"description": "Initiates a withdrawal transaction using Chapa for the authenticated user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Withdraw using Chapa",
"parameters": [
{
"description": "Chapa Withdraw Request",
"name": "request", "name": "request",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaWithdrawRequest" "$ref": "#/definitions/domain.ChapaWebhookPayload"
} }
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Withdrawal requested successfully", "description": "OK",
"schema": { "schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object", "type": "object",
"properties": { "additionalProperties": true
"data": {
"type": "string"
}
}
}
]
} }
}, },
"400": { "400": {
"description": "Invalid request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/domain.Response"
} }
}, },
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
@ -914,6 +844,38 @@ const docTemplate = `{
} }
} }
}, },
"/banks": {
"get": {
"description": "Get list of banks supported by Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Get supported banks",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.Bank"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/bet": { "/bet": {
"get": { "get": {
"description": "Gets all the bets", "description": "Gets all the bets",
@ -4436,6 +4398,55 @@ const docTemplate = `{
} }
} }
}, },
"domain.Bank": {
"type": "object",
"properties": {
"acct_length": {
"type": "integer"
},
"active": {
"type": "integer"
},
"country_id": {
"type": "integer"
},
"created_at": {
"type": "string"
},
"currency": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_24hrs": {
"description": "nullable",
"type": "integer"
},
"is_active": {
"type": "integer"
},
"is_mobilemoney": {
"description": "nullable",
"type": "integer"
},
"is_rtgs": {
"type": "integer"
},
"name": {
"type": "string"
},
"slug": {
"type": "string"
},
"swift": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"domain.BetOutcome": { "domain.BetOutcome": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4568,152 +4579,62 @@ const docTemplate = `{
} }
} }
}, },
"domain.ChapaDepositRequest": { "domain.ChapaDepositRequestPayload": {
"type": "object",
"required": [
"amount"
],
"properties": {
"amount": {
"type": "number"
}
}
},
"domain.ChapaDepositResponse": {
"type": "object",
"properties": {
"checkoutURL": {
"type": "string"
},
"reference": {
"type": "string"
}
}
},
"domain.ChapaVerificationResponse": {
"type": "object",
"properties": {
"amount": {
"type": "number"
},
"currency": {
"type": "string"
},
"status": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.ChapaWebhookPayload": {
"type": "object", "type": "object",
"properties": { "properties": {
"amount": { "amount": {
"type": "integer" "type": "integer"
}, },
"branch_id": {
"type": "integer"
},
"currency": { "currency": {
"type": "string" "type": "string"
}, },
"phone_number": { "status": {
"$ref": "#/definitions/domain.PaymentStatus"
},
"tx_ref": {
"type": "string" "type": "string"
} }
} }
}, },
"domain.ChapaPaymentUrlResponse": {
"type": "object",
"properties": {
"payment_url": {
"type": "string"
}
}
},
"domain.ChapaPaymentUrlResponseWrapper": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.ChapaSupportedBank": {
"type": "object",
"properties": {
"acct_length": {
"type": "integer"
},
"acct_number_regex": {
"type": "string"
},
"active": {
"type": "integer"
},
"country_id": {
"type": "integer"
},
"created_at": {
"type": "string"
},
"currency": {
"type": "string"
},
"example_value": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_24hrs": {
"type": "integer"
},
"is_active": {
"type": "integer"
},
"is_mobilemoney": {
"type": "integer"
},
"is_rtgs": {
"type": "integer"
},
"name": {
"type": "string"
},
"slug": {
"type": "string"
},
"swift": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"domain.ChapaSupportedBanksResponseWrapper": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.ChapaTransactionType": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
}
},
"domain.ChapaWithdrawRequest": {
"type": "object",
"properties": {
"account_name": {
"type": "string"
},
"account_number": {
"type": "string"
},
"amount": {
"type": "integer"
},
"bank_code": {
"type": "string"
},
"beneficiary_name": {
"type": "string"
},
"branch_id": {
"type": "integer"
},
"currency": {
"type": "string"
},
"wallet_id": {
"description": "add this",
"type": "integer"
}
}
},
"domain.CreateBetOutcomeReq": { "domain.CreateBetOutcomeReq": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4867,6 +4788,19 @@ const docTemplate = `{
"BANK" "BANK"
] ]
}, },
"domain.PaymentStatus": {
"type": "string",
"enum": [
"pending",
"completed",
"failed"
],
"x-enum-varnames": [
"PaymentStatusPending",
"PaymentStatusCompleted",
"PaymentStatusFailed"
]
},
"domain.PopOKCallback": { "domain.PopOKCallback": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5007,21 +4941,6 @@ const docTemplate = `{
} }
} }
}, },
"domain.Response": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.Role": { "domain.Role": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -5117,7 +5036,7 @@ const docTemplate = `{
}, },
"awayTeamID": { "awayTeamID": {
"description": "Away team ID (can be empty/null)", "description": "Away team ID (can be empty/null)",
"type": "string" "type": "integer"
}, },
"homeKitImage": { "homeKitImage": {
"description": "Kit or image for home team (optional)", "description": "Kit or image for home team (optional)",
@ -5129,7 +5048,7 @@ const docTemplate = `{
}, },
"homeTeamID": { "homeTeamID": {
"description": "Home team ID", "description": "Home team ID",
"type": "string" "type": "integer"
}, },
"id": { "id": {
"description": "Event ID", "description": "Event ID",
@ -5141,7 +5060,7 @@ const docTemplate = `{
}, },
"leagueID": { "leagueID": {
"description": "League ID", "description": "League ID",
"type": "string" "type": "integer"
}, },
"leagueName": { "leagueName": {
"description": "League name", "description": "League name",
@ -5157,7 +5076,7 @@ const docTemplate = `{
}, },
"sportID": { "sportID": {
"description": "Sport ID", "description": "Sport ID",
"type": "string" "type": "integer"
}, },
"startTime": { "startTime": {
"description": "Converted from \"time\" field in UNIX format", "description": "Converted from \"time\" field in UNIX format",

View File

@ -296,8 +296,9 @@
} }
} }
}, },
"/api/v1/chapa/banks": { "/api/v1/chapa/payments/deposit": {
"get": { "post": {
"description": "Starts a new deposit process using Chapa payment gateway",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -307,50 +308,43 @@
"tags": [ "tags": [
"Chapa" "Chapa"
], ],
"summary": "fetches chapa supported banks", "summary": "Initiate a deposit",
"parameters": [
{
"description": "Deposit request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaDepositRequestPayload"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaSupportedBanksResponseWrapper" "$ref": "#/definitions/domain.ChapaDepositResponse"
} }
}, },
"400": { "400": {
"description": "Bad Request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/domain.Response"
} }
}, },
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
} }
}, },
"/api/v1/chapa/payments/deposit": { "/api/v1/chapa/payments/manual/verify/{tx_ref}": {
"post": { "get": {
"description": "Deposits money into user wallet from user account using Chapa", "description": "Manually verify a payment using Chapa's API",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -360,48 +354,41 @@
"tags": [ "tags": [
"Chapa" "Chapa"
], ],
"summary": "Deposit money into user wallet using Chapa", "summary": "Verify a payment manually",
"parameters": [ "parameters": [
{ {
"description": "Deposit request payload", "type": "string",
"name": "payload", "description": "Transaction Reference",
"in": "body", "name": "tx_ref",
"required": true, "in": "path",
"schema": { "required": true
"$ref": "#/definitions/domain.ChapaDepositRequest"
}
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaPaymentUrlResponseWrapper" "$ref": "#/definitions/domain.ChapaVerificationResponse"
} }
}, },
"400": { "400": {
"description": "Invalid request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.ErrorResponse"
}
},
"422": {
"description": "Validation error",
"schema": {
"$ref": "#/definitions/domain.Response"
} }
}, },
"500": { "500": {
"description": "Internal server error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
} }
}, },
"/api/v1/chapa/payments/verify": { "/api/v1/chapa/payments/webhook/verify": {
"post": { "post": {
"description": "Handles payment notifications from Chapa",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -411,93 +398,36 @@
"tags": [ "tags": [
"Chapa" "Chapa"
], ],
"summary": "Verifies Chapa webhook transaction", "summary": "Chapa payment webhook callback (used by Chapa)",
"parameters": [ "parameters": [
{ {
"description": "Webhook Payload", "description": "Webhook payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaTransactionType"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/payments/withdraw": {
"post": {
"description": "Initiates a withdrawal transaction using Chapa for the authenticated user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Withdraw using Chapa",
"parameters": [
{
"description": "Chapa Withdraw Request",
"name": "request", "name": "request",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaWithdrawRequest" "$ref": "#/definitions/domain.ChapaWebhookPayload"
} }
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Withdrawal requested successfully", "description": "OK",
"schema": { "schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object", "type": "object",
"properties": { "additionalProperties": true
"data": {
"type": "string"
}
}
}
]
} }
}, },
"400": { "400": {
"description": "Invalid request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/domain.Response"
} }
}, },
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/domain.Response" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
@ -906,6 +836,38 @@
} }
} }
}, },
"/banks": {
"get": {
"description": "Get list of banks supported by Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Get supported banks",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.Bank"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/bet": { "/bet": {
"get": { "get": {
"description": "Gets all the bets", "description": "Gets all the bets",
@ -4428,6 +4390,55 @@
} }
} }
}, },
"domain.Bank": {
"type": "object",
"properties": {
"acct_length": {
"type": "integer"
},
"active": {
"type": "integer"
},
"country_id": {
"type": "integer"
},
"created_at": {
"type": "string"
},
"currency": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_24hrs": {
"description": "nullable",
"type": "integer"
},
"is_active": {
"type": "integer"
},
"is_mobilemoney": {
"description": "nullable",
"type": "integer"
},
"is_rtgs": {
"type": "integer"
},
"name": {
"type": "string"
},
"slug": {
"type": "string"
},
"swift": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"domain.BetOutcome": { "domain.BetOutcome": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4560,152 +4571,62 @@
} }
} }
}, },
"domain.ChapaDepositRequest": { "domain.ChapaDepositRequestPayload": {
"type": "object",
"required": [
"amount"
],
"properties": {
"amount": {
"type": "number"
}
}
},
"domain.ChapaDepositResponse": {
"type": "object",
"properties": {
"checkoutURL": {
"type": "string"
},
"reference": {
"type": "string"
}
}
},
"domain.ChapaVerificationResponse": {
"type": "object",
"properties": {
"amount": {
"type": "number"
},
"currency": {
"type": "string"
},
"status": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.ChapaWebhookPayload": {
"type": "object", "type": "object",
"properties": { "properties": {
"amount": { "amount": {
"type": "integer" "type": "integer"
}, },
"branch_id": {
"type": "integer"
},
"currency": { "currency": {
"type": "string" "type": "string"
}, },
"phone_number": { "status": {
"$ref": "#/definitions/domain.PaymentStatus"
},
"tx_ref": {
"type": "string" "type": "string"
} }
} }
}, },
"domain.ChapaPaymentUrlResponse": {
"type": "object",
"properties": {
"payment_url": {
"type": "string"
}
}
},
"domain.ChapaPaymentUrlResponseWrapper": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.ChapaSupportedBank": {
"type": "object",
"properties": {
"acct_length": {
"type": "integer"
},
"acct_number_regex": {
"type": "string"
},
"active": {
"type": "integer"
},
"country_id": {
"type": "integer"
},
"created_at": {
"type": "string"
},
"currency": {
"type": "string"
},
"example_value": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_24hrs": {
"type": "integer"
},
"is_active": {
"type": "integer"
},
"is_mobilemoney": {
"type": "integer"
},
"is_rtgs": {
"type": "integer"
},
"name": {
"type": "string"
},
"slug": {
"type": "string"
},
"swift": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"domain.ChapaSupportedBanksResponseWrapper": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.ChapaTransactionType": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
}
},
"domain.ChapaWithdrawRequest": {
"type": "object",
"properties": {
"account_name": {
"type": "string"
},
"account_number": {
"type": "string"
},
"amount": {
"type": "integer"
},
"bank_code": {
"type": "string"
},
"beneficiary_name": {
"type": "string"
},
"branch_id": {
"type": "integer"
},
"currency": {
"type": "string"
},
"wallet_id": {
"description": "add this",
"type": "integer"
}
}
},
"domain.CreateBetOutcomeReq": { "domain.CreateBetOutcomeReq": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4859,6 +4780,19 @@
"BANK" "BANK"
] ]
}, },
"domain.PaymentStatus": {
"type": "string",
"enum": [
"pending",
"completed",
"failed"
],
"x-enum-varnames": [
"PaymentStatusPending",
"PaymentStatusCompleted",
"PaymentStatusFailed"
]
},
"domain.PopOKCallback": { "domain.PopOKCallback": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4999,21 +4933,6 @@
} }
} }
}, },
"domain.Response": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.Role": { "domain.Role": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -5109,7 +5028,7 @@
}, },
"awayTeamID": { "awayTeamID": {
"description": "Away team ID (can be empty/null)", "description": "Away team ID (can be empty/null)",
"type": "string" "type": "integer"
}, },
"homeKitImage": { "homeKitImage": {
"description": "Kit or image for home team (optional)", "description": "Kit or image for home team (optional)",
@ -5121,7 +5040,7 @@
}, },
"homeTeamID": { "homeTeamID": {
"description": "Home team ID", "description": "Home team ID",
"type": "string" "type": "integer"
}, },
"id": { "id": {
"description": "Event ID", "description": "Event ID",
@ -5133,7 +5052,7 @@
}, },
"leagueID": { "leagueID": {
"description": "League ID", "description": "League ID",
"type": "string" "type": "integer"
}, },
"leagueName": { "leagueName": {
"description": "League name", "description": "League name",
@ -5149,7 +5068,7 @@
}, },
"sportID": { "sportID": {
"description": "Sport ID", "description": "Sport ID",
"type": "string" "type": "integer"
}, },
"startTime": { "startTime": {
"description": "Converted from \"time\" field in UNIX format", "description": "Converted from \"time\" field in UNIX format",

View File

@ -31,6 +31,39 @@ definitions:
user_id: user_id:
type: string type: string
type: object type: object
domain.Bank:
properties:
acct_length:
type: integer
active:
type: integer
country_id:
type: integer
created_at:
type: string
currency:
type: string
id:
type: integer
is_24hrs:
description: nullable
type: integer
is_active:
type: integer
is_mobilemoney:
description: nullable
type: integer
is_rtgs:
type: integer
name:
type: string
slug:
type: string
swift:
type: string
updated_at:
type: string
type: object
domain.BetOutcome: domain.BetOutcome:
properties: properties:
away_team_name: away_team_name:
@ -124,102 +157,42 @@ definitions:
example: 2 example: 2
type: integer type: integer
type: object type: object
domain.ChapaDepositRequest: domain.ChapaDepositRequestPayload:
properties:
amount:
type: number
required:
- amount
type: object
domain.ChapaDepositResponse:
properties:
checkoutURL:
type: string
reference:
type: string
type: object
domain.ChapaVerificationResponse:
properties:
amount:
type: number
currency:
type: string
status:
type: string
tx_ref:
type: string
type: object
domain.ChapaWebhookPayload:
properties: properties:
amount: amount:
type: integer type: integer
branch_id:
type: integer
currency: currency:
type: string type: string
phone_number: status:
$ref: '#/definitions/domain.PaymentStatus'
tx_ref:
type: string type: string
type: object type: object
domain.ChapaPaymentUrlResponse:
properties:
payment_url:
type: string
type: object
domain.ChapaPaymentUrlResponseWrapper:
properties:
data: {}
message:
type: string
status_code:
type: integer
success:
type: boolean
type: object
domain.ChapaSupportedBank:
properties:
acct_length:
type: integer
acct_number_regex:
type: string
active:
type: integer
country_id:
type: integer
created_at:
type: string
currency:
type: string
example_value:
type: string
id:
type: integer
is_24hrs:
type: integer
is_active:
type: integer
is_mobilemoney:
type: integer
is_rtgs:
type: integer
name:
type: string
slug:
type: string
swift:
type: string
updated_at:
type: string
type: object
domain.ChapaSupportedBanksResponseWrapper:
properties:
data: {}
message:
type: string
status_code:
type: integer
success:
type: boolean
type: object
domain.ChapaTransactionType:
properties:
type:
type: string
type: object
domain.ChapaWithdrawRequest:
properties:
account_name:
type: string
account_number:
type: string
amount:
type: integer
bank_code:
type: string
beneficiary_name:
type: string
branch_id:
type: integer
currency:
type: string
wallet_id:
description: add this
type: integer
type: object
domain.CreateBetOutcomeReq: domain.CreateBetOutcomeReq:
properties: properties:
event_id: event_id:
@ -328,6 +301,16 @@ definitions:
- TELEBIRR_TRANSACTION - TELEBIRR_TRANSACTION
- ARIFPAY_TRANSACTION - ARIFPAY_TRANSACTION
- BANK - BANK
domain.PaymentStatus:
enum:
- pending
- completed
- failed
type: string
x-enum-varnames:
- PaymentStatusPending
- PaymentStatusCompleted
- PaymentStatusFailed
domain.PopOKCallback: domain.PopOKCallback:
properties: properties:
amount: amount:
@ -421,16 +404,6 @@ definitions:
totalRewardEarned: totalRewardEarned:
type: number type: number
type: object type: object
domain.Response:
properties:
data: {}
message:
type: string
status_code:
type: integer
success:
type: boolean
type: object
domain.Role: domain.Role:
enum: enum:
- super_admin - super_admin
@ -501,7 +474,7 @@ definitions:
type: string type: string
awayTeamID: awayTeamID:
description: Away team ID (can be empty/null) description: Away team ID (can be empty/null)
type: string type: integer
homeKitImage: homeKitImage:
description: Kit or image for home team (optional) description: Kit or image for home team (optional)
type: string type: string
@ -510,7 +483,7 @@ definitions:
type: string type: string
homeTeamID: homeTeamID:
description: Home team ID description: Home team ID
type: string type: integer
id: id:
description: Event ID description: Event ID
type: string type: string
@ -519,7 +492,7 @@ definitions:
type: string type: string
leagueID: leagueID:
description: League ID description: League ID
type: string type: integer
leagueName: leagueName:
description: League name description: League name
type: string type: string
@ -531,7 +504,7 @@ definitions:
type: string type: string
sportID: sportID:
description: Sport ID description: Sport ID
type: string type: integer
startTime: startTime:
description: Converted from "time" field in UNIX format description: Converted from "time" field in UNIX format
type: string type: string
@ -1673,137 +1646,94 @@ paths:
summary: Launch an Alea Play virtual game summary: Launch an Alea Play virtual game
tags: tags:
- Alea Virtual Games - Alea Virtual Games
/api/v1/chapa/banks:
get:
consumes:
- application/json
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.ChapaSupportedBanksResponseWrapper'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.Response'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/domain.Response'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.Response'
"422":
description: Unprocessable Entity
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.Response'
summary: fetches chapa supported banks
tags:
- Chapa
/api/v1/chapa/payments/deposit: /api/v1/chapa/payments/deposit:
post: post:
consumes: consumes:
- application/json - application/json
description: Deposits money into user wallet from user account using Chapa description: Starts a new deposit process using Chapa payment gateway
parameters: parameters:
- description: Deposit request payload - description: Deposit request
in: body
name: payload
required: true
schema:
$ref: '#/definitions/domain.ChapaDepositRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.ChapaPaymentUrlResponseWrapper'
"400":
description: Invalid request
schema:
$ref: '#/definitions/domain.Response'
"422":
description: Validation error
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal server error
schema:
$ref: '#/definitions/domain.Response'
summary: Deposit money into user wallet using Chapa
tags:
- Chapa
/api/v1/chapa/payments/verify:
post:
consumes:
- application/json
parameters:
- description: Webhook Payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/domain.ChapaTransactionType'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
summary: Verifies Chapa webhook transaction
tags:
- Chapa
/api/v1/chapa/payments/withdraw:
post:
consumes:
- application/json
description: Initiates a withdrawal transaction using Chapa for the authenticated
user.
parameters:
- description: Chapa Withdraw Request
in: body in: body
name: request name: request
required: true required: true
schema: schema:
$ref: '#/definitions/domain.ChapaWithdrawRequest' $ref: '#/definitions/domain.ChapaDepositRequestPayload'
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: Withdrawal requested successfully description: OK
schema: schema:
allOf: $ref: '#/definitions/domain.ChapaDepositResponse'
- $ref: '#/definitions/domain.Response'
- properties:
data:
type: string
type: object
"400": "400":
description: Invalid request description: Bad Request
schema: schema:
$ref: '#/definitions/domain.Response' $ref: '#/definitions/domain.ErrorResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/domain.Response'
"422":
description: Unprocessable Entity
schema:
$ref: '#/definitions/domain.Response'
"500": "500":
description: Internal Server Error description: Internal Server Error
schema: schema:
$ref: '#/definitions/domain.Response' $ref: '#/definitions/domain.ErrorResponse'
summary: Withdraw using Chapa summary: Initiate a deposit
tags:
- Chapa
/api/v1/chapa/payments/manual/verify/{tx_ref}:
get:
consumes:
- application/json
description: Manually verify a payment using Chapa's API
parameters:
- description: Transaction Reference
in: path
name: tx_ref
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.ChapaVerificationResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Verify a payment manually
tags:
- Chapa
/api/v1/chapa/payments/webhook/verify:
post:
consumes:
- application/json
description: Handles payment notifications from Chapa
parameters:
- description: Webhook payload
in: body
name: request
required: true
schema:
$ref: '#/definitions/domain.ChapaWebhookPayload'
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Chapa payment webhook callback (used by Chapa)
tags: tags:
- Chapa - Chapa
/api/v1/reports/dashboard: /api/v1/reports/dashboard:
@ -2067,6 +1997,27 @@ paths:
summary: Refresh token summary: Refresh token
tags: tags:
- auth - auth
/banks:
get:
consumes:
- application/json
description: Get list of banks supported by Chapa
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.Bank'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get supported banks
tags:
- Chapa
/bet: /bet:
get: get:
consumes: consumes:

View File

@ -487,6 +487,7 @@ type WalletTransfer struct {
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
CashierID pgtype.Int8 `json:"cashier_id"` CashierID pgtype.Int8 `json:"cashier_id"`
Verified bool `json:"verified"` Verified bool `json:"verified"`
ReferenceNumber string `json:"reference_number"`
PaymentMethod string `json:"payment_method"` PaymentMethod string `json:"payment_method"`
CreatedAt pgtype.Timestamp `json:"created_at"` CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"`

View File

@ -187,6 +187,40 @@ func (q *Queries) GetNotification(ctx context.Context, id string) (Notification,
return i, err return i, err
} }
const GetNotificationCounts = `-- name: GetNotificationCounts :many
SELECT
COUNT(*) as total,
COUNT(CASE WHEN is_read = true THEN 1 END) as read,
COUNT(CASE WHEN is_read = false THEN 1 END) as unread
FROM notifications
`
type GetNotificationCountsRow struct {
Total int64 `json:"total"`
Read int64 `json:"read"`
Unread int64 `json:"unread"`
}
func (q *Queries) GetNotificationCounts(ctx context.Context) ([]GetNotificationCountsRow, error) {
rows, err := q.db.Query(ctx, GetNotificationCounts)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetNotificationCountsRow
for rows.Next() {
var i GetNotificationCountsRow
if err := rows.Scan(&i.Total, &i.Read, &i.Unread); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ListFailedNotifications = `-- name: ListFailedNotifications :many const ListFailedNotifications = `-- name: ListFailedNotifications :many
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
FROM notifications FROM notifications

View File

@ -19,10 +19,11 @@ INSERT INTO wallet_transfer (
sender_wallet_id, sender_wallet_id,
cashier_id, cashier_id,
verified, verified,
reference_number,
payment_method payment_method
) )
VALUES ($1, $2, $3, $4, $5, $6, $7) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method, created_at, updated_at RETURNING id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at
` `
type CreateTransferParams struct { type CreateTransferParams struct {
@ -32,6 +33,7 @@ type CreateTransferParams struct {
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
CashierID pgtype.Int8 `json:"cashier_id"` CashierID pgtype.Int8 `json:"cashier_id"`
Verified bool `json:"verified"` Verified bool `json:"verified"`
ReferenceNumber string `json:"reference_number"`
PaymentMethod string `json:"payment_method"` PaymentMethod string `json:"payment_method"`
} }
@ -43,6 +45,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
arg.SenderWalletID, arg.SenderWalletID,
arg.CashierID, arg.CashierID,
arg.Verified, arg.Verified,
arg.ReferenceNumber,
arg.PaymentMethod, arg.PaymentMethod,
) )
var i WalletTransfer var i WalletTransfer
@ -54,6 +57,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
&i.SenderWalletID, &i.SenderWalletID,
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber,
&i.PaymentMethod, &i.PaymentMethod,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -62,7 +66,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
} }
const GetAllTransfers = `-- name: GetAllTransfers :many const GetAllTransfers = `-- name: GetAllTransfers :many
SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method, created_at, updated_at SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at
FROM wallet_transfer FROM wallet_transfer
` `
@ -83,6 +87,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error)
&i.SenderWalletID, &i.SenderWalletID,
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber,
&i.PaymentMethod, &i.PaymentMethod,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -98,7 +103,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error)
} }
const GetTransferByID = `-- name: GetTransferByID :one const GetTransferByID = `-- name: GetTransferByID :one
SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method, created_at, updated_at SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at
FROM wallet_transfer FROM wallet_transfer
WHERE id = $1 WHERE id = $1
` `
@ -114,6 +119,32 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer
&i.SenderWalletID, &i.SenderWalletID,
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber,
&i.PaymentMethod,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetTransferByReference = `-- name: GetTransferByReference :one
SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at
FROM wallet_transfer
WHERE reference_number = $1
`
func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber string) (WalletTransfer, error) {
row := q.db.QueryRow(ctx, GetTransferByReference, referenceNumber)
var i WalletTransfer
err := row.Scan(
&i.ID,
&i.Amount,
&i.Type,
&i.ReceiverWalletID,
&i.SenderWalletID,
&i.CashierID,
&i.Verified,
&i.ReferenceNumber,
&i.PaymentMethod, &i.PaymentMethod,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -122,7 +153,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer
} }
const GetTransfersByWallet = `-- name: GetTransfersByWallet :many const GetTransfersByWallet = `-- name: GetTransfersByWallet :many
SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method, created_at, updated_at SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at
FROM wallet_transfer FROM wallet_transfer
WHERE receiver_wallet_id = $1 WHERE receiver_wallet_id = $1
OR sender_wallet_id = $1 OR sender_wallet_id = $1
@ -145,6 +176,7 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID int
&i.SenderWalletID, &i.SenderWalletID,
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber,
&i.PaymentMethod, &i.PaymentMethod,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,

9
go.mod
View File

@ -13,8 +13,6 @@ require (
github.com/jackc/pgx/v5 v5.7.4 github.com/jackc/pgx/v5 v5.7.4
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.10.0
// github.com/stretchr/testify v1.10.0 // github.com/stretchr/testify v1.10.0
github.com/swaggo/fiber-swagger v1.3.0 github.com/swaggo/fiber-swagger v1.3.0
github.com/swaggo/swag v1.16.4 github.com/swaggo/swag v1.16.4
@ -30,7 +28,6 @@ require (
// github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect
@ -49,16 +46,14 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
go.mongodb.org/mongo-driver v1.17.3 go.mongodb.org/mongo-driver v1.17.3
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/net v0.38.0 // direct golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.12.0 // indirect golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.23.0 // indirect
@ -77,6 +72,6 @@ require (
) )
require ( require (
github.com/resend/resend-go/v2 v2.20.0 // indirect github.com/resend/resend-go/v2 v2.20.0 // direct
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
) )

4
go.sum
View File

@ -120,16 +120,12 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 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/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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 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/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -260,7 +260,11 @@ func (c *Config) loadEnv() error {
if c.ADRO_SMS_HOST_URL == "" { if c.ADRO_SMS_HOST_URL == "" {
c.ADRO_SMS_HOST_URL = "https://api.afrosms.com" c.ADRO_SMS_HOST_URL = "https://api.afrosms.com"
} }
popOKClientID := os.Getenv("POPOK_CLIENT_ID") popOKClientID := os.Getenv("POPOK_CLIENT_ID")
popOKPlatform := os.Getenv("POPOK_PLATFORM")
if popOKClientID == "" { if popOKClientID == "" {
return ErrInvalidPopOKClientID return ErrInvalidPopOKClientID
} }
@ -285,6 +289,7 @@ func (c *Config) loadEnv() error {
SecretKey: popOKSecretKey, SecretKey: popOKSecretKey,
BaseURL: popOKBaseURL, BaseURL: popOKBaseURL,
CallbackURL: popOKCallbackURL, CallbackURL: popOKCallbackURL,
Platform: popOKPlatform,
} }
betToken := os.Getenv("BET365_TOKEN") betToken := os.Getenv("BET365_TOKEN")
if betToken == "" { if betToken == "" {

View File

@ -1,16 +1,16 @@
package domain package domain
import ( import "time"
"errors"
"time" type PaymentStatus string
const (
PaymentStatusPending PaymentStatus = "pending"
PaymentStatusCompleted PaymentStatus = "completed"
PaymentStatusFailed PaymentStatus = "failed"
) )
var ( type ChapaDepositRequest struct {
ChapaSecret string
ChapaBaseURL string
)
type InitPaymentRequest struct {
Amount Currency `json:"amount"` Amount Currency `json:"amount"`
Currency string `json:"currency"` Currency string `json:"currency"`
Email string `json:"email"` Email string `json:"email"`
@ -21,208 +21,73 @@ type InitPaymentRequest struct {
ReturnURL string `json:"return_url"` ReturnURL string `json:"return_url"`
} }
type TransferRequest struct { type ChapaDepositRequestPayload struct {
AccountNumber string `json:"account_number"` Amount float64 `json:"amount" validate:"required,gt=0"`
BankCode string `json:"bank_code"`
Amount string `json:"amount"`
Currency string `json:"currency"`
Reference string `json:"reference"`
Reason string `json:"reason"`
RecipientName string `json:"recipient_name"`
} }
type ChapaSupportedBank struct { type ChapaWebhookPayload struct {
Id int64 `json:"id"` TxRef string `json:"tx_ref"`
Amount Currency `json:"amount"`
Currency string `json:"currency"`
Status PaymentStatus `json:"status"`
}
// PaymentResponse contains the response from payment initialization
type ChapaDepositResponse struct {
CheckoutURL string
Reference string
}
// PaymentVerification contains payment verification details
type ChapaDepositVerification struct {
Status PaymentStatus
Amount Currency
Currency string
}
type ChapaVerificationResponse struct {
Status string `json:"status"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
TxRef string `json:"tx_ref"`
}
type Bank struct {
ID int `json:"id"`
Slug string `json:"slug"` Slug string `json:"slug"`
Swift string `json:"swift"` Swift string `json:"swift"`
Name string `json:"name"` Name string `json:"name"`
AcctLength int `json:"acct_length"` AcctLength int `json:"acct_length"`
AcctNumberRegex string `json:"acct_number_regex"` CountryID int `json:"country_id"`
ExampleValue string `json:"example_value"` IsMobileMoney int `json:"is_mobilemoney"` // nullable
CountryId int `json:"country_id"`
IsMobilemoney *int `json:"is_mobilemoney"`
IsActive int `json:"is_active"` IsActive int `json:"is_active"`
IsRtgs *int `json:"is_rtgs"` IsRTGS int `json:"is_rtgs"`
Active int `json:"active"` Active int `json:"active"`
Is24Hrs *int `json:"is_24hrs"` Is24Hrs int `json:"is_24hrs"` // nullable
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
Currency string `json:"currency"` Currency string `json:"currency"`
} }
type ChapaSupportedBanksResponse struct { type BankResponse struct {
Message string `json:"message"` Message string `json:"message"`
Data []ChapaSupportedBank `json:"data"`
}
type InitPaymentData struct {
TxRef string `json:"tx_ref"`
CheckoutURL string `json:"checkout_url"`
}
type InitPaymentResponse struct {
Status string `json:"status"` // "success"
Message string `json:"message"` // e.g., "Payment initialized"
Data InitPaymentData `json:"data"`
}
type WebhookPayload map[string]interface{}
type TransactionData struct {
TxRef string `json:"tx_ref"`
Status string `json:"status"` Status string `json:"status"`
Amount string `json:"amount"` Data []BankData `json:"data"`
Currency string `json:"currency"`
CustomerEmail string `json:"email"`
} }
type VerifyTransactionResponse struct { type BankData struct {
Status string `json:"status"` ID int `json:"id"`
Message string `json:"message"` Slug string `json:"slug"`
Data TransactionData `json:"data"` Swift string `json:"swift"`
} Name string `json:"name"`
AcctLength int `json:"acct_length"`
type TransferData struct { CountryID int `json:"country_id"`
Reference string `json:"reference"` IsMobileMoney int `json:"is_mobilemoney"` // nullable
Status string `json:"status"` IsActive int `json:"is_active"`
Amount string `json:"amount"` IsRTGS int `json:"is_rtgs"`
Currency string `json:"currency"` Active int `json:"active"`
} Is24Hrs int `json:"is_24hrs"` // nullable
type CreateTransferResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Data TransferData `json:"data"`
}
type TransferVerificationData struct {
Reference string `json:"reference"`
Status string `json:"status"`
BankCode string `json:"bank_code"`
AccountName string `json:"account_name"`
}
type VerifyTransferResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Data TransferVerificationData `json:"data"`
}
type ChapaTransactionType struct {
Type string `json:"type"`
}
type ChapaWebHookTransfer struct {
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
BankId string `json:"bank_id"`
BankName string `json:"bank_name"`
Currency string `json:"currency"`
Amount string `json:"amount"`
Type string `json:"type"`
Status string `json:"status"`
Reference string `json:"reference"`
TxRef string `json:"tx_ref"`
ChapaReference string `json:"chapa_reference"`
CreatedAt time.Time `json:"created_at"`
}
type ChapaWebHookPayment struct {
Event string `json:"event"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Mobile interface{} `json:"mobile"`
Currency string `json:"currency"`
Amount string `json:"amount"`
Charge string `json:"charge"`
Status string `json:"status"`
Mode string `json:"mode"`
Reference string `json:"reference"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
Type string `json:"type"`
TxRef string `json:"tx_ref"`
PaymentMethod string `json:"payment_method"`
Customization struct {
Title interface{} `json:"title"`
Description interface{} `json:"description"`
Logo interface{} `json:"logo"`
} `json:"customization"`
Meta string `json:"meta"`
}
type ChapaWithdrawRequest struct {
WalletID int64 `json:"wallet_id"` // add this
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
Amount int64 `json:"amount"`
Currency string `json:"currency"` Currency string `json:"currency"`
BeneficiaryName string `json:"beneficiary_name"`
BankCode string `json:"bank_code"`
BranchID int64 `json:"branch_id"`
}
type ChapaTransferPayload struct {
AccountName string
AccountNumber string
Amount string
Currency string
BeneficiaryName string
TxRef string
Reference string
BankCode string
}
type ChapaDepositRequest struct {
Amount Currency `json:"amount"`
PhoneNumber string `json:"phone_number"`
Currency string `json:"currency"`
BranchID int64 `json:"branch_id"`
}
func (r ChapaDepositRequest) Validate() error {
if r.Amount <= 0 {
return errors.New("amount must be greater than zero")
}
if r.Currency == "" {
return errors.New("currency is required")
}
if r.PhoneNumber == "" {
return errors.New("phone number is required")
}
// if r.BranchID == 0 {
// return errors.New("branch ID is required")
// }
return nil
}
type AcceptChapaPaymentRequest struct {
Amount string `json:"amount"`
Currency string `json:"currency"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber string `json:"phone_number"`
TxRef string `json:"tx_ref"`
CallbackUrl string `json:"callback_url"`
ReturnUrl string `json:"return_url"`
CustomizationTitle string `json:"customization[title]"`
CustomizationDescription string `json:"customization[description]"`
}
type ChapaPaymentUrlResponse struct {
PaymentURL string `json:"payment_url"`
}
type ChapaPaymentUrlResponseWrapper struct {
Data ChapaPaymentUrlResponse `json:"data"`
Response
}
type ChapaSupportedBanksResponseWrapper struct {
Data []ChapaSupportedBank `json:"data"`
Response
} }

View File

@ -3,8 +3,12 @@ package domain
import ( import (
"fmt" "fmt"
"time" "time"
"go.uber.org/zap"
) )
var MongoDBLogger *zap.Logger
type ValidInt64 struct { type ValidInt64 struct {
Value int64 Value int64
Valid bool Valid bool

View File

@ -29,6 +29,8 @@ const (
NotificationRecieverSideAdmin NotificationRecieverSide = "admin" NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
NotificationRecieverSideCustomer NotificationRecieverSide = "customer" NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
NotificationRecieverSideCashier NotificationRecieverSide = "cashier"
NotificationRecieverSideBranchManager NotificationRecieverSide = "branch_manager"
NotificationDeliverySchemeBulk NotificationDeliveryScheme = "bulk" NotificationDeliverySchemeBulk NotificationDeliveryScheme = "bulk"
NotificationDeliverySchemeSingle NotificationDeliveryScheme = "single" NotificationDeliverySchemeSingle NotificationDeliveryScheme = "single"
@ -55,9 +57,9 @@ const (
) )
type NotificationPayload struct { type NotificationPayload struct {
Headline string Headline string `json:"headline"`
Message string Message string `json:"message"`
Tags []string Tags []string `json:"tags"`
} }
type Notification struct { type Notification struct {

View File

@ -2,6 +2,117 @@ package domain
import "time" import "time"
type DashboardSummary struct {
TotalStakes Currency `json:"total_stakes"`
TotalBets int64 `json:"total_bets"`
ActiveBets int64 `json:"active_bets"`
WinBalance Currency `json:"win_balance"`
TotalWins int64 `json:"total_wins"`
TotalLosses int64 `json:"total_losses"`
CustomerCount int64 `json:"customer_count"`
Profit Currency `json:"profit"`
WinRate float64 `json:"win_rate"`
AverageStake Currency `json:"average_stake"`
TotalDeposits Currency `json:"total_deposits"`
TotalWithdrawals Currency `json:"total_withdrawals"`
ActiveCustomers int64 `json:"active_customers"`
BranchesCount int64 `json:"branches_count"`
ActiveBranches int64 `json:"active_branches"`
TotalCashiers int64 `json:"total_cashiers"`
ActiveCashiers int64 `json:"active_cashiers"`
InactiveCashiers int64 `json:"inactive_cashiers"`
TotalWallets int64 `json:"total_wallets"`
TotalGames int64 `json:"total_games"`
ActiveGames int64 `json:"active_games"`
InactiveGames int64 `json:"inactive_games"`
TotalManagers int64 `json:"total_managers"`
ActiveManagers int64 `json:"active_managers"`
InactiveManagers int64 `json:"inactive_managers"`
InactiveBranches int64 `json:"inactive_branches"`
TotalAdmins int64 `json:"total_admins"`
ActiveAdmins int64 `json:"active_admins"`
InactiveAdmins int64 `json:"inactive_admins"`
TotalCompanies int64 `json:"total_companies"`
ActiveCompanies int64 `json:"active_companies"`
InactiveCompanies int64 `json:"inactive_companies"`
InactiveCustomers int64 `json:"inactive_customers"`
TotalNotifications int64 `json:"total_notifications"`
ReadNotifications int64 `json:"read_notifications"`
UnreadNotifications int64 `json:"unread_notifications"`
}
type CustomerActivity struct {
CustomerID int64 `json:"customer_id"`
CustomerName string `json:"customer_name"`
TotalBets int64 `json:"total_bets"`
TotalStakes Currency `json:"total_stakes"`
TotalWins int64 `json:"total_wins"`
TotalPayouts Currency `json:"total_payouts"`
Profit Currency `json:"profit"`
FirstBetDate time.Time `json:"first_bet_date"`
LastBetDate time.Time `json:"last_bet_date"`
FavoriteSport string `json:"favorite_sport"`
FavoriteMarket string `json:"favorite_market"`
AverageStake Currency `json:"average_stake"`
AverageOdds float64 `json:"average_odds"`
WinRate float64 `json:"win_rate"`
ActivityLevel string `json:"activity_level"` // High, Medium, Low
}
type BranchPerformance struct {
BranchID int64 `json:"branch_id"`
BranchName string `json:"branch_name"`
Location string `json:"location"`
ManagerName string `json:"manager_name"`
TotalBets int64 `json:"total_bets"`
TotalStakes Currency `json:"total_stakes"`
TotalWins int64 `json:"total_wins"`
TotalPayouts Currency `json:"total_payouts"`
Profit Currency `json:"profit"`
CustomerCount int64 `json:"customer_count"`
Deposits Currency `json:"deposits"`
Withdrawals Currency `json:"withdrawals"`
WinRate float64 `json:"win_rate"`
AverageStake Currency `json:"average_stake"`
PerformanceScore float64 `json:"performance_score"`
}
type SportPerformance struct {
SportID string `json:"sport_id"`
SportName string `json:"sport_name"`
TotalBets int64 `json:"total_bets"`
TotalStakes Currency `json:"total_stakes"`
TotalWins int64 `json:"total_wins"`
TotalPayouts Currency `json:"total_payouts"`
Profit Currency `json:"profit"`
PopularityRank int `json:"popularity_rank"`
WinRate float64 `json:"win_rate"`
AverageStake Currency `json:"average_stake"`
AverageOdds float64 `json:"average_odds"`
MostPopularMarket string `json:"most_popular_market"`
}
type BetAnalysis struct {
Date time.Time `json:"date"`
TotalBets int64 `json:"total_bets"`
TotalStakes Currency `json:"total_stakes"`
TotalWins int64 `json:"total_wins"`
TotalPayouts Currency `json:"total_payouts"`
Profit Currency `json:"profit"`
MostPopularSport string `json:"most_popular_sport"`
MostPopularMarket string `json:"most_popular_market"`
HighestStake Currency `json:"highest_stake"`
HighestPayout Currency `json:"highest_payout"`
AverageOdds float64 `json:"average_odds"`
}
type ValidOutcomeStatus struct { type ValidOutcomeStatus struct {
Value OutcomeStatus Value OutcomeStatus
Valid bool // Valid is true if Value is not NULL Valid bool // Valid is true if Value is not NULL
@ -13,6 +124,7 @@ type ReportFilter struct {
EndTime ValidTime `json:"end_time"` EndTime ValidTime `json:"end_time"`
CompanyID ValidInt64 `json:"company_id"` CompanyID ValidInt64 `json:"company_id"`
BranchID ValidInt64 `json:"branch_id"` BranchID ValidInt64 `json:"branch_id"`
RecipientID ValidInt64 `json:"recipient_id"`
UserID ValidInt64 `json:"user_id"` UserID ValidInt64 `json:"user_id"`
SportID ValidString `json:"sport_id"` SportID ValidString `json:"sport_id"`
Status ValidOutcomeStatus `json:"status"` Status ValidOutcomeStatus `json:"status"`
@ -46,7 +158,6 @@ type CustomerBetActivity struct {
AverageOdds float64 AverageOdds float64
} }
// BranchBetActivity represents branch betting activity // BranchBetActivity represents branch betting activity
type BranchBetActivity struct { type BranchBetActivity struct {
BranchID int64 BranchID int64
@ -99,25 +210,92 @@ type CustomerPreferences struct {
FavoriteMarket string `json:"favorite_market"` FavoriteMarket string `json:"favorite_market"`
} }
type DashboardSummary struct { // type DashboardSummary struct {
TotalStakes Currency `json:"total_stakes"` // TotalStakes Currency `json:"total_stakes"`
TotalBets int64 `json:"total_bets"` // TotalBets int64 `json:"total_bets"`
ActiveBets int64 `json:"active_bets"` // ActiveBets int64 `json:"active_bets"`
WinBalance Currency `json:"win_balance"` // WinBalance Currency `json:"win_balance"`
TotalWins int64 `json:"total_wins"` // TotalWins int64 `json:"total_wins"`
TotalLosses int64 `json:"total_losses"` // TotalLosses int64 `json:"total_losses"`
CustomerCount int64 `json:"customer_count"` // CustomerCount int64 `json:"customer_count"`
Profit Currency `json:"profit"` // Profit Currency `json:"profit"`
WinRate float64 `json:"win_rate"` // WinRate float64 `json:"win_rate"`
AverageStake Currency `json:"average_stake"` // AverageStake Currency `json:"average_stake"`
TotalDeposits Currency `json:"total_deposits"` // TotalDeposits Currency `json:"total_deposits"`
TotalWithdrawals Currency `json:"total_withdrawals"` // TotalWithdrawals Currency `json:"total_withdrawals"`
ActiveCustomers int64 `json:"active_customers"` // ActiveCustomers int64 `json:"active_customers"`
BranchesCount int64 `json:"branches_count"` // BranchesCount int64 `json:"branches_count"`
ActiveBranches int64 `json:"active_branches"` // ActiveBranches int64 `json:"active_branches"`
} // }
type ErrorResponse struct { type ErrorResponse struct {
Message string `json:"message"` Message string `json:"message"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
type NotificationReport struct {
CountsByType []NotificationTypeCount `json:"counts_by_type"`
DeliveryStats NotificationDeliveryStats `json:"delivery_stats"`
ActiveRecipients []ActiveNotificationRecipient `json:"active_recipients"`
}
type NotificationTypeCount struct {
Type string `json:"type"`
Total int64 `json:"total"`
Read int64 `json:"read"`
Unread int64 `json:"unread"`
}
type NotificationDeliveryStats struct {
TotalSent int64 `json:"total_sent"`
FailedDeliveries int64 `json:"failed_deliveries"`
SuccessRate float64 `json:"success_rate"`
MostUsedChannel string `json:"most_used_channel"`
}
type ActiveNotificationRecipient struct {
RecipientID int64 `json:"recipient_id"`
RecipientName string `json:"recipient_name"`
NotificationCount int64 `json:"notification_count"`
LastNotificationTime time.Time `json:"last_notification_time"`
}
type CompanyPerformance struct {
CompanyID int64 `json:"company_id"`
CompanyName string `json:"company_name"`
ContactEmail string `json:"contact_email"`
TotalBets int64 `json:"total_bets"`
TotalStakes Currency `json:"total_stakes"`
TotalWins int64 `json:"total_wins"`
TotalPayouts Currency `json:"total_payouts"`
Profit Currency `json:"profit"`
WinRate float64 `json:"win_rate"`
AverageStake Currency `json:"average_stake"`
TotalBranches int64 `json:"total_branches"`
ActiveBranches int64 `json:"active_branches"`
TotalCashiers int64 `json:"total_cashiers"`
ActiveCashiers int64 `json:"active_cashiers"`
WalletBalance Currency `json:"wallet_balance"`
LastActivity time.Time `json:"last_activity"`
}
type CashierPerformance struct {
CashierID int64 `json:"cashier_id"`
CashierName string `json:"cashier_name"`
BranchID int64 `json:"branch_id"`
BranchName string `json:"branch_name"`
CompanyID int64 `json:"company_id"`
TotalBets int64 `json:"total_bets"`
TotalStakes Currency `json:"total_stakes"`
TotalWins int64 `json:"total_wins"`
TotalPayouts Currency `json:"total_payouts"`
Profit Currency `json:"profit"`
WinRate float64 `json:"win_rate"`
AverageStake Currency `json:"average_stake"`
Deposits Currency `json:"deposits"`
Withdrawals Currency `json:"withdrawals"`
NetTransactionAmount Currency `json:"net_transaction_amount"`
FirstActivity time.Time `json:"first_activity"`
LastActivity time.Time `json:"last_activity"`
ActiveDays int `json:"active_days"`
}

View File

@ -31,7 +31,8 @@ type Transfer struct {
Type TransferType Type TransferType
PaymentMethod PaymentMethod PaymentMethod PaymentMethod
ReceiverWalletID int64 ReceiverWalletID int64
SenderWalletID ValidInt64 SenderWalletID int64
ReferenceNumber string
CashierID ValidInt64 CashierID ValidInt64
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
@ -40,8 +41,9 @@ type Transfer struct {
type CreateTransfer struct { type CreateTransfer struct {
Amount Currency Amount Currency
Verified bool Verified bool
ReferenceNumber string
ReceiverWalletID int64 ReceiverWalletID int64
SenderWalletID ValidInt64 SenderWalletID int64
CashierID ValidInt64 CashierID ValidInt64
Type TransferType Type TransferType
PaymentMethod PaymentMethod PaymentMethod PaymentMethod

View File

@ -12,6 +12,7 @@ type VirtualGame struct {
MinBet float64 `json:"min_bet"` MinBet float64 `json:"min_bet"`
MaxBet float64 `json:"max_bet"` MaxBet float64 `json:"max_bet"`
Volatility string `json:"volatility"` Volatility string `json:"volatility"`
IsActive bool `json:"is_active"`
RTP float64 `json:"rtp"` RTP float64 `json:"rtp"`
IsFeatured bool `json:"is_featured"` IsFeatured bool `json:"is_featured"`
PopularityScore int `json:"popularity_score"` PopularityScore int `json:"popularity_score"`
@ -46,6 +47,7 @@ type VirtualGameTransaction struct {
Amount int64 `json:"amount"` // Always in cents Amount int64 `json:"amount"` // Always in cents
Currency string `json:"currency"` Currency string `json:"currency"`
ExternalTransactionID string `json:"external_transaction_id"` ExternalTransactionID string `json:"external_transaction_id"`
ReferenceTransactionID string `json:"reference_transaction_id"`
Status string `json:"status"` // PENDING, COMPLETED, FAILED Status string `json:"status"` // PENDING, COMPLETED, FAILED
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@ -86,6 +88,7 @@ type PopOKConfig struct {
SecretKey string SecretKey string
BaseURL string BaseURL string
CallbackURL string CallbackURL string
Platform string
} }
type PopOKCallback struct { type PopOKCallback struct {
@ -98,6 +101,61 @@ type PopOKCallback struct {
Signature string `json:"signature"` // HMAC-SHA256 signature for verification Signature string `json:"signature"` // HMAC-SHA256 signature for verification
} }
type PopOKPlayerInfoRequest struct {
ExternalToken string `json:"externalToken"`
}
type PopOKPlayerInfoResponse struct {
Country string `json:"country"`
Currency string `json:"currency"`
Balance float64 `json:"balance"`
PlayerID string `json:"playerId"`
}
type PopOKBetRequest struct {
ExternalToken string `json:"externalToken"`
PlayerID string `json:"playerId"`
GameID string `json:"gameId"`
TransactionID string `json:"transactionId"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
}
type PopOKBetResponse struct {
TransactionID string `json:"transactionId"`
ExternalTrxID string `json:"externalTrxId"`
Balance float64 `json:"balance"`
}
// domain/popok.go
type PopOKWinRequest struct {
ExternalToken string `json:"externalToken"`
PlayerID string `json:"playerId"`
GameID string `json:"gameId"`
TransactionID string `json:"transactionId"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
}
type PopOKWinResponse struct {
TransactionID string `json:"transactionId"`
ExternalTrxID string `json:"externalTrxId"`
Balance float64 `json:"balance"`
}
type PopOKCancelRequest struct {
ExternalToken string `json:"externalToken"`
PlayerID string `json:"playerId"`
GameID string `json:"gameId"`
TransactionID string `json:"transactionId"`
}
type PopOKCancelResponse struct {
TransactionID string `json:"transactionId"`
ExternalTrxID string `json:"externalTrxId"`
Balance float64 `json:"balance"`
}
type AleaPlayCallback struct { type AleaPlayCallback struct {
EventID string `json:"event_id"` EventID string `json:"event_id"`
TransactionID string `json:"transaction_id"` TransactionID string `json:"transaction_id"`

View File

@ -1,18 +1,22 @@
package mongoLogger package mongoLogger
import ( import (
"log" "fmt"
"os" "os"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
func Init() { func InitLogger() (*zap.Logger, error) {
// Replace localhost if inside Docker mongoCore, err := NewMongoCore(
mongoCore, err := NewMongoCore("mongodb://root:secret@mongo:27017/?authSource=admin", "logdb", "applogs", zapcore.InfoLevel) "mongodb://root:secret@mongo:27017/?authSource=admin",
"logdb",
"applogs",
zapcore.InfoLevel,
)
if err != nil { if err != nil {
log.Fatalf("failed to create MongoDB core: %v", err) return nil, fmt.Errorf("failed to create MongoDB core: %w", err)
} }
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
@ -21,10 +25,6 @@ func Init() {
combinedCore := zapcore.NewTee(mongoCore, consoleCore) combinedCore := zapcore.NewTee(mongoCore, consoleCore)
logger := zap.New(combinedCore, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) logger := zap.New(combinedCore, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
zap.ReplaceGlobals(logger) // Optional but useful if you use zap.L()
defer logger.Sync() return logger, nil
// logger.Info("Application started", zap.String("module", "main"))
// logger.Error("Something went wrong", zap.String("error_code", "E123"))
} }

View File

@ -158,7 +158,7 @@ func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBe
rows, err := s.queries.CreateBetOutcome(ctx, dbParams) rows, err := s.queries.CreateBetOutcome(ctx, dbParams)
if err != nil { if err != nil {
mongoLogger.Error("failed to create bet outcomes in DB", domain.MongoDBLogger.Error("failed to create bet outcomes in DB",
zap.Int("outcome_count", len(outcomes)), zap.Int("outcome_count", len(outcomes)),
zap.Any("bet_id", outcomes[0].BetID), // assumes all outcomes have same BetID zap.Any("bet_id", outcomes[0].BetID), // assumes all outcomes have same BetID
zap.Error(err), zap.Error(err),
@ -172,7 +172,7 @@ func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBe
func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) { func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) {
bet, err := s.queries.GetBetByID(ctx, id) bet, err := s.queries.GetBetByID(ctx, id)
if err != nil { if err != nil {
mongoLogger.Error("failed to get bet by ID", domain.MongoDBLogger.Error("failed to get bet by ID",
zap.Int64("bet_id", id), zap.Int64("bet_id", id),
zap.Error(err), zap.Error(err),
) )
@ -185,7 +185,7 @@ func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error)
func (s *Store) GetBetByCashoutID(ctx context.Context, id string) (domain.GetBet, error) { func (s *Store) GetBetByCashoutID(ctx context.Context, id string) (domain.GetBet, error) {
bet, err := s.queries.GetBetByCashoutID(ctx, id) bet, err := s.queries.GetBetByCashoutID(ctx, id)
if err != nil { if err != nil {
mongoLogger.Error("failed to get bet by cashout ID", domain.MongoDBLogger.Error("failed to get bet by cashout ID",
zap.String("cashout_id", id), zap.String("cashout_id", id),
zap.Error(err), zap.Error(err),
) )
@ -211,7 +211,7 @@ func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]doma
}, },
}) })
if err != nil { if err != nil {
mongoLogger.Error("failed to get all bets", domain.MongoDBLogger.Error("failed to get all bets",
zap.Any("filter", filter), zap.Any("filter", filter),
zap.Error(err), zap.Error(err),
) )
@ -232,7 +232,7 @@ func (s *Store) GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.
Valid: true, Valid: true,
}) })
if err != nil { if err != nil {
mongoLogger.Error("failed to get bets by branch ID", domain.MongoDBLogger.Error("failed to get bets by branch ID",
zap.Int64("branch_id", BranchID), zap.Int64("branch_id", BranchID),
zap.Error(err), zap.Error(err),
) )
@ -271,7 +271,7 @@ func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) err
CashedOut: cashedOut, CashedOut: cashedOut,
}) })
if err != nil { if err != nil {
mongoLogger.Error("failed to update cashout", domain.MongoDBLogger.Error("failed to update cashout",
zap.Int64("id", id), zap.Int64("id", id),
zap.Bool("cashed_out", cashedOut), zap.Bool("cashed_out", cashedOut),
zap.Error(err), zap.Error(err),
@ -286,7 +286,7 @@ func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.Outcom
Status: int32(status), Status: int32(status),
}) })
if err != nil { if err != nil {
mongoLogger.Error("failed to update status", domain.MongoDBLogger.Error("failed to update status",
zap.Int64("id", id), zap.Int64("id", id),
zap.Int32("status", int32(status)), zap.Int32("status", int32(status)),
zap.Error(err), zap.Error(err),
@ -298,7 +298,7 @@ func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.Outcom
func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error) { func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error) {
outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, eventID) outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, eventID)
if err != nil { if err != nil {
mongoLogger.Error("failed to get bet outcomes by event ID", domain.MongoDBLogger.Error("failed to get bet outcomes by event ID",
zap.Int64("event_id", eventID), zap.Int64("event_id", eventID),
zap.Error(err), zap.Error(err),
) )
@ -315,7 +315,7 @@ func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]do
func (s *Store) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error) { func (s *Store) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error) {
outcomes, err := s.queries.GetBetOutcomeByBetID(ctx, betID) outcomes, err := s.queries.GetBetOutcomeByBetID(ctx, betID)
if err != nil { if err != nil {
mongoLogger.Error("failed to get bet outcomes by bet ID", domain.MongoDBLogger.Error("failed to get bet outcomes by bet ID",
zap.Int64("bet_id", betID), zap.Int64("bet_id", betID),
zap.Error(err), zap.Error(err),
) )
@ -335,7 +335,7 @@ func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status dom
ID: id, ID: id,
}) })
if err != nil { if err != nil {
mongoLogger.Error("failed to update bet outcome status", domain.MongoDBLogger.Error("failed to update bet outcome status",
zap.Int64("id", id), zap.Int64("id", id),
zap.Int32("status", int32(status)), zap.Int32("status", int32(status)),
zap.Error(err), zap.Error(err),
@ -428,7 +428,7 @@ func (s *Store) GetBetSummary(ctx context.Context, filter domain.ReportFilter) (
row := s.conn.QueryRow(ctx, query, args...) row := s.conn.QueryRow(ctx, query, args...)
err = row.Scan(&totalStakes, &totalBets, &activeBets, &totalWins, &totalLosses, &winBalance) err = row.Scan(&totalStakes, &totalBets, &activeBets, &totalWins, &totalLosses, &winBalance)
if err != nil { if err != nil {
mongoLogger.Error("failed to get bet summary", domain.MongoDBLogger.Error("failed to get bet summary",
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
zap.Error(err), zap.Error(err),
@ -436,7 +436,7 @@ func (s *Store) GetBetSummary(ctx context.Context, filter domain.ReportFilter) (
return 0, 0, 0, 0, 0, 0, fmt.Errorf("failed to get bet summary: %w", err) return 0, 0, 0, 0, 0, 0, fmt.Errorf("failed to get bet summary: %w", err)
} }
mongoLogger.Info("GetBetSummary executed successfully", domain.MongoDBLogger.Info("GetBetSummary executed successfully",
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
zap.Float64("totalStakes", float64(totalStakes)), // convert if needed zap.Float64("totalStakes", float64(totalStakes)), // convert if needed
@ -519,7 +519,7 @@ func (s *Store) GetBetStats(ctx context.Context, filter domain.ReportFilter) ([]
rows, err := s.conn.Query(ctx, query, args...) rows, err := s.conn.Query(ctx, query, args...)
if err != nil { if err != nil {
mongoLogger.Error("failed to query bet stats", domain.MongoDBLogger.Error("failed to query bet stats",
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
zap.Error(err), zap.Error(err),
@ -539,7 +539,7 @@ func (s *Store) GetBetStats(ctx context.Context, filter domain.ReportFilter) ([]
&stat.TotalPayouts, &stat.TotalPayouts,
&stat.AverageOdds, &stat.AverageOdds,
); err != nil { ); err != nil {
mongoLogger.Error("failed to scan bet stat", domain.MongoDBLogger.Error("failed to scan bet stat",
zap.Error(err), zap.Error(err),
) )
return nil, fmt.Errorf("failed to scan bet stat: %w", err) return nil, fmt.Errorf("failed to scan bet stat: %w", err)
@ -548,13 +548,13 @@ func (s *Store) GetBetStats(ctx context.Context, filter domain.ReportFilter) ([]
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
mongoLogger.Error("rows error after iteration", domain.MongoDBLogger.Error("rows error after iteration",
zap.Error(err), zap.Error(err),
) )
return nil, fmt.Errorf("rows error: %w", err) return nil, fmt.Errorf("rows error: %w", err)
} }
mongoLogger.Info("GetBetStats executed successfully", domain.MongoDBLogger.Info("GetBetStats executed successfully",
zap.Int("result_count", len(stats)), zap.Int("result_count", len(stats)),
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
@ -615,7 +615,7 @@ func (s *Store) GetSportPopularity(ctx context.Context, filter domain.ReportFilt
rows, err := s.conn.Query(ctx, query, args...) rows, err := s.conn.Query(ctx, query, args...)
if err != nil { if err != nil {
mongoLogger.Error("failed to query sport popularity", domain.MongoDBLogger.Error("failed to query sport popularity",
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
zap.Error(err), zap.Error(err),
@ -629,7 +629,7 @@ func (s *Store) GetSportPopularity(ctx context.Context, filter domain.ReportFilt
var date time.Time var date time.Time
var sportID string var sportID string
if err := rows.Scan(&date, &sportID); err != nil { if err := rows.Scan(&date, &sportID); err != nil {
mongoLogger.Error("failed to scan sport popularity", domain.MongoDBLogger.Error("failed to scan sport popularity",
zap.Error(err), zap.Error(err),
) )
return nil, fmt.Errorf("failed to scan sport popularity: %w", err) return nil, fmt.Errorf("failed to scan sport popularity: %w", err)
@ -638,13 +638,13 @@ func (s *Store) GetSportPopularity(ctx context.Context, filter domain.ReportFilt
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
mongoLogger.Error("rows error after iteration", domain.MongoDBLogger.Error("rows error after iteration",
zap.Error(err), zap.Error(err),
) )
return nil, fmt.Errorf("rows error: %w", err) return nil, fmt.Errorf("rows error: %w", err)
} }
mongoLogger.Info("GetSportPopularity executed successfully", domain.MongoDBLogger.Info("GetSportPopularity executed successfully",
zap.Int("result_count", len(popularity)), zap.Int("result_count", len(popularity)),
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
@ -705,7 +705,7 @@ func (s *Store) GetMarketPopularity(ctx context.Context, filter domain.ReportFil
rows, err := s.conn.Query(ctx, query, args...) rows, err := s.conn.Query(ctx, query, args...)
if err != nil { if err != nil {
mongoLogger.Error("failed to query market popularity", domain.MongoDBLogger.Error("failed to query market popularity",
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
zap.Error(err), zap.Error(err),
@ -719,7 +719,7 @@ func (s *Store) GetMarketPopularity(ctx context.Context, filter domain.ReportFil
var date time.Time var date time.Time
var marketName string var marketName string
if err := rows.Scan(&date, &marketName); err != nil { if err := rows.Scan(&date, &marketName); err != nil {
mongoLogger.Error("failed to scan market popularity", domain.MongoDBLogger.Error("failed to scan market popularity",
zap.Error(err), zap.Error(err),
) )
return nil, fmt.Errorf("failed to scan market popularity: %w", err) return nil, fmt.Errorf("failed to scan market popularity: %w", err)
@ -728,13 +728,13 @@ func (s *Store) GetMarketPopularity(ctx context.Context, filter domain.ReportFil
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
mongoLogger.Error("rows error after iteration", domain.MongoDBLogger.Error("rows error after iteration",
zap.Error(err), zap.Error(err),
) )
return nil, fmt.Errorf("rows error: %w", err) return nil, fmt.Errorf("rows error: %w", err)
} }
mongoLogger.Info("GetMarketPopularity executed successfully", domain.MongoDBLogger.Info("GetMarketPopularity executed successfully",
zap.Int("result_count", len(popularity)), zap.Int("result_count", len(popularity)),
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
@ -809,7 +809,7 @@ func (s *Store) GetExtremeValues(ctx context.Context, filter domain.ReportFilter
rows, err := s.conn.Query(ctx, query, args...) rows, err := s.conn.Query(ctx, query, args...)
if err != nil { if err != nil {
mongoLogger.Error("failed to query extreme values", domain.MongoDBLogger.Error("failed to query extreme values",
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
zap.Error(err), zap.Error(err),
@ -823,7 +823,7 @@ func (s *Store) GetExtremeValues(ctx context.Context, filter domain.ReportFilter
var date time.Time var date time.Time
var extreme domain.ExtremeValues var extreme domain.ExtremeValues
if err := rows.Scan(&date, &extreme.HighestStake, &extreme.HighestPayout); err != nil { if err := rows.Scan(&date, &extreme.HighestStake, &extreme.HighestPayout); err != nil {
mongoLogger.Error("failed to scan extreme values", domain.MongoDBLogger.Error("failed to scan extreme values",
zap.Error(err), zap.Error(err),
) )
return nil, fmt.Errorf("failed to scan extreme values: %w", err) return nil, fmt.Errorf("failed to scan extreme values: %w", err)
@ -832,13 +832,13 @@ func (s *Store) GetExtremeValues(ctx context.Context, filter domain.ReportFilter
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
mongoLogger.Error("rows error after iteration", domain.MongoDBLogger.Error("rows error after iteration",
zap.Error(err), zap.Error(err),
) )
return nil, fmt.Errorf("rows error: %w", err) return nil, fmt.Errorf("rows error: %w", err)
} }
mongoLogger.Info("GetExtremeValues executed successfully", domain.MongoDBLogger.Info("GetExtremeValues executed successfully",
zap.Int("result_count", len(extremes)), zap.Int("result_count", len(extremes)),
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
@ -899,7 +899,7 @@ func (s *Store) GetCustomerBetActivity(ctx context.Context, filter domain.Report
rows, err := s.conn.Query(ctx, query, args...) rows, err := s.conn.Query(ctx, query, args...)
if err != nil { if err != nil {
mongoLogger.Error("failed to query customer bet activity", domain.MongoDBLogger.Error("failed to query customer bet activity",
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
zap.Error(err), zap.Error(err),
@ -921,7 +921,7 @@ func (s *Store) GetCustomerBetActivity(ctx context.Context, filter domain.Report
&activity.LastBetDate, &activity.LastBetDate,
&activity.AverageOdds, &activity.AverageOdds,
); err != nil { ); err != nil {
mongoLogger.Error("failed to scan customer bet activity", domain.MongoDBLogger.Error("failed to scan customer bet activity",
zap.Error(err), zap.Error(err),
) )
return nil, fmt.Errorf("failed to scan customer bet activity: %w", err) return nil, fmt.Errorf("failed to scan customer bet activity: %w", err)
@ -930,13 +930,13 @@ func (s *Store) GetCustomerBetActivity(ctx context.Context, filter domain.Report
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
mongoLogger.Error("rows error after iteration", domain.MongoDBLogger.Error("rows error after iteration",
zap.Error(err), zap.Error(err),
) )
return nil, fmt.Errorf("rows error: %w", err) return nil, fmt.Errorf("rows error: %w", err)
} }
mongoLogger.Info("GetCustomerBetActivity executed successfully", domain.MongoDBLogger.Info("GetCustomerBetActivity executed successfully",
zap.Int("result_count", len(activities)), zap.Int("result_count", len(activities)),
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
@ -989,7 +989,7 @@ func (s *Store) GetBranchBetActivity(ctx context.Context, filter domain.ReportFi
rows, err := s.conn.Query(ctx, query, args...) rows, err := s.conn.Query(ctx, query, args...)
if err != nil { if err != nil {
mongoLogger.Error("failed to query branch bet activity", domain.MongoDBLogger.Error("failed to query branch bet activity",
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
zap.Error(err), zap.Error(err),
@ -1008,18 +1008,18 @@ func (s *Store) GetBranchBetActivity(ctx context.Context, filter domain.ReportFi
&activity.TotalWins, &activity.TotalWins,
&activity.TotalPayouts, &activity.TotalPayouts,
); err != nil { ); err != nil {
mongoLogger.Error("failed to scan branch bet activity", zap.Error(err)) domain.MongoDBLogger.Error("failed to scan branch bet activity", zap.Error(err))
return nil, fmt.Errorf("failed to scan branch bet activity: %w", err) return nil, fmt.Errorf("failed to scan branch bet activity: %w", err)
} }
activities = append(activities, activity) activities = append(activities, activity)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
mongoLogger.Error("rows error after iteration", zap.Error(err)) domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err))
return nil, fmt.Errorf("rows error: %w", err) return nil, fmt.Errorf("rows error: %w", err)
} }
mongoLogger.Info("GetBranchBetActivity executed successfully", domain.MongoDBLogger.Info("GetBranchBetActivity executed successfully",
zap.Int("result_count", len(activities)), zap.Int("result_count", len(activities)),
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
@ -1078,7 +1078,7 @@ func (s *Store) GetSportBetActivity(ctx context.Context, filter domain.ReportFil
rows, err := s.conn.Query(ctx, query, args...) rows, err := s.conn.Query(ctx, query, args...)
if err != nil { if err != nil {
mongoLogger.Error("failed to query sport bet activity", domain.MongoDBLogger.Error("failed to query sport bet activity",
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
zap.Error(err), zap.Error(err),
@ -1098,18 +1098,18 @@ func (s *Store) GetSportBetActivity(ctx context.Context, filter domain.ReportFil
&activity.TotalPayouts, &activity.TotalPayouts,
&activity.AverageOdds, &activity.AverageOdds,
); err != nil { ); err != nil {
mongoLogger.Error("failed to scan sport bet activity", zap.Error(err)) domain.MongoDBLogger.Error("failed to scan sport bet activity", zap.Error(err))
return nil, fmt.Errorf("failed to scan sport bet activity: %w", err) return nil, fmt.Errorf("failed to scan sport bet activity: %w", err)
} }
activities = append(activities, activity) activities = append(activities, activity)
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
mongoLogger.Error("rows error after iteration", zap.Error(err)) domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err))
return nil, fmt.Errorf("rows error: %w", err) return nil, fmt.Errorf("rows error: %w", err)
} }
mongoLogger.Info("GetSportBetActivity executed successfully", domain.MongoDBLogger.Info("GetSportBetActivity executed successfully",
zap.Int("result_count", len(activities)), zap.Int("result_count", len(activities)),
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
@ -1156,7 +1156,7 @@ func (s *Store) GetSportDetails(ctx context.Context, filter domain.ReportFilter)
rows, err := s.conn.Query(ctx, query, args...) rows, err := s.conn.Query(ctx, query, args...)
if err != nil { if err != nil {
mongoLogger.Error("failed to query sport details", domain.MongoDBLogger.Error("failed to query sport details",
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
zap.Error(err), zap.Error(err),
@ -1169,18 +1169,18 @@ func (s *Store) GetSportDetails(ctx context.Context, filter domain.ReportFilter)
for rows.Next() { for rows.Next() {
var sportID, matchName string var sportID, matchName string
if err := rows.Scan(&sportID, &matchName); err != nil { if err := rows.Scan(&sportID, &matchName); err != nil {
mongoLogger.Error("failed to scan sport detail", zap.Error(err)) domain.MongoDBLogger.Error("failed to scan sport detail", zap.Error(err))
return nil, fmt.Errorf("failed to scan sport detail: %w", err) return nil, fmt.Errorf("failed to scan sport detail: %w", err)
} }
details[sportID] = matchName details[sportID] = matchName
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
mongoLogger.Error("rows error after iteration", zap.Error(err)) domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err))
return nil, fmt.Errorf("rows error: %w", err) return nil, fmt.Errorf("rows error: %w", err)
} }
mongoLogger.Info("GetSportDetails executed successfully", domain.MongoDBLogger.Info("GetSportDetails executed successfully",
zap.Int("result_count", len(details)), zap.Int("result_count", len(details)),
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
@ -1241,7 +1241,7 @@ func (s *Store) GetSportMarketPopularity(ctx context.Context, filter domain.Repo
rows, err := s.conn.Query(ctx, query, args...) rows, err := s.conn.Query(ctx, query, args...)
if err != nil { if err != nil {
mongoLogger.Error("failed to query sport market popularity", domain.MongoDBLogger.Error("failed to query sport market popularity",
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),
zap.Error(err), zap.Error(err),
@ -1254,18 +1254,18 @@ func (s *Store) GetSportMarketPopularity(ctx context.Context, filter domain.Repo
for rows.Next() { for rows.Next() {
var sportID, marketName string var sportID, marketName string
if err := rows.Scan(&sportID, &marketName); err != nil { if err := rows.Scan(&sportID, &marketName); err != nil {
mongoLogger.Error("failed to scan sport market popularity", zap.Error(err)) domain.MongoDBLogger.Error("failed to scan sport market popularity", zap.Error(err))
return nil, fmt.Errorf("failed to scan sport market popularity: %w", err) return nil, fmt.Errorf("failed to scan sport market popularity: %w", err)
} }
popularity[sportID] = marketName popularity[sportID] = marketName
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
mongoLogger.Error("rows error after iteration", zap.Error(err)) domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err))
return nil, fmt.Errorf("rows error: %w", err) return nil, fmt.Errorf("rows error: %w", err)
} }
mongoLogger.Info("GetSportMarketPopularity executed successfully", domain.MongoDBLogger.Info("GetSportMarketPopularity executed successfully",
zap.Int("result_count", len(popularity)), zap.Int("result_count", len(popularity)),
zap.String("query", query), zap.String("query", query),
zap.Any("args", args), zap.Any("args", args),

View File

@ -260,10 +260,11 @@ func (s *Store) DeleteBranchCashier(ctx context.Context, userID int64) error {
} }
// GetBranchCounts returns total and active branch counts // GetBranchCounts returns total and active branch counts
func (s *Store) GetBranchCounts(ctx context.Context, filter domain.ReportFilter) (total, active int64, err error) { func (s *Store) GetBranchCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
query := `SELECT query := `SELECT
COUNT(*) as total, COUNT(*) as total,
COUNT(CASE WHEN is_active = true THEN 1 END) as active COUNT(CASE WHEN is_active = true THEN 1 END) as active,
COUNT(CASE WHEN is_active = false THEN 1 END) as inactive
FROM branches` FROM branches`
args := []interface{}{} args := []interface{}{}
@ -292,12 +293,12 @@ func (s *Store) GetBranchCounts(ctx context.Context, filter domain.ReportFilter)
} }
row := s.conn.QueryRow(ctx, query, args...) row := s.conn.QueryRow(ctx, query, args...)
err = row.Scan(&total, &active) err = row.Scan(&total, &active, &inactive)
if err != nil { if err != nil {
return 0, 0, fmt.Errorf("failed to get branch counts: %w", err) return 0, 0, 0, fmt.Errorf("failed to get branch counts: %w", err)
} }
return total, active, nil return total, active, inactive, nil
} }
// GetBranchDetails returns branch details map // GetBranchDetails returns branch details map

View File

@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"fmt"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -122,3 +123,40 @@ func (s *Store) UpdateCompany(ctx context.Context, company domain.UpdateCompany)
func (s *Store) DeleteCompany(ctx context.Context, id int64) error { func (s *Store) DeleteCompany(ctx context.Context, id int64) error {
return s.queries.DeleteCompany(ctx, id) return s.queries.DeleteCompany(ctx, id)
} }
func (s *Store) GetCompanyCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
query := `SELECT
COUNT(*) as total,
COUNT(CASE WHEN w.is_active = true THEN 1 END) as active,
COUNT(CASE WHEN w.is_active = false THEN 1 END) as inactive
FROM companies c
JOIN wallets w ON c.wallet_id = w.id`
args := []interface{}{}
argPos := 1
// Add filters if provided
if filter.StartTime.Valid {
query += fmt.Sprintf(" WHERE %screated_at >= $%d", func() string {
if len(args) == 0 {
return ""
}
return " AND "
}(), argPos)
args = append(args, filter.StartTime.Value)
argPos++
}
if filter.EndTime.Valid {
query += fmt.Sprintf(" AND created_at <= $%d", argPos)
args = append(args, filter.EndTime.Value)
argPos++
}
row := s.conn.QueryRow(ctx, query, args...)
err = row.Scan(&total, &active, &inactive)
if err != nil {
return 0, 0, 0, fmt.Errorf("failed to get company counts: %w", err)
}
return total, active, inactive, nil
}

View File

@ -3,11 +3,12 @@ package repository
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gorilla/websocket"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
"golang.org/x/net/websocket"
) )
type NotificationRepository interface { type NotificationRepository interface {
@ -18,6 +19,7 @@ type NotificationRepository interface {
ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error)
CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error)
GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error)
GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error)
} }
type Repository struct { type Repository struct {
@ -28,10 +30,13 @@ func NewNotificationRepository(store *Store) NotificationRepository {
return &Repository{store: store} return &Repository{store: store}
} }
func (r *Repository) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error { func (s *Store) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error {
return nil return nil
} }
func (s *Store) DisconnectWebSocket(recipientID int64) {
}
func (r *Repository) CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error) { func (r *Repository) CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error) {
var errorSeverity pgtype.Text var errorSeverity pgtype.Text
if notification.ErrorSeverity != nil { if notification.ErrorSeverity != nil {
@ -206,3 +211,162 @@ func unmarshalPayload(data []byte) (domain.NotificationPayload, error) {
func (r *Repository) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) { func (r *Repository) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) {
return r.store.queries.CountUnreadNotifications(ctx, recipient_id) return r.store.queries.CountUnreadNotifications(ctx, recipient_id)
} }
func (r *Repository) GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error) {
rows, err := r.store.queries.GetNotificationCounts(ctx)
if err != nil {
return 0, 0, 0, fmt.Errorf("failed to get notification counts: %w", err)
}
// var total, read, unread int64
for _, row := range rows {
total += row.Total
read += row.Read
unread += row.Unread
}
return total, read, unread, nil
}
func (s *Store) GetMostActiveNotificationRecipients(ctx context.Context, filter domain.ReportFilter, limit int) ([]domain.ActiveNotificationRecipient, error) {
query := `SELECT
n.recipient_id,
u.first_name || ' ' || u.last_name as recipient_name,
COUNT(*) as notification_count,
MAX(n.timestamp) as last_notification_time
FROM notifications n
JOIN users u ON n.recipient_id = u.id
WHERE n.timestamp BETWEEN $1 AND $2
GROUP BY n.recipient_id, u.first_name, u.last_name
ORDER BY notification_count DESC
LIMIT $3`
var recipients []domain.ActiveNotificationRecipient
rows, err := s.conn.Query(ctx, query, filter.StartTime.Value, filter.EndTime.Value, limit)
if err != nil {
return nil, fmt.Errorf("failed to get active notification recipients: %w", err)
}
defer rows.Close()
for rows.Next() {
var r domain.ActiveNotificationRecipient
if err := rows.Scan(&r.RecipientID, &r.RecipientName, &r.NotificationCount, &r.LastNotificationTime); err != nil {
return nil, err
}
recipients = append(recipients, r)
}
return recipients, nil
}
// GetNotificationDeliveryStats
func (s *Store) GetNotificationDeliveryStats(ctx context.Context, filter domain.ReportFilter) (domain.NotificationDeliveryStats, error) {
query := `SELECT
COUNT(*) as total_sent,
COUNT(CASE WHEN delivery_status = 'failed' THEN 1 END) as failed_deliveries,
(COUNT(CASE WHEN delivery_status = 'sent' THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0)) as success_rate,
MODE() WITHIN GROUP (ORDER BY delivery_channel) as most_used_channel
FROM notifications
WHERE timestamp BETWEEN $1 AND $2`
var stats domain.NotificationDeliveryStats
row := s.conn.QueryRow(ctx, query, filter.StartTime.Value, filter.EndTime.Value)
err := row.Scan(&stats.TotalSent, &stats.FailedDeliveries, &stats.SuccessRate, &stats.MostUsedChannel)
if err != nil {
return domain.NotificationDeliveryStats{}, fmt.Errorf("failed to get notification delivery stats: %w", err)
}
return stats, nil
}
// GetNotificationCountsByType
func (s *Store) GetNotificationCountsByType(ctx context.Context, filter domain.ReportFilter) (map[string]domain.NotificationTypeCount, error) {
query := `SELECT
type,
COUNT(*) as total,
COUNT(CASE WHEN is_read = true THEN 1 END) as read,
COUNT(CASE WHEN is_read = false THEN 1 END) as unread
FROM notifications
WHERE timestamp BETWEEN $1 AND $2
GROUP BY type`
counts := make(map[string]domain.NotificationTypeCount)
rows, err := s.conn.Query(ctx, query, filter.StartTime.Value, filter.EndTime.Value)
if err != nil {
return nil, fmt.Errorf("failed to get notification counts by type: %w", err)
}
defer rows.Close()
for rows.Next() {
var nt domain.NotificationTypeCount
var typ string
if err := rows.Scan(&typ, &nt.Total, &nt.Read, &nt.Unread); err != nil {
return nil, err
}
counts[typ] = nt
}
return counts, nil
}
func (s *Store) CountUnreadNotifications(ctx context.Context, userID int64) (int64, error) {
count, err := s.queries.CountUnreadNotifications(ctx, userID)
if err != nil {
return 0, err
}
return count, nil
}
// func (s *Store) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
// dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{
// Limit: int32(limit),
// Offset: int32(offset),
// })
// if err != nil {
// return nil, err
// }
// result := make([]domain.Notification, 0, len(dbNotifications))
// for _, dbNotif := range dbNotifications {
// // You may want to move this mapping logic to a shared function if not already present
// var errorSeverity *domain.NotificationErrorSeverity
// if dbNotif.ErrorSeverity.Valid {
// s := domain.NotificationErrorSeverity(dbNotif.ErrorSeverity.String)
// errorSeverity = &s
// }
// var deliveryChannel domain.DeliveryChannel
// if dbNotif.DeliveryChannel.Valid {
// deliveryChannel = domain.DeliveryChannel(dbNotif.DeliveryChannel.String)
// } else {
// deliveryChannel = ""
// }
// var priority int
// if dbNotif.Priority.Valid {
// priority = int(dbNotif.Priority.Int32)
// }
// payload, err := unmarshalPayload(dbNotif.Payload)
// if err != nil {
// payload = domain.NotificationPayload{}
// }
// result = append(result, domain.Notification{
// ID: dbNotif.ID,
// RecipientID: dbNotif.RecipientID,
// Type: domain.NotificationType(dbNotif.Type),
// Level: domain.NotificationLevel(dbNotif.Level),
// ErrorSeverity: errorSeverity,
// Reciever: domain.NotificationRecieverSide(dbNotif.Reciever),
// IsRead: dbNotif.IsRead,
// DeliveryStatus: domain.NotificationDeliveryStatus(dbNotif.DeliveryStatus),
// DeliveryChannel: deliveryChannel,
// Payload: payload,
// Priority: priority,
// Timestamp: dbNotif.Timestamp.Time,
// Metadata: dbNotif.Metadata,
// })
// }
// return result, nil
// }

View File

@ -15,10 +15,7 @@ func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer {
Type: domain.TransferType(transfer.Type), Type: domain.TransferType(transfer.Type),
Verified: transfer.Verified, Verified: transfer.Verified,
ReceiverWalletID: transfer.ReceiverWalletID, ReceiverWalletID: transfer.ReceiverWalletID,
SenderWalletID: domain.ValidInt64{ SenderWalletID: transfer.SenderWalletID.Int64,
Value: transfer.SenderWalletID.Int64,
Valid: transfer.SenderWalletID.Valid,
},
CashierID: domain.ValidInt64{ CashierID: domain.ValidInt64{
Value: transfer.CashierID.Int64, Value: transfer.CashierID.Int64,
Valid: transfer.CashierID.Valid, Valid: transfer.CashierID.Valid,
@ -33,8 +30,8 @@ func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferP
Type: string(transfer.Type), Type: string(transfer.Type),
ReceiverWalletID: transfer.ReceiverWalletID, ReceiverWalletID: transfer.ReceiverWalletID,
SenderWalletID: pgtype.Int8{ SenderWalletID: pgtype.Int8{
Int64: transfer.SenderWalletID.Value, Int64: transfer.SenderWalletID,
Valid: transfer.SenderWalletID.Valid, Valid: true,
}, },
CashierID: pgtype.Int8{ CashierID: pgtype.Int8{
Int64: transfer.CashierID.Value, Int64: transfer.CashierID.Value,
@ -78,6 +75,14 @@ func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]dom
return result, nil return result, nil
} }
func (s *Store) GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error) {
transfer, err := s.queries.GetTransferByReference(ctx, reference)
if err != nil {
return domain.Transfer{}, nil
}
return convertDBTransfer(transfer), nil
}
func (s *Store) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) { func (s *Store) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) {
transfer, err := s.queries.GetTransferByID(ctx, id) transfer, err := s.queries.GetTransferByID(ctx, id)
if err != nil { if err != nil {

View File

@ -466,10 +466,11 @@ func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.User, is_c
} }
// GetCustomerCounts returns total and active customer counts // GetCustomerCounts returns total and active customer counts
func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active int64, err error) { func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
query := `SELECT query := `SELECT
COUNT(*) as total, COUNT(*) as total,
SUM(CASE WHEN suspended = false THEN 1 ELSE 0 END) as active SUM(CASE WHEN suspended = false THEN 1 ELSE 0 END) as active,
SUM(CASE WHEN suspended = true THEN 1 ELSE 0 END) as inactive
FROM users WHERE role = 'customer'` FROM users WHERE role = 'customer'`
args := []interface{}{} args := []interface{}{}
@ -498,12 +499,12 @@ func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilte
} }
row := s.conn.QueryRow(ctx, query, args...) row := s.conn.QueryRow(ctx, query, args...)
err = row.Scan(&total, &active) err = row.Scan(&total, &active, &inactive)
if err != nil { if err != nil {
return 0, 0, fmt.Errorf("failed to get customer counts: %w", err) return 0, 0, 0, fmt.Errorf("failed to get customer counts: %w", err)
} }
return total, active, nil return total, active, inactive, nil
} }
// GetCustomerDetails returns customer details map // GetCustomerDetails returns customer details map
@ -711,3 +712,44 @@ func (s *Store) GetCustomerPreferences(ctx context.Context, filter domain.Report
return preferences, nil return preferences, nil
} }
func (s *Store) GetRoleCounts(ctx context.Context, role string, filter domain.ReportFilter) (total, active, inactive int64, err error) {
query := `SELECT
COUNT(*) as total,
COUNT(CASE WHEN suspended = false THEN 1 END) as active,
COUNT(CASE WHEN suspended = true THEN 1 END) as inactive
FROM users WHERE role = $1`
args := []interface{}{role}
argPos := 2
// Add filters if provided
if filter.CompanyID.Valid {
query += fmt.Sprintf(" AND company_id = $%d", argPos)
args = append(args, filter.CompanyID.Value)
argPos++
}
if filter.StartTime.Valid {
query += fmt.Sprintf(" AND %screated_at >= $%d", func() string {
if len(args) == 1 { // Only role parameter so far
return " "
}
return " AND "
}(), argPos)
args = append(args, filter.StartTime.Value)
argPos++
}
if filter.EndTime.Valid {
query += fmt.Sprintf(" AND created_at <= $%d", argPos)
args = append(args, filter.EndTime.Value)
argPos++
}
row := s.conn.QueryRow(ctx, query, args...)
err = row.Scan(&total, &active, &inactive)
if err != nil {
return 0, 0, 0, fmt.Errorf("failed to get %s counts: %w", role, err)
}
return total, active, inactive, nil
}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"fmt"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -17,12 +18,20 @@ type VirtualGameRepository interface {
CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error
GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error) GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error)
UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error
// WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
} }
type VirtualGameRepo struct { type VirtualGameRepo struct {
store *Store store *Store
} }
// GetGameCounts implements VirtualGameRepository.
// func (r *VirtualGameRepo) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total int64, active int64, inactive int64, err error) {
// panic("unimplemented")
// }
func NewVirtualGameRepository(store *Store) VirtualGameRepository { func NewVirtualGameRepository(store *Store) VirtualGameRepository {
return &VirtualGameRepo{store: store} return &VirtualGameRepo{store: store}
} }
@ -112,3 +121,58 @@ func (r *VirtualGameRepo) UpdateVirtualGameTransactionStatus(ctx context.Context
Status: status, Status: status,
}) })
} }
func (r *VirtualGameRepo) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
query := `SELECT
COUNT(*) as total,
COUNT(CASE WHEN is_active = true THEN 1 END) as active,
COUNT(CASE WHEN is_active = false THEN 1 END) as inactive
FROM virtual_games`
args := []interface{}{}
argPos := 1
// Add filters if provided
if filter.StartTime.Valid {
query += fmt.Sprintf(" WHERE created_at >= $%d", argPos)
args = append(args, filter.StartTime.Value)
argPos++
}
if filter.EndTime.Valid {
query += fmt.Sprintf(" AND created_at <= $%d", argPos)
args = append(args, filter.EndTime.Value)
argPos++
}
row := r.store.conn.QueryRow(ctx, query, args...)
err = row.Scan(&total, &active, &inactive)
if err != nil {
return 0, 0, 0, fmt.Errorf("failed to get game counts: %w", err)
}
return total, active, inactive, nil
}
// func (r *VirtualGameRepo) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
// _, tx, err := r.store.BeginTx(ctx)
// if err != nil {
// return err
// }
// txCtx := context.WithValue(ctx, contextTxKey, tx)
// defer func() {
// if p := recover(); p != nil {
// tx.Rollback(ctx)
// panic(p)
// }
// }()
// err = fn(txCtx)
// if err != nil {
// tx.Rollback(ctx)
// return err
// }
// return tx.Commit(ctx)
// }

View File

@ -225,3 +225,35 @@ func (s *Store) GetBalanceSummary(ctx context.Context, filter domain.ReportFilte
return summary, nil return summary, nil
} }
func (s *Store) GetTotalWallets(ctx context.Context, filter domain.ReportFilter) (int64, error) {
query := `SELECT COUNT(*) FROM wallets WHERE is_active = true`
args := []interface{}{}
argPos := 1
// Add filters if provided
if filter.StartTime.Valid {
query += fmt.Sprintf(" AND %screated_at >= $%d", func() string {
if len(args) == 0 {
return " WHERE "
}
return " AND "
}(), argPos)
args = append(args, filter.StartTime.Value)
argPos++
}
if filter.EndTime.Valid {
query += fmt.Sprintf(" AND created_at <= $%d", argPos)
args = append(args, filter.EndTime.Value)
argPos++
}
var total int64
row := s.conn.QueryRow(ctx, query, args...)
err := row.Scan(&total)
if err != nil {
return 0, fmt.Errorf("failed to get wallet counts: %w", err)
}
return total, nil
}

View File

@ -24,7 +24,7 @@ type BranchStore interface {
GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error) GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error)
DeleteBranchCashier(ctx context.Context, userID int64) error DeleteBranchCashier(ctx context.Context, userID int64) error
GetBranchCounts(ctx context.Context, filter domain.ReportFilter) (total, active int64, err error) GetBranchCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
GetBranchDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.BranchDetail, error) GetBranchDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.BranchDetail, error)
GetAllCompaniesBranch(ctx context.Context) ([]domain.Company, error) GetAllCompaniesBranch(ctx context.Context) ([]domain.Company, error)

View File

@ -5,129 +5,230 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
) )
type ChapaClient interface {
IssuePayment(ctx context.Context, payload domain.ChapaTransferPayload) (bool, error)
InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error)
FetchBanks() ([]domain.ChapaSupportedBank, error)
}
type Client struct { type Client struct {
BaseURL string baseURL string
SecretKey string secretKey string
HTTPClient *http.Client httpClient *http.Client
UserAgent string
} }
func NewClient(baseURL, secretKey string) *Client { func NewClient(baseURL, secretKey string) *Client {
return &Client{ return &Client{
BaseURL: baseURL, baseURL: baseURL,
SecretKey: secretKey, secretKey: secretKey,
HTTPClient: http.DefaultClient, httpClient: &http.Client{
UserAgent: "FortuneBet/1.0", Timeout: 30 * time.Second,
},
} }
} }
func (c *Client) IssuePayment(ctx context.Context, payload domain.ChapaTransferPayload) (bool, error) { func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) {
payload := map[string]interface{}{
"amount": req.Amount,
"currency": req.Currency,
"email": req.Email,
"first_name": req.FirstName,
"last_name": req.LastName,
"tx_ref": req.TxRef,
"callback_url": req.CallbackURL,
"return_url": req.ReturnURL,
}
payloadBytes, err := json.Marshal(payload) payloadBytes, err := json.Marshal(payload)
if err != nil { if err != nil {
return false, fmt.Errorf("failed to serialize payload: %w", err) return domain.ChapaDepositResponse{}, fmt.Errorf("failed to marshal payload: %w", err)
} }
req, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/transfers", bytes.NewBuffer(payloadBytes)) httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/transaction/initialize", bytes.NewBuffer(payloadBytes))
if err != nil { if err != nil {
return false, fmt.Errorf("failed to create HTTP request: %w", err) return domain.ChapaDepositResponse{}, fmt.Errorf("failed to create request: %w", err)
} }
req.Header.Set("Authorization", "Bearer "+c.SecretKey) httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return false, fmt.Errorf("chapa HTTP request failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return true, nil
}
return false, fmt.Errorf("chapa error: status %d, body: %s", resp.StatusCode, string(body))
}
// service/chapa_service.go
func (c *Client) InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error) {
fmt.Println("\n\nInit payment request: ", req)
payloadBytes, err := json.Marshal(req)
if err != nil {
fmt.Println("\n\nWe are here")
return "", fmt.Errorf("failed to serialize payload: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/transaction/initialize", bytes.NewBuffer(payloadBytes))
if err != nil {
fmt.Println("\n\nWe are here 2")
return "", fmt.Errorf("failed to create HTTP request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+c.SecretKey)
httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.HTTPClient.Do(httpReq) resp, err := c.httpClient.Do(httpReq)
if err != nil { if err != nil {
fmt.Println("\n\nWe are here 3") return domain.ChapaDepositResponse{}, fmt.Errorf("request failed: %w", err)
return "", fmt.Errorf("chapa HTTP request failed: %w", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK {
if resp.StatusCode < 200 || resp.StatusCode >= 300 { return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
fmt.Println("\n\nWe are here 4")
return "", fmt.Errorf("chapa error: status %d, body: %s", resp.StatusCode, string(body))
} }
var response struct { var response struct {
Message string `json:"message"`
Status string `json:"status"`
Data struct { Data struct {
CheckoutURL string `json:"checkout_url"` CheckoutURL string `json:"checkout_url"`
} `json:"data"` } `json:"data"`
} }
fmt.Printf("\n\nInit payment response body: %v\n\n", response) if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return domain.ChapaDepositResponse{}, fmt.Errorf("failed to decode response: %w", err)
if err := json.Unmarshal(body, &response); err != nil {
return "", fmt.Errorf("failed to parse chapa response: %w", err)
} }
return response.Data.CheckoutURL, nil return domain.ChapaDepositResponse{
CheckoutURL: response.Data.CheckoutURL,
// Reference: req.TxRef,
}, nil
} }
func (c *Client) FetchBanks() ([]domain.ChapaSupportedBank, error) { func (c *Client) VerifyPayment(ctx context.Context, reference string) (domain.ChapaDepositVerification, error) {
req, _ := http.NewRequest("GET", c.BaseURL+"/banks", nil) httpReq, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/transaction/verify/"+reference, nil)
req.Header.Set("Authorization", "Bearer "+c.SecretKey)
fmt.Printf("\n\nbase URL is: %s\n\n", c.BaseURL)
res, err := c.HTTPClient.Do(req)
if err != nil { if err != nil {
return nil, err return domain.ChapaDepositVerification{}, fmt.Errorf("failed to create request: %w", err)
}
defer res.Body.Close()
var resp struct {
Message string `json:"message"`
Data []domain.ChapaSupportedBank `json:"data"`
} }
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
return nil, err
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return domain.ChapaDepositVerification{}, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return domain.ChapaDepositVerification{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
} }
fmt.Printf("\n\nclient fetched banks: %+v\n\n", resp.Data) var verification domain.ChapaDepositVerification
return resp.Data, nil if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil {
return domain.ChapaDepositVerification{}, fmt.Errorf("failed to decode response: %w", err)
}
var status domain.PaymentStatus
switch verification.Status {
case "success":
status = domain.PaymentStatusCompleted
default:
status = domain.PaymentStatusFailed
}
return domain.ChapaDepositVerification{
Status: status,
Amount: verification.Amount,
Currency: verification.Currency,
}, nil
} }
func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
url := fmt.Sprintf("%s/transaction/verify/%s", c.baseURL, txRef)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.secretKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var response struct {
Status string `json:"status"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
var status domain.PaymentStatus
switch response.Status {
case "success":
status = domain.PaymentStatusCompleted
default:
status = domain.PaymentStatusFailed
}
return &domain.ChapaVerificationResponse{
Status: string(status),
Amount: response.Amount,
Currency: response.Currency,
}, nil
}
func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.secretKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var bankResponse domain.BankResponse
if err := json.NewDecoder(resp.Body).Decode(&bankResponse); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
var banks []domain.Bank
for _, bankData := range bankResponse.Data {
bank := domain.Bank{
ID: bankData.ID,
Slug: bankData.Slug,
Swift: bankData.Swift,
Name: bankData.Name,
AcctLength: bankData.AcctLength,
CountryID: bankData.CountryID,
IsMobileMoney: bankData.IsMobileMoney,
IsActive: bankData.IsActive,
IsRTGS: bankData.IsRTGS,
Active: bankData.Active,
Is24Hrs: bankData.Is24Hrs,
CreatedAt: bankData.CreatedAt,
UpdatedAt: bankData.UpdatedAt,
Currency: bankData.Currency,
}
banks = append(banks, bank)
}
return banks, nil
}
// Helper method to generate account regex based on bank type
// func GetAccountRegex(bank domain.Bank) string {
// if bank.IsMobileMoney != nil && bank.IsMobileMoney == 1 {
// return `^09[0-9]{8}$` // Ethiopian mobile money pattern
// }
// return fmt.Sprintf(`^[0-9]{%d}$`, bank.AcctLength)
// }
// // Helper method to generate example account number
// func GetExampleAccount(bank domain.Bank) string {
// if bank.IsMobileMoney != nil && *bank.IsMobileMoney {
// return "0912345678" // Ethiopian mobile number example
// }
// // Generate example based on length
// example := "1"
// for i := 1; i < bank.AcctLength; i++ {
// example += fmt.Sprintf("%d", i%10)
// }
// return example
// }

View File

@ -6,10 +6,17 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
) )
type ChapaPort interface { // type ChapaPort interface {
HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error // HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error
HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error // HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error
WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error // WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error
DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error) // DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error)
GetSupportedBanks() ([]domain.ChapaSupportedBank, error) // GetSupportedBanks() ([]domain.ChapaSupportedBank, error)
// }
type ChapaStore interface {
InitializePayment(request domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error)
VerifyPayment(reference string) (domain.ChapaDepositVerification, error)
ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error)
FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error)
} }

View File

@ -2,377 +2,191 @@ package chapa
import ( import (
"context" "context"
"database/sql"
"errors" "errors"
"fmt" "fmt"
"time"
// "log/slog"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/shopspring/decimal" )
var (
ErrPaymentNotFound = errors.New("payment not found")
ErrPaymentAlreadyExists = errors.New("payment with this reference already exists")
ErrInvalidPaymentAmount = errors.New("invalid payment amount")
) )
type Service struct { type Service struct {
transactionStore transaction.TransactionStore transferStore wallet.TransferStore
walletStore wallet.WalletStore walletStore wallet.WalletStore
userStore user.UserStore userStore user.UserStore
referralStore referralservice.ReferralStore cfg *config.Config
branchStore branch.BranchStore chapaClient *Client
chapaClient ChapaClient
config *config.Config
// logger *slog.Logger
store *repository.Store
} }
func NewService( func NewService(
txStore transaction.TransactionStore, transferStore wallet.TransferStore,
walletStore wallet.WalletStore, walletStore wallet.WalletStore,
userStore user.UserStore, userStore user.UserStore,
referralStore referralservice.ReferralStore, chapaClient *Client,
branchStore branch.BranchStore,
chapaClient ChapaClient,
store *repository.Store,
) *Service { ) *Service {
return &Service{ return &Service{
transactionStore: txStore, transferStore: transferStore,
walletStore: walletStore, walletStore: walletStore,
userStore: userStore, userStore: userStore,
referralStore: referralStore,
branchStore: branchStore,
chapaClient: chapaClient, chapaClient: chapaClient,
store: store,
} }
} }
func (s *Service) HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error { // InitiateDeposit starts a new deposit process
_, tx, err := s.store.BeginTx(ctx) func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) {
if err != nil { // Validate amount
return err if amount <= 0 {
} return "", ErrInvalidPaymentAmount
defer tx.Rollback(ctx)
// Use your services normally (they dont use the transaction, unless you wire `q`)
referenceID, err := strconv.ParseInt(req.Reference, 10, 64)
if err != nil {
return fmt.Errorf("invalid reference ID: %w", err)
} }
txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID) // Get user details
user, err := s.userStore.GetUserByID(ctx, userID)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { return "", fmt.Errorf("failed to get user: %w", err)
return fmt.Errorf("transaction with ID %d not found", referenceID)
} }
return err
var senderWallet domain.Wallet
// Generate unique reference
reference := uuid.New().String()
senderWallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
if err != nil {
return "", fmt.Errorf("failed to get sender wallets: %w", err)
} }
if txn.Verified { for _, wallet := range senderWallets {
if wallet.IsWithdraw {
senderWallet = wallet
break
}
}
// Check if payment with this reference already exists
// if transfer, err := s.transferStore.GetTransferByReference(ctx, reference); err == nil {
// return fmt.Sprintf("%v", transfer), ErrPaymentAlreadyExists
// }
// Create payment record
transfer := domain.CreateTransfer{
Amount: amount,
Type: domain.DEPOSIT,
PaymentMethod: domain.TRANSFER_CHAPA,
ReferenceNumber: reference,
// ReceiverWalletID: 1,
SenderWalletID: senderWallet.ID,
Verified: false,
}
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
return "", fmt.Errorf("failed to save payment: %w", err)
}
// Initialize payment with Chapa
response, err := s.chapaClient.InitializePayment(ctx, domain.ChapaDepositRequest{
Amount: amount,
Currency: "ETB",
Email: user.Email,
FirstName: user.FirstName,
LastName: user.LastName,
TxRef: reference,
CallbackURL: "https://fortunebet.com/api/v1/payments/callback",
ReturnURL: "https://fortunebet.com/api/v1/payment-success",
})
if err != nil {
// Update payment status to failed
// _ = s.transferStore.(payment.ID, domain.PaymentStatusFailed)
return "", fmt.Errorf("failed to initialize payment: %w", err)
}
return response.CheckoutURL, nil
}
// VerifyDeposit handles payment verification from webhook
func (s *Service) VerifyDeposit(ctx context.Context, reference string) error {
// Find payment by reference
payment, err := s.transferStore.GetTransferByReference(ctx, reference)
if err != nil {
return ErrPaymentNotFound
}
// Skip if already completed
if payment.Verified {
return nil return nil
} }
webhookAmount, _ := decimal.NewFromString(req.Amount) // Verify payment with Chapa
storedAmount, _ := decimal.NewFromString(txn.Amount.String()) verification, err := s.chapaClient.VerifyPayment(ctx, reference)
if !webhookAmount.Equal(storedAmount) { if err != nil {
return fmt.Errorf("amount mismatch") return fmt.Errorf("failed to verify payment: %w", err)
} }
txn.Verified = true // Update payment status
if err := s.transactionStore.UpdateTransactionVerified(ctx, txn.ID, txn.Verified, txn.ApprovedBy.Value, txn.ApproverName.Value); err != nil { if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
return err return fmt.Errorf("failed to update payment status: %w", err)
} }
return tx.Commit(ctx) // If payment is completed, credit user's wallet
if verification.Status == domain.PaymentStatusCompleted {
if err := s.walletStore.UpdateBalance(ctx, payment.SenderWalletID, payment.Amount); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err)
}
}
return nil
} }
func (s *Service) HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error { func (s *Service) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
_, tx, err := s.store.BeginTx(ctx) // First check if we already have a verified record
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
if err == nil && transfer.Verified {
return &domain.ChapaVerificationResponse{
Status: string(domain.PaymentStatusCompleted),
Amount: float64(transfer.Amount) / 100, // Convert from cents/kobo
Currency: "ETB",
}, nil
}
// If not verified or not found, verify with Chapa
verification, err := s.chapaClient.VerifyPayment(ctx, txRef)
if err != nil { if err != nil {
return err return nil, fmt.Errorf("failed to verify payment: %w", err)
}
defer tx.Rollback(ctx)
if req.Status != "success" {
return fmt.Errorf("payment status not successful")
} }
// 1. Parse reference ID // Update our records if payment is successful
referenceID, err := strconv.ParseInt(req.TxRef, 10, 64) if verification.Status == domain.PaymentStatusCompleted {
err = s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true)
if err != nil { if err != nil {
return fmt.Errorf("invalid tx_ref: %w", err) return nil, fmt.Errorf("failed to update verification status: %w", err)
} }
// 2. Fetch transaction // Credit user's wallet
txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID) err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID, transfer.Amount)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("failed to update wallet balance: %w", err)
return fmt.Errorf("transaction with ID %d not found", referenceID)
}
return err
}
if txn.Verified {
return nil // already processed
}
webhookAmount, _ := strconv.ParseFloat(req.Amount, 32)
if webhookAmount < float64(txn.Amount) {
return fmt.Errorf("webhook amount is less than expected")
}
// 4. Fetch wallet
wallet, err := s.walletStore.GetWalletByID(ctx, txn.ID)
if err != nil {
return err
}
// 5. Update wallet balance
newBalance := wallet.Balance + txn.Amount
if err := s.walletStore.UpdateBalance(ctx, wallet.ID, newBalance); err != nil {
return err
}
// 6. Mark transaction as verified
if err := s.transactionStore.UpdateTransactionVerified(ctx, txn.ID, true, txn.ApprovedBy.Value, txn.ApproverName.Value); err != nil {
return err
}
// 7. Check & Create Referral
stats, err := s.referralStore.GetReferralStats(ctx, strconv.FormatInt(wallet.UserID, 10))
if err != nil {
return err
}
if stats == nil {
if err := s.referralStore.CreateReferral(ctx, wallet.UserID); err != nil {
return err
} }
} }
return tx.Commit(ctx) return &domain.ChapaVerificationResponse{
Status: string(verification.Status),
Amount: float64(verification.Amount),
Currency: verification.Currency,
}, nil
} }
func (s *Service) WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error { func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
_, tx, err := s.store.BeginTx(ctx) banks, err := s.chapaClient.FetchSupportedBanks(ctx)
if err != nil { if err != nil {
return err return nil, fmt.Errorf("failed to fetch banks: %w", err)
} }
defer tx.Rollback(ctx)
// Get the requesting user
user, err := s.userStore.GetUserByID(ctx, userID)
if err != nil {
return fmt.Errorf("user not found: %w", err)
}
banks, err := s.GetSupportedBanks()
validBank := false
for _, bank := range banks {
if strconv.FormatInt(bank.Id, 10) == req.BankCode {
validBank = true
break
}
}
if !validBank {
return fmt.Errorf("invalid bank code")
}
// branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID)
// if err != nil {
// return err
// }
var targetWallet domain.Wallet
targetWallet, err = s.walletStore.GetWalletByID(ctx, req.WalletID)
if err != nil {
return err
}
// for _, w := range wallets {
// if w.ID == req.WalletID {
// targetWallet = &w
// break
// }
// }
// if targetWallet == nil {
// return fmt.Errorf("no wallet found with the specified ID")
// }
if !targetWallet.IsTransferable || !targetWallet.IsActive {
return fmt.Errorf("wallet not eligible for withdrawal")
}
if targetWallet.Balance < domain.Currency(req.Amount) {
return fmt.Errorf("insufficient balance")
}
txID := uuid.New().String()
payload := domain.ChapaTransferPayload{
AccountName: req.AccountName,
AccountNumber: req.AccountNumber,
Amount: strconv.FormatInt(req.Amount, 10),
Currency: req.Currency,
BeneficiaryName: req.BeneficiaryName,
TxRef: txID,
Reference: txID,
BankCode: req.BankCode,
}
ok, err := s.chapaClient.IssuePayment(ctx, payload)
if err != nil || !ok {
return fmt.Errorf("chapa transfer failed: %v", err)
}
// Create transaction using user and wallet info
_, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{
Amount: domain.Currency(req.Amount),
Type: domain.TransactionType(domain.TRANSACTION_CASHOUT),
ReferenceNumber: txID,
AccountName: req.AccountName,
AccountNumber: req.AccountNumber,
BankCode: req.BankCode,
BeneficiaryName: req.BeneficiaryName,
PaymentOption: domain.PaymentOption(domain.BANK),
BranchID: req.BranchID,
// BranchName: branch.Name,
// BranchLocation: branch.Location,
// CashierID: user.ID,
// CashierName: user.FullName,
FullName: user.FirstName + " " + user.LastName,
PhoneNumber: user.PhoneNumber,
// CompanyID: branch.CompanyID,
})
if err != nil {
return fmt.Errorf("failed to create transaction: %w", err)
}
newBalance := domain.Currency(req.Amount)
err = s.walletStore.UpdateBalance(ctx, targetWallet.ID, newBalance)
if err != nil {
return fmt.Errorf("failed to update wallet balance: %w", err)
}
return tx.Commit(ctx)
}
func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error) {
_, tx, err := s.store.BeginTx(ctx)
if err != nil {
return "", err
}
defer tx.Rollback(ctx)
if req.Amount <= 0 {
return "", fmt.Errorf("amount must be positive")
}
user, err := s.userStore.GetUserByID(ctx, userID)
if err != nil {
return "", err
}
branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID)
if err != nil {
return "", err
}
txID := uuid.New().String()
fmt.Printf("\n\nChapa deposit transaction created: %v%v\n\n", branch, user)
// _, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{
// Amount: req.Amount,
// Type: domain.TransactionType(domain.TRANSACTION_DEPOSIT),
// ReferenceNumber: txID,
// BranchID: req.BranchID,
// BranchName: branch.Name,
// BranchLocation: branch.Location,
// FullName: user.FirstName + " " + user.LastName,
// PhoneNumber: user.PhoneNumber,
// // CompanyID: branch.CompanyID,
// })
// if err != nil {
// return "", err
// }
// Fetch user details for Chapa payment
userInfo, err := s.userStore.GetUserByID(ctx, userID)
if err != nil {
return "", err
}
// fmt.Printf("\n\nCallbackURL is:%v\n\n", s.config.CHAPA_CALLBACK_URL)
// Build Chapa InitPaymentRequest (matches Chapa API)
paymentReq := domain.InitPaymentRequest{
Amount: req.Amount,
Currency: req.Currency,
Email: userInfo.Email,
FirstName: userInfo.FirstName,
LastName: userInfo.LastName,
TxRef: txID,
CallbackURL: "https://fortunebet.com/api/v1/payments/callback",
ReturnURL: "https://fortunebet.com/api/v1/payment-success",
}
// Call Chapa to initialize payment
var paymentURL string
maxRetries := 3
for range maxRetries {
paymentURL, err = s.chapaClient.InitPayment(ctx, paymentReq)
if err == nil {
break
}
time.Sleep(1 * time.Second) // Backoff
}
// Commit DB transaction
if err := tx.Commit(ctx); err != nil {
return "", err
}
return paymentURL, nil
}
func (s *Service) GetSupportedBanks() ([]domain.ChapaSupportedBank, error) {
banks, err := s.chapaClient.FetchBanks()
fmt.Printf("\n\nfetched banks: %+v\n\n", banks)
if err != nil {
return nil, err
}
// Add formatting logic (same as in original controller)
for i := range banks {
if banks[i].IsMobilemoney != nil && *(banks[i].IsMobilemoney) == 1 {
banks[i].AcctNumberRegex = "/^09[0-9]{8}$/"
banks[i].ExampleValue = "0952097177"
} else {
switch banks[i].AcctLength {
case 8:
banks[i].ExampleValue = "16967608"
case 13:
banks[i].ExampleValue = "1000222215735"
case 14:
banks[i].ExampleValue = "01320089280800"
case 16:
banks[i].ExampleValue = "1000222215735123"
}
banks[i].AcctNumberRegex = formatRegex(banks[i].AcctLength)
}
}
return banks, nil return banks, nil
} }
func formatRegex(length int) string {
return fmt.Sprintf("/^[0-9]{%d}$/", length)
}

View File

@ -13,4 +13,6 @@ type CompanyStore interface {
GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error)
UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error) UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error)
DeleteCompany(ctx context.Context, id int64) error DeleteCompany(ctx context.Context, id int64) error
GetCompanyCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
} }

View File

@ -18,4 +18,6 @@ type NotificationStore interface {
ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) // New method ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) // New method
CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error)
GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error)
GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error)
} }

View File

@ -45,7 +45,7 @@ func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *confi
return svc return svc
} }
func (s *Service) addConnection(ctx context.Context, recipientID int64, c *websocket.Conn) { func (s *Service) addConnection(recipientID int64, c *websocket.Conn) {
if c == nil { if c == nil {
s.logger.Warn("[NotificationSvc.AddConnection] Attempted to add nil WebSocket connection", "recipientID", recipientID) s.logger.Warn("[NotificationSvc.AddConnection] Attempted to add nil WebSocket connection", "recipientID", recipientID)
return return
@ -134,7 +134,7 @@ func (s *Service) GetAllNotifications(ctx context.Context, limit, offset int) ([
} }
func (s *Service) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error { func (s *Service) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error {
s.addConnection(ctx, recipientID, c) s.addConnection(recipientID, c)
s.logger.Info("[NotificationSvc.ConnectWebSocket] WebSocket connection established", "recipientID", recipientID) s.logger.Info("[NotificationSvc.ConnectWebSocket] WebSocket connection established", "recipientID", recipientID)
return nil return nil
} }
@ -283,3 +283,7 @@ func (s *Service) retryFailedNotifications() {
func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) { func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) {
return s.repo.CountUnreadNotifications(ctx, recipient_id) return s.repo.CountUnreadNotifications(ctx, recipient_id)
} }
// func (s *Service) GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error){
// return s.repo.Get(ctx, filter)
// }

View File

@ -7,9 +7,12 @@ import (
) )
type ReportStore interface { type ReportStore interface {
GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (DashboardSummary, error) GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (domain.DashboardSummary, error)
GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]BetAnalysis, error) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]domain.BetAnalysis, error)
GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]CustomerActivity, error) GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.CustomerActivity, error)
GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]BranchPerformance, error) GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.BranchPerformance, error)
GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]SportPerformance, error) GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.SportPerformance, error)
// GetNotificationReport(ctx context.Context, filter domain.ReportFilter) (domain.NotificationReport, error)
// GetCashierPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CashierPerformance, error)
// GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error)
} }

View File

@ -5,13 +5,16 @@ import (
"errors" "errors"
"log/slog" "log/slog"
"sort" "sort"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
// notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
// virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
) )
@ -26,6 +29,9 @@ type Service struct {
transactionStore transaction.TransactionStore transactionStore transaction.TransactionStore
branchStore branch.BranchStore branchStore branch.BranchStore
userStore user.UserStore userStore user.UserStore
companyStore company.CompanyStore
virtulaGamesStore repository.VirtualGameRepository
notificationStore repository.NotificationRepository
logger *slog.Logger logger *slog.Logger
} }
@ -35,6 +41,9 @@ func NewService(
transactionStore transaction.TransactionStore, transactionStore transaction.TransactionStore,
branchStore branch.BranchStore, branchStore branch.BranchStore,
userStore user.UserStore, userStore user.UserStore,
companyStore company.CompanyStore,
virtulaGamesStore repository.VirtualGameRepository,
notificationStore repository.NotificationRepository,
logger *slog.Logger, logger *slog.Logger,
) *Service { ) *Service {
return &Service{ return &Service{
@ -43,36 +52,20 @@ func NewService(
transactionStore: transactionStore, transactionStore: transactionStore,
branchStore: branchStore, branchStore: branchStore,
userStore: userStore, userStore: userStore,
companyStore: companyStore,
virtulaGamesStore: virtulaGamesStore,
notificationStore: notificationStore,
logger: logger, logger: logger,
} }
} }
// DashboardSummary represents comprehensive dashboard metrics
type DashboardSummary struct {
TotalStakes domain.Currency `json:"total_stakes"`
TotalBets int64 `json:"total_bets"`
ActiveBets int64 `json:"active_bets"`
WinBalance domain.Currency `json:"win_balance"`
TotalWins int64 `json:"total_wins"`
TotalLosses int64 `json:"total_losses"`
CustomerCount int64 `json:"customer_count"`
Profit domain.Currency `json:"profit"`
WinRate float64 `json:"win_rate"`
AverageStake domain.Currency `json:"average_stake"`
TotalDeposits domain.Currency `json:"total_deposits"`
TotalWithdrawals domain.Currency `json:"total_withdrawals"`
ActiveCustomers int64 `json:"active_customers"`
BranchesCount int64 `json:"branches_count"`
ActiveBranches int64 `json:"active_branches"`
}
// GetDashboardSummary returns comprehensive dashboard metrics // GetDashboardSummary returns comprehensive dashboard metrics
func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (DashboardSummary, error) { func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (domain.DashboardSummary, error) {
if err := validateTimeRange(filter); err != nil { if err := validateTimeRange(filter); err != nil {
return DashboardSummary{}, err return domain.DashboardSummary{}, err
} }
var summary DashboardSummary var summary domain.DashboardSummary
var err error var err error
// Get bets summary // Get bets summary
@ -80,28 +73,75 @@ func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportF
s.betStore.GetBetSummary(ctx, filter) s.betStore.GetBetSummary(ctx, filter)
if err != nil { if err != nil {
s.logger.Error("failed to get bet summary", "error", err) s.logger.Error("failed to get bet summary", "error", err)
return DashboardSummary{}, err return domain.DashboardSummary{}, err
} }
// Get customer metrics // Get customer metrics
summary.CustomerCount, summary.ActiveCustomers, err = s.userStore.GetCustomerCounts(ctx, filter) summary.CustomerCount, summary.ActiveCustomers, summary.InactiveCustomers, err = s.userStore.GetCustomerCounts(ctx, filter)
if err != nil { if err != nil {
s.logger.Error("failed to get customer counts", "error", err) s.logger.Error("failed to get customer counts", "error", err)
return DashboardSummary{}, err return domain.DashboardSummary{}, err
} }
// Get branch metrics // Get branch metrics
summary.BranchesCount, summary.ActiveBranches, err = s.branchStore.GetBranchCounts(ctx, filter) summary.BranchesCount, summary.ActiveBranches, summary.InactiveBranches, err = s.branchStore.GetBranchCounts(ctx, filter)
if err != nil { if err != nil {
s.logger.Error("failed to get branch counts", "error", err) s.logger.Error("failed to get branch counts", "error", err)
return DashboardSummary{}, err return domain.DashboardSummary{}, err
} }
// Get transaction metrics // Get transaction metrics
summary.TotalDeposits, summary.TotalWithdrawals, err = s.transactionStore.GetTransactionTotals(ctx, filter) summary.TotalDeposits, summary.TotalWithdrawals, err = s.transactionStore.GetTransactionTotals(ctx, filter)
if err != nil { if err != nil {
s.logger.Error("failed to get transaction totals", "error", err) s.logger.Error("failed to get transaction totals", "error", err)
return DashboardSummary{}, err return domain.DashboardSummary{}, err
}
// Get user role metrics
summary.TotalCashiers, summary.ActiveCashiers, summary.InactiveCashiers, err = s.userStore.GetRoleCounts(ctx, string(domain.RoleCashier), filter)
if err != nil {
s.logger.Error("failed to get cashier counts", "error", err)
return domain.DashboardSummary{}, err
}
summary.TotalManagers, summary.ActiveManagers, summary.InactiveManagers, err = s.userStore.GetRoleCounts(ctx, string(domain.RoleBranchManager), filter)
if err != nil {
s.logger.Error("failed to get manager counts", "error", err)
return domain.DashboardSummary{}, err
}
summary.TotalAdmins, summary.ActiveAdmins, summary.InactiveAdmins, err = s.userStore.GetRoleCounts(ctx, string(domain.RoleAdmin), filter)
if err != nil {
s.logger.Error("failed to get admin counts", "error", err)
return domain.DashboardSummary{}, err
}
// Get wallet metrics
summary.TotalWallets, err = s.walletStore.GetTotalWallets(ctx, filter)
if err != nil {
s.logger.Error("failed to get wallet counts", "error", err)
return domain.DashboardSummary{}, err
}
// Get sport/game metrics
summary.TotalGames, summary.ActiveGames, summary.InactiveGames, err = s.virtulaGamesStore.GetGameCounts(ctx, filter)
if err != nil {
s.logger.Error("failed to get game counts", "error", err)
return domain.DashboardSummary{}, err
}
// Get company metrics
summary.TotalCompanies, summary.ActiveCompanies, summary.InactiveCompanies, err = s.companyStore.GetCompanyCounts(ctx, filter)
if err != nil {
s.logger.Error("failed to get company counts", "error", err)
return domain.DashboardSummary{}, err
}
// Get notification metrics
summary.TotalNotifications, summary.ReadNotifications, summary.UnreadNotifications, err = s.notificationStore.GetNotificationCounts(ctx, filter)
if err != nil {
s.logger.Error("failed to get notification counts", "error", err)
return domain.DashboardSummary{}, err
} }
// Calculate derived metrics // Calculate derived metrics
@ -114,23 +154,8 @@ func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportF
return summary, nil return summary, nil
} }
// BetAnalysis represents detailed bet analysis // Getdomain.BetAnalysis returns detailed bet analysis
type BetAnalysis struct { func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]domain.BetAnalysis, error) {
Date time.Time `json:"date"`
TotalBets int64 `json:"total_bets"`
TotalStakes domain.Currency `json:"total_stakes"`
TotalWins int64 `json:"total_wins"`
TotalPayouts domain.Currency `json:"total_payouts"`
Profit domain.Currency `json:"profit"`
MostPopularSport string `json:"most_popular_sport"`
MostPopularMarket string `json:"most_popular_market"`
HighestStake domain.Currency `json:"highest_stake"`
HighestPayout domain.Currency `json:"highest_payout"`
AverageOdds float64 `json:"average_odds"`
}
// GetBetAnalysis returns detailed bet analysis
func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]BetAnalysis, error) {
if err := validateTimeRange(filter); err != nil { if err := validateTimeRange(filter); err != nil {
return nil, err return nil, err
} }
@ -164,9 +189,9 @@ func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter
} }
// Combine data into analysis // Combine data into analysis
var analysis []BetAnalysis var analysis []domain.BetAnalysis
for _, stat := range betStats { for _, stat := range betStats {
a := BetAnalysis{ a := domain.BetAnalysis{
Date: stat.Date, Date: stat.Date,
TotalBets: stat.TotalBets, TotalBets: stat.TotalBets,
TotalStakes: stat.TotalStakes, TotalStakes: stat.TotalStakes,
@ -203,27 +228,8 @@ func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter
return analysis, nil return analysis, nil
} }
// CustomerActivity represents customer activity metrics // Getdomain.CustomerActivity returns customer activity report
type CustomerActivity struct { func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.CustomerActivity, error) {
CustomerID int64 `json:"customer_id"`
CustomerName string `json:"customer_name"`
TotalBets int64 `json:"total_bets"`
TotalStakes domain.Currency `json:"total_stakes"`
TotalWins int64 `json:"total_wins"`
TotalPayouts domain.Currency `json:"total_payouts"`
Profit domain.Currency `json:"profit"`
FirstBetDate time.Time `json:"first_bet_date"`
LastBetDate time.Time `json:"last_bet_date"`
FavoriteSport string `json:"favorite_sport"`
FavoriteMarket string `json:"favorite_market"`
AverageStake domain.Currency `json:"average_stake"`
AverageOdds float64 `json:"average_odds"`
WinRate float64 `json:"win_rate"`
ActivityLevel string `json:"activity_level"` // High, Medium, Low
}
// GetCustomerActivity returns customer activity report
func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]CustomerActivity, error) {
if err := validateTimeRange(filter); err != nil { if err := validateTimeRange(filter); err != nil {
return nil, err return nil, err
} }
@ -250,9 +256,9 @@ func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportF
} }
// Combine data into activity report // Combine data into activity report
var activities []CustomerActivity var activities []domain.CustomerActivity
for _, bet := range customerBets { for _, bet := range customerBets {
activity := CustomerActivity{ activity := domain.CustomerActivity{
CustomerID: bet.CustomerID, CustomerID: bet.CustomerID,
TotalBets: bet.TotalBets, TotalBets: bet.TotalBets,
TotalStakes: bet.TotalStakes, TotalStakes: bet.TotalStakes,
@ -295,27 +301,8 @@ func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportF
return activities, nil return activities, nil
} }
// BranchPerformance represents branch performance metrics // Getdomain.BranchPerformance returns branch performance report
type BranchPerformance struct { func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.BranchPerformance, error) {
BranchID int64 `json:"branch_id"`
BranchName string `json:"branch_name"`
Location string `json:"location"`
ManagerName string `json:"manager_name"`
TotalBets int64 `json:"total_bets"`
TotalStakes domain.Currency `json:"total_stakes"`
TotalWins int64 `json:"total_wins"`
TotalPayouts domain.Currency `json:"total_payouts"`
Profit domain.Currency `json:"profit"`
CustomerCount int64 `json:"customer_count"`
Deposits domain.Currency `json:"deposits"`
Withdrawals domain.Currency `json:"withdrawals"`
WinRate float64 `json:"win_rate"`
AverageStake domain.Currency `json:"average_stake"`
PerformanceScore float64 `json:"performance_score"`
}
// GetBranchPerformance returns branch performance report
func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]BranchPerformance, error) {
// Get branch bet activity // Get branch bet activity
branchBets, err := s.betStore.GetBranchBetActivity(ctx, filter) branchBets, err := s.betStore.GetBranchBetActivity(ctx, filter)
if err != nil { if err != nil {
@ -345,9 +332,9 @@ func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.Report
} }
// Combine data into performance report // Combine data into performance report
var performances []BranchPerformance var performances []domain.BranchPerformance
for _, bet := range branchBets { for _, bet := range branchBets {
performance := BranchPerformance{ performance := domain.BranchPerformance{
BranchID: bet.BranchID, BranchID: bet.BranchID,
TotalBets: bet.TotalBets, TotalBets: bet.TotalBets,
TotalStakes: bet.TotalStakes, TotalStakes: bet.TotalStakes,
@ -394,24 +381,8 @@ func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.Report
return performances, nil return performances, nil
} }
// SportPerformance represents sport performance metrics // Getdomain.SportPerformance returns sport performance report
type SportPerformance struct { func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.SportPerformance, error) {
SportID string `json:"sport_id"`
SportName string `json:"sport_name"`
TotalBets int64 `json:"total_bets"`
TotalStakes domain.Currency `json:"total_stakes"`
TotalWins int64 `json:"total_wins"`
TotalPayouts domain.Currency `json:"total_payouts"`
Profit domain.Currency `json:"profit"`
PopularityRank int `json:"popularity_rank"`
WinRate float64 `json:"win_rate"`
AverageStake domain.Currency `json:"average_stake"`
AverageOdds float64 `json:"average_odds"`
MostPopularMarket string `json:"most_popular_market"`
}
// GetSportPerformance returns sport performance report
func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]SportPerformance, error) {
// Get sport bet activity // Get sport bet activity
sportBets, err := s.betStore.GetSportBetActivity(ctx, filter) sportBets, err := s.betStore.GetSportBetActivity(ctx, filter)
if err != nil { if err != nil {
@ -434,9 +405,9 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF
} }
// Combine data into performance report // Combine data into performance report
var performances []SportPerformance var performances []domain.SportPerformance
for _, bet := range sportBets { for _, bet := range sportBets {
performance := SportPerformance{ performance := domain.SportPerformance{
SportID: bet.SportID, SportID: bet.SportID,
TotalBets: bet.TotalBets, TotalBets: bet.TotalBets,
TotalStakes: bet.TotalStakes, TotalStakes: bet.TotalStakes,
@ -477,6 +448,164 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF
return performances, nil return performances, nil
} }
// 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 // Helper functions
func validateTimeRange(filter domain.ReportFilter) error { func validateTimeRange(filter domain.ReportFilter) error {
if filter.StartTime.Valid && filter.EndTime.Valid { if filter.StartTime.Valid && filter.EndTime.Valid {
@ -498,7 +627,7 @@ func calculateActivityLevel(totalBets int64, totalStakes domain.Currency) string
} }
} }
func calculatePerformanceScore(perf BranchPerformance) float64 { func calculatePerformanceScore(perf domain.BranchPerformance) float64 {
// Simple scoring algorithm - can be enhanced based on business rules // Simple scoring algorithm - can be enhanced based on business rules
profitScore := float64(perf.Profit) / 1000 profitScore := float64(perf.Profit) / 1000
customerScore := float64(perf.CustomerCount) * 0.1 customerScore := float64(perf.CustomerCount) * 0.1

View File

@ -24,9 +24,10 @@ type UserStore interface {
SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error) SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error)
UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error // identifier verified email or phone UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error // identifier verified email or phone
GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active int64, err error) GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
GetCustomerDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerDetail, error) GetCustomerDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerDetail, error)
GetBranchCustomerCounts(ctx context.Context, filter domain.ReportFilter) (map[int64]int64, error) GetBranchCustomerCounts(ctx context.Context, filter domain.ReportFilter) (map[int64]int64, error)
GetRoleCounts(ctx context.Context, role string, filter domain.ReportFilter) (total, active, inactive int64, err error)
} }
type SmsGateway interface { type SmsGateway interface {
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error SendSMSOTP(ctx context.Context, phoneNumber, otp string) error

View File

@ -9,5 +9,10 @@ import (
type VirtualGameService interface { type VirtualGameService interface {
GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error)
HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error
} ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (*domain.PopOKBetResponse, error)
GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfoRequest) (*domain.PopOKPlayerInfoResponse, error)
ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error)
ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error)
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
}

View File

@ -22,6 +22,7 @@ type service struct {
repo repository.VirtualGameRepository repo repository.VirtualGameRepository
walletSvc wallet.Service walletSvc wallet.Service
store *repository.Store store *repository.Store
// virtualGameStore repository.VirtualGameRepository
config *config.Config config *config.Config
logger *slog.Logger logger *slog.Logger
} }
@ -59,11 +60,18 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
} }
params := fmt.Sprintf( params := fmt.Sprintf(
"client_id=%s&game_id=%s¤cy=%s&lang=en&mode=%s&token=%s", "partnerId=%s&gameId=%s&gameMode=%s&lang=en&platform=%s&externalToken=%s",
s.config.PopOK.ClientID, gameID, currency, mode, token, s.config.PopOK.ClientID, gameID, mode, s.config.PopOK.Platform, token,
) )
signature := s.generateSignature(params)
return fmt.Sprintf("%s/game/launch?%s&signature=%s", s.config.PopOK.BaseURL, params, signature), nil // params = fmt.Sprintf(
// "partnerId=%s&gameId=%sgameMode=%s&lang=en&platform=%s",
// "1", "1", "fun", "111",
// )
// signature := s.generateSignature(params)
return fmt.Sprintf("%s?%s", s.config.PopOK.BaseURL, params), nil
// return fmt.Sprintf("%s?%s", s.config.PopOK.BaseURL, params), nil
} }
func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error { func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error {
@ -138,7 +146,228 @@ func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCall
return nil return nil
} }
func (s *service) generateSignature(params string) string { func (s *service) GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfoRequest) (*domain.PopOKPlayerInfoResponse, error) {
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
if err != nil {
s.logger.Error("Failed to parse JWT", "error", err)
return nil, fmt.Errorf("invalid token")
}
wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
if err != nil || len(wallets) == 0 {
s.logger.Error("No wallets found for user", "userID", claims.UserID)
return nil, fmt.Errorf("no wallet found")
}
return &domain.PopOKPlayerInfoResponse{
Country: "ET",
Currency: claims.Currency,
Balance: float64(wallets[0].Balance) / 100, // Convert cents to currency
PlayerID: fmt.Sprintf("%d", claims.UserID),
}, nil
}
func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (*domain.PopOKBetResponse, error) {
// Validate token and get user ID
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
if err != nil {
return nil, fmt.Errorf("invalid token")
}
// Convert amount to cents (assuming wallet uses cents)
amountCents := int64(req.Amount * 100)
// Deduct from wallet
userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
if err != nil {
return &domain.PopOKBetResponse{}, fmt.Errorf("Failed to read user wallets")
}
if err := s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents)); err != nil {
return nil, fmt.Errorf("insufficient balance")
}
// Create transaction record
tx := &domain.VirtualGameTransaction{
UserID: claims.UserID,
TransactionType: "BET",
Amount: -amountCents, // Negative for bets
Currency: req.Currency,
ExternalTransactionID: req.TransactionID,
Status: "COMPLETED",
CreatedAt: time.Now(),
}
if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil {
s.logger.Error("Failed to create bet transaction", "error", err)
return nil, fmt.Errorf("transaction failed")
}
return &domain.PopOKBetResponse{
TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", tx.ID), // Your internal transaction ID
Balance: float64(userWallets[0].Balance) / 100,
}, nil
}
func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) {
// 1. Validate token and get user ID
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
if err != nil {
s.logger.Error("Invalid token in win request", "error", err)
return nil, fmt.Errorf("invalid token")
}
// 2. Check for duplicate transaction (idempotency)
existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID)
if err != nil {
s.logger.Error("Failed to check existing transaction", "error", err)
return nil, fmt.Errorf("transaction check failed")
}
if existingTx != nil && existingTx.TransactionType == "WIN" {
s.logger.Warn("Duplicate win transaction", "transactionID", req.TransactionID)
wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
balance := 0.0
if len(wallets) > 0 {
balance = float64(wallets[0].Balance) / 100
}
return &domain.PopOKWinResponse{
TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%d", existingTx.ID),
Balance: balance,
}, nil
}
// 3. Convert amount to cents
amountCents := int64(req.Amount * 100)
// 4. Credit to wallet
if err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents)); err != nil {
s.logger.Error("Failed to credit wallet", "userID", claims.UserID, "error", err)
return nil, fmt.Errorf("wallet credit failed")
}
userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
if err != nil {
return &domain.PopOKWinResponse{}, fmt.Errorf("Failed to read user wallets")
}
// 5. Create transaction record
tx := &domain.VirtualGameTransaction{
UserID: claims.UserID,
TransactionType: "WIN",
Amount: amountCents,
Currency: req.Currency,
ExternalTransactionID: req.TransactionID,
Status: "COMPLETED",
CreatedAt: time.Now(),
}
if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil {
s.logger.Error("Failed to create win transaction", "error", err)
return nil, fmt.Errorf("transaction recording failed")
}
return &domain.PopOKWinResponse{
TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", tx.ID),
Balance: float64(userWallets[0].Balance) / 100,
}, nil
}
func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error) {
// 1. Validate token and get user ID
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
if err != nil {
s.logger.Error("Invalid token in cancel request", "error", err)
return nil, fmt.Errorf("invalid token")
}
// 2. Find the original bet transaction
originalBet, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID)
if err != nil {
s.logger.Error("Failed to find original bet", "transactionID", req.TransactionID, "error", err)
return nil, fmt.Errorf("original bet not found")
}
// 3. Validate the original transaction
if originalBet == nil || originalBet.TransactionType != "BET" {
s.logger.Error("Invalid original transaction for cancel", "transactionID", req.TransactionID)
return nil, fmt.Errorf("invalid original transaction")
}
// 4. Check if already cancelled
if originalBet.Status == "CANCELLED" {
s.logger.Warn("Transaction already cancelled", "transactionID", req.TransactionID)
wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
balance := 0.0
if len(wallets) > 0 {
balance = float64(wallets[0].Balance) / 100
}
return &domain.PopOKCancelResponse{
TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", originalBet.ID),
Balance: balance,
}, nil
}
// 5. Refund the bet amount (absolute value since bet amount is negative)
refundAmount := -originalBet.Amount
if err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(refundAmount)); err != nil {
s.logger.Error("Failed to refund bet", "userID", claims.UserID, "error", err)
return nil, fmt.Errorf("refund failed")
}
userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
if err != nil {
return &domain.PopOKCancelResponse{}, fmt.Errorf("Failed to read user wallets")
}
// 6. Mark original bet as cancelled and create cancel record
cancelTx := &domain.VirtualGameTransaction{
UserID: claims.UserID,
TransactionType: "CANCEL",
Amount: refundAmount,
Currency: originalBet.Currency,
ExternalTransactionID: req.TransactionID,
ReferenceTransactionID: fmt.Sprintf("%v", originalBet.ID),
Status: "COMPLETED",
CreatedAt: time.Now(),
}
if err := s.repo.UpdateVirtualGameTransactionStatus(ctx, originalBet.ID, "CANCELLED"); err != nil {
s.logger.Error("Failed to update transaction status", "error", err)
return nil, fmt.Errorf("update failed")
}
// Create cancel transaction
if err := s.repo.CreateVirtualGameTransaction(ctx, cancelTx); err != nil {
s.logger.Error("Failed to create cancel transaction", "error", err)
// Attempt to revert the status update
if revertErr := s.repo.UpdateVirtualGameTransactionStatus(ctx, originalBet.ID, originalBet.Status); revertErr != nil {
s.logger.Error("Failed to revert transaction status", "error", revertErr)
}
return nil, fmt.Errorf("create failed")
}
// if err != nil {
// s.logger.Error("Failed to process cancel transaction", "error", err)
// return nil, fmt.Errorf("transaction processing failed")
// }
return &domain.PopOKCancelResponse{
TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", cancelTx.ID),
Balance: float64(userWallets[0].Balance) / 100,
}, nil
}
func (s *service) GenerateSignature(params string) string {
h := hmac.New(sha256.New, []byte(s.config.PopOK.SecretKey)) h := hmac.New(sha256.New, []byte(s.config.PopOK.SecretKey))
h.Write([]byte(params)) h.Write([]byte(params))
return hex.EncodeToString(h.Sum(nil)) return hex.EncodeToString(h.Sum(nil))
@ -166,3 +395,7 @@ func (s *service) verifySignature(callback *domain.PopOKCallback) bool {
expected := hex.EncodeToString(h.Sum(nil)) expected := hex.EncodeToString(h.Sum(nil))
return expected == callback.Signature return expected == callback.Signature
} }
func (s *service) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
return s.repo.GetGameCounts(ctx, filter)
}

View File

@ -18,12 +18,14 @@ type WalletStore interface {
UpdateWalletActive(ctx context.Context, id int64, isActive bool) error UpdateWalletActive(ctx context.Context, id int64, isActive bool) error
GetBalanceSummary(ctx context.Context, filter domain.ReportFilter) (domain.BalanceSummary, error) GetBalanceSummary(ctx context.Context, filter domain.ReportFilter) (domain.BalanceSummary, error)
GetTotalWallets(ctx context.Context, filter domain.ReportFilter) (int64, error)
} }
type TransferStore interface { type TransferStore interface {
CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error)
GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error)
GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error)
GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error)
GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error)
UpdateTransferVerification(ctx context.Context, id int64, verified bool) error UpdateTransferVerification(ctx context.Context, id int64, verified bool) error
} }

View File

@ -14,7 +14,7 @@ var (
) )
func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
senderWallet, err := s.walletStore.GetWalletByID(ctx, transfer.SenderWalletID.Value) senderWallet, err := s.walletStore.GetWalletByID(ctx, transfer.SenderWalletID)
receiverWallet, err := s.walletStore.GetWalletByID(ctx, transfer.ReceiverWalletID) receiverWallet, err := s.walletStore.GetWalletByID(ctx, transfer.ReceiverWalletID)
if err != nil { if err != nil {
return domain.Transfer{}, fmt.Errorf("failed to get sender wallet: %w", err) return domain.Transfer{}, fmt.Errorf("failed to get sender wallet: %w", err)
@ -39,7 +39,7 @@ func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTran
"current_balance": %d, "current_balance": %d,
"wallet_id": %d, "wallet_id": %d,
"notification_type": "customer_facing" "notification_type": "customer_facing"
}`, transfer.Amount, senderWallet.Balance, transfer.SenderWalletID.Value)), }`, transfer.Amount, senderWallet.Balance, transfer.SenderWalletID)),
} }
// Send notification to admin team // Send notification to admin team
@ -53,7 +53,7 @@ func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTran
Headline: "CREDIT WARNING: System Running Out of Funds", Headline: "CREDIT WARNING: System Running Out of Funds",
Message: fmt.Sprintf( Message: fmt.Sprintf(
"Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f", "Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f",
transfer.SenderWalletID.Value, transfer.SenderWalletID,
float64(senderWallet.Balance)/100, float64(senderWallet.Balance)/100,
float64(transfer.Amount)/100, float64(transfer.Amount)/100,
), ),
@ -64,7 +64,7 @@ func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTran
"balance": %d, "balance": %d,
"required_amount": %d, "required_amount": %d,
"notification_type": "admin_alert" "notification_type": "admin_alert"
}`, transfer.SenderWalletID.Value, senderWallet.Balance, transfer.Amount), }`, transfer.SenderWalletID, senderWallet.Balance, transfer.Amount),
} }
// Send both notifications // Send both notifications
@ -100,6 +100,10 @@ func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error
return s.transferStore.GetAllTransfers(ctx) return s.transferStore.GetAllTransfers(ctx)
} }
func (s *Service) GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error) {
return s.transferStore.GetTransferByReference(ctx, reference)
}
func (s *Service) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) { func (s *Service) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) {
return s.transferStore.GetTransferByID(ctx, id) return s.transferStore.GetTransferByID(ctx, id)
} }
@ -119,7 +123,7 @@ func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransf
} }
// Add to receiver // Add to receiver
senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID.Value) senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID)
if err != nil { if err != nil {
return domain.Transfer{}, err return domain.Transfer{}, err
} else if senderWallet.Balance < transfer.Amount { } else if senderWallet.Balance < transfer.Amount {
@ -188,10 +192,7 @@ func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiver
// Log the transfer so that if there is a mistake, it can be reverted // Log the transfer so that if there is a mistake, it can be reverted
transfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ transfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
SenderWalletID: domain.ValidInt64{ SenderWalletID: senderID,
Value: senderID,
Valid: true,
},
CashierID: cashierID, CashierID: cashierID,
ReceiverWalletID: receiverID, ReceiverWalletID: receiverID,
Amount: amount, Amount: amount,

View File

@ -1,464 +1,163 @@
package handlers package handlers
import ( import (
// "bytes"
// "encoding/json"
// "fmt"
// "io"
// "net/http"
"fmt" "fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
// // GetBanks godoc // InitiateDeposit godoc
// // @Summary Get list of banks // @Summary Initiate a deposit
// // @Description Fetch all supported banks from Chapa // @Description Starts a new deposit process using Chapa payment gateway
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Success 200 {object} domain.ChapaSupportedBanksResponse
// // @Router /api/v1/chapa/banks [get]
// func (h *Handler) GetBanks(c *fiber.Ctx) error {
// httpReq, err := http.NewRequest("GET", h.Cfg.CHAPA_BASE_URL+"/banks", nil)
// // log.Printf("\n\nbase url is: %v\n\n", h.Cfg.CHAPA_BASE_URL)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{"error": "Failed to create request", "details": err.Error()})
// }
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{"error": "Failed to fetch banks", "details": err.Error()})
// }
// defer resp.Body.Close()
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{"error": "Failed to read response", "details": err.Error()})
// }
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// // InitializePayment godoc
// // @Summary Initialize a payment transaction
// // @Description Initiate a payment through Chapa
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param payload body domain.InitPaymentRequest true "Payment initialization request"
// // @Success 200 {object} domain.InitPaymentResponse
// // @Router /api/v1/chapa/payments/initialize [post]
// func (h *Handler) InitializePayment(c *fiber.Ctx) error {
// var req InitPaymentRequest
// if err := c.BodyParser(&req); err != nil {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Invalid request body",
// "details": err.Error(),
// })
// }
// // Generate and assign a unique transaction reference
// req.TxRef = uuid.New().String()
// payload, err := json.Marshal(req)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to serialize request",
// "details": err.Error(),
// })
// }
// httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transaction/initialize", bytes.NewBuffer(payload))
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to create request",
// "details": err.Error(),
// })
// }
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// httpReq.Header.Set("Content-Type", "application/json")
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to initialize payment",
// "details": err.Error(),
// })
// }
// defer resp.Body.Close()
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to read response",
// "details": err.Error(),
// })
// }
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// // VerifyTransaction godoc
// // @Summary Verify a payment transaction
// // @Description Verify the transaction status from Chapa using tx_ref
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param tx_ref path string true "Transaction Reference"
// // @Success 200 {object} domain.VerifyTransactionResponse
// // @Router /api/v1/chapa/payments/verify/{tx_ref} [get]
// func (h *Handler) VerifyTransaction(c *fiber.Ctx) error {
// txRef := c.Params("tx_ref")
// if txRef == "" {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Missing transaction reference",
// })
// }
// url := fmt.Sprintf("%s/transaction/verify/%s", h.Cfg.CHAPA_BASE_URL, txRef)
// httpReq, err := http.NewRequest("GET", url, nil)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to create request",
// "details": err.Error(),
// })
// }
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to verify transaction",
// "details": err.Error(),
// })
// }
// defer resp.Body.Close()
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to read response",
// "details": err.Error(),
// })
// }
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// // ReceiveWebhook godoc
// // @Summary Receive Chapa webhook
// // @Description Endpoint to receive webhook payloads from Chapa
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param payload body object true "Webhook Payload (dynamic)"
// // @Success 200 {string} string "ok"
// // @Router /api/v1/chapa/payments/callback [post]
// func (h *Handler) ReceiveWebhook(c *fiber.Ctx) error {
// var payload map[string]interface{}
// if err := c.BodyParser(&payload); err != nil {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Invalid webhook data",
// "details": err.Error(),
// })
// }
// h.logger.Info("Chapa webhook received", "payload", payload)
// // Optional: you can verify tx_ref here again if needed
// return c.SendStatus(fiber.StatusOK)
// }
// // CreateTransfer godoc
// // @Summary Create a money transfer
// // @Description Initiate a transfer request via Chapa
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param payload body domain.TransferRequest true "Transfer request body"
// // @Success 200 {object} domain.CreateTransferResponse
// // @Router /api/v1/chapa/transfers [post]
// func (h *Handler) CreateTransfer(c *fiber.Ctx) error {
// var req TransferRequest
// if err := c.BodyParser(&req); err != nil {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Invalid request",
// "details": err.Error(),
// })
// }
// // Inject unique transaction reference
// req.Reference = uuid.New().String()
// payload, err := json.Marshal(req)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to serialize request",
// "details": err.Error(),
// })
// }
// httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transfers", bytes.NewBuffer(payload))
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to create HTTP request",
// "details": err.Error(),
// })
// }
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// httpReq.Header.Set("Content-Type", "application/json")
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Transfer request failed",
// "details": err.Error(),
// })
// }
// defer resp.Body.Close()
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to read response",
// "details": err.Error(),
// })
// }
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// // VerifyTransfer godoc
// // @Summary Verify a transfer
// // @Description Check the status of a money transfer via reference
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param transfer_ref path string true "Transfer Reference"
// // @Success 200 {object} domain.VerifyTransferResponse
// // @Router /api/v1/chapa/transfers/verify/{transfer_ref} [get]
// func (h *Handler) VerifyTransfer(c *fiber.Ctx) error {
// transferRef := c.Params("transfer_ref")
// if transferRef == "" {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Missing transfer reference in URL",
// })
// }
// url := fmt.Sprintf("%s/transfers/verify/%s", h.Cfg.CHAPA_BASE_URL, transferRef)
// httpReq, err := http.NewRequest("GET", url, nil)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to create HTTP request",
// "details": err.Error(),
// })
// }
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Verification request failed",
// "details": err.Error(),
// })
// }
// defer resp.Body.Close()
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to read response body",
// "details": err.Error(),
// })
// }
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// VerifyChapaPayment godoc
// @Summary Verifies Chapa webhook transaction
// @Tags Chapa // @Tags Chapa
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param payload body domain.ChapaTransactionType true "Webhook Payload" // @Param request body domain.ChapaDepositRequestPayload true "Deposit request"
// @Success 200 {object} domain.Response // @Success 200 {object} domain.ChapaDepositResponse
// @Router /api/v1/chapa/payments/verify [post] // @Failure 400 {object} domain.ErrorResponse
func (h *Handler) VerifyChapaPayment(c *fiber.Ctx) error { // @Failure 500 {object} domain.ErrorResponse
var txType domain.ChapaTransactionType
if err := c.BodyParser(&txType); err != nil {
return domain.UnProcessableEntityResponse(c)
}
switch txType.Type {
case "Payout":
var payload domain.ChapaWebHookTransfer
if err := c.BodyParser(&payload); err != nil {
return domain.UnProcessableEntityResponse(c)
}
if err := h.chapaSvc.HandleChapaTransferWebhook(c.Context(), payload); err != nil {
return domain.FiberErrorResponse(c, err)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa transfer verified successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
case "API":
var payload domain.ChapaWebHookPayment
if err := c.BodyParser(&payload); err != nil {
return domain.UnProcessableEntityResponse(c)
}
if err := h.chapaSvc.HandleChapaPaymentWebhook(c.Context(), payload); err != nil {
return domain.FiberErrorResponse(c, err)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa payment verified successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
default:
return c.Status(fiber.StatusBadRequest).JSON(domain.Response{
Message: "Invalid Chapa transaction type",
Success: false,
StatusCode: fiber.StatusBadRequest,
})
}
}
// WithdrawUsingChapa godoc
// @Summary Withdraw using Chapa
// @Description Initiates a withdrawal transaction using Chapa for the authenticated user.
// @Tags Chapa
// @Accept json
// @Produce json
// @Param request body domain.ChapaWithdrawRequest true "Chapa Withdraw Request"
// @Success 200 {object} domain.Response{data=string} "Withdrawal requested successfully"
// @Failure 400 {object} domain.Response "Invalid request"
// @Failure 401 {object} domain.Response "Unauthorized"
// @Failure 422 {object} domain.Response "Unprocessable Entity"
// @Failure 500 {object} domain.Response "Internal Server Error"
// @Router /api/v1/chapa/payments/withdraw [post]
func (h *Handler) WithdrawUsingChapa(c *fiber.Ctx) error {
var req domain.ChapaWithdrawRequest
if err := c.BodyParser(&req); err != nil {
return domain.UnProcessableEntityResponse(c)
}
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
return c.Status(fiber.StatusUnauthorized).JSON(domain.Response{
Message: "Unauthorized",
Success: false,
StatusCode: fiber.StatusUnauthorized,
})
}
if err := h.chapaSvc.WithdrawUsingChapa(c.Context(), userID, req); err != nil {
return domain.FiberErrorResponse(c, err)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Withdrawal requested successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
}
// DepositUsingChapa godoc
// @Summary Deposit money into user wallet using Chapa
// @Description Deposits money into user wallet from user account using Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Param payload body domain.ChapaDepositRequest true "Deposit request payload"
// @Success 200 {object} domain.ChapaPaymentUrlResponseWrapper
// @Failure 400 {object} domain.Response "Invalid request"
// @Failure 422 {object} domain.Response "Validation error"
// @Failure 500 {object} domain.Response "Internal server error"
// @Router /api/v1/chapa/payments/deposit [post] // @Router /api/v1/chapa/payments/deposit [post]
func (h *Handler) DepositUsingChapa(c *fiber.Ctx) error { func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
// Extract user info from token (adjust as per your auth middleware) // Get user ID from context (set by your auth middleware)
userID, ok := c.Locals("user_id").(int64) userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 { if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(domain.Response{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Unauthorized", Error: "invalid user ID",
Success: false,
StatusCode: fiber.StatusUnauthorized,
}) })
} }
var req domain.ChapaDepositRequest var req domain.ChapaDepositRequestPayload
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return domain.UnProcessableEntityResponse(c) fmt.Sprintln("We first first are here init Chapa payment")
} return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: err.Error(),
// Validate input in domain/domain (you may have a Validate method)
if err := req.Validate(); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.Response{
Message: err.Error(),
Success: false,
StatusCode: fiber.StatusBadRequest,
}) })
} }
// Call service to handle the deposit logic and get payment URL amount := domain.Currency(req.Amount * 100)
paymentUrl, svcErr := h.chapaSvc.DepositUsingChapa(c.Context(), userID, req)
if svcErr != nil { fmt.Sprintln("We are here init Chapa payment")
return domain.FiberErrorResponse(c, svcErr)
checkoutURL, err := h.chapaSvc.InitiateDeposit(c.Context(), userID, amount)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: checkoutURL,
})
} }
return c.Status(fiber.StatusOK).JSON(domain.ResponseWDataFactory[domain.ChapaPaymentUrlResponse]{ return c.Status(fiber.StatusOK).JSON(domain.ChapaDepositResponse{
Data: domain.ChapaPaymentUrlResponse{ CheckoutURL: checkoutURL,
PaymentURL: paymentUrl,
},
Response: domain.Response{
Message: "Deposit process started on wallet, fulfill payment using the URL provided",
Success: true,
StatusCode: fiber.StatusOK,
},
}) })
} }
// ReadChapaBanks godoc // WebhookCallback godoc
// @Summary fetches chapa supported banks // @Summary Chapa payment webhook callback (used by Chapa)
// @Description Handles payment notifications from Chapa
// @Tags Chapa // @Tags Chapa
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} domain.ChapaSupportedBanksResponseWrapper // @Param request body domain.ChapaWebhookPayload true "Webhook payload"
// @Failure 400,401,404,422,500 {object} domain.Response // @Success 200 {object} map[string]interface{}
// @Router /api/v1/chapa/banks [get] // @Failure 400 {object} domain.ErrorResponse
func (h *Handler) ReadChapaBanks(c *fiber.Ctx) error { // @Failure 500 {object} domain.ErrorResponse
banks, err := h.chapaSvc.GetSupportedBanks() // @Router /api/v1/chapa/payments/webhook/verify [post]
fmt.Printf("\n\nhandler fetched banks: %+v\n\n", banks) func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
if err != nil { // Verify webhook signature first
return c.Status(fiber.StatusInternalServerError).JSON(domain.Response{ // signature := c.Get("Chapa-Signature")
Message: "Internal server error", // if !verifySignature(signature, c.Body()) {
Success: false, // return c.Status(fiber.StatusUnauthorized).JSON(ErrorResponse{
StatusCode: fiber.StatusInternalServerError, // Error: "invalid signature",
// })
// }
var payload struct {
TxRef string `json:"tx_ref"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Status string `json:"status"`
}
if err := c.BodyParser(&payload); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: err.Error(),
}) })
} }
return c.Status(fiber.StatusOK).JSON(domain.ResponseWDataFactory[[]domain.ChapaSupportedBank]{ if err := h.chapaSvc.VerifyDeposit(c.Context(), payload.TxRef); err != nil {
Data: banks, return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Response: domain.Response{ Error: err.Error(),
Message: "read successful on chapa supported banks", })
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
StatusCode: 200,
Message: "payment verified successfully",
Data: payload.TxRef,
Success: true, Success: true,
StatusCode: fiber.StatusOK, })
}, }
// VerifyPayment godoc
// @Summary Verify a payment manually
// @Description Manually verify a payment using Chapa's API
// @Tags Chapa
// @Accept json
// @Produce json
// @Param tx_ref path string true "Transaction Reference"
// @Success 200 {object} domain.ChapaVerificationResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/payments/manual/verify/{tx_ref} [get]
func (h *Handler) ManualVerifyPayment(c *fiber.Ctx) error {
txRef := c.Params("tx_ref")
if txRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to verify Chapa transaction",
Error: "Transaction reference is required",
})
}
verification, err := h.chapaSvc.ManualVerifyPayment(c.Context(), txRef)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to verify Chapa transaction",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.ChapaVerificationResponse{
Status: string(verification.Status),
Amount: verification.Amount,
Currency: verification.Currency,
TxRef: txRef,
})
}
// GetSupportedBanks godoc
// @Summary Get supported banks
// @Description Get list of banks supported by Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Success 200 {array} domain.Bank
// @Failure 500 {object} domain.ErrorResponse
// @Router /banks [get]
func (h *Handler) GetSupportedBanks(c *fiber.Ctx) error {
banks, err := h.chapaSvc.GetSupportedBanks(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Failed to fetch banks",
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Banks fetched successfully",
StatusCode: 200,
Success: true,
Data: banks,
}) })
} }

View File

@ -34,7 +34,7 @@ type Handler struct {
userSvc *user.Service userSvc *user.Service
referralSvc referralservice.ReferralStore referralSvc referralservice.ReferralStore
reportSvc report.ReportStore reportSvc report.ReportStore
chapaSvc chapa.ChapaPort chapaSvc *chapa.Service
walletSvc *wallet.Service walletSvc *wallet.Service
transactionSvc *transaction.Service transactionSvc *transaction.Service
ticketSvc *ticket.Service ticketSvc *ticket.Service
@ -60,7 +60,7 @@ func New(
notificationSvc *notificationservice.Service, notificationSvc *notificationservice.Service,
validator *customvalidator.CustomValidator, validator *customvalidator.CustomValidator,
reportSvc report.ReportStore, reportSvc report.ReportStore,
chapaSvc chapa.ChapaPort, chapaSvc *chapa.Service,
walletSvc *wallet.Service, walletSvc *wallet.Service,
referralSvc referralservice.ReferralStore, referralSvc referralservice.ReferralStore,
virtualGameSvc virtualgameservice.VirtualGameService, virtualGameSvc virtualgameservice.VirtualGameService,

View File

@ -1,131 +0,0 @@
package handlers
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"testing"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
// --- Mock service ---
type MockChapaService struct {
mock.Mock
}
func (m *MockChapaService) GetSupportedBanks() ([]domain.ChapaSupportedBank, error) {
args := m.Called()
return args.Get(0).([]domain.ChapaSupportedBank), args.Error(1)
}
// --- Tests ---
func (h *Handler) TestReadChapaBanks_Success(t *testing.T) {
app := fiber.New()
mockService := new(MockChapaService)
now := time.Now()
isMobile := 1
isRtgs := 1
is24hrs := 1
mockBanks := []domain.ChapaSupportedBank{
{
Id: 101,
Slug: "bank-a",
Swift: "BKAETHAA",
Name: "Bank A",
AcctLength: 13,
AcctNumberRegex: "^[0-9]{13}$",
ExampleValue: "1000222215735",
CountryId: 1,
IsMobilemoney: &isMobile,
IsActive: 1,
IsRtgs: &isRtgs,
Active: 1,
Is24Hrs: &is24hrs,
CreatedAt: now,
UpdatedAt: now,
Currency: "ETB",
},
}
mockService.On("GetSupportedBanks").Return(mockBanks, nil)
// handler := handlers.NewChapaHandler(mockService)
app.Post("/chapa/banks", h.ReadChapaBanks)
req := createTestRequest(t, "POST", "/chapa/banks", nil)
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
var body domain.ResponseWDataFactory[[]domain.ChapaSupportedBank]
err = parseJSONBody(resp, &body)
require.NoError(t, err)
assert.True(t, body.Success)
assert.Equal(t, "read successful on chapa supported banks", body.Message)
require.Len(t, body.Data, 1)
assert.Equal(t, mockBanks[0].Name, body.Data[0].Name)
assert.Equal(t, mockBanks[0].AcctNumberRegex, body.Data[0].AcctNumberRegex)
mockService.AssertExpectations(t)
}
func (h *Handler) TestReadChapaBanks_Failure(t *testing.T) {
app := fiber.New()
mockService := new(MockChapaService)
mockService.On("GetSupportedBanks").Return(nil, errors.New("chapa service unavailable"))
// handler := handlers.NewChapaHandler(mockService)
app.Post("/chapa/banks", h.ReadChapaBanks)
req := createTestRequest(t, "POST", "/chapa/banks", nil)
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)
var body domain.Response
err = parseJSONBody(resp, &body)
require.NoError(t, err)
assert.False(t, body.Success)
assert.Equal(t, "Internal server error", body.Message)
mockService.AssertExpectations(t)
}
func createTestRequest(t *testing.T, method, url string, body interface{}) *http.Request {
var buf io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
t.Fatal(err)
}
buf = bytes.NewBuffer(b)
}
req, err := http.NewRequest(method, url, buf)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
return req
}
func parseJSONBody(resp *http.Response, target interface{}) error {
return json.NewDecoder(resp.Body).Decode(target)
}

View File

@ -36,9 +36,7 @@ type RefillRes struct {
func convertTransfer(transfer domain.Transfer) TransferWalletRes { func convertTransfer(transfer domain.Transfer) TransferWalletRes {
var senderWalletID *int64 var senderWalletID *int64
if transfer.SenderWalletID.Valid { senderWalletID = &transfer.SenderWalletID
senderWalletID = &transfer.SenderWalletID.Value
}
var cashierID *int64 var cashierID *int64
if transfer.CashierID.Valid { if transfer.CashierID.Valid {

View File

@ -5,10 +5,11 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type launchVirtualGameReq struct { type launchVirtualGameReq struct {
GameID string `json:"game_id" validate:"required" example:"crash_001"` GameID string `json:"game_id" validate:"required" example:"crash_001"`
Currency string `json:"currency" validate:"required,len=3" example:"USD"` Currency string `json:"currency" validate:"required,len=3" example:"USD"`
Mode string `json:"mode" validate:"required,oneof=REAL DEMO" example:"REAL"` Mode string `json:"mode" validate:"required,oneof=fun real" example:"real"`
} }
type launchVirtualGameRes struct { type launchVirtualGameRes struct {
@ -81,3 +82,76 @@ func (h *Handler) HandleVirtualGameCallback(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Callback processed successfully", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "Callback processed successfully", nil, nil)
} }
func (h *Handler) HandlePlayerInfo(c *fiber.Ctx) error {
var req domain.PopOKPlayerInfoRequest
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request")
}
resp, err := h.virtualGameSvc.GetPlayerInfo(c.Context(), &req)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return response.WriteJSON(c, fiber.StatusOK, "Player info retrieved", resp, nil)
}
func (h *Handler) HandleBet(c *fiber.Ctx) error {
var req domain.PopOKBetRequest
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid bet request")
}
resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req)
if err != nil {
code := fiber.StatusInternalServerError
if err.Error() == "invalid token" {
code = fiber.StatusUnauthorized
} else if err.Error() == "insufficient balance" {
code = fiber.StatusBadRequest
}
return fiber.NewError(code, err.Error())
}
return response.WriteJSON(c, fiber.StatusOK, "Bet processed", resp, nil)
}
func (h *Handler) HandleWin(c *fiber.Ctx) error {
var req domain.PopOKWinRequest
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid win request")
}
resp, err := h.virtualGameSvc.ProcessWin(c.Context(), &req)
if err != nil {
code := fiber.StatusInternalServerError
if err.Error() == "invalid token" {
code = fiber.StatusUnauthorized
}
return fiber.NewError(code, err.Error())
}
return response.WriteJSON(c, fiber.StatusOK, "Win processed", resp, nil)
}
func (h *Handler) HandleCancel(c *fiber.Ctx) error {
var req domain.PopOKCancelRequest
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid cancel request")
}
resp, err := h.virtualGameSvc.ProcessCancel(c.Context(), &req)
if err != nil {
code := fiber.StatusInternalServerError
switch err.Error() {
case "invalid token":
code = fiber.StatusUnauthorized
case "original bet not found", "invalid original transaction":
code = fiber.StatusBadRequest
}
return fiber.NewError(code, err.Error())
}
return response.WriteJSON(c, fiber.StatusOK, "Cancel processed", resp, nil)
}

View File

@ -7,6 +7,7 @@ import (
_ "github.com/SamuelTariku/FortuneBet-Backend/docs" _ "github.com/SamuelTariku/FortuneBet-Backend/docs"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger" // "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor" // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor"
@ -197,13 +198,13 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet) a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet)
//Chapa Routes //Chapa Routes
group.Post("/chapa/payments/verify", a.authMiddleware, h.VerifyChapaPayment) group.Post("/chapa/payments/webhook/verify", h.WebhookCallback)
group.Post("/chapa/payments/withdraw", a.authMiddleware, h.WithdrawUsingChapa) group.Get("/chapa/payments/manual/verify/:tx_ref", h.ManualVerifyPayment)
group.Post("/chapa/payments/deposit", a.authMiddleware, h.DepositUsingChapa) group.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit)
group.Get("/chapa/banks", a.authMiddleware, h.ReadChapaBanks) group.Get("/chapa/banks", h.GetSupportedBanks)
//Report Routes //Report Routes
group.Get("/reports/dashboard", a.authMiddleware, h.GetDashboardReport) group.Get("/reports/dashboard", h.GetDashboardReport)
//Wallet Monitor Service //Wallet Monitor Service
// group.Get("/debug/wallet-monitor/status", func(c *fiber.Ctx) error { // group.Get("/debug/wallet-monitor/status", func(c *fiber.Ctx) error {
@ -235,7 +236,7 @@ func (a *App) initAppRoutes() {
//mongoDB logs //mongoDB logs
ctx := context.Background() ctx := context.Background()
group.Get("/logs", handlers.GetLogsHandler(ctx)) group.Get("/logs", a.authMiddleware, a.SuperAdminOnly, handlers.GetLogsHandler(ctx))
// Recommendation Routes // Recommendation Routes
group.Get("/virtual-games/recommendations/:userID", h.GetRecommendations) group.Get("/virtual-games/recommendations/:userID", h.GetRecommendations)
@ -252,11 +253,15 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/notifications/all", a.authMiddleware, h.GetAllNotifications) a.fiber.Get("/notifications/all", a.authMiddleware, h.GetAllNotifications)
a.fiber.Post("/notifications/mark-as-read", a.authMiddleware, h.MarkNotificationAsRead) a.fiber.Post("/notifications/mark-as-read", a.authMiddleware, h.MarkNotificationAsRead)
a.fiber.Get("/notifications/unread", a.authMiddleware, h.CountUnreadNotifications) a.fiber.Get("/notifications/unread", a.authMiddleware, h.CountUnreadNotifications)
a.fiber.Post("/notifications/create", h.CreateAndSendNotification) a.fiber.Post("/notifications/create", a.authMiddleware, h.CreateAndSendNotification)
// Virtual Game Routes // Virtual Game Routes
a.fiber.Post("/virtual-game/launch", a.authMiddleware, h.LaunchVirtualGame) a.fiber.Post("/virtual-game/launch", a.authMiddleware, h.LaunchVirtualGame)
a.fiber.Post("/virtual-game/callback", h.HandleVirtualGameCallback) a.fiber.Post("/virtual-game/callback", h.HandleVirtualGameCallback)
a.fiber.Post("/playerInfo", h.HandlePlayerInfo)
a.fiber.Post("/bet", h.HandleBet)
a.fiber.Post("/win", h.HandleWin)
a.fiber.Post("/cancel", h.HandleCancel)
} }