Merge branch 'main' into ticket-bet
This commit is contained in:
commit
2a6e892f5e
22
cmd/main.go
22
cmd/main.go
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
// "context"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
|
@ -15,6 +16,7 @@ import (
|
|||
// "github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
||||
|
||||
|
|
@ -82,8 +84,13 @@ func main() {
|
|||
|
||||
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
|
||||
|
||||
mongoLogger.Init()
|
||||
mongoDBLogger := zap.L()
|
||||
domain.MongoDBLogger, err = mongoLogger.InitLogger()
|
||||
if err != nil {
|
||||
log.Fatalf("Logger initialization failed: %v", err)
|
||||
}
|
||||
defer domain.MongoDBLogger.Sync()
|
||||
|
||||
zap.ReplaceGlobals(domain.MongoDBLogger)
|
||||
|
||||
// client := mongoLogger.InitDB()
|
||||
// defer func() {
|
||||
|
|
@ -119,6 +126,7 @@ func main() {
|
|||
oddsSvc := odds.New(store, cfg, logger)
|
||||
ticketSvc := ticket.NewService(store)
|
||||
notificationRepo := repository.NewNotificationRepository(store)
|
||||
virtuaGamesRepo := repository.NewVirtualGameRepository(store)
|
||||
|
||||
notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
|
||||
|
||||
|
|
@ -140,7 +148,7 @@ func main() {
|
|||
branchSvc := branch.NewService(store)
|
||||
companySvc := company.NewService(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)
|
||||
referalRepo := repository.NewReferralRepository(store)
|
||||
vitualGameRepo := repository.NewVirtualGameRepository(store)
|
||||
|
|
@ -164,13 +172,10 @@ func main() {
|
|||
chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY)
|
||||
|
||||
chapaSvc := chapa.NewService(
|
||||
transaction.TransactionStore(store),
|
||||
wallet.TransferStore(store),
|
||||
wallet.WalletStore(store),
|
||||
user.UserStore(store),
|
||||
referalSvc,
|
||||
branch.BranchStore(store),
|
||||
chapaClient,
|
||||
store,
|
||||
)
|
||||
|
||||
reportSvc := report.NewService(
|
||||
|
|
@ -179,6 +184,9 @@ func main() {
|
|||
transaction.TransactionStore(store),
|
||||
branch.BranchStore(store),
|
||||
user.UserStore(store),
|
||||
company.CompanyStore(store),
|
||||
virtuaGamesRepo,
|
||||
notificationRepo,
|
||||
logger,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ CREATE TABLE IF NOT EXISTS wallet_transfer (
|
|||
sender_wallet_id BIGINT,
|
||||
cashier_id BIGINT,
|
||||
verified BOOLEAN NOT NULL DEFAULT false,
|
||||
reference_number VARCHAR(255) NOT NULL,
|
||||
payment_method VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
|
|
|
|||
|
|
@ -69,3 +69,10 @@ LIMIT $1;
|
|||
SELECT recipient_id
|
||||
FROM notifications
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ INSERT INTO wallet_transfer (
|
|||
sender_wallet_id,
|
||||
cashier_id,
|
||||
verified,
|
||||
reference_number,
|
||||
payment_method
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING *;
|
||||
-- name: GetAllTransfers :many
|
||||
SELECT *
|
||||
|
|
@ -22,6 +23,10 @@ WHERE receiver_wallet_id = $1
|
|||
SELECT *
|
||||
FROM wallet_transfer
|
||||
WHERE id = $1;
|
||||
-- name: GetTransferByReference :one
|
||||
SELECT *
|
||||
FROM wallet_transfer
|
||||
WHERE reference_number = $1;
|
||||
-- name: UpdateTransferVerification :exec
|
||||
UPDATE wallet_transfer
|
||||
SET verified = $1,
|
||||
|
|
|
|||
451
docs/docs.go
451
docs/docs.go
|
|
@ -304,8 +304,9 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/chapa/banks": {
|
||||
"get": {
|
||||
"/api/v1/chapa/payments/deposit": {
|
||||
"post": {
|
||||
"description": "Starts a new deposit process using Chapa payment gateway",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
|
@ -315,50 +316,43 @@ const docTemplate = `{
|
|||
"tags": [
|
||||
"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": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ChapaSupportedBanksResponseWrapper"
|
||||
"$ref": "#/definitions/domain.ChapaDepositResponse"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/chapa/payments/deposit": {
|
||||
"post": {
|
||||
"description": "Deposits money into user wallet from user account using Chapa",
|
||||
"/api/v1/chapa/payments/manual/verify/{tx_ref}": {
|
||||
"get": {
|
||||
"description": "Manually verify a payment using Chapa's API",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
|
@ -368,48 +362,41 @@ const docTemplate = `{
|
|||
"tags": [
|
||||
"Chapa"
|
||||
],
|
||||
"summary": "Deposit money into user wallet using Chapa",
|
||||
"summary": "Verify a payment manually",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Deposit request payload",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ChapaDepositRequest"
|
||||
}
|
||||
"type": "string",
|
||||
"description": "Transaction Reference",
|
||||
"name": "tx_ref",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ChapaPaymentUrlResponseWrapper"
|
||||
"$ref": "#/definitions/domain.ChapaVerificationResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/chapa/payments/verify": {
|
||||
"/api/v1/chapa/payments/webhook/verify": {
|
||||
"post": {
|
||||
"description": "Handles payment notifications from Chapa",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
|
@ -419,93 +406,36 @@ const docTemplate = `{
|
|||
"tags": [
|
||||
"Chapa"
|
||||
],
|
||||
"summary": "Verifies Chapa webhook transaction",
|
||||
"summary": "Chapa payment webhook callback (used by Chapa)",
|
||||
"parameters": [
|
||||
{
|
||||
"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",
|
||||
"description": "Webhook payload",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ChapaWithdrawRequest"
|
||||
"$ref": "#/definitions/domain.ChapaWebhookPayload"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Withdrawal requested successfully",
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"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": {
|
||||
"get": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"branch_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone_number": {
|
||||
"status": {
|
||||
"$ref": "#/definitions/domain.PaymentStatus"
|
||||
},
|
||||
"tx_ref": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -4867,6 +4788,19 @@ const docTemplate = `{
|
|||
"BANK"
|
||||
]
|
||||
},
|
||||
"domain.PaymentStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pending",
|
||||
"completed",
|
||||
"failed"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"PaymentStatusPending",
|
||||
"PaymentStatusCompleted",
|
||||
"PaymentStatusFailed"
|
||||
]
|
||||
},
|
||||
"domain.PopOKCallback": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
|
@ -5117,7 +5036,7 @@ const docTemplate = `{
|
|||
},
|
||||
"awayTeamID": {
|
||||
"description": "Away team ID (can be empty/null)",
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"homeKitImage": {
|
||||
"description": "Kit or image for home team (optional)",
|
||||
|
|
@ -5129,7 +5048,7 @@ const docTemplate = `{
|
|||
},
|
||||
"homeTeamID": {
|
||||
"description": "Home team ID",
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"description": "Event ID",
|
||||
|
|
@ -5141,7 +5060,7 @@ const docTemplate = `{
|
|||
},
|
||||
"leagueID": {
|
||||
"description": "League ID",
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"leagueName": {
|
||||
"description": "League name",
|
||||
|
|
@ -5157,7 +5076,7 @@ const docTemplate = `{
|
|||
},
|
||||
"sportID": {
|
||||
"description": "Sport ID",
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"startTime": {
|
||||
"description": "Converted from \"time\" field in UNIX format",
|
||||
|
|
|
|||
|
|
@ -296,8 +296,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/chapa/banks": {
|
||||
"get": {
|
||||
"/api/v1/chapa/payments/deposit": {
|
||||
"post": {
|
||||
"description": "Starts a new deposit process using Chapa payment gateway",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
|
@ -307,50 +308,43 @@
|
|||
"tags": [
|
||||
"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": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ChapaSupportedBanksResponseWrapper"
|
||||
"$ref": "#/definitions/domain.ChapaDepositResponse"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/chapa/payments/deposit": {
|
||||
"post": {
|
||||
"description": "Deposits money into user wallet from user account using Chapa",
|
||||
"/api/v1/chapa/payments/manual/verify/{tx_ref}": {
|
||||
"get": {
|
||||
"description": "Manually verify a payment using Chapa's API",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
|
@ -360,48 +354,41 @@
|
|||
"tags": [
|
||||
"Chapa"
|
||||
],
|
||||
"summary": "Deposit money into user wallet using Chapa",
|
||||
"summary": "Verify a payment manually",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Deposit request payload",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ChapaDepositRequest"
|
||||
}
|
||||
"type": "string",
|
||||
"description": "Transaction Reference",
|
||||
"name": "tx_ref",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ChapaPaymentUrlResponseWrapper"
|
||||
"$ref": "#/definitions/domain.ChapaVerificationResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/chapa/payments/verify": {
|
||||
"/api/v1/chapa/payments/webhook/verify": {
|
||||
"post": {
|
||||
"description": "Handles payment notifications from Chapa",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
|
@ -411,93 +398,36 @@
|
|||
"tags": [
|
||||
"Chapa"
|
||||
],
|
||||
"summary": "Verifies Chapa webhook transaction",
|
||||
"summary": "Chapa payment webhook callback (used by Chapa)",
|
||||
"parameters": [
|
||||
{
|
||||
"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",
|
||||
"description": "Webhook payload",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ChapaWithdrawRequest"
|
||||
"$ref": "#/definitions/domain.ChapaWebhookPayload"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Withdrawal requested successfully",
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"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": {
|
||||
"get": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"branch_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone_number": {
|
||||
"status": {
|
||||
"$ref": "#/definitions/domain.PaymentStatus"
|
||||
},
|
||||
"tx_ref": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -4859,6 +4780,19 @@
|
|||
"BANK"
|
||||
]
|
||||
},
|
||||
"domain.PaymentStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pending",
|
||||
"completed",
|
||||
"failed"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"PaymentStatusPending",
|
||||
"PaymentStatusCompleted",
|
||||
"PaymentStatusFailed"
|
||||
]
|
||||
},
|
||||
"domain.PopOKCallback": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -4999,21 +4933,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.Response": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"status_code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.Role": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
|
@ -5109,7 +5028,7 @@
|
|||
},
|
||||
"awayTeamID": {
|
||||
"description": "Away team ID (can be empty/null)",
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"homeKitImage": {
|
||||
"description": "Kit or image for home team (optional)",
|
||||
|
|
@ -5121,7 +5040,7 @@
|
|||
},
|
||||
"homeTeamID": {
|
||||
"description": "Home team ID",
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"description": "Event ID",
|
||||
|
|
@ -5133,7 +5052,7 @@
|
|||
},
|
||||
"leagueID": {
|
||||
"description": "League ID",
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"leagueName": {
|
||||
"description": "League name",
|
||||
|
|
@ -5149,7 +5068,7 @@
|
|||
},
|
||||
"sportID": {
|
||||
"description": "Sport ID",
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"startTime": {
|
||||
"description": "Converted from \"time\" field in UNIX format",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,39 @@ definitions:
|
|||
user_id:
|
||||
type: string
|
||||
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:
|
||||
properties:
|
||||
away_team_name:
|
||||
|
|
@ -124,102 +157,42 @@ definitions:
|
|||
example: 2
|
||||
type: integer
|
||||
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:
|
||||
amount:
|
||||
type: integer
|
||||
branch_id:
|
||||
type: integer
|
||||
currency:
|
||||
type: string
|
||||
phone_number:
|
||||
status:
|
||||
$ref: '#/definitions/domain.PaymentStatus'
|
||||
tx_ref:
|
||||
type: string
|
||||
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:
|
||||
properties:
|
||||
event_id:
|
||||
|
|
@ -328,6 +301,16 @@ definitions:
|
|||
- TELEBIRR_TRANSACTION
|
||||
- ARIFPAY_TRANSACTION
|
||||
- BANK
|
||||
domain.PaymentStatus:
|
||||
enum:
|
||||
- pending
|
||||
- completed
|
||||
- failed
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- PaymentStatusPending
|
||||
- PaymentStatusCompleted
|
||||
- PaymentStatusFailed
|
||||
domain.PopOKCallback:
|
||||
properties:
|
||||
amount:
|
||||
|
|
@ -421,16 +404,6 @@ definitions:
|
|||
totalRewardEarned:
|
||||
type: number
|
||||
type: object
|
||||
domain.Response:
|
||||
properties:
|
||||
data: {}
|
||||
message:
|
||||
type: string
|
||||
status_code:
|
||||
type: integer
|
||||
success:
|
||||
type: boolean
|
||||
type: object
|
||||
domain.Role:
|
||||
enum:
|
||||
- super_admin
|
||||
|
|
@ -501,7 +474,7 @@ definitions:
|
|||
type: string
|
||||
awayTeamID:
|
||||
description: Away team ID (can be empty/null)
|
||||
type: string
|
||||
type: integer
|
||||
homeKitImage:
|
||||
description: Kit or image for home team (optional)
|
||||
type: string
|
||||
|
|
@ -510,7 +483,7 @@ definitions:
|
|||
type: string
|
||||
homeTeamID:
|
||||
description: Home team ID
|
||||
type: string
|
||||
type: integer
|
||||
id:
|
||||
description: Event ID
|
||||
type: string
|
||||
|
|
@ -519,7 +492,7 @@ definitions:
|
|||
type: string
|
||||
leagueID:
|
||||
description: League ID
|
||||
type: string
|
||||
type: integer
|
||||
leagueName:
|
||||
description: League name
|
||||
type: string
|
||||
|
|
@ -531,7 +504,7 @@ definitions:
|
|||
type: string
|
||||
sportID:
|
||||
description: Sport ID
|
||||
type: string
|
||||
type: integer
|
||||
startTime:
|
||||
description: Converted from "time" field in UNIX format
|
||||
type: string
|
||||
|
|
@ -1673,137 +1646,94 @@ paths:
|
|||
summary: Launch an Alea Play virtual game
|
||||
tags:
|
||||
- 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:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Deposits money into user wallet from user account using Chapa
|
||||
description: Starts a new deposit process using Chapa payment gateway
|
||||
parameters:
|
||||
- description: Deposit request payload
|
||||
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
|
||||
- description: Deposit request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ChapaWithdrawRequest'
|
||||
$ref: '#/definitions/domain.ChapaDepositRequestPayload'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Withdrawal requested successfully
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/domain.Response'
|
||||
- properties:
|
||||
data:
|
||||
type: string
|
||||
type: object
|
||||
$ref: '#/definitions/domain.ChapaDepositResponse'
|
||||
"400":
|
||||
description: Invalid request
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/domain.Response'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/domain.Response'
|
||||
"422":
|
||||
description: Unprocessable Entity
|
||||
schema:
|
||||
$ref: '#/definitions/domain.Response'
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.Response'
|
||||
summary: Withdraw using Chapa
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
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:
|
||||
- Chapa
|
||||
/api/v1/reports/dashboard:
|
||||
|
|
@ -2067,6 +1997,27 @@ paths:
|
|||
summary: Refresh token
|
||||
tags:
|
||||
- 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:
|
||||
get:
|
||||
consumes:
|
||||
|
|
|
|||
|
|
@ -487,6 +487,7 @@ type WalletTransfer struct {
|
|||
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
|
||||
CashierID pgtype.Int8 `json:"cashier_id"`
|
||||
Verified bool `json:"verified"`
|
||||
ReferenceNumber string `json:"reference_number"`
|
||||
PaymentMethod string `json:"payment_method"`
|
||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
||||
|
|
|
|||
|
|
@ -187,6 +187,40 @@ func (q *Queries) GetNotification(ctx context.Context, id string) (Notification,
|
|||
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
|
||||
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
|
||||
FROM notifications
|
||||
|
|
|
|||
|
|
@ -19,10 +19,11 @@ INSERT INTO wallet_transfer (
|
|||
sender_wallet_id,
|
||||
cashier_id,
|
||||
verified,
|
||||
reference_number,
|
||||
payment_method
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method, created_at, updated_at
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateTransferParams struct {
|
||||
|
|
@ -32,6 +33,7 @@ type CreateTransferParams struct {
|
|||
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
|
||||
CashierID pgtype.Int8 `json:"cashier_id"`
|
||||
Verified bool `json:"verified"`
|
||||
ReferenceNumber string `json:"reference_number"`
|
||||
PaymentMethod string `json:"payment_method"`
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +45,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
|
|||
arg.SenderWalletID,
|
||||
arg.CashierID,
|
||||
arg.Verified,
|
||||
arg.ReferenceNumber,
|
||||
arg.PaymentMethod,
|
||||
)
|
||||
var i WalletTransfer
|
||||
|
|
@ -54,6 +57,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
|
|||
&i.SenderWalletID,
|
||||
&i.CashierID,
|
||||
&i.Verified,
|
||||
&i.ReferenceNumber,
|
||||
&i.PaymentMethod,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
|
|
@ -62,7 +66,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
|
|||
}
|
||||
|
||||
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
|
||||
`
|
||||
|
||||
|
|
@ -83,6 +87,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error)
|
|||
&i.SenderWalletID,
|
||||
&i.CashierID,
|
||||
&i.Verified,
|
||||
&i.ReferenceNumber,
|
||||
&i.PaymentMethod,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
|
|
@ -98,7 +103,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error)
|
|||
}
|
||||
|
||||
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
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -114,6 +119,32 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer
|
|||
&i.SenderWalletID,
|
||||
&i.CashierID,
|
||||
&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.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
|
|
@ -122,7 +153,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer
|
|||
}
|
||||
|
||||
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
|
||||
WHERE receiver_wallet_id = $1
|
||||
OR sender_wallet_id = $1
|
||||
|
|
@ -145,6 +176,7 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID int
|
|||
&i.SenderWalletID,
|
||||
&i.CashierID,
|
||||
&i.Verified,
|
||||
&i.ReferenceNumber,
|
||||
&i.PaymentMethod,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
|
|
|
|||
9
go.mod
9
go.mod
|
|
@ -13,8 +13,6 @@ require (
|
|||
github.com/jackc/pgx/v5 v5.7.4
|
||||
github.com/joho/godotenv v1.5.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/swaggo/fiber-swagger v1.3.0
|
||||
github.com/swaggo/swag v1.16.4
|
||||
|
|
@ -30,7 +28,6 @@ require (
|
|||
// github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.1 // 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-isatty v0.0.20 // 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/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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.3
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/net v0.38.0 // direct
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
|
|
@ -77,6 +72,6 @@ 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
|
||||
)
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -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/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
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/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/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.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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
|
|
|
|||
|
|
@ -260,7 +260,11 @@ func (c *Config) loadEnv() error {
|
|||
if c.ADRO_SMS_HOST_URL == "" {
|
||||
c.ADRO_SMS_HOST_URL = "https://api.afrosms.com"
|
||||
}
|
||||
|
||||
popOKClientID := os.Getenv("POPOK_CLIENT_ID")
|
||||
|
||||
popOKPlatform := os.Getenv("POPOK_PLATFORM")
|
||||
|
||||
if popOKClientID == "" {
|
||||
return ErrInvalidPopOKClientID
|
||||
}
|
||||
|
|
@ -285,6 +289,7 @@ func (c *Config) loadEnv() error {
|
|||
SecretKey: popOKSecretKey,
|
||||
BaseURL: popOKBaseURL,
|
||||
CallbackURL: popOKCallbackURL,
|
||||
Platform: popOKPlatform,
|
||||
}
|
||||
betToken := os.Getenv("BET365_TOKEN")
|
||||
if betToken == "" {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
import "time"
|
||||
|
||||
type PaymentStatus string
|
||||
|
||||
const (
|
||||
PaymentStatusPending PaymentStatus = "pending"
|
||||
PaymentStatusCompleted PaymentStatus = "completed"
|
||||
PaymentStatusFailed PaymentStatus = "failed"
|
||||
)
|
||||
|
||||
var (
|
||||
ChapaSecret string
|
||||
ChapaBaseURL string
|
||||
)
|
||||
|
||||
type InitPaymentRequest struct {
|
||||
type ChapaDepositRequest struct {
|
||||
Amount Currency `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Email string `json:"email"`
|
||||
|
|
@ -21,208 +21,73 @@ type InitPaymentRequest struct {
|
|||
ReturnURL string `json:"return_url"`
|
||||
}
|
||||
|
||||
type TransferRequest struct {
|
||||
AccountNumber string `json:"account_number"`
|
||||
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 ChapaDepositRequestPayload struct {
|
||||
Amount float64 `json:"amount" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type ChapaSupportedBank struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Swift string `json:"swift"`
|
||||
Name string `json:"name"`
|
||||
AcctLength int `json:"acct_length"`
|
||||
AcctNumberRegex string `json:"acct_number_regex"`
|
||||
ExampleValue string `json:"example_value"`
|
||||
CountryId int `json:"country_id"`
|
||||
IsMobilemoney *int `json:"is_mobilemoney"`
|
||||
|
||||
IsActive int `json:"is_active"`
|
||||
IsRtgs *int `json:"is_rtgs"`
|
||||
Active int `json:"active"`
|
||||
Is24Hrs *int `json:"is_24hrs"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Currency string `json:"currency"`
|
||||
type ChapaWebhookPayload struct {
|
||||
TxRef string `json:"tx_ref"`
|
||||
Amount Currency `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Status PaymentStatus `json:"status"`
|
||||
}
|
||||
|
||||
type ChapaSupportedBanksResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data []ChapaSupportedBank `json:"data"`
|
||||
// PaymentResponse contains the response from payment initialization
|
||||
type ChapaDepositResponse struct {
|
||||
CheckoutURL string
|
||||
Reference string
|
||||
}
|
||||
|
||||
type InitPaymentData struct {
|
||||
TxRef string `json:"tx_ref"`
|
||||
CheckoutURL string `json:"checkout_url"`
|
||||
// PaymentVerification contains payment verification details
|
||||
type ChapaDepositVerification struct {
|
||||
Status PaymentStatus
|
||||
Amount Currency
|
||||
Currency string
|
||||
}
|
||||
|
||||
type InitPaymentResponse struct {
|
||||
Status string `json:"status"` // "success"
|
||||
Message string `json:"message"` // e.g., "Payment initialized"
|
||||
Data InitPaymentData `json:"data"`
|
||||
type ChapaVerificationResponse struct {
|
||||
Status string `json:"status"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
TxRef string `json:"tx_ref"`
|
||||
}
|
||||
|
||||
type WebhookPayload map[string]interface{}
|
||||
|
||||
type TransactionData struct {
|
||||
TxRef string `json:"tx_ref"`
|
||||
Status string `json:"status"`
|
||||
Amount string `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
CustomerEmail string `json:"email"`
|
||||
type Bank struct {
|
||||
ID int `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Swift string `json:"swift"`
|
||||
Name string `json:"name"`
|
||||
AcctLength int `json:"acct_length"`
|
||||
CountryID int `json:"country_id"`
|
||||
IsMobileMoney int `json:"is_mobilemoney"` // nullable
|
||||
IsActive int `json:"is_active"`
|
||||
IsRTGS int `json:"is_rtgs"`
|
||||
Active int `json:"active"`
|
||||
Is24Hrs int `json:"is_24hrs"` // nullable
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
type VerifyTransactionResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data TransactionData `json:"data"`
|
||||
type BankResponse struct {
|
||||
Message string `json:"message"`
|
||||
Status string `json:"status"`
|
||||
Data []BankData `json:"data"`
|
||||
}
|
||||
|
||||
type TransferData struct {
|
||||
Reference string `json:"reference"`
|
||||
Status string `json:"status"`
|
||||
Amount string `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
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"`
|
||||
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"`
|
||||
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
|
||||
type BankData struct {
|
||||
ID int `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Swift string `json:"swift"`
|
||||
Name string `json:"name"`
|
||||
AcctLength int `json:"acct_length"`
|
||||
CountryID int `json:"country_id"`
|
||||
IsMobileMoney int `json:"is_mobilemoney"` // nullable
|
||||
IsActive int `json:"is_active"`
|
||||
IsRTGS int `json:"is_rtgs"`
|
||||
Active int `json:"active"`
|
||||
Is24Hrs int `json:"is_24hrs"` // nullable
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,12 @@ package domain
|
|||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var MongoDBLogger *zap.Logger
|
||||
|
||||
type ValidInt64 struct {
|
||||
Value int64
|
||||
Valid bool
|
||||
|
|
|
|||
|
|
@ -27,8 +27,10 @@ const (
|
|||
NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert"
|
||||
NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin"
|
||||
|
||||
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
|
||||
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
||||
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
|
||||
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
||||
NotificationRecieverSideCashier NotificationRecieverSide = "cashier"
|
||||
NotificationRecieverSideBranchManager NotificationRecieverSide = "branch_manager"
|
||||
|
||||
NotificationDeliverySchemeBulk NotificationDeliveryScheme = "bulk"
|
||||
NotificationDeliverySchemeSingle NotificationDeliveryScheme = "single"
|
||||
|
|
@ -55,9 +57,9 @@ const (
|
|||
)
|
||||
|
||||
type NotificationPayload struct {
|
||||
Headline string
|
||||
Message string
|
||||
Tags []string
|
||||
Headline string `json:"headline"`
|
||||
Message string `json:"message"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,117 @@ package domain
|
|||
|
||||
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 {
|
||||
Value OutcomeStatus
|
||||
Valid bool // Valid is true if Value is not NULL
|
||||
|
|
@ -9,13 +120,14 @@ type ValidOutcomeStatus struct {
|
|||
|
||||
// ReportFilter contains filters for report generation
|
||||
type ReportFilter struct {
|
||||
StartTime ValidTime `json:"start_time"`
|
||||
EndTime ValidTime `json:"end_time"`
|
||||
CompanyID ValidInt64 `json:"company_id"`
|
||||
BranchID ValidInt64 `json:"branch_id"`
|
||||
UserID ValidInt64 `json:"user_id"`
|
||||
SportID ValidString `json:"sport_id"`
|
||||
Status ValidOutcomeStatus `json:"status"`
|
||||
StartTime ValidTime `json:"start_time"`
|
||||
EndTime ValidTime `json:"end_time"`
|
||||
CompanyID ValidInt64 `json:"company_id"`
|
||||
BranchID ValidInt64 `json:"branch_id"`
|
||||
RecipientID ValidInt64 `json:"recipient_id"`
|
||||
UserID ValidInt64 `json:"user_id"`
|
||||
SportID ValidString `json:"sport_id"`
|
||||
Status ValidOutcomeStatus `json:"status"`
|
||||
}
|
||||
|
||||
// BetStat represents aggregated bet statistics
|
||||
|
|
@ -46,7 +158,6 @@ type CustomerBetActivity struct {
|
|||
AverageOdds float64
|
||||
}
|
||||
|
||||
|
||||
// BranchBetActivity represents branch betting activity
|
||||
type BranchBetActivity struct {
|
||||
BranchID int64
|
||||
|
|
@ -99,25 +210,92 @@ type CustomerPreferences struct {
|
|||
FavoriteMarket string `json:"favorite_market"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
// 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"`
|
||||
// }
|
||||
|
||||
type ErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
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"`
|
||||
}
|
||||
|
|
@ -31,7 +31,8 @@ type Transfer struct {
|
|||
Type TransferType
|
||||
PaymentMethod PaymentMethod
|
||||
ReceiverWalletID int64
|
||||
SenderWalletID ValidInt64
|
||||
SenderWalletID int64
|
||||
ReferenceNumber string
|
||||
CashierID ValidInt64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
|
@ -40,8 +41,9 @@ type Transfer struct {
|
|||
type CreateTransfer struct {
|
||||
Amount Currency
|
||||
Verified bool
|
||||
ReferenceNumber string
|
||||
ReceiverWalletID int64
|
||||
SenderWalletID ValidInt64
|
||||
SenderWalletID int64
|
||||
CashierID ValidInt64
|
||||
Type TransferType
|
||||
PaymentMethod PaymentMethod
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ type VirtualGame struct {
|
|||
MinBet float64 `json:"min_bet"`
|
||||
MaxBet float64 `json:"max_bet"`
|
||||
Volatility string `json:"volatility"`
|
||||
IsActive bool `json:"is_active"`
|
||||
RTP float64 `json:"rtp"`
|
||||
IsFeatured bool `json:"is_featured"`
|
||||
PopularityScore int `json:"popularity_score"`
|
||||
|
|
@ -38,17 +39,18 @@ type VirtualGameSession struct {
|
|||
}
|
||||
|
||||
type VirtualGameTransaction struct {
|
||||
ID int64 `json:"id"`
|
||||
SessionID int64 `json:"session_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
WalletID int64 `json:"wallet_id"`
|
||||
TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, CASHOUT, etc.
|
||||
Amount int64 `json:"amount"` // Always in cents
|
||||
Currency string `json:"currency"`
|
||||
ExternalTransactionID string `json:"external_transaction_id"`
|
||||
Status string `json:"status"` // PENDING, COMPLETED, FAILED
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID int64 `json:"id"`
|
||||
SessionID int64 `json:"session_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
WalletID int64 `json:"wallet_id"`
|
||||
TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, CASHOUT, etc.
|
||||
Amount int64 `json:"amount"` // Always in cents
|
||||
Currency string `json:"currency"`
|
||||
ExternalTransactionID string `json:"external_transaction_id"`
|
||||
ReferenceTransactionID string `json:"reference_transaction_id"`
|
||||
Status string `json:"status"` // PENDING, COMPLETED, FAILED
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Alea Play specific fields
|
||||
GameRoundID string `json:"game_round_id"` // Round identifier
|
||||
|
|
@ -86,6 +88,7 @@ type PopOKConfig struct {
|
|||
SecretKey string
|
||||
BaseURL string
|
||||
CallbackURL string
|
||||
Platform string
|
||||
}
|
||||
|
||||
type PopOKCallback struct {
|
||||
|
|
@ -98,6 +101,61 @@ type PopOKCallback struct {
|
|||
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 {
|
||||
EventID string `json:"event_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
package mongoLogger
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
// Replace localhost if inside Docker
|
||||
mongoCore, err := NewMongoCore("mongodb://root:secret@mongo:27017/?authSource=admin", "logdb", "applogs", zapcore.InfoLevel)
|
||||
func InitLogger() (*zap.Logger, error) {
|
||||
mongoCore, err := NewMongoCore(
|
||||
"mongodb://root:secret@mongo:27017/?authSource=admin",
|
||||
"logdb",
|
||||
"applogs",
|
||||
zapcore.InfoLevel,
|
||||
)
|
||||
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())
|
||||
|
|
@ -21,10 +25,6 @@ func Init() {
|
|||
combinedCore := zapcore.NewTee(mongoCore, consoleCore)
|
||||
|
||||
logger := zap.New(combinedCore, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
|
||||
zap.ReplaceGlobals(logger) // Optional but useful if you use zap.L()
|
||||
|
||||
defer logger.Sync()
|
||||
|
||||
// logger.Info("Application started", zap.String("module", "main"))
|
||||
// logger.Error("Something went wrong", zap.String("error_code", "E123"))
|
||||
return logger, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBe
|
|||
|
||||
rows, err := s.queries.CreateBetOutcome(ctx, dbParams)
|
||||
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.Any("bet_id", outcomes[0].BetID), // assumes all outcomes have same BetID
|
||||
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) {
|
||||
bet, err := s.queries.GetBetByID(ctx, id)
|
||||
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.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) {
|
||||
bet, err := s.queries.GetBetByCashoutID(ctx, id)
|
||||
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.Error(err),
|
||||
)
|
||||
|
|
@ -211,7 +211,7 @@ func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]doma
|
|||
},
|
||||
})
|
||||
if err != nil {
|
||||
mongoLogger.Error("failed to get all bets",
|
||||
domain.MongoDBLogger.Error("failed to get all bets",
|
||||
zap.Any("filter", filter),
|
||||
zap.Error(err),
|
||||
)
|
||||
|
|
@ -232,7 +232,7 @@ func (s *Store) GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.
|
|||
Valid: true,
|
||||
})
|
||||
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.Error(err),
|
||||
)
|
||||
|
|
@ -271,7 +271,7 @@ func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) err
|
|||
CashedOut: cashedOut,
|
||||
})
|
||||
if err != nil {
|
||||
mongoLogger.Error("failed to update cashout",
|
||||
domain.MongoDBLogger.Error("failed to update cashout",
|
||||
zap.Int64("id", id),
|
||||
zap.Bool("cashed_out", cashedOut),
|
||||
zap.Error(err),
|
||||
|
|
@ -286,7 +286,7 @@ func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.Outcom
|
|||
Status: int32(status),
|
||||
})
|
||||
if err != nil {
|
||||
mongoLogger.Error("failed to update status",
|
||||
domain.MongoDBLogger.Error("failed to update status",
|
||||
zap.Int64("id", id),
|
||||
zap.Int32("status", int32(status)),
|
||||
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) {
|
||||
outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, eventID)
|
||||
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.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) {
|
||||
outcomes, err := s.queries.GetBetOutcomeByBetID(ctx, betID)
|
||||
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.Error(err),
|
||||
)
|
||||
|
|
@ -335,7 +335,7 @@ func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status dom
|
|||
ID: id,
|
||||
})
|
||||
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.Int32("status", int32(status)),
|
||||
zap.Error(err),
|
||||
|
|
@ -428,7 +428,7 @@ func (s *Store) GetBetSummary(ctx context.Context, filter domain.ReportFilter) (
|
|||
row := s.conn.QueryRow(ctx, query, args...)
|
||||
err = row.Scan(&totalStakes, &totalBets, &activeBets, &totalWins, &totalLosses, &winBalance)
|
||||
if err != nil {
|
||||
mongoLogger.Error("failed to get bet summary",
|
||||
domain.MongoDBLogger.Error("failed to get bet summary",
|
||||
zap.String("query", query),
|
||||
zap.Any("args", args),
|
||||
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)
|
||||
}
|
||||
|
||||
mongoLogger.Info("GetBetSummary executed successfully",
|
||||
domain.MongoDBLogger.Info("GetBetSummary executed successfully",
|
||||
zap.String("query", query),
|
||||
zap.Any("args", args),
|
||||
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...)
|
||||
if err != nil {
|
||||
mongoLogger.Error("failed to query bet stats",
|
||||
domain.MongoDBLogger.Error("failed to query bet stats",
|
||||
zap.String("query", query),
|
||||
zap.Any("args", args),
|
||||
zap.Error(err),
|
||||
|
|
@ -539,7 +539,7 @@ func (s *Store) GetBetStats(ctx context.Context, filter domain.ReportFilter) ([]
|
|||
&stat.TotalPayouts,
|
||||
&stat.AverageOdds,
|
||||
); err != nil {
|
||||
mongoLogger.Error("failed to scan bet stat",
|
||||
domain.MongoDBLogger.Error("failed to scan bet stat",
|
||||
zap.Error(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 {
|
||||
mongoLogger.Error("rows error after iteration",
|
||||
domain.MongoDBLogger.Error("rows error after iteration",
|
||||
zap.Error(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.String("query", query),
|
||||
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...)
|
||||
if err != nil {
|
||||
mongoLogger.Error("failed to query sport popularity",
|
||||
domain.MongoDBLogger.Error("failed to query sport popularity",
|
||||
zap.String("query", query),
|
||||
zap.Any("args", args),
|
||||
zap.Error(err),
|
||||
|
|
@ -629,7 +629,7 @@ func (s *Store) GetSportPopularity(ctx context.Context, filter domain.ReportFilt
|
|||
var date time.Time
|
||||
var sportID string
|
||||
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),
|
||||
)
|
||||
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 {
|
||||
mongoLogger.Error("rows error after iteration",
|
||||
domain.MongoDBLogger.Error("rows error after iteration",
|
||||
zap.Error(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.String("query", query),
|
||||
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...)
|
||||
if err != nil {
|
||||
mongoLogger.Error("failed to query market popularity",
|
||||
domain.MongoDBLogger.Error("failed to query market popularity",
|
||||
zap.String("query", query),
|
||||
zap.Any("args", args),
|
||||
zap.Error(err),
|
||||
|
|
@ -719,7 +719,7 @@ func (s *Store) GetMarketPopularity(ctx context.Context, filter domain.ReportFil
|
|||
var date time.Time
|
||||
var marketName string
|
||||
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),
|
||||
)
|
||||
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 {
|
||||
mongoLogger.Error("rows error after iteration",
|
||||
domain.MongoDBLogger.Error("rows error after iteration",
|
||||
zap.Error(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.String("query", query),
|
||||
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...)
|
||||
if err != nil {
|
||||
mongoLogger.Error("failed to query extreme values",
|
||||
domain.MongoDBLogger.Error("failed to query extreme values",
|
||||
zap.String("query", query),
|
||||
zap.Any("args", args),
|
||||
zap.Error(err),
|
||||
|
|
@ -823,7 +823,7 @@ func (s *Store) GetExtremeValues(ctx context.Context, filter domain.ReportFilter
|
|||
var date time.Time
|
||||
var extreme domain.ExtremeValues
|
||||
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),
|
||||
)
|
||||
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 {
|
||||
mongoLogger.Error("rows error after iteration",
|
||||
domain.MongoDBLogger.Error("rows error after iteration",
|
||||
zap.Error(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.String("query", query),
|
||||
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...)
|
||||
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.Any("args", args),
|
||||
zap.Error(err),
|
||||
|
|
@ -921,7 +921,7 @@ func (s *Store) GetCustomerBetActivity(ctx context.Context, filter domain.Report
|
|||
&activity.LastBetDate,
|
||||
&activity.AverageOdds,
|
||||
); err != nil {
|
||||
mongoLogger.Error("failed to scan customer bet activity",
|
||||
domain.MongoDBLogger.Error("failed to scan customer bet activity",
|
||||
zap.Error(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 {
|
||||
mongoLogger.Error("rows error after iteration",
|
||||
domain.MongoDBLogger.Error("rows error after iteration",
|
||||
zap.Error(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.String("query", query),
|
||||
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...)
|
||||
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.Any("args", args),
|
||||
zap.Error(err),
|
||||
|
|
@ -1008,18 +1008,18 @@ func (s *Store) GetBranchBetActivity(ctx context.Context, filter domain.ReportFi
|
|||
&activity.TotalWins,
|
||||
&activity.TotalPayouts,
|
||||
); 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)
|
||||
}
|
||||
activities = append(activities, activity)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
mongoLogger.Info("GetBranchBetActivity executed successfully",
|
||||
domain.MongoDBLogger.Info("GetBranchBetActivity executed successfully",
|
||||
zap.Int("result_count", len(activities)),
|
||||
zap.String("query", query),
|
||||
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...)
|
||||
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.Any("args", args),
|
||||
zap.Error(err),
|
||||
|
|
@ -1098,18 +1098,18 @@ func (s *Store) GetSportBetActivity(ctx context.Context, filter domain.ReportFil
|
|||
&activity.TotalPayouts,
|
||||
&activity.AverageOdds,
|
||||
); 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)
|
||||
}
|
||||
activities = append(activities, activity)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
mongoLogger.Info("GetSportBetActivity executed successfully",
|
||||
domain.MongoDBLogger.Info("GetSportBetActivity executed successfully",
|
||||
zap.Int("result_count", len(activities)),
|
||||
zap.String("query", query),
|
||||
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...)
|
||||
if err != nil {
|
||||
mongoLogger.Error("failed to query sport details",
|
||||
domain.MongoDBLogger.Error("failed to query sport details",
|
||||
zap.String("query", query),
|
||||
zap.Any("args", args),
|
||||
zap.Error(err),
|
||||
|
|
@ -1169,18 +1169,18 @@ func (s *Store) GetSportDetails(ctx context.Context, filter domain.ReportFilter)
|
|||
for rows.Next() {
|
||||
var sportID, matchName string
|
||||
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)
|
||||
}
|
||||
details[sportID] = matchName
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
mongoLogger.Info("GetSportDetails executed successfully",
|
||||
domain.MongoDBLogger.Info("GetSportDetails executed successfully",
|
||||
zap.Int("result_count", len(details)),
|
||||
zap.String("query", query),
|
||||
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...)
|
||||
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.Any("args", args),
|
||||
zap.Error(err),
|
||||
|
|
@ -1254,18 +1254,18 @@ func (s *Store) GetSportMarketPopularity(ctx context.Context, filter domain.Repo
|
|||
for rows.Next() {
|
||||
var sportID, marketName string
|
||||
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)
|
||||
}
|
||||
popularity[sportID] = marketName
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
mongoLogger.Info("GetSportMarketPopularity executed successfully",
|
||||
domain.MongoDBLogger.Info("GetSportMarketPopularity executed successfully",
|
||||
zap.Int("result_count", len(popularity)),
|
||||
zap.String("query", query),
|
||||
zap.Any("args", args),
|
||||
|
|
|
|||
|
|
@ -260,10 +260,11 @@ func (s *Store) DeleteBranchCashier(ctx context.Context, userID int64) error {
|
|||
}
|
||||
|
||||
// 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
|
||||
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`
|
||||
|
||||
args := []interface{}{}
|
||||
|
|
@ -292,12 +293,12 @@ func (s *Store) GetBranchCounts(ctx context.Context, filter domain.ReportFilter)
|
|||
}
|
||||
|
||||
row := s.conn.QueryRow(ctx, query, args...)
|
||||
err = row.Scan(&total, &active)
|
||||
err = row.Scan(&total, &active, &inactive)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package repository
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||
"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 {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ package repository
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
type NotificationRepository interface {
|
||||
|
|
@ -18,6 +19,7 @@ type NotificationRepository interface {
|
|||
ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error)
|
||||
CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, 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 {
|
||||
|
|
@ -28,10 +30,13 @@ func NewNotificationRepository(store *Store) NotificationRepository {
|
|||
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
|
||||
}
|
||||
|
||||
func (s *Store) DisconnectWebSocket(recipientID int64) {
|
||||
}
|
||||
|
||||
func (r *Repository) CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error) {
|
||||
var errorSeverity pgtype.Text
|
||||
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) {
|
||||
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
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -15,10 +15,7 @@ func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer {
|
|||
Type: domain.TransferType(transfer.Type),
|
||||
Verified: transfer.Verified,
|
||||
ReceiverWalletID: transfer.ReceiverWalletID,
|
||||
SenderWalletID: domain.ValidInt64{
|
||||
Value: transfer.SenderWalletID.Int64,
|
||||
Valid: transfer.SenderWalletID.Valid,
|
||||
},
|
||||
SenderWalletID: transfer.SenderWalletID.Int64,
|
||||
CashierID: domain.ValidInt64{
|
||||
Value: transfer.CashierID.Int64,
|
||||
Valid: transfer.CashierID.Valid,
|
||||
|
|
@ -33,8 +30,8 @@ func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferP
|
|||
Type: string(transfer.Type),
|
||||
ReceiverWalletID: transfer.ReceiverWalletID,
|
||||
SenderWalletID: pgtype.Int8{
|
||||
Int64: transfer.SenderWalletID.Value,
|
||||
Valid: transfer.SenderWalletID.Valid,
|
||||
Int64: transfer.SenderWalletID,
|
||||
Valid: true,
|
||||
},
|
||||
CashierID: pgtype.Int8{
|
||||
Int64: transfer.CashierID.Value,
|
||||
|
|
@ -78,6 +75,14 @@ func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]dom
|
|||
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) {
|
||||
transfer, err := s.queries.GetTransferByID(ctx, id)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -466,10 +466,11 @@ func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.User, is_c
|
|||
}
|
||||
|
||||
// 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
|
||||
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'`
|
||||
|
||||
args := []interface{}{}
|
||||
|
|
@ -498,12 +499,12 @@ func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilte
|
|||
}
|
||||
|
||||
row := s.conn.QueryRow(ctx, query, args...)
|
||||
err = row.Scan(&total, &active)
|
||||
err = row.Scan(&total, &active, &inactive)
|
||||
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
|
||||
|
|
@ -711,3 +712,44 @@ func (s *Store) GetCustomerPreferences(ctx context.Context, filter domain.Report
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
|
|
@ -17,12 +18,20 @@ type VirtualGameRepository interface {
|
|||
CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error
|
||||
GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, 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 {
|
||||
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 {
|
||||
return &VirtualGameRepo{store: store}
|
||||
}
|
||||
|
|
@ -112,3 +121,58 @@ func (r *VirtualGameRepo) UpdateVirtualGameTransactionStatus(ctx context.Context
|
|||
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)
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -225,3 +225,35 @@ func (s *Store) GetBalanceSummary(ctx context.Context, filter domain.ReportFilte
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ type BranchStore interface {
|
|||
GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, 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)
|
||||
|
||||
GetAllCompaniesBranch(ctx context.Context) ([]domain.Company, error)
|
||||
|
|
|
|||
|
|
@ -5,129 +5,230 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
BaseURL string
|
||||
SecretKey string
|
||||
HTTPClient *http.Client
|
||||
UserAgent string
|
||||
baseURL string
|
||||
secretKey string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewClient(baseURL, secretKey string) *Client {
|
||||
return &Client{
|
||||
BaseURL: baseURL,
|
||||
SecretKey: secretKey,
|
||||
HTTPClient: http.DefaultClient,
|
||||
UserAgent: "FortuneBet/1.0",
|
||||
baseURL: baseURL,
|
||||
secretKey: secretKey,
|
||||
httpClient: &http.Client{
|
||||
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)
|
||||
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 {
|
||||
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)
|
||||
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("Authorization", "Bearer "+c.secretKey)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.HTTPClient.Do(httpReq)
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
fmt.Println("\n\nWe are here 3")
|
||||
return "", fmt.Errorf("chapa HTTP request failed: %w", err)
|
||||
return domain.ChapaDepositResponse{}, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
fmt.Println("\n\nWe are here 4")
|
||||
return "", fmt.Errorf("chapa error: status %d, body: %s", resp.StatusCode, string(body))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Data struct {
|
||||
Message string `json:"message"`
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
CheckoutURL string `json:"checkout_url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
fmt.Printf("\n\nInit payment response body: %v\n\n", response)
|
||||
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return "", fmt.Errorf("failed to parse chapa response: %w", err)
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return domain.ChapaDepositResponse{}, fmt.Errorf("failed to decode 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) {
|
||||
req, _ := http.NewRequest("GET", c.BaseURL+"/banks", 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)
|
||||
func (c *Client) VerifyPayment(ctx context.Context, reference string) (domain.ChapaDepositVerification, error) {
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/transaction/verify/"+reference, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
var resp struct {
|
||||
Message string `json:"message"`
|
||||
Data []domain.ChapaSupportedBank `json:"data"`
|
||||
return domain.ChapaDepositVerification{}, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
||||
return nil, err
|
||||
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
|
||||
|
||||
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
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -6,10 +6,17 @@ import (
|
|||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
)
|
||||
|
||||
type ChapaPort interface {
|
||||
HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error
|
||||
HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error
|
||||
WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error
|
||||
DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error)
|
||||
GetSupportedBanks() ([]domain.ChapaSupportedBank, error)
|
||||
// type ChapaPort interface {
|
||||
// HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error
|
||||
// HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error
|
||||
// WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error
|
||||
// DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,377 +2,191 @@ package chapa
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
// "log/slog"
|
||||
"strconv"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||
"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/wallet"
|
||||
"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 {
|
||||
transactionStore transaction.TransactionStore
|
||||
walletStore wallet.WalletStore
|
||||
userStore user.UserStore
|
||||
referralStore referralservice.ReferralStore
|
||||
branchStore branch.BranchStore
|
||||
chapaClient ChapaClient
|
||||
config *config.Config
|
||||
// logger *slog.Logger
|
||||
store *repository.Store
|
||||
transferStore wallet.TransferStore
|
||||
walletStore wallet.WalletStore
|
||||
userStore user.UserStore
|
||||
cfg *config.Config
|
||||
chapaClient *Client
|
||||
}
|
||||
|
||||
func NewService(
|
||||
txStore transaction.TransactionStore,
|
||||
transferStore wallet.TransferStore,
|
||||
walletStore wallet.WalletStore,
|
||||
userStore user.UserStore,
|
||||
referralStore referralservice.ReferralStore,
|
||||
branchStore branch.BranchStore,
|
||||
chapaClient ChapaClient,
|
||||
store *repository.Store,
|
||||
chapaClient *Client,
|
||||
|
||||
) *Service {
|
||||
return &Service{
|
||||
transactionStore: txStore,
|
||||
walletStore: walletStore,
|
||||
userStore: userStore,
|
||||
referralStore: referralStore,
|
||||
branchStore: branchStore,
|
||||
chapaClient: chapaClient,
|
||||
store: store,
|
||||
transferStore: transferStore,
|
||||
walletStore: walletStore,
|
||||
userStore: userStore,
|
||||
chapaClient: chapaClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error {
|
||||
_, tx, err := s.store.BeginTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
// Use your services normally (they don’t 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)
|
||||
// InitiateDeposit starts a new deposit process
|
||||
func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) {
|
||||
// Validate amount
|
||||
if amount <= 0 {
|
||||
return "", ErrInvalidPaymentAmount
|
||||
}
|
||||
|
||||
txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID)
|
||||
// Get user details
|
||||
user, err := s.userStore.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("transaction with ID %d not found", referenceID)
|
||||
return "", fmt.Errorf("failed to get user: %w", 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)
|
||||
}
|
||||
for _, wallet := range senderWallets {
|
||||
if wallet.IsWithdraw {
|
||||
senderWallet = wallet
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
if txn.Verified {
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
webhookAmount, _ := decimal.NewFromString(req.Amount)
|
||||
storedAmount, _ := decimal.NewFromString(txn.Amount.String())
|
||||
if !webhookAmount.Equal(storedAmount) {
|
||||
return fmt.Errorf("amount mismatch")
|
||||
// Verify payment with Chapa
|
||||
verification, err := s.chapaClient.VerifyPayment(ctx, reference)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify payment: %w", err)
|
||||
}
|
||||
|
||||
txn.Verified = true
|
||||
if err := s.transactionStore.UpdateTransactionVerified(ctx, txn.ID, txn.Verified, txn.ApprovedBy.Value, txn.ApproverName.Value); err != nil {
|
||||
return err
|
||||
// Update payment status
|
||||
if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
|
||||
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 {
|
||||
_, tx, err := s.store.BeginTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
if req.Status != "success" {
|
||||
return fmt.Errorf("payment status not successful")
|
||||
func (s *Service) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
|
||||
// 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
|
||||
}
|
||||
|
||||
// 1. Parse reference ID
|
||||
referenceID, err := strconv.ParseInt(req.TxRef, 10, 64)
|
||||
// If not verified or not found, verify with Chapa
|
||||
verification, err := s.chapaClient.VerifyPayment(ctx, txRef)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid tx_ref: %w", err)
|
||||
return nil, fmt.Errorf("failed to verify payment: %w", err)
|
||||
}
|
||||
|
||||
// 2. Fetch transaction
|
||||
txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("transaction with ID %d not found", referenceID)
|
||||
// Update our records if payment is successful
|
||||
if verification.Status == domain.PaymentStatusCompleted {
|
||||
err = s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update verification status: %w", err)
|
||||
}
|
||||
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
|
||||
// Credit user's wallet
|
||||
err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID, transfer.Amount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update wallet balance: %w", 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 {
|
||||
_, tx, err := s.store.BeginTx(ctx)
|
||||
func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
|
||||
banks, err := s.chapaClient.FetchSupportedBanks(ctx)
|
||||
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
|
||||
}
|
||||
|
||||
func formatRegex(length int) string {
|
||||
return fmt.Sprintf("/^[0-9]{%d}$/", length)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,6 @@ type CompanyStore interface {
|
|||
GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error)
|
||||
UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error)
|
||||
DeleteCompany(ctx context.Context, id int64) error
|
||||
|
||||
GetCompanyCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,4 +18,6 @@ type NotificationStore interface {
|
|||
ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) // New method
|
||||
CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error)
|
||||
GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error)
|
||||
|
||||
GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *confi
|
|||
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 {
|
||||
s.logger.Warn("[NotificationSvc.AddConnection] Attempted to add nil WebSocket connection", "recipientID", recipientID)
|
||||
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 {
|
||||
s.addConnection(ctx, recipientID, c)
|
||||
s.addConnection(recipientID, c)
|
||||
s.logger.Info("[NotificationSvc.ConnectWebSocket] WebSocket connection established", "recipientID", recipientID)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -283,3 +283,7 @@ func (s *Service) retryFailedNotifications() {
|
|||
func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) {
|
||||
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)
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -7,9 +7,12 @@ import (
|
|||
)
|
||||
|
||||
type ReportStore interface {
|
||||
GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (DashboardSummary, error)
|
||||
GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]BetAnalysis, error)
|
||||
GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]CustomerActivity, error)
|
||||
GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]BranchPerformance, error)
|
||||
GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]SportPerformance, error)
|
||||
GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (domain.DashboardSummary, error)
|
||||
GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]domain.BetAnalysis, error)
|
||||
GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.CustomerActivity, error)
|
||||
GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.BranchPerformance, 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,16 @@ import (
|
|||
"errors"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"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/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/user"
|
||||
// virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||
)
|
||||
|
||||
|
|
@ -21,12 +24,15 @@ var (
|
|||
)
|
||||
|
||||
type Service struct {
|
||||
betStore bet.BetStore
|
||||
walletStore wallet.WalletStore
|
||||
transactionStore transaction.TransactionStore
|
||||
branchStore branch.BranchStore
|
||||
userStore user.UserStore
|
||||
logger *slog.Logger
|
||||
betStore bet.BetStore
|
||||
walletStore wallet.WalletStore
|
||||
transactionStore transaction.TransactionStore
|
||||
branchStore branch.BranchStore
|
||||
userStore user.UserStore
|
||||
companyStore company.CompanyStore
|
||||
virtulaGamesStore repository.VirtualGameRepository
|
||||
notificationStore repository.NotificationRepository
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewService(
|
||||
|
|
@ -35,44 +41,31 @@ func NewService(
|
|||
transactionStore transaction.TransactionStore,
|
||||
branchStore branch.BranchStore,
|
||||
userStore user.UserStore,
|
||||
companyStore company.CompanyStore,
|
||||
virtulaGamesStore repository.VirtualGameRepository,
|
||||
notificationStore repository.NotificationRepository,
|
||||
logger *slog.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
betStore: betStore,
|
||||
walletStore: walletStore,
|
||||
transactionStore: transactionStore,
|
||||
branchStore: branchStore,
|
||||
userStore: userStore,
|
||||
logger: logger,
|
||||
betStore: betStore,
|
||||
walletStore: walletStore,
|
||||
transactionStore: transactionStore,
|
||||
branchStore: branchStore,
|
||||
userStore: userStore,
|
||||
companyStore: companyStore,
|
||||
virtulaGamesStore: virtulaGamesStore,
|
||||
notificationStore: notificationStore,
|
||||
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
|
||||
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 {
|
||||
return DashboardSummary{}, err
|
||||
return domain.DashboardSummary{}, err
|
||||
}
|
||||
|
||||
var summary DashboardSummary
|
||||
var summary domain.DashboardSummary
|
||||
var err error
|
||||
|
||||
// Get bets summary
|
||||
|
|
@ -80,28 +73,75 @@ func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportF
|
|||
s.betStore.GetBetSummary(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get bet summary", "error", err)
|
||||
return DashboardSummary{}, err
|
||||
return domain.DashboardSummary{}, err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
s.logger.Error("failed to get customer counts", "error", err)
|
||||
return DashboardSummary{}, err
|
||||
return domain.DashboardSummary{}, err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
s.logger.Error("failed to get branch counts", "error", err)
|
||||
return DashboardSummary{}, err
|
||||
return domain.DashboardSummary{}, err
|
||||
}
|
||||
|
||||
// Get transaction metrics
|
||||
summary.TotalDeposits, summary.TotalWithdrawals, err = s.transactionStore.GetTransactionTotals(ctx, filter)
|
||||
if err != nil {
|
||||
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
|
||||
|
|
@ -114,23 +154,8 @@ func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportF
|
|||
return summary, nil
|
||||
}
|
||||
|
||||
// BetAnalysis represents detailed bet analysis
|
||||
type BetAnalysis struct {
|
||||
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) {
|
||||
// Getdomain.BetAnalysis returns detailed bet analysis
|
||||
func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]domain.BetAnalysis, error) {
|
||||
if err := validateTimeRange(filter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -164,9 +189,9 @@ func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter
|
|||
}
|
||||
|
||||
// Combine data into analysis
|
||||
var analysis []BetAnalysis
|
||||
var analysis []domain.BetAnalysis
|
||||
for _, stat := range betStats {
|
||||
a := BetAnalysis{
|
||||
a := domain.BetAnalysis{
|
||||
Date: stat.Date,
|
||||
TotalBets: stat.TotalBets,
|
||||
TotalStakes: stat.TotalStakes,
|
||||
|
|
@ -203,27 +228,8 @@ func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter
|
|||
return analysis, nil
|
||||
}
|
||||
|
||||
// CustomerActivity represents customer activity metrics
|
||||
type CustomerActivity struct {
|
||||
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) {
|
||||
// Getdomain.CustomerActivity returns customer activity report
|
||||
func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.CustomerActivity, error) {
|
||||
if err := validateTimeRange(filter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -250,9 +256,9 @@ func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportF
|
|||
}
|
||||
|
||||
// Combine data into activity report
|
||||
var activities []CustomerActivity
|
||||
var activities []domain.CustomerActivity
|
||||
for _, bet := range customerBets {
|
||||
activity := CustomerActivity{
|
||||
activity := domain.CustomerActivity{
|
||||
CustomerID: bet.CustomerID,
|
||||
TotalBets: bet.TotalBets,
|
||||
TotalStakes: bet.TotalStakes,
|
||||
|
|
@ -295,27 +301,8 @@ func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportF
|
|||
return activities, nil
|
||||
}
|
||||
|
||||
// BranchPerformance represents branch performance metrics
|
||||
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 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) {
|
||||
// Getdomain.BranchPerformance returns branch performance report
|
||||
func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.BranchPerformance, error) {
|
||||
// Get branch bet activity
|
||||
branchBets, err := s.betStore.GetBranchBetActivity(ctx, filter)
|
||||
if err != nil {
|
||||
|
|
@ -345,9 +332,9 @@ func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.Report
|
|||
}
|
||||
|
||||
// Combine data into performance report
|
||||
var performances []BranchPerformance
|
||||
var performances []domain.BranchPerformance
|
||||
for _, bet := range branchBets {
|
||||
performance := BranchPerformance{
|
||||
performance := domain.BranchPerformance{
|
||||
BranchID: bet.BranchID,
|
||||
TotalBets: bet.TotalBets,
|
||||
TotalStakes: bet.TotalStakes,
|
||||
|
|
@ -394,24 +381,8 @@ func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.Report
|
|||
return performances, nil
|
||||
}
|
||||
|
||||
// SportPerformance represents sport performance metrics
|
||||
type SportPerformance struct {
|
||||
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) {
|
||||
// Getdomain.SportPerformance returns sport performance report
|
||||
func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.SportPerformance, error) {
|
||||
// Get sport bet activity
|
||||
sportBets, err := s.betStore.GetSportBetActivity(ctx, filter)
|
||||
if err != nil {
|
||||
|
|
@ -434,9 +405,9 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF
|
|||
}
|
||||
|
||||
// Combine data into performance report
|
||||
var performances []SportPerformance
|
||||
var performances []domain.SportPerformance
|
||||
for _, bet := range sportBets {
|
||||
performance := SportPerformance{
|
||||
performance := domain.SportPerformance{
|
||||
SportID: bet.SportID,
|
||||
TotalBets: bet.TotalBets,
|
||||
TotalStakes: bet.TotalStakes,
|
||||
|
|
@ -477,6 +448,164 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF
|
|||
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
|
||||
func validateTimeRange(filter domain.ReportFilter) error {
|
||||
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
|
||||
profitScore := float64(perf.Profit) / 1000
|
||||
customerScore := float64(perf.CustomerCount) * 0.1
|
||||
|
|
|
|||
|
|
@ -24,9 +24,10 @@ type UserStore interface {
|
|||
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
|
||||
|
||||
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)
|
||||
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 {
|
||||
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
|
||||
|
|
|
|||
|
|
@ -9,5 +9,10 @@ import (
|
|||
type VirtualGameService interface {
|
||||
GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ type service struct {
|
|||
repo repository.VirtualGameRepository
|
||||
walletSvc wallet.Service
|
||||
store *repository.Store
|
||||
config *config.Config
|
||||
logger *slog.Logger
|
||||
// virtualGameStore repository.VirtualGameRepository
|
||||
config *config.Config
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func New(repo repository.VirtualGameRepository, walletSvc wallet.Service, store *repository.Store, cfg *config.Config, logger *slog.Logger) VirtualGameService {
|
||||
|
|
@ -59,11 +60,18 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
|
|||
}
|
||||
|
||||
params := fmt.Sprintf(
|
||||
"client_id=%s&game_id=%s¤cy=%s&lang=en&mode=%s&token=%s",
|
||||
s.config.PopOK.ClientID, gameID, currency, mode, token,
|
||||
"partnerId=%s&gameId=%s&gameMode=%s&lang=en&platform=%s&externalToken=%s",
|
||||
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 {
|
||||
|
|
@ -138,7 +146,228 @@ func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCall
|
|||
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.Write([]byte(params))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
|
|
@ -166,3 +395,7 @@ func (s *service) verifySignature(callback *domain.PopOKCallback) bool {
|
|||
expected := hex.EncodeToString(h.Sum(nil))
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ type WalletStore interface {
|
|||
UpdateWalletActive(ctx context.Context, id int64, isActive bool) error
|
||||
|
||||
GetBalanceSummary(ctx context.Context, filter domain.ReportFilter) (domain.BalanceSummary, error)
|
||||
GetTotalWallets(ctx context.Context, filter domain.ReportFilter) (int64, error)
|
||||
}
|
||||
|
||||
type TransferStore interface {
|
||||
CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error)
|
||||
GetAllTransfers(ctx context.Context) ([]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)
|
||||
UpdateTransferVerification(ctx context.Context, id int64, verified bool) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ var (
|
|||
)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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,
|
||||
"wallet_id": %d,
|
||||
"notification_type": "customer_facing"
|
||||
}`, transfer.Amount, senderWallet.Balance, transfer.SenderWalletID.Value)),
|
||||
}`, transfer.Amount, senderWallet.Balance, transfer.SenderWalletID)),
|
||||
}
|
||||
|
||||
// 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",
|
||||
Message: fmt.Sprintf(
|
||||
"Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f",
|
||||
transfer.SenderWalletID.Value,
|
||||
transfer.SenderWalletID,
|
||||
float64(senderWallet.Balance)/100,
|
||||
float64(transfer.Amount)/100,
|
||||
),
|
||||
|
|
@ -64,7 +64,7 @@ func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTran
|
|||
"balance": %d,
|
||||
"required_amount": %d,
|
||||
"notification_type": "admin_alert"
|
||||
}`, transfer.SenderWalletID.Value, senderWallet.Balance, transfer.Amount),
|
||||
}`, transfer.SenderWalletID, senderWallet.Balance, transfer.Amount),
|
||||
}
|
||||
|
||||
// Send both notifications
|
||||
|
|
@ -100,6 +100,10 @@ func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error
|
|||
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) {
|
||||
return s.transferStore.GetTransferByID(ctx, id)
|
||||
}
|
||||
|
|
@ -119,7 +123,7 @@ func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransf
|
|||
}
|
||||
|
||||
// Add to receiver
|
||||
senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID.Value)
|
||||
senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID)
|
||||
if err != nil {
|
||||
return domain.Transfer{}, err
|
||||
} 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
|
||||
transfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
|
||||
SenderWalletID: domain.ValidInt64{
|
||||
Value: senderID,
|
||||
Valid: true,
|
||||
},
|
||||
SenderWalletID: senderID,
|
||||
CashierID: cashierID,
|
||||
ReceiverWalletID: receiverID,
|
||||
Amount: amount,
|
||||
|
|
|
|||
|
|
@ -1,464 +1,163 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
// "bytes"
|
||||
// "encoding/json"
|
||||
// "fmt"
|
||||
// "io"
|
||||
// "net/http"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// // GetBanks godoc
|
||||
// // @Summary Get list of banks
|
||||
// // @Description Fetch all supported banks from Chapa
|
||||
// // @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
|
||||
// InitiateDeposit godoc
|
||||
// @Summary Initiate a deposit
|
||||
// @Description Starts a new deposit process using Chapa payment gateway
|
||||
// @Tags Chapa
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body domain.ChapaTransactionType true "Webhook Payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/chapa/payments/verify [post]
|
||||
func (h *Handler) VerifyChapaPayment(c *fiber.Ctx) error {
|
||||
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,
|
||||
// @Param request body domain.ChapaDepositRequestPayload true "Deposit request"
|
||||
// @Success 200 {object} domain.ChapaDepositResponse
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/chapa/payments/deposit [post]
|
||||
func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
|
||||
// Get user ID from context (set by your auth middleware)
|
||||
userID, ok := c.Locals("user_id").(int64)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Error: "invalid user ID",
|
||||
})
|
||||
}
|
||||
|
||||
var req domain.ChapaDepositRequestPayload
|
||||
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
fmt.Sprintln("We first first are here init Chapa payment")
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
amount := domain.Currency(req.Amount * 100)
|
||||
|
||||
fmt.Sprintln("We are here init Chapa payment")
|
||||
|
||||
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.ChapaDepositResponse{
|
||||
CheckoutURL: checkoutURL,
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
// WebhookCallback godoc
|
||||
// @Summary Chapa payment webhook callback (used by Chapa)
|
||||
// @Description Handles payment notifications from Chapa
|
||||
// @Tags Chapa
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.ChapaWebhookPayload true "Webhook payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/chapa/payments/webhook/verify [post]
|
||||
func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
|
||||
// Verify webhook signature first
|
||||
// signature := c.Get("Chapa-Signature")
|
||||
// if !verifySignature(signature, c.Body()) {
|
||||
// return c.Status(fiber.StatusUnauthorized).JSON(ErrorResponse{
|
||||
// Error: "invalid signature",
|
||||
// })
|
||||
// }
|
||||
|
||||
var payload struct {
|
||||
TxRef string `json:"tx_ref"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
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 := c.BodyParser(&payload); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.chapaSvc.WithdrawUsingChapa(c.Context(), userID, req); err != nil {
|
||||
return domain.FiberErrorResponse(c, err)
|
||||
if err := h.chapaSvc.VerifyDeposit(c.Context(), payload.TxRef); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Withdrawal requested successfully",
|
||||
StatusCode: 200,
|
||||
Message: "payment verified successfully",
|
||||
Data: payload.TxRef,
|
||||
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]
|
||||
func (h *Handler) DepositUsingChapa(c *fiber.Ctx) error {
|
||||
// Extract user info from token (adjust as per your auth middleware)
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
var req domain.ChapaDepositRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return domain.UnProcessableEntityResponse(c)
|
||||
}
|
||||
|
||||
// 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
|
||||
paymentUrl, svcErr := h.chapaSvc.DepositUsingChapa(c.Context(), userID, req)
|
||||
if svcErr != nil {
|
||||
return domain.FiberErrorResponse(c, svcErr)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.ResponseWDataFactory[domain.ChapaPaymentUrlResponse]{
|
||||
Data: domain.ChapaPaymentUrlResponse{
|
||||
PaymentURL: paymentUrl,
|
||||
},
|
||||
Response: domain.Response{
|
||||
Message: "Deposit process started on wallet, fulfill payment using the URL provided",
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ReadChapaBanks godoc
|
||||
// @Summary fetches chapa supported banks
|
||||
// VerifyPayment godoc
|
||||
// @Summary Verify a payment manually
|
||||
// @Description Manually verify a payment using Chapa's API
|
||||
// @Tags Chapa
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} domain.ChapaSupportedBanksResponseWrapper
|
||||
// @Failure 400,401,404,422,500 {object} domain.Response
|
||||
// @Router /api/v1/chapa/banks [get]
|
||||
func (h *Handler) ReadChapaBanks(c *fiber.Ctx) error {
|
||||
banks, err := h.chapaSvc.GetSupportedBanks()
|
||||
fmt.Printf("\n\nhandler fetched banks: %+v\n\n", banks)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.Response{
|
||||
Message: "Internal server error",
|
||||
Success: false,
|
||||
StatusCode: fiber.StatusInternalServerError,
|
||||
// @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",
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.ResponseWDataFactory[[]domain.ChapaSupportedBank]{
|
||||
Data: banks,
|
||||
Response: domain.Response{
|
||||
Message: "read successful on chapa supported banks",
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
},
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ type Handler struct {
|
|||
userSvc *user.Service
|
||||
referralSvc referralservice.ReferralStore
|
||||
reportSvc report.ReportStore
|
||||
chapaSvc chapa.ChapaPort
|
||||
chapaSvc *chapa.Service
|
||||
walletSvc *wallet.Service
|
||||
transactionSvc *transaction.Service
|
||||
ticketSvc *ticket.Service
|
||||
|
|
@ -60,7 +60,7 @@ func New(
|
|||
notificationSvc *notificationservice.Service,
|
||||
validator *customvalidator.CustomValidator,
|
||||
reportSvc report.ReportStore,
|
||||
chapaSvc chapa.ChapaPort,
|
||||
chapaSvc *chapa.Service,
|
||||
walletSvc *wallet.Service,
|
||||
referralSvc referralservice.ReferralStore,
|
||||
virtualGameSvc virtualgameservice.VirtualGameService,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -36,9 +36,7 @@ type RefillRes struct {
|
|||
|
||||
func convertTransfer(transfer domain.Transfer) TransferWalletRes {
|
||||
var senderWalletID *int64
|
||||
if transfer.SenderWalletID.Valid {
|
||||
senderWalletID = &transfer.SenderWalletID.Value
|
||||
}
|
||||
senderWalletID = &transfer.SenderWalletID
|
||||
|
||||
var cashierID *int64
|
||||
if transfer.CashierID.Valid {
|
||||
|
|
|
|||
|
|
@ -5,14 +5,15 @@ import (
|
|||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type launchVirtualGameReq struct {
|
||||
GameID string `json:"game_id" validate:"required" example:"crash_001"`
|
||||
Currency string `json:"currency" validate:"required,len=3" example:"USD"`
|
||||
Mode string `json:"mode" validate:"required,oneof=REAL DEMO" example:"REAL"`
|
||||
GameID string `json:"game_id" validate:"required" example:"crash_001"`
|
||||
Currency string `json:"currency" validate:"required,len=3" example:"USD"`
|
||||
Mode string `json:"mode" validate:"required,oneof=fun real" example:"real"`
|
||||
}
|
||||
|
||||
type launchVirtualGameRes struct {
|
||||
LaunchURL string `json:"launch_url"`
|
||||
LaunchURL string `json:"launch_url"`
|
||||
}
|
||||
|
||||
// LaunchVirtualGame godoc
|
||||
|
|
@ -81,3 +82,76 @@ func (h *Handler) HandleVirtualGameCallback(c *fiber.Ctx) error {
|
|||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
_ "github.com/SamuelTariku/FortuneBet-Backend/docs"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
|
||||
// "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
||||
|
||||
// "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)
|
||||
|
||||
//Chapa Routes
|
||||
group.Post("/chapa/payments/verify", a.authMiddleware, h.VerifyChapaPayment)
|
||||
group.Post("/chapa/payments/withdraw", a.authMiddleware, h.WithdrawUsingChapa)
|
||||
group.Post("/chapa/payments/deposit", a.authMiddleware, h.DepositUsingChapa)
|
||||
group.Get("/chapa/banks", a.authMiddleware, h.ReadChapaBanks)
|
||||
group.Post("/chapa/payments/webhook/verify", h.WebhookCallback)
|
||||
group.Get("/chapa/payments/manual/verify/:tx_ref", h.ManualVerifyPayment)
|
||||
group.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit)
|
||||
group.Get("/chapa/banks", h.GetSupportedBanks)
|
||||
|
||||
//Report Routes
|
||||
group.Get("/reports/dashboard", a.authMiddleware, h.GetDashboardReport)
|
||||
group.Get("/reports/dashboard", h.GetDashboardReport)
|
||||
|
||||
//Wallet Monitor Service
|
||||
// group.Get("/debug/wallet-monitor/status", func(c *fiber.Ctx) error {
|
||||
|
|
@ -235,7 +236,7 @@ func (a *App) initAppRoutes() {
|
|||
|
||||
//mongoDB logs
|
||||
ctx := context.Background()
|
||||
group.Get("/logs", handlers.GetLogsHandler(ctx))
|
||||
group.Get("/logs", a.authMiddleware, a.SuperAdminOnly, handlers.GetLogsHandler(ctx))
|
||||
|
||||
// Recommendation Routes
|
||||
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.Post("/notifications/mark-as-read", a.authMiddleware, h.MarkNotificationAsRead)
|
||||
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
|
||||
a.fiber.Post("/virtual-game/launch", a.authMiddleware, h.LaunchVirtualGame)
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user