Chapa webhook + popok fix
This commit is contained in:
parent
8f2713b920
commit
6d5bdd8a56
22
cmd/main.go
22
cmd/main.go
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
// "context"
|
// "context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -15,6 +16,7 @@ import (
|
||||||
// "github.com/gofiber/fiber/v2"
|
// "github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
||||||
|
|
||||||
|
|
@ -83,8 +85,13 @@ func main() {
|
||||||
|
|
||||||
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
|
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
|
||||||
|
|
||||||
mongoLogger.Init()
|
domain.MongoDBLogger, err = mongoLogger.InitLogger()
|
||||||
mongoDBLogger := zap.L()
|
if err != nil {
|
||||||
|
log.Fatalf("Logger initialization failed: %v", err)
|
||||||
|
}
|
||||||
|
defer domain.MongoDBLogger.Sync()
|
||||||
|
|
||||||
|
zap.ReplaceGlobals(domain.MongoDBLogger)
|
||||||
|
|
||||||
// client := mongoLogger.InitDB()
|
// client := mongoLogger.InitDB()
|
||||||
// defer func() {
|
// defer func() {
|
||||||
|
|
@ -122,6 +129,7 @@ func main() {
|
||||||
oddsSvc := odds.New(store, cfg, logger)
|
oddsSvc := odds.New(store, cfg, logger)
|
||||||
ticketSvc := ticket.NewService(store)
|
ticketSvc := ticket.NewService(store)
|
||||||
notificationRepo := repository.NewNotificationRepository(store)
|
notificationRepo := repository.NewNotificationRepository(store)
|
||||||
|
virtuaGamesRepo := repository.NewVirtualGameRepository(store)
|
||||||
|
|
||||||
notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
|
notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
|
||||||
|
|
||||||
|
|
@ -143,7 +151,7 @@ func main() {
|
||||||
branchSvc := branch.NewService(store)
|
branchSvc := branch.NewService(store)
|
||||||
companySvc := company.NewService(store)
|
companySvc := company.NewService(store)
|
||||||
leagueSvc := league.New(store)
|
leagueSvc := league.New(store)
|
||||||
betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger, mongoDBLogger)
|
betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger)
|
||||||
resultSvc := result.NewService(store, cfg, logger, *betSvc)
|
resultSvc := result.NewService(store, cfg, logger, *betSvc)
|
||||||
referalRepo := repository.NewReferralRepository(store)
|
referalRepo := repository.NewReferralRepository(store)
|
||||||
vitualGameRepo := repository.NewVirtualGameRepository(store)
|
vitualGameRepo := repository.NewVirtualGameRepository(store)
|
||||||
|
|
@ -167,13 +175,10 @@ func main() {
|
||||||
chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY)
|
chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY)
|
||||||
|
|
||||||
chapaSvc := chapa.NewService(
|
chapaSvc := chapa.NewService(
|
||||||
transaction.TransactionStore(store),
|
wallet.TransferStore(store),
|
||||||
wallet.WalletStore(store),
|
wallet.WalletStore(store),
|
||||||
user.UserStore(store),
|
user.UserStore(store),
|
||||||
referalSvc,
|
|
||||||
branch.BranchStore(store),
|
|
||||||
chapaClient,
|
chapaClient,
|
||||||
store,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
reportSvc := report.NewService(
|
reportSvc := report.NewService(
|
||||||
|
|
@ -182,6 +187,9 @@ func main() {
|
||||||
transaction.TransactionStore(store),
|
transaction.TransactionStore(store),
|
||||||
branch.BranchStore(store),
|
branch.BranchStore(store),
|
||||||
user.UserStore(store),
|
user.UserStore(store),
|
||||||
|
company.CompanyStore(store),
|
||||||
|
virtuaGamesRepo,
|
||||||
|
notificationRepo,
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ CREATE TABLE IF NOT EXISTS wallet_transfer (
|
||||||
sender_wallet_id BIGINT,
|
sender_wallet_id BIGINT,
|
||||||
cashier_id BIGINT,
|
cashier_id BIGINT,
|
||||||
verified BOOLEAN NOT NULL DEFAULT false,
|
verified BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
reference_number VARCHAR(255) NOT NULL,
|
||||||
payment_method VARCHAR(255) NOT NULL,
|
payment_method VARCHAR(255) NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS virtual_games (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
provider VARCHAR(255) NOT NULL,
|
||||||
|
category VARCHAR(100),
|
||||||
|
min_bet NUMERIC(10, 2) NOT NULL,
|
||||||
|
max_bet NUMERIC(10, 2) NOT NULL,
|
||||||
|
volatility VARCHAR(50),
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
rtp NUMERIC(5, 2) CHECK (rtp >= 0 AND rtp <= 100),
|
||||||
|
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
popularity_score INT DEFAULT 0,
|
||||||
|
thumbnail_url TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
CREATE TABLE virtual_game_sessions (
|
CREATE TABLE virtual_game_sessions (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||||
|
|
|
||||||
|
|
@ -69,3 +69,10 @@ LIMIT $1;
|
||||||
SELECT recipient_id
|
SELECT recipient_id
|
||||||
FROM notifications
|
FROM notifications
|
||||||
WHERE reciever = $1;
|
WHERE reciever = $1;
|
||||||
|
|
||||||
|
-- name: GetNotificationCounts :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total,
|
||||||
|
COUNT(CASE WHEN is_read = true THEN 1 END) as read,
|
||||||
|
COUNT(CASE WHEN is_read = false THEN 1 END) as unread
|
||||||
|
FROM notifications;
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ INSERT INTO wallet_transfer (
|
||||||
sender_wallet_id,
|
sender_wallet_id,
|
||||||
cashier_id,
|
cashier_id,
|
||||||
verified,
|
verified,
|
||||||
|
reference_number,
|
||||||
payment_method
|
payment_method
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
-- name: GetAllTransfers :many
|
-- name: GetAllTransfers :many
|
||||||
SELECT *
|
SELECT *
|
||||||
|
|
@ -22,6 +23,10 @@ WHERE receiver_wallet_id = $1
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM wallet_transfer
|
FROM wallet_transfer
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
-- name: GetTransferByReference :one
|
||||||
|
SELECT *
|
||||||
|
FROM wallet_transfer
|
||||||
|
WHERE reference_number = $1;
|
||||||
-- name: UpdateTransferVerification :exec
|
-- name: UpdateTransferVerification :exec
|
||||||
UPDATE wallet_transfer
|
UPDATE wallet_transfer
|
||||||
SET verified = $1,
|
SET verified = $1,
|
||||||
|
|
|
||||||
449
docs/docs.go
449
docs/docs.go
|
|
@ -304,8 +304,9 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/chapa/banks": {
|
"/api/v1/chapa/payments/deposit": {
|
||||||
"get": {
|
"post": {
|
||||||
|
"description": "Starts a new deposit process using Chapa payment gateway",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -315,50 +316,43 @@ const docTemplate = `{
|
||||||
"tags": [
|
"tags": [
|
||||||
"Chapa"
|
"Chapa"
|
||||||
],
|
],
|
||||||
"summary": "fetches chapa supported banks",
|
"summary": "Initiate a deposit",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Deposit request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ChapaDepositRequestPayload"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.ChapaSupportedBanksResponseWrapper"
|
"$ref": "#/definitions/domain.ChapaDepositResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.Response"
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Unprocessable Entity",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.Response"
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/chapa/payments/deposit": {
|
"/api/v1/chapa/payments/manual/verify/{tx_ref}": {
|
||||||
"post": {
|
"get": {
|
||||||
"description": "Deposits money into user wallet from user account using Chapa",
|
"description": "Manually verify a payment using Chapa's API",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -368,48 +362,41 @@ const docTemplate = `{
|
||||||
"tags": [
|
"tags": [
|
||||||
"Chapa"
|
"Chapa"
|
||||||
],
|
],
|
||||||
"summary": "Deposit money into user wallet using Chapa",
|
"summary": "Verify a payment manually",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Deposit request payload",
|
"type": "string",
|
||||||
"name": "payload",
|
"description": "Transaction Reference",
|
||||||
"in": "body",
|
"name": "tx_ref",
|
||||||
"required": true,
|
"in": "path",
|
||||||
"schema": {
|
"required": true
|
||||||
"$ref": "#/definitions/domain.ChapaDepositRequest"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.ChapaPaymentUrlResponseWrapper"
|
"$ref": "#/definitions/domain.ChapaVerificationResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.Response"
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal server error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.Response"
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/chapa/payments/verify": {
|
"/api/v1/chapa/payments/webhook/verify": {
|
||||||
"post": {
|
"post": {
|
||||||
|
"description": "Handles payment notifications from Chapa",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -419,93 +406,36 @@ const docTemplate = `{
|
||||||
"tags": [
|
"tags": [
|
||||||
"Chapa"
|
"Chapa"
|
||||||
],
|
],
|
||||||
"summary": "Verifies Chapa webhook transaction",
|
"summary": "Chapa payment webhook callback (used by Chapa)",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Webhook Payload",
|
"description": "Webhook payload",
|
||||||
"name": "payload",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ChapaTransactionType"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/chapa/payments/withdraw": {
|
|
||||||
"post": {
|
|
||||||
"description": "Initiates a withdrawal transaction using Chapa for the authenticated user.",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Chapa"
|
|
||||||
],
|
|
||||||
"summary": "Withdraw using Chapa",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "Chapa Withdraw Request",
|
|
||||||
"name": "request",
|
"name": "request",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.ChapaWithdrawRequest"
|
"$ref": "#/definitions/domain.ChapaWebhookPayload"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Withdrawal requested successfully",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"additionalProperties": true
|
||||||
"data": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.Response"
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Unprocessable Entity",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.Response"
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -914,6 +844,38 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/banks": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get list of banks supported by Chapa",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Chapa"
|
||||||
|
],
|
||||||
|
"summary": "Get supported banks",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.Bank"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/bet": {
|
"/bet": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Gets all the bets",
|
"description": "Gets all the bets",
|
||||||
|
|
@ -4389,6 +4351,55 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.Bank": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"acct_length": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"country_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"currency": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"is_24hrs": {
|
||||||
|
"description": "nullable",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"is_mobilemoney": {
|
||||||
|
"description": "nullable",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"is_rtgs": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"swift": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.BetOutcome": {
|
"domain.BetOutcome": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -4521,152 +4532,62 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.ChapaDepositRequest": {
|
"domain.ChapaDepositRequestPayload": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"amount"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"amount": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.ChapaDepositResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"checkoutURL": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.ChapaVerificationResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"amount": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"currency": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tx_ref": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.ChapaWebhookPayload": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"amount": {
|
"amount": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"branch_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"currency": {
|
"currency": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"phone_number": {
|
"status": {
|
||||||
|
"$ref": "#/definitions/domain.PaymentStatus"
|
||||||
|
},
|
||||||
|
"tx_ref": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.ChapaPaymentUrlResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"payment_url": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.ChapaPaymentUrlResponseWrapper": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {},
|
|
||||||
"message": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"status_code": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.ChapaSupportedBank": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"acct_length": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"acct_number_regex": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"active": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"country_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"currency": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"example_value": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"is_24hrs": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"is_active": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"is_mobilemoney": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"is_rtgs": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"slug": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"swift": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.ChapaSupportedBanksResponseWrapper": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {},
|
|
||||||
"message": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"status_code": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.ChapaTransactionType": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.ChapaWithdrawRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"account_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"account_number": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"amount": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"bank_code": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"beneficiary_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"branch_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"currency": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"wallet_id": {
|
|
||||||
"description": "add this",
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.CreateBetOutcomeReq": {
|
"domain.CreateBetOutcomeReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -4820,6 +4741,19 @@ const docTemplate = `{
|
||||||
"BANK"
|
"BANK"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"domain.PaymentStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"pending",
|
||||||
|
"completed",
|
||||||
|
"failed"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"PaymentStatusPending",
|
||||||
|
"PaymentStatusCompleted",
|
||||||
|
"PaymentStatusFailed"
|
||||||
|
]
|
||||||
|
},
|
||||||
"domain.PopOKCallback": {
|
"domain.PopOKCallback": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -4960,21 +4894,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.Response": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {},
|
|
||||||
"message": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"status_code": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.Role": {
|
"domain.Role": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -5070,7 +4989,7 @@ const docTemplate = `{
|
||||||
},
|
},
|
||||||
"awayTeamID": {
|
"awayTeamID": {
|
||||||
"description": "Away team ID (can be empty/null)",
|
"description": "Away team ID (can be empty/null)",
|
||||||
"type": "string"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"homeKitImage": {
|
"homeKitImage": {
|
||||||
"description": "Kit or image for home team (optional)",
|
"description": "Kit or image for home team (optional)",
|
||||||
|
|
@ -5082,7 +5001,7 @@ const docTemplate = `{
|
||||||
},
|
},
|
||||||
"homeTeamID": {
|
"homeTeamID": {
|
||||||
"description": "Home team ID",
|
"description": "Home team ID",
|
||||||
"type": "string"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"description": "Event ID",
|
"description": "Event ID",
|
||||||
|
|
@ -5094,7 +5013,7 @@ const docTemplate = `{
|
||||||
},
|
},
|
||||||
"leagueID": {
|
"leagueID": {
|
||||||
"description": "League ID",
|
"description": "League ID",
|
||||||
"type": "string"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"leagueName": {
|
"leagueName": {
|
||||||
"description": "League name",
|
"description": "League name",
|
||||||
|
|
@ -5110,7 +5029,7 @@ const docTemplate = `{
|
||||||
},
|
},
|
||||||
"sportID": {
|
"sportID": {
|
||||||
"description": "Sport ID",
|
"description": "Sport ID",
|
||||||
"type": "string"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"startTime": {
|
"startTime": {
|
||||||
"description": "Converted from \"time\" field in UNIX format",
|
"description": "Converted from \"time\" field in UNIX format",
|
||||||
|
|
|
||||||
|
|
@ -296,8 +296,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/chapa/banks": {
|
"/api/v1/chapa/payments/deposit": {
|
||||||
"get": {
|
"post": {
|
||||||
|
"description": "Starts a new deposit process using Chapa payment gateway",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -307,50 +308,43 @@
|
||||||
"tags": [
|
"tags": [
|
||||||
"Chapa"
|
"Chapa"
|
||||||
],
|
],
|
||||||
"summary": "fetches chapa supported banks",
|
"summary": "Initiate a deposit",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Deposit request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ChapaDepositRequestPayload"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.ChapaSupportedBanksResponseWrapper"
|
"$ref": "#/definitions/domain.ChapaDepositResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.Response"
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Unprocessable Entity",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.Response"
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/chapa/payments/deposit": {
|
"/api/v1/chapa/payments/manual/verify/{tx_ref}": {
|
||||||
"post": {
|
"get": {
|
||||||
"description": "Deposits money into user wallet from user account using Chapa",
|
"description": "Manually verify a payment using Chapa's API",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -360,48 +354,41 @@
|
||||||
"tags": [
|
"tags": [
|
||||||
"Chapa"
|
"Chapa"
|
||||||
],
|
],
|
||||||
"summary": "Deposit money into user wallet using Chapa",
|
"summary": "Verify a payment manually",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Deposit request payload",
|
"type": "string",
|
||||||
"name": "payload",
|
"description": "Transaction Reference",
|
||||||
"in": "body",
|
"name": "tx_ref",
|
||||||
"required": true,
|
"in": "path",
|
||||||
"schema": {
|
"required": true
|
||||||
"$ref": "#/definitions/domain.ChapaDepositRequest"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.ChapaPaymentUrlResponseWrapper"
|
"$ref": "#/definitions/domain.ChapaVerificationResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.Response"
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal server error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.Response"
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/chapa/payments/verify": {
|
"/api/v1/chapa/payments/webhook/verify": {
|
||||||
"post": {
|
"post": {
|
||||||
|
"description": "Handles payment notifications from Chapa",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -411,93 +398,36 @@
|
||||||
"tags": [
|
"tags": [
|
||||||
"Chapa"
|
"Chapa"
|
||||||
],
|
],
|
||||||
"summary": "Verifies Chapa webhook transaction",
|
"summary": "Chapa payment webhook callback (used by Chapa)",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Webhook Payload",
|
"description": "Webhook payload",
|
||||||
"name": "payload",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ChapaTransactionType"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/chapa/payments/withdraw": {
|
|
||||||
"post": {
|
|
||||||
"description": "Initiates a withdrawal transaction using Chapa for the authenticated user.",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Chapa"
|
|
||||||
],
|
|
||||||
"summary": "Withdraw using Chapa",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "Chapa Withdraw Request",
|
|
||||||
"name": "request",
|
"name": "request",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.ChapaWithdrawRequest"
|
"$ref": "#/definitions/domain.ChapaWebhookPayload"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Withdrawal requested successfully",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"additionalProperties": true
|
||||||
"data": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Invalid request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.Response"
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Unprocessable Entity",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.Response"
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -906,6 +836,38 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/banks": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get list of banks supported by Chapa",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Chapa"
|
||||||
|
],
|
||||||
|
"summary": "Get supported banks",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.Bank"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/bet": {
|
"/bet": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Gets all the bets",
|
"description": "Gets all the bets",
|
||||||
|
|
@ -4381,6 +4343,55 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.Bank": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"acct_length": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"country_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"currency": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"is_24hrs": {
|
||||||
|
"description": "nullable",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"is_mobilemoney": {
|
||||||
|
"description": "nullable",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"is_rtgs": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"swift": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.BetOutcome": {
|
"domain.BetOutcome": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -4513,152 +4524,62 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.ChapaDepositRequest": {
|
"domain.ChapaDepositRequestPayload": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"amount"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"amount": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.ChapaDepositResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"checkoutURL": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.ChapaVerificationResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"amount": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"currency": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tx_ref": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.ChapaWebhookPayload": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"amount": {
|
"amount": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"branch_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"currency": {
|
"currency": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"phone_number": {
|
"status": {
|
||||||
|
"$ref": "#/definitions/domain.PaymentStatus"
|
||||||
|
},
|
||||||
|
"tx_ref": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.ChapaPaymentUrlResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"payment_url": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.ChapaPaymentUrlResponseWrapper": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {},
|
|
||||||
"message": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"status_code": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.ChapaSupportedBank": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"acct_length": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"acct_number_regex": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"active": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"country_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"currency": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"example_value": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"is_24hrs": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"is_active": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"is_mobilemoney": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"is_rtgs": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"slug": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"swift": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.ChapaSupportedBanksResponseWrapper": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {},
|
|
||||||
"message": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"status_code": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.ChapaTransactionType": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.ChapaWithdrawRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"account_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"account_number": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"amount": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"bank_code": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"beneficiary_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"branch_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"currency": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"wallet_id": {
|
|
||||||
"description": "add this",
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.CreateBetOutcomeReq": {
|
"domain.CreateBetOutcomeReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -4812,6 +4733,19 @@
|
||||||
"BANK"
|
"BANK"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"domain.PaymentStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"pending",
|
||||||
|
"completed",
|
||||||
|
"failed"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"PaymentStatusPending",
|
||||||
|
"PaymentStatusCompleted",
|
||||||
|
"PaymentStatusFailed"
|
||||||
|
]
|
||||||
|
},
|
||||||
"domain.PopOKCallback": {
|
"domain.PopOKCallback": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -4952,21 +4886,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.Response": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {},
|
|
||||||
"message": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"status_code": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.Role": {
|
"domain.Role": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -5062,7 +4981,7 @@
|
||||||
},
|
},
|
||||||
"awayTeamID": {
|
"awayTeamID": {
|
||||||
"description": "Away team ID (can be empty/null)",
|
"description": "Away team ID (can be empty/null)",
|
||||||
"type": "string"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"homeKitImage": {
|
"homeKitImage": {
|
||||||
"description": "Kit or image for home team (optional)",
|
"description": "Kit or image for home team (optional)",
|
||||||
|
|
@ -5074,7 +4993,7 @@
|
||||||
},
|
},
|
||||||
"homeTeamID": {
|
"homeTeamID": {
|
||||||
"description": "Home team ID",
|
"description": "Home team ID",
|
||||||
"type": "string"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"description": "Event ID",
|
"description": "Event ID",
|
||||||
|
|
@ -5086,7 +5005,7 @@
|
||||||
},
|
},
|
||||||
"leagueID": {
|
"leagueID": {
|
||||||
"description": "League ID",
|
"description": "League ID",
|
||||||
"type": "string"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"leagueName": {
|
"leagueName": {
|
||||||
"description": "League name",
|
"description": "League name",
|
||||||
|
|
@ -5102,7 +5021,7 @@
|
||||||
},
|
},
|
||||||
"sportID": {
|
"sportID": {
|
||||||
"description": "Sport ID",
|
"description": "Sport ID",
|
||||||
"type": "string"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"startTime": {
|
"startTime": {
|
||||||
"description": "Converted from \"time\" field in UNIX format",
|
"description": "Converted from \"time\" field in UNIX format",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,39 @@ definitions:
|
||||||
user_id:
|
user_id:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
domain.Bank:
|
||||||
|
properties:
|
||||||
|
acct_length:
|
||||||
|
type: integer
|
||||||
|
active:
|
||||||
|
type: integer
|
||||||
|
country_id:
|
||||||
|
type: integer
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
currency:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
is_24hrs:
|
||||||
|
description: nullable
|
||||||
|
type: integer
|
||||||
|
is_active:
|
||||||
|
type: integer
|
||||||
|
is_mobilemoney:
|
||||||
|
description: nullable
|
||||||
|
type: integer
|
||||||
|
is_rtgs:
|
||||||
|
type: integer
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
slug:
|
||||||
|
type: string
|
||||||
|
swift:
|
||||||
|
type: string
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
domain.BetOutcome:
|
domain.BetOutcome:
|
||||||
properties:
|
properties:
|
||||||
away_team_name:
|
away_team_name:
|
||||||
|
|
@ -124,102 +157,42 @@ definitions:
|
||||||
example: 2
|
example: 2
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
domain.ChapaDepositRequest:
|
domain.ChapaDepositRequestPayload:
|
||||||
|
properties:
|
||||||
|
amount:
|
||||||
|
type: number
|
||||||
|
required:
|
||||||
|
- amount
|
||||||
|
type: object
|
||||||
|
domain.ChapaDepositResponse:
|
||||||
|
properties:
|
||||||
|
checkoutURL:
|
||||||
|
type: string
|
||||||
|
reference:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
domain.ChapaVerificationResponse:
|
||||||
|
properties:
|
||||||
|
amount:
|
||||||
|
type: number
|
||||||
|
currency:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
tx_ref:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
domain.ChapaWebhookPayload:
|
||||||
properties:
|
properties:
|
||||||
amount:
|
amount:
|
||||||
type: integer
|
type: integer
|
||||||
branch_id:
|
|
||||||
type: integer
|
|
||||||
currency:
|
currency:
|
||||||
type: string
|
type: string
|
||||||
phone_number:
|
status:
|
||||||
|
$ref: '#/definitions/domain.PaymentStatus'
|
||||||
|
tx_ref:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
domain.ChapaPaymentUrlResponse:
|
|
||||||
properties:
|
|
||||||
payment_url:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
domain.ChapaPaymentUrlResponseWrapper:
|
|
||||||
properties:
|
|
||||||
data: {}
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
status_code:
|
|
||||||
type: integer
|
|
||||||
success:
|
|
||||||
type: boolean
|
|
||||||
type: object
|
|
||||||
domain.ChapaSupportedBank:
|
|
||||||
properties:
|
|
||||||
acct_length:
|
|
||||||
type: integer
|
|
||||||
acct_number_regex:
|
|
||||||
type: string
|
|
||||||
active:
|
|
||||||
type: integer
|
|
||||||
country_id:
|
|
||||||
type: integer
|
|
||||||
created_at:
|
|
||||||
type: string
|
|
||||||
currency:
|
|
||||||
type: string
|
|
||||||
example_value:
|
|
||||||
type: string
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
is_24hrs:
|
|
||||||
type: integer
|
|
||||||
is_active:
|
|
||||||
type: integer
|
|
||||||
is_mobilemoney:
|
|
||||||
type: integer
|
|
||||||
is_rtgs:
|
|
||||||
type: integer
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
slug:
|
|
||||||
type: string
|
|
||||||
swift:
|
|
||||||
type: string
|
|
||||||
updated_at:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
domain.ChapaSupportedBanksResponseWrapper:
|
|
||||||
properties:
|
|
||||||
data: {}
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
status_code:
|
|
||||||
type: integer
|
|
||||||
success:
|
|
||||||
type: boolean
|
|
||||||
type: object
|
|
||||||
domain.ChapaTransactionType:
|
|
||||||
properties:
|
|
||||||
type:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
domain.ChapaWithdrawRequest:
|
|
||||||
properties:
|
|
||||||
account_name:
|
|
||||||
type: string
|
|
||||||
account_number:
|
|
||||||
type: string
|
|
||||||
amount:
|
|
||||||
type: integer
|
|
||||||
bank_code:
|
|
||||||
type: string
|
|
||||||
beneficiary_name:
|
|
||||||
type: string
|
|
||||||
branch_id:
|
|
||||||
type: integer
|
|
||||||
currency:
|
|
||||||
type: string
|
|
||||||
wallet_id:
|
|
||||||
description: add this
|
|
||||||
type: integer
|
|
||||||
type: object
|
|
||||||
domain.CreateBetOutcomeReq:
|
domain.CreateBetOutcomeReq:
|
||||||
properties:
|
properties:
|
||||||
event_id:
|
event_id:
|
||||||
|
|
@ -328,6 +301,16 @@ definitions:
|
||||||
- TELEBIRR_TRANSACTION
|
- TELEBIRR_TRANSACTION
|
||||||
- ARIFPAY_TRANSACTION
|
- ARIFPAY_TRANSACTION
|
||||||
- BANK
|
- BANK
|
||||||
|
domain.PaymentStatus:
|
||||||
|
enum:
|
||||||
|
- pending
|
||||||
|
- completed
|
||||||
|
- failed
|
||||||
|
type: string
|
||||||
|
x-enum-varnames:
|
||||||
|
- PaymentStatusPending
|
||||||
|
- PaymentStatusCompleted
|
||||||
|
- PaymentStatusFailed
|
||||||
domain.PopOKCallback:
|
domain.PopOKCallback:
|
||||||
properties:
|
properties:
|
||||||
amount:
|
amount:
|
||||||
|
|
@ -421,16 +404,6 @@ definitions:
|
||||||
totalRewardEarned:
|
totalRewardEarned:
|
||||||
type: number
|
type: number
|
||||||
type: object
|
type: object
|
||||||
domain.Response:
|
|
||||||
properties:
|
|
||||||
data: {}
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
status_code:
|
|
||||||
type: integer
|
|
||||||
success:
|
|
||||||
type: boolean
|
|
||||||
type: object
|
|
||||||
domain.Role:
|
domain.Role:
|
||||||
enum:
|
enum:
|
||||||
- super_admin
|
- super_admin
|
||||||
|
|
@ -501,7 +474,7 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
awayTeamID:
|
awayTeamID:
|
||||||
description: Away team ID (can be empty/null)
|
description: Away team ID (can be empty/null)
|
||||||
type: string
|
type: integer
|
||||||
homeKitImage:
|
homeKitImage:
|
||||||
description: Kit or image for home team (optional)
|
description: Kit or image for home team (optional)
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -510,7 +483,7 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
homeTeamID:
|
homeTeamID:
|
||||||
description: Home team ID
|
description: Home team ID
|
||||||
type: string
|
type: integer
|
||||||
id:
|
id:
|
||||||
description: Event ID
|
description: Event ID
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -519,7 +492,7 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
leagueID:
|
leagueID:
|
||||||
description: League ID
|
description: League ID
|
||||||
type: string
|
type: integer
|
||||||
leagueName:
|
leagueName:
|
||||||
description: League name
|
description: League name
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -531,7 +504,7 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
sportID:
|
sportID:
|
||||||
description: Sport ID
|
description: Sport ID
|
||||||
type: string
|
type: integer
|
||||||
startTime:
|
startTime:
|
||||||
description: Converted from "time" field in UNIX format
|
description: Converted from "time" field in UNIX format
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -1673,137 +1646,94 @@ paths:
|
||||||
summary: Launch an Alea Play virtual game
|
summary: Launch an Alea Play virtual game
|
||||||
tags:
|
tags:
|
||||||
- Alea Virtual Games
|
- Alea Virtual Games
|
||||||
/api/v1/chapa/banks:
|
|
||||||
get:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ChapaSupportedBanksResponseWrapper'
|
|
||||||
"400":
|
|
||||||
description: Bad Request
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"401":
|
|
||||||
description: Unauthorized
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"404":
|
|
||||||
description: Not Found
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"422":
|
|
||||||
description: Unprocessable Entity
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
summary: fetches chapa supported banks
|
|
||||||
tags:
|
|
||||||
- Chapa
|
|
||||||
/api/v1/chapa/payments/deposit:
|
/api/v1/chapa/payments/deposit:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Deposits money into user wallet from user account using Chapa
|
description: Starts a new deposit process using Chapa payment gateway
|
||||||
parameters:
|
parameters:
|
||||||
- description: Deposit request payload
|
- description: Deposit request
|
||||||
in: body
|
|
||||||
name: payload
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ChapaDepositRequest'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ChapaPaymentUrlResponseWrapper'
|
|
||||||
"400":
|
|
||||||
description: Invalid request
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"422":
|
|
||||||
description: Validation error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"500":
|
|
||||||
description: Internal server error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
summary: Deposit money into user wallet using Chapa
|
|
||||||
tags:
|
|
||||||
- Chapa
|
|
||||||
/api/v1/chapa/payments/verify:
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
parameters:
|
|
||||||
- description: Webhook Payload
|
|
||||||
in: body
|
|
||||||
name: payload
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ChapaTransactionType'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
summary: Verifies Chapa webhook transaction
|
|
||||||
tags:
|
|
||||||
- Chapa
|
|
||||||
/api/v1/chapa/payments/withdraw:
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Initiates a withdrawal transaction using Chapa for the authenticated
|
|
||||||
user.
|
|
||||||
parameters:
|
|
||||||
- description: Chapa Withdraw Request
|
|
||||||
in: body
|
in: body
|
||||||
name: request
|
name: request
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.ChapaWithdrawRequest'
|
$ref: '#/definitions/domain.ChapaDepositRequestPayload'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Withdrawal requested successfully
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
allOf:
|
$ref: '#/definitions/domain.ChapaDepositResponse'
|
||||||
- $ref: '#/definitions/domain.Response'
|
|
||||||
- properties:
|
|
||||||
data:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
"400":
|
"400":
|
||||||
description: Invalid request
|
description: Bad Request
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.Response'
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
"401":
|
|
||||||
description: Unauthorized
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"422":
|
|
||||||
description: Unprocessable Entity
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"500":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.Response'
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
summary: Withdraw using Chapa
|
summary: Initiate a deposit
|
||||||
|
tags:
|
||||||
|
- Chapa
|
||||||
|
/api/v1/chapa/payments/manual/verify/{tx_ref}:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Manually verify a payment using Chapa's API
|
||||||
|
parameters:
|
||||||
|
- description: Transaction Reference
|
||||||
|
in: path
|
||||||
|
name: tx_ref
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ChapaVerificationResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: Verify a payment manually
|
||||||
|
tags:
|
||||||
|
- Chapa
|
||||||
|
/api/v1/chapa/payments/webhook/verify:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Handles payment notifications from Chapa
|
||||||
|
parameters:
|
||||||
|
- description: Webhook payload
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ChapaWebhookPayload'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
additionalProperties: true
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: Chapa payment webhook callback (used by Chapa)
|
||||||
tags:
|
tags:
|
||||||
- Chapa
|
- Chapa
|
||||||
/api/v1/reports/dashboard:
|
/api/v1/reports/dashboard:
|
||||||
|
|
@ -2067,6 +1997,27 @@ paths:
|
||||||
summary: Refresh token
|
summary: Refresh token
|
||||||
tags:
|
tags:
|
||||||
- auth
|
- auth
|
||||||
|
/banks:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Get list of banks supported by Chapa
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/domain.Bank'
|
||||||
|
type: array
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: Get supported banks
|
||||||
|
tags:
|
||||||
|
- Chapa
|
||||||
/bet:
|
/bet:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
||||||
|
|
@ -483,6 +483,7 @@ type WalletTransfer struct {
|
||||||
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
|
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
|
||||||
CashierID pgtype.Int8 `json:"cashier_id"`
|
CashierID pgtype.Int8 `json:"cashier_id"`
|
||||||
Verified bool `json:"verified"`
|
Verified bool `json:"verified"`
|
||||||
|
ReferenceNumber string `json:"reference_number"`
|
||||||
PaymentMethod string `json:"payment_method"`
|
PaymentMethod string `json:"payment_method"`
|
||||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
CreatedAt pgtype.Timestamp `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,40 @@ func (q *Queries) GetNotification(ctx context.Context, id string) (Notification,
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetNotificationCounts = `-- name: GetNotificationCounts :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total,
|
||||||
|
COUNT(CASE WHEN is_read = true THEN 1 END) as read,
|
||||||
|
COUNT(CASE WHEN is_read = false THEN 1 END) as unread
|
||||||
|
FROM notifications
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetNotificationCountsRow struct {
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Read int64 `json:"read"`
|
||||||
|
Unread int64 `json:"unread"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetNotificationCounts(ctx context.Context) ([]GetNotificationCountsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetNotificationCounts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetNotificationCountsRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetNotificationCountsRow
|
||||||
|
if err := rows.Scan(&i.Total, &i.Read, &i.Unread); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const ListFailedNotifications = `-- name: ListFailedNotifications :many
|
const ListFailedNotifications = `-- name: ListFailedNotifications :many
|
||||||
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
|
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
|
||||||
FROM notifications
|
FROM notifications
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,11 @@ INSERT INTO wallet_transfer (
|
||||||
sender_wallet_id,
|
sender_wallet_id,
|
||||||
cashier_id,
|
cashier_id,
|
||||||
verified,
|
verified,
|
||||||
|
reference_number,
|
||||||
payment_method
|
payment_method
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
RETURNING id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method, created_at, updated_at
|
RETURNING id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateTransferParams struct {
|
type CreateTransferParams struct {
|
||||||
|
|
@ -32,6 +33,7 @@ type CreateTransferParams struct {
|
||||||
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
|
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
|
||||||
CashierID pgtype.Int8 `json:"cashier_id"`
|
CashierID pgtype.Int8 `json:"cashier_id"`
|
||||||
Verified bool `json:"verified"`
|
Verified bool `json:"verified"`
|
||||||
|
ReferenceNumber string `json:"reference_number"`
|
||||||
PaymentMethod string `json:"payment_method"`
|
PaymentMethod string `json:"payment_method"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,6 +45,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
|
||||||
arg.SenderWalletID,
|
arg.SenderWalletID,
|
||||||
arg.CashierID,
|
arg.CashierID,
|
||||||
arg.Verified,
|
arg.Verified,
|
||||||
|
arg.ReferenceNumber,
|
||||||
arg.PaymentMethod,
|
arg.PaymentMethod,
|
||||||
)
|
)
|
||||||
var i WalletTransfer
|
var i WalletTransfer
|
||||||
|
|
@ -54,6 +57,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
|
||||||
&i.SenderWalletID,
|
&i.SenderWalletID,
|
||||||
&i.CashierID,
|
&i.CashierID,
|
||||||
&i.Verified,
|
&i.Verified,
|
||||||
|
&i.ReferenceNumber,
|
||||||
&i.PaymentMethod,
|
&i.PaymentMethod,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
|
@ -62,7 +66,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetAllTransfers = `-- name: GetAllTransfers :many
|
const GetAllTransfers = `-- name: GetAllTransfers :many
|
||||||
SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method, created_at, updated_at
|
SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at
|
||||||
FROM wallet_transfer
|
FROM wallet_transfer
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -83,6 +87,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error)
|
||||||
&i.SenderWalletID,
|
&i.SenderWalletID,
|
||||||
&i.CashierID,
|
&i.CashierID,
|
||||||
&i.Verified,
|
&i.Verified,
|
||||||
|
&i.ReferenceNumber,
|
||||||
&i.PaymentMethod,
|
&i.PaymentMethod,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
|
@ -98,7 +103,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetTransferByID = `-- name: GetTransferByID :one
|
const GetTransferByID = `-- name: GetTransferByID :one
|
||||||
SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method, created_at, updated_at
|
SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at
|
||||||
FROM wallet_transfer
|
FROM wallet_transfer
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -114,6 +119,32 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer
|
||||||
&i.SenderWalletID,
|
&i.SenderWalletID,
|
||||||
&i.CashierID,
|
&i.CashierID,
|
||||||
&i.Verified,
|
&i.Verified,
|
||||||
|
&i.ReferenceNumber,
|
||||||
|
&i.PaymentMethod,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetTransferByReference = `-- name: GetTransferByReference :one
|
||||||
|
SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at
|
||||||
|
FROM wallet_transfer
|
||||||
|
WHERE reference_number = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber string) (WalletTransfer, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetTransferByReference, referenceNumber)
|
||||||
|
var i WalletTransfer
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Amount,
|
||||||
|
&i.Type,
|
||||||
|
&i.ReceiverWalletID,
|
||||||
|
&i.SenderWalletID,
|
||||||
|
&i.CashierID,
|
||||||
|
&i.Verified,
|
||||||
|
&i.ReferenceNumber,
|
||||||
&i.PaymentMethod,
|
&i.PaymentMethod,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
|
@ -122,7 +153,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetTransfersByWallet = `-- name: GetTransfersByWallet :many
|
const GetTransfersByWallet = `-- name: GetTransfersByWallet :many
|
||||||
SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method, created_at, updated_at
|
SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at
|
||||||
FROM wallet_transfer
|
FROM wallet_transfer
|
||||||
WHERE receiver_wallet_id = $1
|
WHERE receiver_wallet_id = $1
|
||||||
OR sender_wallet_id = $1
|
OR sender_wallet_id = $1
|
||||||
|
|
@ -145,6 +176,7 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID int
|
||||||
&i.SenderWalletID,
|
&i.SenderWalletID,
|
||||||
&i.CashierID,
|
&i.CashierID,
|
||||||
&i.Verified,
|
&i.Verified,
|
||||||
|
&i.ReferenceNumber,
|
||||||
&i.PaymentMethod,
|
&i.PaymentMethod,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
|
|
||||||
7
go.mod
7
go.mod
|
|
@ -13,8 +13,6 @@ require (
|
||||||
github.com/jackc/pgx/v5 v5.7.4
|
github.com/jackc/pgx/v5 v5.7.4
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/shopspring/decimal v1.4.0
|
|
||||||
github.com/stretchr/testify v1.10.0
|
|
||||||
// github.com/stretchr/testify v1.10.0
|
// github.com/stretchr/testify v1.10.0
|
||||||
github.com/swaggo/fiber-swagger v1.3.0
|
github.com/swaggo/fiber-swagger v1.3.0
|
||||||
github.com/swaggo/swag v1.16.4
|
github.com/swaggo/swag v1.16.4
|
||||||
|
|
@ -30,7 +28,6 @@ require (
|
||||||
// github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
// github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
|
|
@ -49,16 +46,14 @@ require (
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
|
||||||
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
|
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
go.mongodb.org/mongo-driver v1.17.3
|
go.mongodb.org/mongo-driver v1.17.3
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||||
golang.org/x/net v0.38.0 // direct
|
golang.org/x/net v0.38.0 // indirect
|
||||||
golang.org/x/sync v0.12.0 // indirect
|
golang.org/x/sync v0.12.0 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -118,16 +118,12 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
|
||||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import "time"
|
||||||
"errors"
|
|
||||||
"time"
|
type PaymentStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PaymentStatusPending PaymentStatus = "pending"
|
||||||
|
PaymentStatusCompleted PaymentStatus = "completed"
|
||||||
|
PaymentStatusFailed PaymentStatus = "failed"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
type ChapaDepositRequest struct {
|
||||||
ChapaSecret string
|
|
||||||
ChapaBaseURL string
|
|
||||||
)
|
|
||||||
|
|
||||||
type InitPaymentRequest struct {
|
|
||||||
Amount Currency `json:"amount"`
|
Amount Currency `json:"amount"`
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
|
@ -21,208 +21,73 @@ type InitPaymentRequest struct {
|
||||||
ReturnURL string `json:"return_url"`
|
ReturnURL string `json:"return_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransferRequest struct {
|
type ChapaDepositRequestPayload struct {
|
||||||
AccountNumber string `json:"account_number"`
|
Amount float64 `json:"amount" validate:"required,gt=0"`
|
||||||
BankCode string `json:"bank_code"`
|
|
||||||
Amount string `json:"amount"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
Reference string `json:"reference"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
RecipientName string `json:"recipient_name"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChapaSupportedBank struct {
|
type ChapaWebhookPayload struct {
|
||||||
Id int64 `json:"id"`
|
TxRef string `json:"tx_ref"`
|
||||||
|
Amount Currency `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Status PaymentStatus `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentResponse contains the response from payment initialization
|
||||||
|
type ChapaDepositResponse struct {
|
||||||
|
CheckoutURL string
|
||||||
|
Reference string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaymentVerification contains payment verification details
|
||||||
|
type ChapaDepositVerification struct {
|
||||||
|
Status PaymentStatus
|
||||||
|
Amount Currency
|
||||||
|
Currency string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChapaVerificationResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
TxRef string `json:"tx_ref"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bank struct {
|
||||||
|
ID int `json:"id"`
|
||||||
Slug string `json:"slug"`
|
Slug string `json:"slug"`
|
||||||
Swift string `json:"swift"`
|
Swift string `json:"swift"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
AcctLength int `json:"acct_length"`
|
AcctLength int `json:"acct_length"`
|
||||||
AcctNumberRegex string `json:"acct_number_regex"`
|
CountryID int `json:"country_id"`
|
||||||
ExampleValue string `json:"example_value"`
|
IsMobileMoney int `json:"is_mobilemoney"` // nullable
|
||||||
CountryId int `json:"country_id"`
|
|
||||||
IsMobilemoney *int `json:"is_mobilemoney"`
|
|
||||||
|
|
||||||
IsActive int `json:"is_active"`
|
IsActive int `json:"is_active"`
|
||||||
IsRtgs *int `json:"is_rtgs"`
|
IsRTGS int `json:"is_rtgs"`
|
||||||
Active int `json:"active"`
|
Active int `json:"active"`
|
||||||
Is24Hrs *int `json:"is_24hrs"`
|
Is24Hrs int `json:"is_24hrs"` // nullable
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChapaSupportedBanksResponse struct {
|
type BankResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Data []ChapaSupportedBank `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitPaymentData struct {
|
|
||||||
TxRef string `json:"tx_ref"`
|
|
||||||
CheckoutURL string `json:"checkout_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitPaymentResponse struct {
|
|
||||||
Status string `json:"status"` // "success"
|
|
||||||
Message string `json:"message"` // e.g., "Payment initialized"
|
|
||||||
Data InitPaymentData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebhookPayload map[string]interface{}
|
|
||||||
|
|
||||||
type TransactionData struct {
|
|
||||||
TxRef string `json:"tx_ref"`
|
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Amount string `json:"amount"`
|
Data []BankData `json:"data"`
|
||||||
Currency string `json:"currency"`
|
|
||||||
CustomerEmail string `json:"email"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type VerifyTransactionResponse struct {
|
type BankData struct {
|
||||||
Status string `json:"status"`
|
ID int `json:"id"`
|
||||||
Message string `json:"message"`
|
Slug string `json:"slug"`
|
||||||
Data TransactionData `json:"data"`
|
Swift string `json:"swift"`
|
||||||
}
|
Name string `json:"name"`
|
||||||
|
AcctLength int `json:"acct_length"`
|
||||||
type TransferData struct {
|
CountryID int `json:"country_id"`
|
||||||
Reference string `json:"reference"`
|
IsMobileMoney int `json:"is_mobilemoney"` // nullable
|
||||||
Status string `json:"status"`
|
IsActive int `json:"is_active"`
|
||||||
Amount string `json:"amount"`
|
IsRTGS int `json:"is_rtgs"`
|
||||||
Currency string `json:"currency"`
|
Active int `json:"active"`
|
||||||
}
|
Is24Hrs int `json:"is_24hrs"` // nullable
|
||||||
|
|
||||||
type CreateTransferResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data TransferData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TransferVerificationData struct {
|
|
||||||
Reference string `json:"reference"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
BankCode string `json:"bank_code"`
|
|
||||||
AccountName string `json:"account_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type VerifyTransferResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data TransferVerificationData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChapaTransactionType struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChapaWebHookTransfer struct {
|
|
||||||
AccountName string `json:"account_name"`
|
|
||||||
AccountNumber string `json:"account_number"`
|
|
||||||
BankId string `json:"bank_id"`
|
|
||||||
BankName string `json:"bank_name"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
Amount string `json:"amount"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Reference string `json:"reference"`
|
|
||||||
TxRef string `json:"tx_ref"`
|
|
||||||
ChapaReference string `json:"chapa_reference"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChapaWebHookPayment struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
FirstName string `json:"first_name"`
|
|
||||||
LastName string `json:"last_name"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
Mobile interface{} `json:"mobile"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
Amount string `json:"amount"`
|
|
||||||
Charge string `json:"charge"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Mode string `json:"mode"`
|
|
||||||
Reference string `json:"reference"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Type string `json:"type"`
|
|
||||||
TxRef string `json:"tx_ref"`
|
|
||||||
PaymentMethod string `json:"payment_method"`
|
|
||||||
Customization struct {
|
|
||||||
Title interface{} `json:"title"`
|
|
||||||
Description interface{} `json:"description"`
|
|
||||||
Logo interface{} `json:"logo"`
|
|
||||||
} `json:"customization"`
|
|
||||||
Meta string `json:"meta"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChapaWithdrawRequest struct {
|
|
||||||
WalletID int64 `json:"wallet_id"` // add this
|
|
||||||
AccountName string `json:"account_name"`
|
|
||||||
AccountNumber string `json:"account_number"`
|
|
||||||
Amount int64 `json:"amount"`
|
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
BeneficiaryName string `json:"beneficiary_name"`
|
|
||||||
BankCode string `json:"bank_code"`
|
|
||||||
BranchID int64 `json:"branch_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChapaTransferPayload struct {
|
|
||||||
AccountName string
|
|
||||||
AccountNumber string
|
|
||||||
Amount string
|
|
||||||
Currency string
|
|
||||||
BeneficiaryName string
|
|
||||||
TxRef string
|
|
||||||
Reference string
|
|
||||||
BankCode string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChapaDepositRequest struct {
|
|
||||||
Amount Currency `json:"amount"`
|
|
||||||
PhoneNumber string `json:"phone_number"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
BranchID int64 `json:"branch_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ChapaDepositRequest) Validate() error {
|
|
||||||
if r.Amount <= 0 {
|
|
||||||
return errors.New("amount must be greater than zero")
|
|
||||||
}
|
|
||||||
if r.Currency == "" {
|
|
||||||
return errors.New("currency is required")
|
|
||||||
}
|
|
||||||
if r.PhoneNumber == "" {
|
|
||||||
return errors.New("phone number is required")
|
|
||||||
}
|
|
||||||
// if r.BranchID == 0 {
|
|
||||||
// return errors.New("branch ID is required")
|
|
||||||
// }
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type AcceptChapaPaymentRequest struct {
|
|
||||||
Amount string `json:"amount"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
FirstName string `json:"first_name"`
|
|
||||||
LastName string `json:"last_name"`
|
|
||||||
PhoneNumber string `json:"phone_number"`
|
|
||||||
TxRef string `json:"tx_ref"`
|
|
||||||
CallbackUrl string `json:"callback_url"`
|
|
||||||
ReturnUrl string `json:"return_url"`
|
|
||||||
CustomizationTitle string `json:"customization[title]"`
|
|
||||||
CustomizationDescription string `json:"customization[description]"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChapaPaymentUrlResponse struct {
|
|
||||||
PaymentURL string `json:"payment_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChapaPaymentUrlResponseWrapper struct {
|
|
||||||
Data ChapaPaymentUrlResponse `json:"data"`
|
|
||||||
Response
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChapaSupportedBanksResponseWrapper struct {
|
|
||||||
Data []ChapaSupportedBank `json:"data"`
|
|
||||||
Response
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,12 @@ package domain
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var MongoDBLogger *zap.Logger
|
||||||
|
|
||||||
type ValidInt64 struct {
|
type ValidInt64 struct {
|
||||||
Value int64
|
Value int64
|
||||||
Valid bool
|
Valid bool
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ const (
|
||||||
|
|
||||||
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
|
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
|
||||||
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
||||||
|
NotificationRecieverSideCashier NotificationRecieverSide = "cashier"
|
||||||
|
NotificationRecieverSideBranchManager NotificationRecieverSide = "branch_manager"
|
||||||
|
|
||||||
NotificationDeliverySchemeBulk NotificationDeliveryScheme = "bulk"
|
NotificationDeliverySchemeBulk NotificationDeliveryScheme = "bulk"
|
||||||
NotificationDeliverySchemeSingle NotificationDeliveryScheme = "single"
|
NotificationDeliverySchemeSingle NotificationDeliveryScheme = "single"
|
||||||
|
|
@ -55,9 +57,9 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type NotificationPayload struct {
|
type NotificationPayload struct {
|
||||||
Headline string
|
Headline string `json:"headline"`
|
||||||
Message string
|
Message string `json:"message"`
|
||||||
Tags []string
|
Tags []string `json:"tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,117 @@ package domain
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
type DashboardSummary struct {
|
||||||
|
TotalStakes Currency `json:"total_stakes"`
|
||||||
|
TotalBets int64 `json:"total_bets"`
|
||||||
|
ActiveBets int64 `json:"active_bets"`
|
||||||
|
WinBalance Currency `json:"win_balance"`
|
||||||
|
TotalWins int64 `json:"total_wins"`
|
||||||
|
TotalLosses int64 `json:"total_losses"`
|
||||||
|
CustomerCount int64 `json:"customer_count"`
|
||||||
|
Profit Currency `json:"profit"`
|
||||||
|
WinRate float64 `json:"win_rate"`
|
||||||
|
AverageStake Currency `json:"average_stake"`
|
||||||
|
TotalDeposits Currency `json:"total_deposits"`
|
||||||
|
TotalWithdrawals Currency `json:"total_withdrawals"`
|
||||||
|
ActiveCustomers int64 `json:"active_customers"`
|
||||||
|
BranchesCount int64 `json:"branches_count"`
|
||||||
|
ActiveBranches int64 `json:"active_branches"`
|
||||||
|
|
||||||
|
TotalCashiers int64 `json:"total_cashiers"`
|
||||||
|
ActiveCashiers int64 `json:"active_cashiers"`
|
||||||
|
InactiveCashiers int64 `json:"inactive_cashiers"`
|
||||||
|
|
||||||
|
TotalWallets int64 `json:"total_wallets"`
|
||||||
|
TotalGames int64 `json:"total_games"`
|
||||||
|
ActiveGames int64 `json:"active_games"`
|
||||||
|
InactiveGames int64 `json:"inactive_games"`
|
||||||
|
|
||||||
|
TotalManagers int64 `json:"total_managers"`
|
||||||
|
ActiveManagers int64 `json:"active_managers"`
|
||||||
|
InactiveManagers int64 `json:"inactive_managers"`
|
||||||
|
InactiveBranches int64 `json:"inactive_branches"`
|
||||||
|
|
||||||
|
TotalAdmins int64 `json:"total_admins"`
|
||||||
|
ActiveAdmins int64 `json:"active_admins"`
|
||||||
|
InactiveAdmins int64 `json:"inactive_admins"`
|
||||||
|
|
||||||
|
TotalCompanies int64 `json:"total_companies"`
|
||||||
|
ActiveCompanies int64 `json:"active_companies"`
|
||||||
|
InactiveCompanies int64 `json:"inactive_companies"`
|
||||||
|
|
||||||
|
InactiveCustomers int64 `json:"inactive_customers"`
|
||||||
|
|
||||||
|
TotalNotifications int64 `json:"total_notifications"`
|
||||||
|
ReadNotifications int64 `json:"read_notifications"`
|
||||||
|
UnreadNotifications int64 `json:"unread_notifications"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomerActivity struct {
|
||||||
|
CustomerID int64 `json:"customer_id"`
|
||||||
|
CustomerName string `json:"customer_name"`
|
||||||
|
TotalBets int64 `json:"total_bets"`
|
||||||
|
TotalStakes Currency `json:"total_stakes"`
|
||||||
|
TotalWins int64 `json:"total_wins"`
|
||||||
|
TotalPayouts Currency `json:"total_payouts"`
|
||||||
|
Profit Currency `json:"profit"`
|
||||||
|
FirstBetDate time.Time `json:"first_bet_date"`
|
||||||
|
LastBetDate time.Time `json:"last_bet_date"`
|
||||||
|
FavoriteSport string `json:"favorite_sport"`
|
||||||
|
FavoriteMarket string `json:"favorite_market"`
|
||||||
|
AverageStake Currency `json:"average_stake"`
|
||||||
|
AverageOdds float64 `json:"average_odds"`
|
||||||
|
WinRate float64 `json:"win_rate"`
|
||||||
|
ActivityLevel string `json:"activity_level"` // High, Medium, Low
|
||||||
|
}
|
||||||
|
|
||||||
|
type BranchPerformance struct {
|
||||||
|
BranchID int64 `json:"branch_id"`
|
||||||
|
BranchName string `json:"branch_name"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
ManagerName string `json:"manager_name"`
|
||||||
|
TotalBets int64 `json:"total_bets"`
|
||||||
|
TotalStakes Currency `json:"total_stakes"`
|
||||||
|
TotalWins int64 `json:"total_wins"`
|
||||||
|
TotalPayouts Currency `json:"total_payouts"`
|
||||||
|
Profit Currency `json:"profit"`
|
||||||
|
CustomerCount int64 `json:"customer_count"`
|
||||||
|
Deposits Currency `json:"deposits"`
|
||||||
|
Withdrawals Currency `json:"withdrawals"`
|
||||||
|
WinRate float64 `json:"win_rate"`
|
||||||
|
AverageStake Currency `json:"average_stake"`
|
||||||
|
PerformanceScore float64 `json:"performance_score"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SportPerformance struct {
|
||||||
|
SportID string `json:"sport_id"`
|
||||||
|
SportName string `json:"sport_name"`
|
||||||
|
TotalBets int64 `json:"total_bets"`
|
||||||
|
TotalStakes Currency `json:"total_stakes"`
|
||||||
|
TotalWins int64 `json:"total_wins"`
|
||||||
|
TotalPayouts Currency `json:"total_payouts"`
|
||||||
|
Profit Currency `json:"profit"`
|
||||||
|
PopularityRank int `json:"popularity_rank"`
|
||||||
|
WinRate float64 `json:"win_rate"`
|
||||||
|
AverageStake Currency `json:"average_stake"`
|
||||||
|
AverageOdds float64 `json:"average_odds"`
|
||||||
|
MostPopularMarket string `json:"most_popular_market"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BetAnalysis struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
TotalBets int64 `json:"total_bets"`
|
||||||
|
TotalStakes Currency `json:"total_stakes"`
|
||||||
|
TotalWins int64 `json:"total_wins"`
|
||||||
|
TotalPayouts Currency `json:"total_payouts"`
|
||||||
|
Profit Currency `json:"profit"`
|
||||||
|
MostPopularSport string `json:"most_popular_sport"`
|
||||||
|
MostPopularMarket string `json:"most_popular_market"`
|
||||||
|
HighestStake Currency `json:"highest_stake"`
|
||||||
|
HighestPayout Currency `json:"highest_payout"`
|
||||||
|
AverageOdds float64 `json:"average_odds"`
|
||||||
|
}
|
||||||
|
|
||||||
type ValidOutcomeStatus struct {
|
type ValidOutcomeStatus struct {
|
||||||
Value OutcomeStatus
|
Value OutcomeStatus
|
||||||
Valid bool // Valid is true if Value is not NULL
|
Valid bool // Valid is true if Value is not NULL
|
||||||
|
|
@ -13,6 +124,7 @@ type ReportFilter struct {
|
||||||
EndTime ValidTime `json:"end_time"`
|
EndTime ValidTime `json:"end_time"`
|
||||||
CompanyID ValidInt64 `json:"company_id"`
|
CompanyID ValidInt64 `json:"company_id"`
|
||||||
BranchID ValidInt64 `json:"branch_id"`
|
BranchID ValidInt64 `json:"branch_id"`
|
||||||
|
RecipientID ValidInt64 `json:"recipient_id"`
|
||||||
UserID ValidInt64 `json:"user_id"`
|
UserID ValidInt64 `json:"user_id"`
|
||||||
SportID ValidString `json:"sport_id"`
|
SportID ValidString `json:"sport_id"`
|
||||||
Status ValidOutcomeStatus `json:"status"`
|
Status ValidOutcomeStatus `json:"status"`
|
||||||
|
|
@ -46,7 +158,6 @@ type CustomerBetActivity struct {
|
||||||
AverageOdds float64
|
AverageOdds float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// BranchBetActivity represents branch betting activity
|
// BranchBetActivity represents branch betting activity
|
||||||
type BranchBetActivity struct {
|
type BranchBetActivity struct {
|
||||||
BranchID int64
|
BranchID int64
|
||||||
|
|
@ -99,25 +210,92 @@ type CustomerPreferences struct {
|
||||||
FavoriteMarket string `json:"favorite_market"`
|
FavoriteMarket string `json:"favorite_market"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardSummary struct {
|
// type DashboardSummary struct {
|
||||||
TotalStakes Currency `json:"total_stakes"`
|
// TotalStakes Currency `json:"total_stakes"`
|
||||||
TotalBets int64 `json:"total_bets"`
|
// TotalBets int64 `json:"total_bets"`
|
||||||
ActiveBets int64 `json:"active_bets"`
|
// ActiveBets int64 `json:"active_bets"`
|
||||||
WinBalance Currency `json:"win_balance"`
|
// WinBalance Currency `json:"win_balance"`
|
||||||
TotalWins int64 `json:"total_wins"`
|
// TotalWins int64 `json:"total_wins"`
|
||||||
TotalLosses int64 `json:"total_losses"`
|
// TotalLosses int64 `json:"total_losses"`
|
||||||
CustomerCount int64 `json:"customer_count"`
|
// CustomerCount int64 `json:"customer_count"`
|
||||||
Profit Currency `json:"profit"`
|
// Profit Currency `json:"profit"`
|
||||||
WinRate float64 `json:"win_rate"`
|
// WinRate float64 `json:"win_rate"`
|
||||||
AverageStake Currency `json:"average_stake"`
|
// AverageStake Currency `json:"average_stake"`
|
||||||
TotalDeposits Currency `json:"total_deposits"`
|
// TotalDeposits Currency `json:"total_deposits"`
|
||||||
TotalWithdrawals Currency `json:"total_withdrawals"`
|
// TotalWithdrawals Currency `json:"total_withdrawals"`
|
||||||
ActiveCustomers int64 `json:"active_customers"`
|
// ActiveCustomers int64 `json:"active_customers"`
|
||||||
BranchesCount int64 `json:"branches_count"`
|
// BranchesCount int64 `json:"branches_count"`
|
||||||
ActiveBranches int64 `json:"active_branches"`
|
// ActiveBranches int64 `json:"active_branches"`
|
||||||
}
|
// }
|
||||||
|
|
||||||
type ErrorResponse struct {
|
type ErrorResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NotificationReport struct {
|
||||||
|
CountsByType []NotificationTypeCount `json:"counts_by_type"`
|
||||||
|
DeliveryStats NotificationDeliveryStats `json:"delivery_stats"`
|
||||||
|
ActiveRecipients []ActiveNotificationRecipient `json:"active_recipients"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationTypeCount struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Read int64 `json:"read"`
|
||||||
|
Unread int64 `json:"unread"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationDeliveryStats struct {
|
||||||
|
TotalSent int64 `json:"total_sent"`
|
||||||
|
FailedDeliveries int64 `json:"failed_deliveries"`
|
||||||
|
SuccessRate float64 `json:"success_rate"`
|
||||||
|
MostUsedChannel string `json:"most_used_channel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveNotificationRecipient struct {
|
||||||
|
RecipientID int64 `json:"recipient_id"`
|
||||||
|
RecipientName string `json:"recipient_name"`
|
||||||
|
NotificationCount int64 `json:"notification_count"`
|
||||||
|
LastNotificationTime time.Time `json:"last_notification_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompanyPerformance struct {
|
||||||
|
CompanyID int64 `json:"company_id"`
|
||||||
|
CompanyName string `json:"company_name"`
|
||||||
|
ContactEmail string `json:"contact_email"`
|
||||||
|
TotalBets int64 `json:"total_bets"`
|
||||||
|
TotalStakes Currency `json:"total_stakes"`
|
||||||
|
TotalWins int64 `json:"total_wins"`
|
||||||
|
TotalPayouts Currency `json:"total_payouts"`
|
||||||
|
Profit Currency `json:"profit"`
|
||||||
|
WinRate float64 `json:"win_rate"`
|
||||||
|
AverageStake Currency `json:"average_stake"`
|
||||||
|
TotalBranches int64 `json:"total_branches"`
|
||||||
|
ActiveBranches int64 `json:"active_branches"`
|
||||||
|
TotalCashiers int64 `json:"total_cashiers"`
|
||||||
|
ActiveCashiers int64 `json:"active_cashiers"`
|
||||||
|
WalletBalance Currency `json:"wallet_balance"`
|
||||||
|
LastActivity time.Time `json:"last_activity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CashierPerformance struct {
|
||||||
|
CashierID int64 `json:"cashier_id"`
|
||||||
|
CashierName string `json:"cashier_name"`
|
||||||
|
BranchID int64 `json:"branch_id"`
|
||||||
|
BranchName string `json:"branch_name"`
|
||||||
|
CompanyID int64 `json:"company_id"`
|
||||||
|
TotalBets int64 `json:"total_bets"`
|
||||||
|
TotalStakes Currency `json:"total_stakes"`
|
||||||
|
TotalWins int64 `json:"total_wins"`
|
||||||
|
TotalPayouts Currency `json:"total_payouts"`
|
||||||
|
Profit Currency `json:"profit"`
|
||||||
|
WinRate float64 `json:"win_rate"`
|
||||||
|
AverageStake Currency `json:"average_stake"`
|
||||||
|
Deposits Currency `json:"deposits"`
|
||||||
|
Withdrawals Currency `json:"withdrawals"`
|
||||||
|
NetTransactionAmount Currency `json:"net_transaction_amount"`
|
||||||
|
FirstActivity time.Time `json:"first_activity"`
|
||||||
|
LastActivity time.Time `json:"last_activity"`
|
||||||
|
ActiveDays int `json:"active_days"`
|
||||||
|
}
|
||||||
|
|
@ -31,7 +31,8 @@ type Transfer struct {
|
||||||
Type TransferType
|
Type TransferType
|
||||||
PaymentMethod PaymentMethod
|
PaymentMethod PaymentMethod
|
||||||
ReceiverWalletID int64
|
ReceiverWalletID int64
|
||||||
SenderWalletID ValidInt64
|
SenderWalletID int64
|
||||||
|
ReferenceNumber string
|
||||||
CashierID ValidInt64
|
CashierID ValidInt64
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
|
|
@ -40,8 +41,9 @@ type Transfer struct {
|
||||||
type CreateTransfer struct {
|
type CreateTransfer struct {
|
||||||
Amount Currency
|
Amount Currency
|
||||||
Verified bool
|
Verified bool
|
||||||
|
ReferenceNumber string
|
||||||
ReceiverWalletID int64
|
ReceiverWalletID int64
|
||||||
SenderWalletID ValidInt64
|
SenderWalletID int64
|
||||||
CashierID ValidInt64
|
CashierID ValidInt64
|
||||||
Type TransferType
|
Type TransferType
|
||||||
PaymentMethod PaymentMethod
|
PaymentMethod PaymentMethod
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ type VirtualGame struct {
|
||||||
MinBet float64 `json:"min_bet"`
|
MinBet float64 `json:"min_bet"`
|
||||||
MaxBet float64 `json:"max_bet"`
|
MaxBet float64 `json:"max_bet"`
|
||||||
Volatility string `json:"volatility"`
|
Volatility string `json:"volatility"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
RTP float64 `json:"rtp"`
|
RTP float64 `json:"rtp"`
|
||||||
IsFeatured bool `json:"is_featured"`
|
IsFeatured bool `json:"is_featured"`
|
||||||
PopularityScore int `json:"popularity_score"`
|
PopularityScore int `json:"popularity_score"`
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
package mongoLogger
|
package mongoLogger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func InitLogger() (*zap.Logger, error) {
|
||||||
// Replace localhost if inside Docker
|
mongoCore, err := NewMongoCore(
|
||||||
mongoCore, err := NewMongoCore("mongodb://root:secret@mongo:27017/?authSource=admin", "logdb", "applogs", zapcore.InfoLevel)
|
"mongodb://root:secret@mongo:27017/?authSource=admin",
|
||||||
|
"logdb",
|
||||||
|
"applogs",
|
||||||
|
zapcore.InfoLevel,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create MongoDB core: %v", err)
|
return nil, fmt.Errorf("failed to create MongoDB core: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
|
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
|
||||||
|
|
@ -21,10 +25,6 @@ func Init() {
|
||||||
combinedCore := zapcore.NewTee(mongoCore, consoleCore)
|
combinedCore := zapcore.NewTee(mongoCore, consoleCore)
|
||||||
|
|
||||||
logger := zap.New(combinedCore, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
|
logger := zap.New(combinedCore, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
|
||||||
zap.ReplaceGlobals(logger) // Optional but useful if you use zap.L()
|
|
||||||
|
|
||||||
defer logger.Sync()
|
return logger, nil
|
||||||
|
|
||||||
// logger.Info("Application started", zap.String("module", "main"))
|
|
||||||
// logger.Error("Something went wrong", zap.String("error_code", "E123"))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBe
|
||||||
|
|
||||||
rows, err := s.queries.CreateBetOutcome(ctx, dbParams)
|
rows, err := s.queries.CreateBetOutcome(ctx, dbParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to create bet outcomes in DB",
|
domain.MongoDBLogger.Error("failed to create bet outcomes in DB",
|
||||||
zap.Int("outcome_count", len(outcomes)),
|
zap.Int("outcome_count", len(outcomes)),
|
||||||
zap.Any("bet_id", outcomes[0].BetID), // assumes all outcomes have same BetID
|
zap.Any("bet_id", outcomes[0].BetID), // assumes all outcomes have same BetID
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -172,7 +172,7 @@ func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBe
|
||||||
func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) {
|
func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) {
|
||||||
bet, err := s.queries.GetBetByID(ctx, id)
|
bet, err := s.queries.GetBetByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to get bet by ID",
|
domain.MongoDBLogger.Error("failed to get bet by ID",
|
||||||
zap.Int64("bet_id", id),
|
zap.Int64("bet_id", id),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
|
|
@ -185,7 +185,7 @@ func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error)
|
||||||
func (s *Store) GetBetByCashoutID(ctx context.Context, id string) (domain.GetBet, error) {
|
func (s *Store) GetBetByCashoutID(ctx context.Context, id string) (domain.GetBet, error) {
|
||||||
bet, err := s.queries.GetBetByCashoutID(ctx, id)
|
bet, err := s.queries.GetBetByCashoutID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to get bet by cashout ID",
|
domain.MongoDBLogger.Error("failed to get bet by cashout ID",
|
||||||
zap.String("cashout_id", id),
|
zap.String("cashout_id", id),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
|
|
@ -211,7 +211,7 @@ func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]doma
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to get all bets",
|
domain.MongoDBLogger.Error("failed to get all bets",
|
||||||
zap.Any("filter", filter),
|
zap.Any("filter", filter),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
|
|
@ -232,7 +232,7 @@ func (s *Store) GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.
|
||||||
Valid: true,
|
Valid: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to get bets by branch ID",
|
domain.MongoDBLogger.Error("failed to get bets by branch ID",
|
||||||
zap.Int64("branch_id", BranchID),
|
zap.Int64("branch_id", BranchID),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
|
|
@ -271,7 +271,7 @@ func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) err
|
||||||
CashedOut: cashedOut,
|
CashedOut: cashedOut,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to update cashout",
|
domain.MongoDBLogger.Error("failed to update cashout",
|
||||||
zap.Int64("id", id),
|
zap.Int64("id", id),
|
||||||
zap.Bool("cashed_out", cashedOut),
|
zap.Bool("cashed_out", cashedOut),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -286,7 +286,7 @@ func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.Outcom
|
||||||
Status: int32(status),
|
Status: int32(status),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to update status",
|
domain.MongoDBLogger.Error("failed to update status",
|
||||||
zap.Int64("id", id),
|
zap.Int64("id", id),
|
||||||
zap.Int32("status", int32(status)),
|
zap.Int32("status", int32(status)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -298,7 +298,7 @@ func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.Outcom
|
||||||
func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error) {
|
func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error) {
|
||||||
outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, eventID)
|
outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, eventID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to get bet outcomes by event ID",
|
domain.MongoDBLogger.Error("failed to get bet outcomes by event ID",
|
||||||
zap.Int64("event_id", eventID),
|
zap.Int64("event_id", eventID),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
|
|
@ -315,7 +315,7 @@ func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]do
|
||||||
func (s *Store) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error) {
|
func (s *Store) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error) {
|
||||||
outcomes, err := s.queries.GetBetOutcomeByBetID(ctx, betID)
|
outcomes, err := s.queries.GetBetOutcomeByBetID(ctx, betID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to get bet outcomes by bet ID",
|
domain.MongoDBLogger.Error("failed to get bet outcomes by bet ID",
|
||||||
zap.Int64("bet_id", betID),
|
zap.Int64("bet_id", betID),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
|
|
@ -335,7 +335,7 @@ func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status dom
|
||||||
ID: id,
|
ID: id,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to update bet outcome status",
|
domain.MongoDBLogger.Error("failed to update bet outcome status",
|
||||||
zap.Int64("id", id),
|
zap.Int64("id", id),
|
||||||
zap.Int32("status", int32(status)),
|
zap.Int32("status", int32(status)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -428,7 +428,7 @@ func (s *Store) GetBetSummary(ctx context.Context, filter domain.ReportFilter) (
|
||||||
row := s.conn.QueryRow(ctx, query, args...)
|
row := s.conn.QueryRow(ctx, query, args...)
|
||||||
err = row.Scan(&totalStakes, &totalBets, &activeBets, &totalWins, &totalLosses, &winBalance)
|
err = row.Scan(&totalStakes, &totalBets, &activeBets, &totalWins, &totalLosses, &winBalance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to get bet summary",
|
domain.MongoDBLogger.Error("failed to get bet summary",
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -436,7 +436,7 @@ func (s *Store) GetBetSummary(ctx context.Context, filter domain.ReportFilter) (
|
||||||
return 0, 0, 0, 0, 0, 0, fmt.Errorf("failed to get bet summary: %w", err)
|
return 0, 0, 0, 0, 0, 0, fmt.Errorf("failed to get bet summary: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mongoLogger.Info("GetBetSummary executed successfully",
|
domain.MongoDBLogger.Info("GetBetSummary executed successfully",
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
zap.Float64("totalStakes", float64(totalStakes)), // convert if needed
|
zap.Float64("totalStakes", float64(totalStakes)), // convert if needed
|
||||||
|
|
@ -519,7 +519,7 @@ func (s *Store) GetBetStats(ctx context.Context, filter domain.ReportFilter) ([]
|
||||||
|
|
||||||
rows, err := s.conn.Query(ctx, query, args...)
|
rows, err := s.conn.Query(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to query bet stats",
|
domain.MongoDBLogger.Error("failed to query bet stats",
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -539,7 +539,7 @@ func (s *Store) GetBetStats(ctx context.Context, filter domain.ReportFilter) ([]
|
||||||
&stat.TotalPayouts,
|
&stat.TotalPayouts,
|
||||||
&stat.AverageOdds,
|
&stat.AverageOdds,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
mongoLogger.Error("failed to scan bet stat",
|
domain.MongoDBLogger.Error("failed to scan bet stat",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return nil, fmt.Errorf("failed to scan bet stat: %w", err)
|
return nil, fmt.Errorf("failed to scan bet stat: %w", err)
|
||||||
|
|
@ -548,13 +548,13 @@ func (s *Store) GetBetStats(ctx context.Context, filter domain.ReportFilter) ([]
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
mongoLogger.Error("rows error after iteration",
|
domain.MongoDBLogger.Error("rows error after iteration",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return nil, fmt.Errorf("rows error: %w", err)
|
return nil, fmt.Errorf("rows error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mongoLogger.Info("GetBetStats executed successfully",
|
domain.MongoDBLogger.Info("GetBetStats executed successfully",
|
||||||
zap.Int("result_count", len(stats)),
|
zap.Int("result_count", len(stats)),
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
|
|
@ -615,7 +615,7 @@ func (s *Store) GetSportPopularity(ctx context.Context, filter domain.ReportFilt
|
||||||
|
|
||||||
rows, err := s.conn.Query(ctx, query, args...)
|
rows, err := s.conn.Query(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to query sport popularity",
|
domain.MongoDBLogger.Error("failed to query sport popularity",
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -629,7 +629,7 @@ func (s *Store) GetSportPopularity(ctx context.Context, filter domain.ReportFilt
|
||||||
var date time.Time
|
var date time.Time
|
||||||
var sportID string
|
var sportID string
|
||||||
if err := rows.Scan(&date, &sportID); err != nil {
|
if err := rows.Scan(&date, &sportID); err != nil {
|
||||||
mongoLogger.Error("failed to scan sport popularity",
|
domain.MongoDBLogger.Error("failed to scan sport popularity",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return nil, fmt.Errorf("failed to scan sport popularity: %w", err)
|
return nil, fmt.Errorf("failed to scan sport popularity: %w", err)
|
||||||
|
|
@ -638,13 +638,13 @@ func (s *Store) GetSportPopularity(ctx context.Context, filter domain.ReportFilt
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
mongoLogger.Error("rows error after iteration",
|
domain.MongoDBLogger.Error("rows error after iteration",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return nil, fmt.Errorf("rows error: %w", err)
|
return nil, fmt.Errorf("rows error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mongoLogger.Info("GetSportPopularity executed successfully",
|
domain.MongoDBLogger.Info("GetSportPopularity executed successfully",
|
||||||
zap.Int("result_count", len(popularity)),
|
zap.Int("result_count", len(popularity)),
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
|
|
@ -705,7 +705,7 @@ func (s *Store) GetMarketPopularity(ctx context.Context, filter domain.ReportFil
|
||||||
|
|
||||||
rows, err := s.conn.Query(ctx, query, args...)
|
rows, err := s.conn.Query(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to query market popularity",
|
domain.MongoDBLogger.Error("failed to query market popularity",
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -719,7 +719,7 @@ func (s *Store) GetMarketPopularity(ctx context.Context, filter domain.ReportFil
|
||||||
var date time.Time
|
var date time.Time
|
||||||
var marketName string
|
var marketName string
|
||||||
if err := rows.Scan(&date, &marketName); err != nil {
|
if err := rows.Scan(&date, &marketName); err != nil {
|
||||||
mongoLogger.Error("failed to scan market popularity",
|
domain.MongoDBLogger.Error("failed to scan market popularity",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return nil, fmt.Errorf("failed to scan market popularity: %w", err)
|
return nil, fmt.Errorf("failed to scan market popularity: %w", err)
|
||||||
|
|
@ -728,13 +728,13 @@ func (s *Store) GetMarketPopularity(ctx context.Context, filter domain.ReportFil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
mongoLogger.Error("rows error after iteration",
|
domain.MongoDBLogger.Error("rows error after iteration",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return nil, fmt.Errorf("rows error: %w", err)
|
return nil, fmt.Errorf("rows error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mongoLogger.Info("GetMarketPopularity executed successfully",
|
domain.MongoDBLogger.Info("GetMarketPopularity executed successfully",
|
||||||
zap.Int("result_count", len(popularity)),
|
zap.Int("result_count", len(popularity)),
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
|
|
@ -809,7 +809,7 @@ func (s *Store) GetExtremeValues(ctx context.Context, filter domain.ReportFilter
|
||||||
|
|
||||||
rows, err := s.conn.Query(ctx, query, args...)
|
rows, err := s.conn.Query(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to query extreme values",
|
domain.MongoDBLogger.Error("failed to query extreme values",
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -823,7 +823,7 @@ func (s *Store) GetExtremeValues(ctx context.Context, filter domain.ReportFilter
|
||||||
var date time.Time
|
var date time.Time
|
||||||
var extreme domain.ExtremeValues
|
var extreme domain.ExtremeValues
|
||||||
if err := rows.Scan(&date, &extreme.HighestStake, &extreme.HighestPayout); err != nil {
|
if err := rows.Scan(&date, &extreme.HighestStake, &extreme.HighestPayout); err != nil {
|
||||||
mongoLogger.Error("failed to scan extreme values",
|
domain.MongoDBLogger.Error("failed to scan extreme values",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return nil, fmt.Errorf("failed to scan extreme values: %w", err)
|
return nil, fmt.Errorf("failed to scan extreme values: %w", err)
|
||||||
|
|
@ -832,13 +832,13 @@ func (s *Store) GetExtremeValues(ctx context.Context, filter domain.ReportFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
mongoLogger.Error("rows error after iteration",
|
domain.MongoDBLogger.Error("rows error after iteration",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return nil, fmt.Errorf("rows error: %w", err)
|
return nil, fmt.Errorf("rows error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mongoLogger.Info("GetExtremeValues executed successfully",
|
domain.MongoDBLogger.Info("GetExtremeValues executed successfully",
|
||||||
zap.Int("result_count", len(extremes)),
|
zap.Int("result_count", len(extremes)),
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
|
|
@ -899,7 +899,7 @@ func (s *Store) GetCustomerBetActivity(ctx context.Context, filter domain.Report
|
||||||
|
|
||||||
rows, err := s.conn.Query(ctx, query, args...)
|
rows, err := s.conn.Query(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to query customer bet activity",
|
domain.MongoDBLogger.Error("failed to query customer bet activity",
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -921,7 +921,7 @@ func (s *Store) GetCustomerBetActivity(ctx context.Context, filter domain.Report
|
||||||
&activity.LastBetDate,
|
&activity.LastBetDate,
|
||||||
&activity.AverageOdds,
|
&activity.AverageOdds,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
mongoLogger.Error("failed to scan customer bet activity",
|
domain.MongoDBLogger.Error("failed to scan customer bet activity",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return nil, fmt.Errorf("failed to scan customer bet activity: %w", err)
|
return nil, fmt.Errorf("failed to scan customer bet activity: %w", err)
|
||||||
|
|
@ -930,13 +930,13 @@ func (s *Store) GetCustomerBetActivity(ctx context.Context, filter domain.Report
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
mongoLogger.Error("rows error after iteration",
|
domain.MongoDBLogger.Error("rows error after iteration",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return nil, fmt.Errorf("rows error: %w", err)
|
return nil, fmt.Errorf("rows error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mongoLogger.Info("GetCustomerBetActivity executed successfully",
|
domain.MongoDBLogger.Info("GetCustomerBetActivity executed successfully",
|
||||||
zap.Int("result_count", len(activities)),
|
zap.Int("result_count", len(activities)),
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
|
|
@ -989,7 +989,7 @@ func (s *Store) GetBranchBetActivity(ctx context.Context, filter domain.ReportFi
|
||||||
|
|
||||||
rows, err := s.conn.Query(ctx, query, args...)
|
rows, err := s.conn.Query(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to query branch bet activity",
|
domain.MongoDBLogger.Error("failed to query branch bet activity",
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -1008,18 +1008,18 @@ func (s *Store) GetBranchBetActivity(ctx context.Context, filter domain.ReportFi
|
||||||
&activity.TotalWins,
|
&activity.TotalWins,
|
||||||
&activity.TotalPayouts,
|
&activity.TotalPayouts,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
mongoLogger.Error("failed to scan branch bet activity", zap.Error(err))
|
domain.MongoDBLogger.Error("failed to scan branch bet activity", zap.Error(err))
|
||||||
return nil, fmt.Errorf("failed to scan branch bet activity: %w", err)
|
return nil, fmt.Errorf("failed to scan branch bet activity: %w", err)
|
||||||
}
|
}
|
||||||
activities = append(activities, activity)
|
activities = append(activities, activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
mongoLogger.Error("rows error after iteration", zap.Error(err))
|
domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err))
|
||||||
return nil, fmt.Errorf("rows error: %w", err)
|
return nil, fmt.Errorf("rows error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mongoLogger.Info("GetBranchBetActivity executed successfully",
|
domain.MongoDBLogger.Info("GetBranchBetActivity executed successfully",
|
||||||
zap.Int("result_count", len(activities)),
|
zap.Int("result_count", len(activities)),
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
|
|
@ -1078,7 +1078,7 @@ func (s *Store) GetSportBetActivity(ctx context.Context, filter domain.ReportFil
|
||||||
|
|
||||||
rows, err := s.conn.Query(ctx, query, args...)
|
rows, err := s.conn.Query(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to query sport bet activity",
|
domain.MongoDBLogger.Error("failed to query sport bet activity",
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -1098,18 +1098,18 @@ func (s *Store) GetSportBetActivity(ctx context.Context, filter domain.ReportFil
|
||||||
&activity.TotalPayouts,
|
&activity.TotalPayouts,
|
||||||
&activity.AverageOdds,
|
&activity.AverageOdds,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
mongoLogger.Error("failed to scan sport bet activity", zap.Error(err))
|
domain.MongoDBLogger.Error("failed to scan sport bet activity", zap.Error(err))
|
||||||
return nil, fmt.Errorf("failed to scan sport bet activity: %w", err)
|
return nil, fmt.Errorf("failed to scan sport bet activity: %w", err)
|
||||||
}
|
}
|
||||||
activities = append(activities, activity)
|
activities = append(activities, activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
mongoLogger.Error("rows error after iteration", zap.Error(err))
|
domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err))
|
||||||
return nil, fmt.Errorf("rows error: %w", err)
|
return nil, fmt.Errorf("rows error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mongoLogger.Info("GetSportBetActivity executed successfully",
|
domain.MongoDBLogger.Info("GetSportBetActivity executed successfully",
|
||||||
zap.Int("result_count", len(activities)),
|
zap.Int("result_count", len(activities)),
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
|
|
@ -1156,7 +1156,7 @@ func (s *Store) GetSportDetails(ctx context.Context, filter domain.ReportFilter)
|
||||||
|
|
||||||
rows, err := s.conn.Query(ctx, query, args...)
|
rows, err := s.conn.Query(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to query sport details",
|
domain.MongoDBLogger.Error("failed to query sport details",
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -1169,18 +1169,18 @@ func (s *Store) GetSportDetails(ctx context.Context, filter domain.ReportFilter)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var sportID, matchName string
|
var sportID, matchName string
|
||||||
if err := rows.Scan(&sportID, &matchName); err != nil {
|
if err := rows.Scan(&sportID, &matchName); err != nil {
|
||||||
mongoLogger.Error("failed to scan sport detail", zap.Error(err))
|
domain.MongoDBLogger.Error("failed to scan sport detail", zap.Error(err))
|
||||||
return nil, fmt.Errorf("failed to scan sport detail: %w", err)
|
return nil, fmt.Errorf("failed to scan sport detail: %w", err)
|
||||||
}
|
}
|
||||||
details[sportID] = matchName
|
details[sportID] = matchName
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
mongoLogger.Error("rows error after iteration", zap.Error(err))
|
domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err))
|
||||||
return nil, fmt.Errorf("rows error: %w", err)
|
return nil, fmt.Errorf("rows error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mongoLogger.Info("GetSportDetails executed successfully",
|
domain.MongoDBLogger.Info("GetSportDetails executed successfully",
|
||||||
zap.Int("result_count", len(details)),
|
zap.Int("result_count", len(details)),
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
|
|
@ -1241,7 +1241,7 @@ func (s *Store) GetSportMarketPopularity(ctx context.Context, filter domain.Repo
|
||||||
|
|
||||||
rows, err := s.conn.Query(ctx, query, args...)
|
rows, err := s.conn.Query(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mongoLogger.Error("failed to query sport market popularity",
|
domain.MongoDBLogger.Error("failed to query sport market popularity",
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|
@ -1254,18 +1254,18 @@ func (s *Store) GetSportMarketPopularity(ctx context.Context, filter domain.Repo
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var sportID, marketName string
|
var sportID, marketName string
|
||||||
if err := rows.Scan(&sportID, &marketName); err != nil {
|
if err := rows.Scan(&sportID, &marketName); err != nil {
|
||||||
mongoLogger.Error("failed to scan sport market popularity", zap.Error(err))
|
domain.MongoDBLogger.Error("failed to scan sport market popularity", zap.Error(err))
|
||||||
return nil, fmt.Errorf("failed to scan sport market popularity: %w", err)
|
return nil, fmt.Errorf("failed to scan sport market popularity: %w", err)
|
||||||
}
|
}
|
||||||
popularity[sportID] = marketName
|
popularity[sportID] = marketName
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
mongoLogger.Error("rows error after iteration", zap.Error(err))
|
domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err))
|
||||||
return nil, fmt.Errorf("rows error: %w", err)
|
return nil, fmt.Errorf("rows error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mongoLogger.Info("GetSportMarketPopularity executed successfully",
|
domain.MongoDBLogger.Info("GetSportMarketPopularity executed successfully",
|
||||||
zap.Int("result_count", len(popularity)),
|
zap.Int("result_count", len(popularity)),
|
||||||
zap.String("query", query),
|
zap.String("query", query),
|
||||||
zap.Any("args", args),
|
zap.Any("args", args),
|
||||||
|
|
|
||||||
|
|
@ -259,10 +259,11 @@ func (s *Store) DeleteBranchCashier(ctx context.Context, userID int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBranchCounts returns total and active branch counts
|
// GetBranchCounts returns total and active branch counts
|
||||||
func (s *Store) GetBranchCounts(ctx context.Context, filter domain.ReportFilter) (total, active int64, err error) {
|
func (s *Store) GetBranchCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
|
||||||
query := `SELECT
|
query := `SELECT
|
||||||
COUNT(*) as total,
|
COUNT(*) as total,
|
||||||
COUNT(CASE WHEN is_active = true THEN 1 END) as active
|
COUNT(CASE WHEN is_active = true THEN 1 END) as active,
|
||||||
|
COUNT(CASE WHEN is_active = false THEN 1 END) as inactive
|
||||||
FROM branches`
|
FROM branches`
|
||||||
|
|
||||||
args := []interface{}{}
|
args := []interface{}{}
|
||||||
|
|
@ -291,12 +292,12 @@ func (s *Store) GetBranchCounts(ctx context.Context, filter domain.ReportFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
row := s.conn.QueryRow(ctx, query, args...)
|
row := s.conn.QueryRow(ctx, query, args...)
|
||||||
err = row.Scan(&total, &active)
|
err = row.Scan(&total, &active, &inactive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, fmt.Errorf("failed to get branch counts: %w", err)
|
return 0, 0, 0, fmt.Errorf("failed to get branch counts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return total, active, nil
|
return total, active, inactive, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBranchDetails returns branch details map
|
// GetBranchDetails returns branch details map
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
|
@ -122,3 +123,40 @@ func (s *Store) UpdateCompany(ctx context.Context, company domain.UpdateCompany)
|
||||||
func (s *Store) DeleteCompany(ctx context.Context, id int64) error {
|
func (s *Store) DeleteCompany(ctx context.Context, id int64) error {
|
||||||
return s.queries.DeleteCompany(ctx, id)
|
return s.queries.DeleteCompany(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetCompanyCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
|
||||||
|
query := `SELECT
|
||||||
|
COUNT(*) as total,
|
||||||
|
COUNT(CASE WHEN w.is_active = true THEN 1 END) as active,
|
||||||
|
COUNT(CASE WHEN w.is_active = false THEN 1 END) as inactive
|
||||||
|
FROM companies c
|
||||||
|
JOIN wallets w ON c.wallet_id = w.id`
|
||||||
|
|
||||||
|
args := []interface{}{}
|
||||||
|
argPos := 1
|
||||||
|
|
||||||
|
// Add filters if provided
|
||||||
|
if filter.StartTime.Valid {
|
||||||
|
query += fmt.Sprintf(" WHERE %screated_at >= $%d", func() string {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return " AND "
|
||||||
|
}(), argPos)
|
||||||
|
args = append(args, filter.StartTime.Value)
|
||||||
|
argPos++
|
||||||
|
}
|
||||||
|
if filter.EndTime.Valid {
|
||||||
|
query += fmt.Sprintf(" AND created_at <= $%d", argPos)
|
||||||
|
args = append(args, filter.EndTime.Value)
|
||||||
|
argPos++
|
||||||
|
}
|
||||||
|
|
||||||
|
row := s.conn.QueryRow(ctx, query, args...)
|
||||||
|
err = row.Scan(&total, &active, &inactive)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, fmt.Errorf("failed to get company counts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, active, inactive, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@ package repository
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
"golang.org/x/net/websocket"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type NotificationRepository interface {
|
type NotificationRepository interface {
|
||||||
|
|
@ -18,6 +19,7 @@ type NotificationRepository interface {
|
||||||
ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error)
|
ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error)
|
||||||
CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error)
|
CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error)
|
||||||
GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error)
|
GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error)
|
||||||
|
GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Repository struct {
|
type Repository struct {
|
||||||
|
|
@ -28,10 +30,13 @@ func NewNotificationRepository(store *Store) NotificationRepository {
|
||||||
return &Repository{store: store}
|
return &Repository{store: store}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error {
|
func (s *Store) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) DisconnectWebSocket(recipientID int64) {
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error) {
|
func (r *Repository) CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error) {
|
||||||
var errorSeverity pgtype.Text
|
var errorSeverity pgtype.Text
|
||||||
if notification.ErrorSeverity != nil {
|
if notification.ErrorSeverity != nil {
|
||||||
|
|
@ -206,3 +211,162 @@ func unmarshalPayload(data []byte) (domain.NotificationPayload, error) {
|
||||||
func (r *Repository) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) {
|
func (r *Repository) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) {
|
||||||
return r.store.queries.CountUnreadNotifications(ctx, recipient_id)
|
return r.store.queries.CountUnreadNotifications(ctx, recipient_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error) {
|
||||||
|
rows, err := r.store.queries.GetNotificationCounts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, fmt.Errorf("failed to get notification counts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// var total, read, unread int64
|
||||||
|
for _, row := range rows {
|
||||||
|
total += row.Total
|
||||||
|
read += row.Read
|
||||||
|
unread += row.Unread
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, read, unread, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetMostActiveNotificationRecipients(ctx context.Context, filter domain.ReportFilter, limit int) ([]domain.ActiveNotificationRecipient, error) {
|
||||||
|
query := `SELECT
|
||||||
|
n.recipient_id,
|
||||||
|
u.first_name || ' ' || u.last_name as recipient_name,
|
||||||
|
COUNT(*) as notification_count,
|
||||||
|
MAX(n.timestamp) as last_notification_time
|
||||||
|
FROM notifications n
|
||||||
|
JOIN users u ON n.recipient_id = u.id
|
||||||
|
WHERE n.timestamp BETWEEN $1 AND $2
|
||||||
|
GROUP BY n.recipient_id, u.first_name, u.last_name
|
||||||
|
ORDER BY notification_count DESC
|
||||||
|
LIMIT $3`
|
||||||
|
|
||||||
|
var recipients []domain.ActiveNotificationRecipient
|
||||||
|
rows, err := s.conn.Query(ctx, query, filter.StartTime.Value, filter.EndTime.Value, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get active notification recipients: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var r domain.ActiveNotificationRecipient
|
||||||
|
if err := rows.Scan(&r.RecipientID, &r.RecipientName, &r.NotificationCount, &r.LastNotificationTime); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
recipients = append(recipients, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return recipients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationDeliveryStats
|
||||||
|
func (s *Store) GetNotificationDeliveryStats(ctx context.Context, filter domain.ReportFilter) (domain.NotificationDeliveryStats, error) {
|
||||||
|
query := `SELECT
|
||||||
|
COUNT(*) as total_sent,
|
||||||
|
COUNT(CASE WHEN delivery_status = 'failed' THEN 1 END) as failed_deliveries,
|
||||||
|
(COUNT(CASE WHEN delivery_status = 'sent' THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0)) as success_rate,
|
||||||
|
MODE() WITHIN GROUP (ORDER BY delivery_channel) as most_used_channel
|
||||||
|
FROM notifications
|
||||||
|
WHERE timestamp BETWEEN $1 AND $2`
|
||||||
|
|
||||||
|
var stats domain.NotificationDeliveryStats
|
||||||
|
row := s.conn.QueryRow(ctx, query, filter.StartTime.Value, filter.EndTime.Value)
|
||||||
|
err := row.Scan(&stats.TotalSent, &stats.FailedDeliveries, &stats.SuccessRate, &stats.MostUsedChannel)
|
||||||
|
if err != nil {
|
||||||
|
return domain.NotificationDeliveryStats{}, fmt.Errorf("failed to get notification delivery stats: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationCountsByType
|
||||||
|
func (s *Store) GetNotificationCountsByType(ctx context.Context, filter domain.ReportFilter) (map[string]domain.NotificationTypeCount, error) {
|
||||||
|
query := `SELECT
|
||||||
|
type,
|
||||||
|
COUNT(*) as total,
|
||||||
|
COUNT(CASE WHEN is_read = true THEN 1 END) as read,
|
||||||
|
COUNT(CASE WHEN is_read = false THEN 1 END) as unread
|
||||||
|
FROM notifications
|
||||||
|
WHERE timestamp BETWEEN $1 AND $2
|
||||||
|
GROUP BY type`
|
||||||
|
|
||||||
|
counts := make(map[string]domain.NotificationTypeCount)
|
||||||
|
rows, err := s.conn.Query(ctx, query, filter.StartTime.Value, filter.EndTime.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get notification counts by type: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var nt domain.NotificationTypeCount
|
||||||
|
var typ string
|
||||||
|
if err := rows.Scan(&typ, &nt.Total, &nt.Read, &nt.Unread); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
counts[typ] = nt
|
||||||
|
}
|
||||||
|
|
||||||
|
return counts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CountUnreadNotifications(ctx context.Context, userID int64) (int64, error) {
|
||||||
|
count, err := s.queries.CountUnreadNotifications(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (s *Store) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
|
||||||
|
// dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{
|
||||||
|
// Limit: int32(limit),
|
||||||
|
// Offset: int32(offset),
|
||||||
|
// })
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// result := make([]domain.Notification, 0, len(dbNotifications))
|
||||||
|
// for _, dbNotif := range dbNotifications {
|
||||||
|
// // You may want to move this mapping logic to a shared function if not already present
|
||||||
|
// var errorSeverity *domain.NotificationErrorSeverity
|
||||||
|
// if dbNotif.ErrorSeverity.Valid {
|
||||||
|
// s := domain.NotificationErrorSeverity(dbNotif.ErrorSeverity.String)
|
||||||
|
// errorSeverity = &s
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var deliveryChannel domain.DeliveryChannel
|
||||||
|
// if dbNotif.DeliveryChannel.Valid {
|
||||||
|
// deliveryChannel = domain.DeliveryChannel(dbNotif.DeliveryChannel.String)
|
||||||
|
// } else {
|
||||||
|
// deliveryChannel = ""
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var priority int
|
||||||
|
// if dbNotif.Priority.Valid {
|
||||||
|
// priority = int(dbNotif.Priority.Int32)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// payload, err := unmarshalPayload(dbNotif.Payload)
|
||||||
|
// if err != nil {
|
||||||
|
// payload = domain.NotificationPayload{}
|
||||||
|
// }
|
||||||
|
|
||||||
|
// result = append(result, domain.Notification{
|
||||||
|
// ID: dbNotif.ID,
|
||||||
|
// RecipientID: dbNotif.RecipientID,
|
||||||
|
// Type: domain.NotificationType(dbNotif.Type),
|
||||||
|
// Level: domain.NotificationLevel(dbNotif.Level),
|
||||||
|
// ErrorSeverity: errorSeverity,
|
||||||
|
// Reciever: domain.NotificationRecieverSide(dbNotif.Reciever),
|
||||||
|
// IsRead: dbNotif.IsRead,
|
||||||
|
// DeliveryStatus: domain.NotificationDeliveryStatus(dbNotif.DeliveryStatus),
|
||||||
|
// DeliveryChannel: deliveryChannel,
|
||||||
|
// Payload: payload,
|
||||||
|
// Priority: priority,
|
||||||
|
// Timestamp: dbNotif.Timestamp.Time,
|
||||||
|
// Metadata: dbNotif.Metadata,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// return result, nil
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,7 @@ func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer {
|
||||||
Type: domain.TransferType(transfer.Type),
|
Type: domain.TransferType(transfer.Type),
|
||||||
Verified: transfer.Verified,
|
Verified: transfer.Verified,
|
||||||
ReceiverWalletID: transfer.ReceiverWalletID,
|
ReceiverWalletID: transfer.ReceiverWalletID,
|
||||||
SenderWalletID: domain.ValidInt64{
|
SenderWalletID: transfer.SenderWalletID.Int64,
|
||||||
Value: transfer.SenderWalletID.Int64,
|
|
||||||
Valid: transfer.SenderWalletID.Valid,
|
|
||||||
},
|
|
||||||
CashierID: domain.ValidInt64{
|
CashierID: domain.ValidInt64{
|
||||||
Value: transfer.CashierID.Int64,
|
Value: transfer.CashierID.Int64,
|
||||||
Valid: transfer.CashierID.Valid,
|
Valid: transfer.CashierID.Valid,
|
||||||
|
|
@ -33,8 +30,8 @@ func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferP
|
||||||
Type: string(transfer.Type),
|
Type: string(transfer.Type),
|
||||||
ReceiverWalletID: transfer.ReceiverWalletID,
|
ReceiverWalletID: transfer.ReceiverWalletID,
|
||||||
SenderWalletID: pgtype.Int8{
|
SenderWalletID: pgtype.Int8{
|
||||||
Int64: transfer.SenderWalletID.Value,
|
Int64: transfer.SenderWalletID,
|
||||||
Valid: transfer.SenderWalletID.Valid,
|
Valid: true,
|
||||||
},
|
},
|
||||||
CashierID: pgtype.Int8{
|
CashierID: pgtype.Int8{
|
||||||
Int64: transfer.CashierID.Value,
|
Int64: transfer.CashierID.Value,
|
||||||
|
|
@ -78,6 +75,14 @@ func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]dom
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error) {
|
||||||
|
transfer, err := s.queries.GetTransferByReference(ctx, reference)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Transfer{}, nil
|
||||||
|
}
|
||||||
|
return convertDBTransfer(transfer), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) {
|
func (s *Store) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) {
|
||||||
transfer, err := s.queries.GetTransferByID(ctx, id)
|
transfer, err := s.queries.GetTransferByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -448,10 +448,11 @@ func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.User, is_c
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCustomerCounts returns total and active customer counts
|
// GetCustomerCounts returns total and active customer counts
|
||||||
func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active int64, err error) {
|
func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
|
||||||
query := `SELECT
|
query := `SELECT
|
||||||
COUNT(*) as total,
|
COUNT(*) as total,
|
||||||
SUM(CASE WHEN suspended = false THEN 1 ELSE 0 END) as active
|
SUM(CASE WHEN suspended = false THEN 1 ELSE 0 END) as active,
|
||||||
|
SUM(CASE WHEN suspended = true THEN 1 ELSE 0 END) as inactive
|
||||||
FROM users WHERE role = 'customer'`
|
FROM users WHERE role = 'customer'`
|
||||||
|
|
||||||
args := []interface{}{}
|
args := []interface{}{}
|
||||||
|
|
@ -480,12 +481,12 @@ func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilte
|
||||||
}
|
}
|
||||||
|
|
||||||
row := s.conn.QueryRow(ctx, query, args...)
|
row := s.conn.QueryRow(ctx, query, args...)
|
||||||
err = row.Scan(&total, &active)
|
err = row.Scan(&total, &active, &inactive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, fmt.Errorf("failed to get customer counts: %w", err)
|
return 0, 0, 0, fmt.Errorf("failed to get customer counts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return total, active, nil
|
return total, active, inactive, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCustomerDetails returns customer details map
|
// GetCustomerDetails returns customer details map
|
||||||
|
|
@ -693,3 +694,44 @@ func (s *Store) GetCustomerPreferences(ctx context.Context, filter domain.Report
|
||||||
|
|
||||||
return preferences, nil
|
return preferences, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetRoleCounts(ctx context.Context, role string, filter domain.ReportFilter) (total, active, inactive int64, err error) {
|
||||||
|
query := `SELECT
|
||||||
|
COUNT(*) as total,
|
||||||
|
COUNT(CASE WHEN suspended = false THEN 1 END) as active,
|
||||||
|
COUNT(CASE WHEN suspended = true THEN 1 END) as inactive
|
||||||
|
FROM users WHERE role = $1`
|
||||||
|
|
||||||
|
args := []interface{}{role}
|
||||||
|
argPos := 2
|
||||||
|
|
||||||
|
// Add filters if provided
|
||||||
|
if filter.CompanyID.Valid {
|
||||||
|
query += fmt.Sprintf(" AND company_id = $%d", argPos)
|
||||||
|
args = append(args, filter.CompanyID.Value)
|
||||||
|
argPos++
|
||||||
|
}
|
||||||
|
if filter.StartTime.Valid {
|
||||||
|
query += fmt.Sprintf(" AND %screated_at >= $%d", func() string {
|
||||||
|
if len(args) == 1 { // Only role parameter so far
|
||||||
|
return " "
|
||||||
|
}
|
||||||
|
return " AND "
|
||||||
|
}(), argPos)
|
||||||
|
args = append(args, filter.StartTime.Value)
|
||||||
|
argPos++
|
||||||
|
}
|
||||||
|
if filter.EndTime.Valid {
|
||||||
|
query += fmt.Sprintf(" AND created_at <= $%d", argPos)
|
||||||
|
args = append(args, filter.EndTime.Value)
|
||||||
|
argPos++
|
||||||
|
}
|
||||||
|
|
||||||
|
row := s.conn.QueryRow(ctx, query, args...)
|
||||||
|
err = row.Scan(&total, &active, &inactive)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, fmt.Errorf("failed to get %s counts: %w", role, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, active, inactive, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
|
@ -17,12 +18,19 @@ type VirtualGameRepository interface {
|
||||||
CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error
|
CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error
|
||||||
GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error)
|
GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error)
|
||||||
UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error
|
UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error
|
||||||
|
|
||||||
|
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type VirtualGameRepo struct {
|
type VirtualGameRepo struct {
|
||||||
store *Store
|
store *Store
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetGameCounts implements VirtualGameRepository.
|
||||||
|
// func (r *VirtualGameRepo) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total int64, active int64, inactive int64, err error) {
|
||||||
|
// panic("unimplemented")
|
||||||
|
// }
|
||||||
|
|
||||||
func NewVirtualGameRepository(store *Store) VirtualGameRepository {
|
func NewVirtualGameRepository(store *Store) VirtualGameRepository {
|
||||||
return &VirtualGameRepo{store: store}
|
return &VirtualGameRepo{store: store}
|
||||||
}
|
}
|
||||||
|
|
@ -112,3 +120,34 @@ func (r *VirtualGameRepo) UpdateVirtualGameTransactionStatus(ctx context.Context
|
||||||
Status: status,
|
Status: status,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *VirtualGameRepo) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
|
||||||
|
query := `SELECT
|
||||||
|
COUNT(*) as total,
|
||||||
|
COUNT(CASE WHEN is_active = true THEN 1 END) as active,
|
||||||
|
COUNT(CASE WHEN is_active = false THEN 1 END) as inactive
|
||||||
|
FROM virtual_games`
|
||||||
|
|
||||||
|
args := []interface{}{}
|
||||||
|
argPos := 1
|
||||||
|
|
||||||
|
// Add filters if provided
|
||||||
|
if filter.StartTime.Valid {
|
||||||
|
query += fmt.Sprintf(" WHERE created_at >= $%d", argPos)
|
||||||
|
args = append(args, filter.StartTime.Value)
|
||||||
|
argPos++
|
||||||
|
}
|
||||||
|
if filter.EndTime.Valid {
|
||||||
|
query += fmt.Sprintf(" AND created_at <= $%d", argPos)
|
||||||
|
args = append(args, filter.EndTime.Value)
|
||||||
|
argPos++
|
||||||
|
}
|
||||||
|
|
||||||
|
row := r.store.conn.QueryRow(ctx, query, args...)
|
||||||
|
err = row.Scan(&total, &active, &inactive)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, fmt.Errorf("failed to get game counts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, active, inactive, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -225,3 +225,35 @@ func (s *Store) GetBalanceSummary(ctx context.Context, filter domain.ReportFilte
|
||||||
|
|
||||||
return summary, nil
|
return summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetTotalWallets(ctx context.Context, filter domain.ReportFilter) (int64, error) {
|
||||||
|
query := `SELECT COUNT(*) FROM wallets WHERE is_active = true`
|
||||||
|
args := []interface{}{}
|
||||||
|
argPos := 1
|
||||||
|
|
||||||
|
// Add filters if provided
|
||||||
|
if filter.StartTime.Valid {
|
||||||
|
query += fmt.Sprintf(" AND %screated_at >= $%d", func() string {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return " WHERE "
|
||||||
|
}
|
||||||
|
return " AND "
|
||||||
|
}(), argPos)
|
||||||
|
args = append(args, filter.StartTime.Value)
|
||||||
|
argPos++
|
||||||
|
}
|
||||||
|
if filter.EndTime.Valid {
|
||||||
|
query += fmt.Sprintf(" AND created_at <= $%d", argPos)
|
||||||
|
args = append(args, filter.EndTime.Value)
|
||||||
|
argPos++
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
row := s.conn.QueryRow(ctx, query, args...)
|
||||||
|
err := row.Scan(&total)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to get wallet counts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ type BranchStore interface {
|
||||||
GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error)
|
GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error)
|
||||||
DeleteBranchCashier(ctx context.Context, userID int64) error
|
DeleteBranchCashier(ctx context.Context, userID int64) error
|
||||||
|
|
||||||
GetBranchCounts(ctx context.Context, filter domain.ReportFilter) (total, active int64, err error)
|
GetBranchCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
|
||||||
GetBranchDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.BranchDetail, error)
|
GetBranchDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.BranchDetail, error)
|
||||||
|
|
||||||
GetAllCompaniesBranch(ctx context.Context) ([]domain.Company, error)
|
GetAllCompaniesBranch(ctx context.Context) ([]domain.Company, error)
|
||||||
|
|
|
||||||
|
|
@ -5,129 +5,230 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChapaClient interface {
|
|
||||||
IssuePayment(ctx context.Context, payload domain.ChapaTransferPayload) (bool, error)
|
|
||||||
InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error)
|
|
||||||
FetchBanks() ([]domain.ChapaSupportedBank, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
BaseURL string
|
baseURL string
|
||||||
SecretKey string
|
secretKey string
|
||||||
HTTPClient *http.Client
|
httpClient *http.Client
|
||||||
UserAgent string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(baseURL, secretKey string) *Client {
|
func NewClient(baseURL, secretKey string) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
BaseURL: baseURL,
|
baseURL: baseURL,
|
||||||
SecretKey: secretKey,
|
secretKey: secretKey,
|
||||||
HTTPClient: http.DefaultClient,
|
httpClient: &http.Client{
|
||||||
UserAgent: "FortuneBet/1.0",
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) IssuePayment(ctx context.Context, payload domain.ChapaTransferPayload) (bool, error) {
|
func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) {
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"amount": req.Amount,
|
||||||
|
"currency": req.Currency,
|
||||||
|
"email": req.Email,
|
||||||
|
"first_name": req.FirstName,
|
||||||
|
"last_name": req.LastName,
|
||||||
|
"tx_ref": req.TxRef,
|
||||||
|
"callback_url": req.CallbackURL,
|
||||||
|
"return_url": req.ReturnURL,
|
||||||
|
}
|
||||||
|
|
||||||
payloadBytes, err := json.Marshal(payload)
|
payloadBytes, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to serialize payload: %w", err)
|
return domain.ChapaDepositResponse{}, fmt.Errorf("failed to marshal payload: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/transfers", bytes.NewBuffer(payloadBytes))
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/transaction/initialize", bytes.NewBuffer(payloadBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to create HTTP request: %w", err)
|
return domain.ChapaDepositResponse{}, fmt.Errorf("failed to create request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Authorization", "Bearer "+c.SecretKey)
|
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
resp, err := c.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("chapa HTTP request failed: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, fmt.Errorf("chapa error: status %d, body: %s", resp.StatusCode, string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
// service/chapa_service.go
|
|
||||||
func (c *Client) InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error) {
|
|
||||||
fmt.Println("\n\nInit payment request: ", req)
|
|
||||||
payloadBytes, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("\n\nWe are here")
|
|
||||||
return "", fmt.Errorf("failed to serialize payload: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/transaction/initialize", bytes.NewBuffer(payloadBytes))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("\n\nWe are here 2")
|
|
||||||
return "", fmt.Errorf("failed to create HTTP request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
httpReq.Header.Set("Authorization", "Bearer "+c.SecretKey)
|
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
resp, err := c.HTTPClient.Do(httpReq)
|
resp, err := c.httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("\n\nWe are here 3")
|
return domain.ChapaDepositResponse{}, fmt.Errorf("request failed: %w", err)
|
||||||
return "", fmt.Errorf("chapa HTTP request failed: %w", err)
|
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
if resp.StatusCode != http.StatusOK {
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
fmt.Println("\n\nWe are here 4")
|
|
||||||
return "", fmt.Errorf("chapa error: status %d, body: %s", resp.StatusCode, string(body))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var response struct {
|
var response struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Status string `json:"status"`
|
||||||
Data struct {
|
Data struct {
|
||||||
CheckoutURL string `json:"checkout_url"`
|
CheckoutURL string `json:"checkout_url"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n\nInit payment response body: %v\n\n", response)
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
return domain.ChapaDepositResponse{}, fmt.Errorf("failed to decode response: %w", err)
|
||||||
if err := json.Unmarshal(body, &response); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to parse chapa response: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Data.CheckoutURL, nil
|
return domain.ChapaDepositResponse{
|
||||||
|
CheckoutURL: response.Data.CheckoutURL,
|
||||||
|
// Reference: req.TxRef,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) FetchBanks() ([]domain.ChapaSupportedBank, error) {
|
func (c *Client) VerifyPayment(ctx context.Context, reference string) (domain.ChapaDepositVerification, error) {
|
||||||
req, _ := http.NewRequest("GET", c.BaseURL+"/banks", nil)
|
httpReq, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/transaction/verify/"+reference, nil)
|
||||||
req.Header.Set("Authorization", "Bearer "+c.SecretKey)
|
|
||||||
fmt.Printf("\n\nbase URL is: %s\n\n", c.BaseURL)
|
|
||||||
|
|
||||||
res, err := c.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return domain.ChapaDepositVerification{}, fmt.Errorf("failed to create request: %w", err)
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
var resp struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data []domain.ChapaSupportedBank `json:"data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
|
||||||
return nil, err
|
|
||||||
|
resp, err := c.httpClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return domain.ChapaDepositVerification{}, fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return domain.ChapaDepositVerification{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n\nclient fetched banks: %+v\n\n", resp.Data)
|
var verification domain.ChapaDepositVerification
|
||||||
|
|
||||||
return resp.Data, nil
|
if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil {
|
||||||
|
return domain.ChapaDepositVerification{}, fmt.Errorf("failed to decode response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var status domain.PaymentStatus
|
||||||
|
switch verification.Status {
|
||||||
|
case "success":
|
||||||
|
status = domain.PaymentStatusCompleted
|
||||||
|
default:
|
||||||
|
status = domain.PaymentStatusFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.ChapaDepositVerification{
|
||||||
|
Status: status,
|
||||||
|
Amount: verification.Amount,
|
||||||
|
Currency: verification.Currency,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
|
||||||
|
url := fmt.Sprintf("%s/transaction/verify/%s", c.baseURL, txRef)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.secretKey)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var status domain.PaymentStatus
|
||||||
|
switch response.Status {
|
||||||
|
case "success":
|
||||||
|
status = domain.PaymentStatusCompleted
|
||||||
|
default:
|
||||||
|
status = domain.PaymentStatusFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return &domain.ChapaVerificationResponse{
|
||||||
|
Status: string(status),
|
||||||
|
Amount: response.Amount,
|
||||||
|
Currency: response.Currency,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.secretKey)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var bankResponse domain.BankResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&bankResponse); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var banks []domain.Bank
|
||||||
|
for _, bankData := range bankResponse.Data {
|
||||||
|
bank := domain.Bank{
|
||||||
|
ID: bankData.ID,
|
||||||
|
Slug: bankData.Slug,
|
||||||
|
Swift: bankData.Swift,
|
||||||
|
Name: bankData.Name,
|
||||||
|
AcctLength: bankData.AcctLength,
|
||||||
|
CountryID: bankData.CountryID,
|
||||||
|
IsMobileMoney: bankData.IsMobileMoney,
|
||||||
|
IsActive: bankData.IsActive,
|
||||||
|
IsRTGS: bankData.IsRTGS,
|
||||||
|
Active: bankData.Active,
|
||||||
|
Is24Hrs: bankData.Is24Hrs,
|
||||||
|
CreatedAt: bankData.CreatedAt,
|
||||||
|
UpdatedAt: bankData.UpdatedAt,
|
||||||
|
Currency: bankData.Currency,
|
||||||
|
}
|
||||||
|
banks = append(banks, bank)
|
||||||
|
}
|
||||||
|
|
||||||
|
return banks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to generate account regex based on bank type
|
||||||
|
// func GetAccountRegex(bank domain.Bank) string {
|
||||||
|
// if bank.IsMobileMoney != nil && bank.IsMobileMoney == 1 {
|
||||||
|
// return `^09[0-9]{8}$` // Ethiopian mobile money pattern
|
||||||
|
// }
|
||||||
|
// return fmt.Sprintf(`^[0-9]{%d}$`, bank.AcctLength)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Helper method to generate example account number
|
||||||
|
// func GetExampleAccount(bank domain.Bank) string {
|
||||||
|
// if bank.IsMobileMoney != nil && *bank.IsMobileMoney {
|
||||||
|
// return "0912345678" // Ethiopian mobile number example
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Generate example based on length
|
||||||
|
// example := "1"
|
||||||
|
// for i := 1; i < bank.AcctLength; i++ {
|
||||||
|
// example += fmt.Sprintf("%d", i%10)
|
||||||
|
// }
|
||||||
|
// return example
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,17 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChapaPort interface {
|
// type ChapaPort interface {
|
||||||
HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error
|
// HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error
|
||||||
HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error
|
// HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error
|
||||||
WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error
|
// WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error
|
||||||
DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error)
|
// DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error)
|
||||||
GetSupportedBanks() ([]domain.ChapaSupportedBank, error)
|
// GetSupportedBanks() ([]domain.ChapaSupportedBank, error)
|
||||||
|
// }
|
||||||
|
|
||||||
|
type ChapaStore interface {
|
||||||
|
InitializePayment(request domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error)
|
||||||
|
VerifyPayment(reference string) (domain.ChapaDepositVerification, error)
|
||||||
|
ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error)
|
||||||
|
FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,377 +2,191 @@ package chapa
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
// "log/slog"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
|
|
||||||
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/shopspring/decimal"
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrPaymentNotFound = errors.New("payment not found")
|
||||||
|
ErrPaymentAlreadyExists = errors.New("payment with this reference already exists")
|
||||||
|
ErrInvalidPaymentAmount = errors.New("invalid payment amount")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
transactionStore transaction.TransactionStore
|
transferStore wallet.TransferStore
|
||||||
walletStore wallet.WalletStore
|
walletStore wallet.WalletStore
|
||||||
userStore user.UserStore
|
userStore user.UserStore
|
||||||
referralStore referralservice.ReferralStore
|
cfg *config.Config
|
||||||
branchStore branch.BranchStore
|
chapaClient *Client
|
||||||
chapaClient ChapaClient
|
|
||||||
config *config.Config
|
|
||||||
// logger *slog.Logger
|
|
||||||
store *repository.Store
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
txStore transaction.TransactionStore,
|
transferStore wallet.TransferStore,
|
||||||
walletStore wallet.WalletStore,
|
walletStore wallet.WalletStore,
|
||||||
userStore user.UserStore,
|
userStore user.UserStore,
|
||||||
referralStore referralservice.ReferralStore,
|
chapaClient *Client,
|
||||||
branchStore branch.BranchStore,
|
|
||||||
chapaClient ChapaClient,
|
|
||||||
store *repository.Store,
|
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
transactionStore: txStore,
|
transferStore: transferStore,
|
||||||
walletStore: walletStore,
|
walletStore: walletStore,
|
||||||
userStore: userStore,
|
userStore: userStore,
|
||||||
referralStore: referralStore,
|
|
||||||
branchStore: branchStore,
|
|
||||||
chapaClient: chapaClient,
|
chapaClient: chapaClient,
|
||||||
store: store,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error {
|
// InitiateDeposit starts a new deposit process
|
||||||
_, tx, err := s.store.BeginTx(ctx)
|
func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) {
|
||||||
if err != nil {
|
// Validate amount
|
||||||
return err
|
if amount <= 0 {
|
||||||
}
|
return "", ErrInvalidPaymentAmount
|
||||||
defer tx.Rollback(ctx)
|
|
||||||
|
|
||||||
// Use your services normally (they 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID)
|
// Get user details
|
||||||
|
user, err := s.userStore.GetUserByID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
return "", fmt.Errorf("failed to get user: %w", err)
|
||||||
return fmt.Errorf("transaction with ID %d not found", referenceID)
|
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
|
var senderWallet domain.Wallet
|
||||||
|
|
||||||
|
// Generate unique reference
|
||||||
|
reference := uuid.New().String()
|
||||||
|
senderWallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get sender wallets: %w", err)
|
||||||
}
|
}
|
||||||
if txn.Verified {
|
for _, wallet := range senderWallets {
|
||||||
|
if wallet.IsWithdraw {
|
||||||
|
senderWallet = wallet
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if payment with this reference already exists
|
||||||
|
// if transfer, err := s.transferStore.GetTransferByReference(ctx, reference); err == nil {
|
||||||
|
|
||||||
|
// return fmt.Sprintf("%v", transfer), ErrPaymentAlreadyExists
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Create payment record
|
||||||
|
transfer := domain.CreateTransfer{
|
||||||
|
Amount: amount,
|
||||||
|
Type: domain.DEPOSIT,
|
||||||
|
PaymentMethod: domain.TRANSFER_CHAPA,
|
||||||
|
ReferenceNumber: reference,
|
||||||
|
// ReceiverWalletID: 1,
|
||||||
|
SenderWalletID: senderWallet.ID,
|
||||||
|
Verified: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to save payment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize payment with Chapa
|
||||||
|
response, err := s.chapaClient.InitializePayment(ctx, domain.ChapaDepositRequest{
|
||||||
|
Amount: amount,
|
||||||
|
Currency: "ETB",
|
||||||
|
Email: user.Email,
|
||||||
|
FirstName: user.FirstName,
|
||||||
|
LastName: user.LastName,
|
||||||
|
TxRef: reference,
|
||||||
|
CallbackURL: "https://fortunebet.com/api/v1/payments/callback",
|
||||||
|
ReturnURL: "https://fortunebet.com/api/v1/payment-success",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Update payment status to failed
|
||||||
|
// _ = s.transferStore.(payment.ID, domain.PaymentStatusFailed)
|
||||||
|
return "", fmt.Errorf("failed to initialize payment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.CheckoutURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyDeposit handles payment verification from webhook
|
||||||
|
func (s *Service) VerifyDeposit(ctx context.Context, reference string) error {
|
||||||
|
// Find payment by reference
|
||||||
|
payment, err := s.transferStore.GetTransferByReference(ctx, reference)
|
||||||
|
if err != nil {
|
||||||
|
return ErrPaymentNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if already completed
|
||||||
|
if payment.Verified {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
webhookAmount, _ := decimal.NewFromString(req.Amount)
|
// Verify payment with Chapa
|
||||||
storedAmount, _ := decimal.NewFromString(txn.Amount.String())
|
verification, err := s.chapaClient.VerifyPayment(ctx, reference)
|
||||||
if !webhookAmount.Equal(storedAmount) {
|
|
||||||
return fmt.Errorf("amount mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
txn.Verified = true
|
|
||||||
if err := s.transactionStore.UpdateTransactionVerified(ctx, txn.ID, txn.Verified, txn.ApprovedBy.Value, txn.ApproverName.Value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error {
|
|
||||||
_, tx, err := s.store.BeginTx(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to verify payment: %w", err)
|
||||||
}
|
|
||||||
defer tx.Rollback(ctx)
|
|
||||||
|
|
||||||
if req.Status != "success" {
|
|
||||||
return fmt.Errorf("payment status not successful")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Parse reference ID
|
// Update payment status
|
||||||
referenceID, err := strconv.ParseInt(req.TxRef, 10, 64)
|
if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
|
||||||
|
return fmt.Errorf("failed to update payment status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not verified or not found, verify with Chapa
|
||||||
|
verification, err := s.chapaClient.VerifyPayment(ctx, txRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid tx_ref: %w", err)
|
return nil, fmt.Errorf("failed to verify payment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fetch transaction
|
// Update our records if payment is successful
|
||||||
txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID)
|
if verification.Status == domain.PaymentStatusCompleted {
|
||||||
|
err = s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
return nil, fmt.Errorf("failed to update verification status: %w", err)
|
||||||
return fmt.Errorf("transaction with ID %d not found", referenceID)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if txn.Verified {
|
// Credit user's wallet
|
||||||
return nil // already processed
|
err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID, transfer.Amount)
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Update wallet balance
|
return &domain.ChapaVerificationResponse{
|
||||||
newBalance := wallet.Balance + txn.Amount
|
Status: string(verification.Status),
|
||||||
if err := s.walletStore.UpdateBalance(ctx, wallet.ID, newBalance); err != nil {
|
Amount: float64(verification.Amount),
|
||||||
return err
|
Currency: verification.Currency,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Mark transaction as verified
|
func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
|
||||||
if err := s.transactionStore.UpdateTransactionVerified(ctx, txn.ID, true, txn.ApprovedBy.Value, txn.ApproverName.Value); err != nil {
|
banks, err := s.chapaClient.FetchSupportedBanks(ctx)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Check & Create Referral
|
|
||||||
stats, err := s.referralStore.GetReferralStats(ctx, strconv.FormatInt(wallet.UserID, 10))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, fmt.Errorf("failed to fetch banks: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if stats == nil {
|
|
||||||
if err := s.referralStore.CreateReferral(ctx, wallet.UserID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error {
|
|
||||||
_, tx, err := s.store.BeginTx(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer tx.Rollback(ctx)
|
|
||||||
|
|
||||||
// Get the requesting user
|
|
||||||
user, err := s.userStore.GetUserByID(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("user not found: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
banks, err := s.GetSupportedBanks()
|
|
||||||
validBank := false
|
|
||||||
for _, bank := range banks {
|
|
||||||
if strconv.FormatInt(bank.Id, 10) == req.BankCode {
|
|
||||||
validBank = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !validBank {
|
|
||||||
return fmt.Errorf("invalid bank code")
|
|
||||||
}
|
|
||||||
|
|
||||||
// branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID)
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
var targetWallet domain.Wallet
|
|
||||||
targetWallet, err = s.walletStore.GetWalletByID(ctx, req.WalletID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// for _, w := range wallets {
|
|
||||||
// if w.ID == req.WalletID {
|
|
||||||
// targetWallet = &w
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if targetWallet == nil {
|
|
||||||
// return fmt.Errorf("no wallet found with the specified ID")
|
|
||||||
// }
|
|
||||||
|
|
||||||
if !targetWallet.IsTransferable || !targetWallet.IsActive {
|
|
||||||
return fmt.Errorf("wallet not eligible for withdrawal")
|
|
||||||
}
|
|
||||||
|
|
||||||
if targetWallet.Balance < domain.Currency(req.Amount) {
|
|
||||||
return fmt.Errorf("insufficient balance")
|
|
||||||
}
|
|
||||||
|
|
||||||
txID := uuid.New().String()
|
|
||||||
|
|
||||||
payload := domain.ChapaTransferPayload{
|
|
||||||
AccountName: req.AccountName,
|
|
||||||
AccountNumber: req.AccountNumber,
|
|
||||||
Amount: strconv.FormatInt(req.Amount, 10),
|
|
||||||
Currency: req.Currency,
|
|
||||||
BeneficiaryName: req.BeneficiaryName,
|
|
||||||
TxRef: txID,
|
|
||||||
Reference: txID,
|
|
||||||
BankCode: req.BankCode,
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err := s.chapaClient.IssuePayment(ctx, payload)
|
|
||||||
if err != nil || !ok {
|
|
||||||
return fmt.Errorf("chapa transfer failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create transaction using user and wallet info
|
|
||||||
_, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{
|
|
||||||
Amount: domain.Currency(req.Amount),
|
|
||||||
Type: domain.TransactionType(domain.TRANSACTION_CASHOUT),
|
|
||||||
ReferenceNumber: txID,
|
|
||||||
AccountName: req.AccountName,
|
|
||||||
AccountNumber: req.AccountNumber,
|
|
||||||
BankCode: req.BankCode,
|
|
||||||
BeneficiaryName: req.BeneficiaryName,
|
|
||||||
PaymentOption: domain.PaymentOption(domain.BANK),
|
|
||||||
BranchID: req.BranchID,
|
|
||||||
// BranchName: branch.Name,
|
|
||||||
// BranchLocation: branch.Location,
|
|
||||||
// CashierID: user.ID,
|
|
||||||
// CashierName: user.FullName,
|
|
||||||
FullName: user.FirstName + " " + user.LastName,
|
|
||||||
PhoneNumber: user.PhoneNumber,
|
|
||||||
// CompanyID: branch.CompanyID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create transaction: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newBalance := domain.Currency(req.Amount)
|
|
||||||
err = s.walletStore.UpdateBalance(ctx, targetWallet.ID, newBalance)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to update wallet balance: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error) {
|
|
||||||
_, tx, err := s.store.BeginTx(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer tx.Rollback(ctx)
|
|
||||||
|
|
||||||
if req.Amount <= 0 {
|
|
||||||
return "", fmt.Errorf("amount must be positive")
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := s.userStore.GetUserByID(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
txID := uuid.New().String()
|
|
||||||
|
|
||||||
fmt.Printf("\n\nChapa deposit transaction created: %v%v\n\n", branch, user)
|
|
||||||
|
|
||||||
// _, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{
|
|
||||||
// Amount: req.Amount,
|
|
||||||
// Type: domain.TransactionType(domain.TRANSACTION_DEPOSIT),
|
|
||||||
// ReferenceNumber: txID,
|
|
||||||
// BranchID: req.BranchID,
|
|
||||||
// BranchName: branch.Name,
|
|
||||||
// BranchLocation: branch.Location,
|
|
||||||
// FullName: user.FirstName + " " + user.LastName,
|
|
||||||
// PhoneNumber: user.PhoneNumber,
|
|
||||||
// // CompanyID: branch.CompanyID,
|
|
||||||
// })
|
|
||||||
// if err != nil {
|
|
||||||
// return "", err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Fetch user details for Chapa payment
|
|
||||||
userInfo, err := s.userStore.GetUserByID(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// fmt.Printf("\n\nCallbackURL is:%v\n\n", s.config.CHAPA_CALLBACK_URL)
|
|
||||||
|
|
||||||
// Build Chapa InitPaymentRequest (matches Chapa API)
|
|
||||||
paymentReq := domain.InitPaymentRequest{
|
|
||||||
Amount: req.Amount,
|
|
||||||
Currency: req.Currency,
|
|
||||||
Email: userInfo.Email,
|
|
||||||
FirstName: userInfo.FirstName,
|
|
||||||
LastName: userInfo.LastName,
|
|
||||||
TxRef: txID,
|
|
||||||
CallbackURL: "https://fortunebet.com/api/v1/payments/callback",
|
|
||||||
ReturnURL: "https://fortunebet.com/api/v1/payment-success",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call Chapa to initialize payment
|
|
||||||
var paymentURL string
|
|
||||||
maxRetries := 3
|
|
||||||
for range maxRetries {
|
|
||||||
paymentURL, err = s.chapaClient.InitPayment(ctx, paymentReq)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(1 * time.Second) // Backoff
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit DB transaction
|
|
||||||
if err := tx.Commit(ctx); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return paymentURL, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetSupportedBanks() ([]domain.ChapaSupportedBank, error) {
|
|
||||||
banks, err := s.chapaClient.FetchBanks()
|
|
||||||
fmt.Printf("\n\nfetched banks: %+v\n\n", banks)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add formatting logic (same as in original controller)
|
|
||||||
for i := range banks {
|
|
||||||
if banks[i].IsMobilemoney != nil && *(banks[i].IsMobilemoney) == 1 {
|
|
||||||
banks[i].AcctNumberRegex = "/^09[0-9]{8}$/"
|
|
||||||
banks[i].ExampleValue = "0952097177"
|
|
||||||
} else {
|
|
||||||
switch banks[i].AcctLength {
|
|
||||||
case 8:
|
|
||||||
banks[i].ExampleValue = "16967608"
|
|
||||||
case 13:
|
|
||||||
banks[i].ExampleValue = "1000222215735"
|
|
||||||
case 14:
|
|
||||||
banks[i].ExampleValue = "01320089280800"
|
|
||||||
case 16:
|
|
||||||
banks[i].ExampleValue = "1000222215735123"
|
|
||||||
}
|
|
||||||
banks[i].AcctNumberRegex = formatRegex(banks[i].AcctLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return banks, nil
|
return banks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatRegex(length int) string {
|
|
||||||
return fmt.Sprintf("/^[0-9]{%d}$/", length)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,6 @@ type CompanyStore interface {
|
||||||
GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error)
|
GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error)
|
||||||
UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error)
|
UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error)
|
||||||
DeleteCompany(ctx context.Context, id int64) error
|
DeleteCompany(ctx context.Context, id int64) error
|
||||||
|
|
||||||
|
GetCompanyCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,6 @@ type NotificationStore interface {
|
||||||
ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) // New method
|
ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) // New method
|
||||||
CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error)
|
CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error)
|
||||||
GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error)
|
GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error)
|
||||||
|
|
||||||
|
GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *confi
|
||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) addConnection(ctx context.Context, recipientID int64, c *websocket.Conn) {
|
func (s *Service) addConnection(recipientID int64, c *websocket.Conn) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
s.logger.Warn("[NotificationSvc.AddConnection] Attempted to add nil WebSocket connection", "recipientID", recipientID)
|
s.logger.Warn("[NotificationSvc.AddConnection] Attempted to add nil WebSocket connection", "recipientID", recipientID)
|
||||||
return
|
return
|
||||||
|
|
@ -134,7 +134,7 @@ func (s *Service) GetAllNotifications(ctx context.Context, limit, offset int) ([
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error {
|
func (s *Service) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error {
|
||||||
s.addConnection(ctx, recipientID, c)
|
s.addConnection(recipientID, c)
|
||||||
s.logger.Info("[NotificationSvc.ConnectWebSocket] WebSocket connection established", "recipientID", recipientID)
|
s.logger.Info("[NotificationSvc.ConnectWebSocket] WebSocket connection established", "recipientID", recipientID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -283,3 +283,7 @@ func (s *Service) retryFailedNotifications() {
|
||||||
func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) {
|
func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) {
|
||||||
return s.repo.CountUnreadNotifications(ctx, recipient_id)
|
return s.repo.CountUnreadNotifications(ctx, recipient_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// func (s *Service) GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error){
|
||||||
|
// return s.repo.Get(ctx, filter)
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReportStore interface {
|
type ReportStore interface {
|
||||||
GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (DashboardSummary, error)
|
GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (domain.DashboardSummary, error)
|
||||||
GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]BetAnalysis, error)
|
GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]domain.BetAnalysis, error)
|
||||||
GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]CustomerActivity, error)
|
GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.CustomerActivity, error)
|
||||||
GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]BranchPerformance, error)
|
GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.BranchPerformance, error)
|
||||||
GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]SportPerformance, error)
|
GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.SportPerformance, error)
|
||||||
|
// GetNotificationReport(ctx context.Context, filter domain.ReportFilter) (domain.NotificationReport, error)
|
||||||
|
// GetCashierPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CashierPerformance, error)
|
||||||
|
// GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,16 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
|
||||||
|
// notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
|
// virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -26,6 +29,9 @@ type Service struct {
|
||||||
transactionStore transaction.TransactionStore
|
transactionStore transaction.TransactionStore
|
||||||
branchStore branch.BranchStore
|
branchStore branch.BranchStore
|
||||||
userStore user.UserStore
|
userStore user.UserStore
|
||||||
|
companyStore company.CompanyStore
|
||||||
|
virtulaGamesStore repository.VirtualGameRepository
|
||||||
|
notificationStore repository.NotificationRepository
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,6 +41,9 @@ func NewService(
|
||||||
transactionStore transaction.TransactionStore,
|
transactionStore transaction.TransactionStore,
|
||||||
branchStore branch.BranchStore,
|
branchStore branch.BranchStore,
|
||||||
userStore user.UserStore,
|
userStore user.UserStore,
|
||||||
|
companyStore company.CompanyStore,
|
||||||
|
virtulaGamesStore repository.VirtualGameRepository,
|
||||||
|
notificationStore repository.NotificationRepository,
|
||||||
logger *slog.Logger,
|
logger *slog.Logger,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
|
|
@ -43,36 +52,20 @@ func NewService(
|
||||||
transactionStore: transactionStore,
|
transactionStore: transactionStore,
|
||||||
branchStore: branchStore,
|
branchStore: branchStore,
|
||||||
userStore: userStore,
|
userStore: userStore,
|
||||||
|
companyStore: companyStore,
|
||||||
|
virtulaGamesStore: virtulaGamesStore,
|
||||||
|
notificationStore: notificationStore,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DashboardSummary represents comprehensive dashboard metrics
|
|
||||||
type DashboardSummary struct {
|
|
||||||
TotalStakes domain.Currency `json:"total_stakes"`
|
|
||||||
TotalBets int64 `json:"total_bets"`
|
|
||||||
ActiveBets int64 `json:"active_bets"`
|
|
||||||
WinBalance domain.Currency `json:"win_balance"`
|
|
||||||
TotalWins int64 `json:"total_wins"`
|
|
||||||
TotalLosses int64 `json:"total_losses"`
|
|
||||||
CustomerCount int64 `json:"customer_count"`
|
|
||||||
Profit domain.Currency `json:"profit"`
|
|
||||||
WinRate float64 `json:"win_rate"`
|
|
||||||
AverageStake domain.Currency `json:"average_stake"`
|
|
||||||
TotalDeposits domain.Currency `json:"total_deposits"`
|
|
||||||
TotalWithdrawals domain.Currency `json:"total_withdrawals"`
|
|
||||||
ActiveCustomers int64 `json:"active_customers"`
|
|
||||||
BranchesCount int64 `json:"branches_count"`
|
|
||||||
ActiveBranches int64 `json:"active_branches"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDashboardSummary returns comprehensive dashboard metrics
|
// GetDashboardSummary returns comprehensive dashboard metrics
|
||||||
func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (DashboardSummary, error) {
|
func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (domain.DashboardSummary, error) {
|
||||||
if err := validateTimeRange(filter); err != nil {
|
if err := validateTimeRange(filter); err != nil {
|
||||||
return DashboardSummary{}, err
|
return domain.DashboardSummary{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var summary DashboardSummary
|
var summary domain.DashboardSummary
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Get bets summary
|
// Get bets summary
|
||||||
|
|
@ -80,28 +73,75 @@ func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportF
|
||||||
s.betStore.GetBetSummary(ctx, filter)
|
s.betStore.GetBetSummary(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("failed to get bet summary", "error", err)
|
s.logger.Error("failed to get bet summary", "error", err)
|
||||||
return DashboardSummary{}, err
|
return domain.DashboardSummary{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get customer metrics
|
// Get customer metrics
|
||||||
summary.CustomerCount, summary.ActiveCustomers, err = s.userStore.GetCustomerCounts(ctx, filter)
|
summary.CustomerCount, summary.ActiveCustomers, summary.InactiveCustomers, err = s.userStore.GetCustomerCounts(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("failed to get customer counts", "error", err)
|
s.logger.Error("failed to get customer counts", "error", err)
|
||||||
return DashboardSummary{}, err
|
return domain.DashboardSummary{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get branch metrics
|
// Get branch metrics
|
||||||
summary.BranchesCount, summary.ActiveBranches, err = s.branchStore.GetBranchCounts(ctx, filter)
|
summary.BranchesCount, summary.ActiveBranches, summary.InactiveBranches, err = s.branchStore.GetBranchCounts(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("failed to get branch counts", "error", err)
|
s.logger.Error("failed to get branch counts", "error", err)
|
||||||
return DashboardSummary{}, err
|
return domain.DashboardSummary{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get transaction metrics
|
// Get transaction metrics
|
||||||
summary.TotalDeposits, summary.TotalWithdrawals, err = s.transactionStore.GetTransactionTotals(ctx, filter)
|
summary.TotalDeposits, summary.TotalWithdrawals, err = s.transactionStore.GetTransactionTotals(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("failed to get transaction totals", "error", err)
|
s.logger.Error("failed to get transaction totals", "error", err)
|
||||||
return DashboardSummary{}, err
|
return domain.DashboardSummary{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user role metrics
|
||||||
|
summary.TotalCashiers, summary.ActiveCashiers, summary.InactiveCashiers, err = s.userStore.GetRoleCounts(ctx, string(domain.RoleCashier), filter)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("failed to get cashier counts", "error", err)
|
||||||
|
return domain.DashboardSummary{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.TotalManagers, summary.ActiveManagers, summary.InactiveManagers, err = s.userStore.GetRoleCounts(ctx, string(domain.RoleBranchManager), filter)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("failed to get manager counts", "error", err)
|
||||||
|
return domain.DashboardSummary{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.TotalAdmins, summary.ActiveAdmins, summary.InactiveAdmins, err = s.userStore.GetRoleCounts(ctx, string(domain.RoleAdmin), filter)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("failed to get admin counts", "error", err)
|
||||||
|
return domain.DashboardSummary{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get wallet metrics
|
||||||
|
summary.TotalWallets, err = s.walletStore.GetTotalWallets(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("failed to get wallet counts", "error", err)
|
||||||
|
return domain.DashboardSummary{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get sport/game metrics
|
||||||
|
summary.TotalGames, summary.ActiveGames, summary.InactiveGames, err = s.virtulaGamesStore.GetGameCounts(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("failed to get game counts", "error", err)
|
||||||
|
return domain.DashboardSummary{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get company metrics
|
||||||
|
summary.TotalCompanies, summary.ActiveCompanies, summary.InactiveCompanies, err = s.companyStore.GetCompanyCounts(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("failed to get company counts", "error", err)
|
||||||
|
return domain.DashboardSummary{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get notification metrics
|
||||||
|
summary.TotalNotifications, summary.ReadNotifications, summary.UnreadNotifications, err = s.notificationStore.GetNotificationCounts(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("failed to get notification counts", "error", err)
|
||||||
|
return domain.DashboardSummary{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate derived metrics
|
// Calculate derived metrics
|
||||||
|
|
@ -114,23 +154,8 @@ func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportF
|
||||||
return summary, nil
|
return summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BetAnalysis represents detailed bet analysis
|
// Getdomain.BetAnalysis returns detailed bet analysis
|
||||||
type BetAnalysis struct {
|
func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]domain.BetAnalysis, error) {
|
||||||
Date time.Time `json:"date"`
|
|
||||||
TotalBets int64 `json:"total_bets"`
|
|
||||||
TotalStakes domain.Currency `json:"total_stakes"`
|
|
||||||
TotalWins int64 `json:"total_wins"`
|
|
||||||
TotalPayouts domain.Currency `json:"total_payouts"`
|
|
||||||
Profit domain.Currency `json:"profit"`
|
|
||||||
MostPopularSport string `json:"most_popular_sport"`
|
|
||||||
MostPopularMarket string `json:"most_popular_market"`
|
|
||||||
HighestStake domain.Currency `json:"highest_stake"`
|
|
||||||
HighestPayout domain.Currency `json:"highest_payout"`
|
|
||||||
AverageOdds float64 `json:"average_odds"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBetAnalysis returns detailed bet analysis
|
|
||||||
func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]BetAnalysis, error) {
|
|
||||||
if err := validateTimeRange(filter); err != nil {
|
if err := validateTimeRange(filter); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -164,9 +189,9 @@ func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine data into analysis
|
// Combine data into analysis
|
||||||
var analysis []BetAnalysis
|
var analysis []domain.BetAnalysis
|
||||||
for _, stat := range betStats {
|
for _, stat := range betStats {
|
||||||
a := BetAnalysis{
|
a := domain.BetAnalysis{
|
||||||
Date: stat.Date,
|
Date: stat.Date,
|
||||||
TotalBets: stat.TotalBets,
|
TotalBets: stat.TotalBets,
|
||||||
TotalStakes: stat.TotalStakes,
|
TotalStakes: stat.TotalStakes,
|
||||||
|
|
@ -203,27 +228,8 @@ func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter
|
||||||
return analysis, nil
|
return analysis, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CustomerActivity represents customer activity metrics
|
// Getdomain.CustomerActivity returns customer activity report
|
||||||
type CustomerActivity struct {
|
func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.CustomerActivity, error) {
|
||||||
CustomerID int64 `json:"customer_id"`
|
|
||||||
CustomerName string `json:"customer_name"`
|
|
||||||
TotalBets int64 `json:"total_bets"`
|
|
||||||
TotalStakes domain.Currency `json:"total_stakes"`
|
|
||||||
TotalWins int64 `json:"total_wins"`
|
|
||||||
TotalPayouts domain.Currency `json:"total_payouts"`
|
|
||||||
Profit domain.Currency `json:"profit"`
|
|
||||||
FirstBetDate time.Time `json:"first_bet_date"`
|
|
||||||
LastBetDate time.Time `json:"last_bet_date"`
|
|
||||||
FavoriteSport string `json:"favorite_sport"`
|
|
||||||
FavoriteMarket string `json:"favorite_market"`
|
|
||||||
AverageStake domain.Currency `json:"average_stake"`
|
|
||||||
AverageOdds float64 `json:"average_odds"`
|
|
||||||
WinRate float64 `json:"win_rate"`
|
|
||||||
ActivityLevel string `json:"activity_level"` // High, Medium, Low
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCustomerActivity returns customer activity report
|
|
||||||
func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]CustomerActivity, error) {
|
|
||||||
if err := validateTimeRange(filter); err != nil {
|
if err := validateTimeRange(filter); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -250,9 +256,9 @@ func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportF
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine data into activity report
|
// Combine data into activity report
|
||||||
var activities []CustomerActivity
|
var activities []domain.CustomerActivity
|
||||||
for _, bet := range customerBets {
|
for _, bet := range customerBets {
|
||||||
activity := CustomerActivity{
|
activity := domain.CustomerActivity{
|
||||||
CustomerID: bet.CustomerID,
|
CustomerID: bet.CustomerID,
|
||||||
TotalBets: bet.TotalBets,
|
TotalBets: bet.TotalBets,
|
||||||
TotalStakes: bet.TotalStakes,
|
TotalStakes: bet.TotalStakes,
|
||||||
|
|
@ -295,27 +301,8 @@ func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportF
|
||||||
return activities, nil
|
return activities, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BranchPerformance represents branch performance metrics
|
// Getdomain.BranchPerformance returns branch performance report
|
||||||
type BranchPerformance struct {
|
func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.BranchPerformance, error) {
|
||||||
BranchID int64 `json:"branch_id"`
|
|
||||||
BranchName string `json:"branch_name"`
|
|
||||||
Location string `json:"location"`
|
|
||||||
ManagerName string `json:"manager_name"`
|
|
||||||
TotalBets int64 `json:"total_bets"`
|
|
||||||
TotalStakes domain.Currency `json:"total_stakes"`
|
|
||||||
TotalWins int64 `json:"total_wins"`
|
|
||||||
TotalPayouts domain.Currency `json:"total_payouts"`
|
|
||||||
Profit domain.Currency `json:"profit"`
|
|
||||||
CustomerCount int64 `json:"customer_count"`
|
|
||||||
Deposits domain.Currency `json:"deposits"`
|
|
||||||
Withdrawals domain.Currency `json:"withdrawals"`
|
|
||||||
WinRate float64 `json:"win_rate"`
|
|
||||||
AverageStake domain.Currency `json:"average_stake"`
|
|
||||||
PerformanceScore float64 `json:"performance_score"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBranchPerformance returns branch performance report
|
|
||||||
func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]BranchPerformance, error) {
|
|
||||||
// Get branch bet activity
|
// Get branch bet activity
|
||||||
branchBets, err := s.betStore.GetBranchBetActivity(ctx, filter)
|
branchBets, err := s.betStore.GetBranchBetActivity(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -345,9 +332,9 @@ func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.Report
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine data into performance report
|
// Combine data into performance report
|
||||||
var performances []BranchPerformance
|
var performances []domain.BranchPerformance
|
||||||
for _, bet := range branchBets {
|
for _, bet := range branchBets {
|
||||||
performance := BranchPerformance{
|
performance := domain.BranchPerformance{
|
||||||
BranchID: bet.BranchID,
|
BranchID: bet.BranchID,
|
||||||
TotalBets: bet.TotalBets,
|
TotalBets: bet.TotalBets,
|
||||||
TotalStakes: bet.TotalStakes,
|
TotalStakes: bet.TotalStakes,
|
||||||
|
|
@ -394,24 +381,8 @@ func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.Report
|
||||||
return performances, nil
|
return performances, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SportPerformance represents sport performance metrics
|
// Getdomain.SportPerformance returns sport performance report
|
||||||
type SportPerformance struct {
|
func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.SportPerformance, error) {
|
||||||
SportID string `json:"sport_id"`
|
|
||||||
SportName string `json:"sport_name"`
|
|
||||||
TotalBets int64 `json:"total_bets"`
|
|
||||||
TotalStakes domain.Currency `json:"total_stakes"`
|
|
||||||
TotalWins int64 `json:"total_wins"`
|
|
||||||
TotalPayouts domain.Currency `json:"total_payouts"`
|
|
||||||
Profit domain.Currency `json:"profit"`
|
|
||||||
PopularityRank int `json:"popularity_rank"`
|
|
||||||
WinRate float64 `json:"win_rate"`
|
|
||||||
AverageStake domain.Currency `json:"average_stake"`
|
|
||||||
AverageOdds float64 `json:"average_odds"`
|
|
||||||
MostPopularMarket string `json:"most_popular_market"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSportPerformance returns sport performance report
|
|
||||||
func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]SportPerformance, error) {
|
|
||||||
// Get sport bet activity
|
// Get sport bet activity
|
||||||
sportBets, err := s.betStore.GetSportBetActivity(ctx, filter)
|
sportBets, err := s.betStore.GetSportBetActivity(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -434,9 +405,9 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine data into performance report
|
// Combine data into performance report
|
||||||
var performances []SportPerformance
|
var performances []domain.SportPerformance
|
||||||
for _, bet := range sportBets {
|
for _, bet := range sportBets {
|
||||||
performance := SportPerformance{
|
performance := domain.SportPerformance{
|
||||||
SportID: bet.SportID,
|
SportID: bet.SportID,
|
||||||
TotalBets: bet.TotalBets,
|
TotalBets: bet.TotalBets,
|
||||||
TotalStakes: bet.TotalStakes,
|
TotalStakes: bet.TotalStakes,
|
||||||
|
|
@ -477,6 +448,164 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF
|
||||||
return performances, nil
|
return performances, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// func (s *Service) GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error) {
|
||||||
|
// // Get company bet activity
|
||||||
|
// companyBets, err := s.betStore.GetCompanyBetActivity(ctx, filter)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("failed to get company bet activity", "error", err)
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Get company details
|
||||||
|
// companyDetails, err := s.branchStore.GetCompanyDetails(ctx, filter)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("failed to get company details", "error", err)
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Get company branches
|
||||||
|
// companyBranches, err := s.branchStore.GetCompanyBranchCounts(ctx, filter)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("failed to get company branch counts", "error", err)
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Combine data into performance report
|
||||||
|
// var performances []domain.CompanyPerformance
|
||||||
|
// for _, bet := range companyBets {
|
||||||
|
// performance := domain.CompanyPerformance{
|
||||||
|
// CompanyID: bet.CompanyID,
|
||||||
|
// TotalBets: bet.TotalBets,
|
||||||
|
// TotalStakes: bet.TotalStakes,
|
||||||
|
// TotalWins: bet.TotalWins,
|
||||||
|
// TotalPayouts: bet.TotalPayouts,
|
||||||
|
// Profit: bet.TotalStakes - bet.TotalPayouts,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Add company details
|
||||||
|
// if details, ok := companyDetails[bet.CompanyID]; ok {
|
||||||
|
// performance.CompanyName = details.Name
|
||||||
|
// performance.ContactEmail = details.ContactEmail
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Add branch counts
|
||||||
|
// if branches, ok := companyBranches[bet.CompanyID]; ok {
|
||||||
|
// performance.TotalBranches = branches.Total
|
||||||
|
// performance.ActiveBranches = branches.Active
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Calculate metrics
|
||||||
|
// if bet.TotalBets > 0 {
|
||||||
|
// performance.WinRate = float64(bet.TotalWins) / float64(bet.TotalBets) * 100
|
||||||
|
// performance.AverageStake = bet.TotalStakes / domain.Currency(bet.TotalBets)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// performances = append(performances, performance)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Sort by profit (descending)
|
||||||
|
// sort.Slice(performances, func(i, j int) bool {
|
||||||
|
// return performances[i].Profit > performances[j].Profit
|
||||||
|
// })
|
||||||
|
|
||||||
|
// return performances, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// GetCashierPerformance returns cashier performance report
|
||||||
|
// func (s *Service) GetCashierPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CashierPerformance, error) {
|
||||||
|
// // Get cashier bet activity
|
||||||
|
// cashierBets, err := s.betStore.GetCashierBetActivity(ctx, filter)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("failed to get cashier bet activity", "error", err)
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Get cashier details
|
||||||
|
// cashierDetails, err := s.userStore.GetCashierDetails(ctx, filter)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("failed to get cashier details", "error", err)
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Get cashier transactions
|
||||||
|
// cashierTransactions, err := s.transactionStore.GetCashierTransactionTotals(ctx, filter)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("failed to get cashier transactions", "error", err)
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Combine data into performance report
|
||||||
|
// var performances []domain.CashierPerformance
|
||||||
|
// for _, bet := range cashierBets {
|
||||||
|
// performance := domain.CashierPerformance{
|
||||||
|
// CashierID: bet.CashierID,
|
||||||
|
// TotalBets: bet.TotalBets,
|
||||||
|
// TotalStakes: bet.TotalStakes,
|
||||||
|
// TotalWins: bet.TotalWins,
|
||||||
|
// TotalPayouts: bet.TotalPayouts,
|
||||||
|
// Profit: bet.TotalStakes - bet.TotalPayouts,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Add cashier details
|
||||||
|
// if details, ok := cashierDetails[bet.CashierID]; ok {
|
||||||
|
// performance.CashierName = details.Name
|
||||||
|
// performance.BranchID = details.BranchID
|
||||||
|
// performance.BranchName = details.BranchName
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Add transactions
|
||||||
|
// if transactions, ok := cashierTransactions[bet.CashierID]; ok {
|
||||||
|
// performance.Deposits = transactions.Deposits
|
||||||
|
// performance.Withdrawals = transactions.Withdrawals
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Calculate metrics
|
||||||
|
// if bet.TotalBets > 0 {
|
||||||
|
// performance.WinRate = float64(bet.TotalWins) / float64(bet.TotalBets) * 100
|
||||||
|
// performance.AverageStake = bet.TotalStakes / domain.Currency(bet.TotalBets)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// performances = append(performances, performance)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Sort by total stakes (descending)
|
||||||
|
// sort.Slice(performances, func(i, j int) bool {
|
||||||
|
// return performances[i].TotalStakes > performances[j].TotalStakes
|
||||||
|
// })
|
||||||
|
|
||||||
|
// return performances, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// GetNotificationReport returns notification statistics report
|
||||||
|
// func (s *Service) GetNotificationReport(ctx context.Context, filter domain.ReportFilter) (domain.NotificationReport, error) {
|
||||||
|
// // Get notification counts by type
|
||||||
|
// countsByType, err := s.notificationStore.GetNotificationCountsByType(ctx, filter)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("failed to get notification counts by type", "error", err)
|
||||||
|
// return domain.NotificationReport{}, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Get notification delivery stats
|
||||||
|
// deliveryStats, err := s.notificationStore.GetNotificationDeliveryStats(ctx, filter)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("failed to get notification delivery stats", "error", err)
|
||||||
|
// return domain.NotificationReport{}, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Get most active notification recipients
|
||||||
|
// activeRecipients, err := s.notificationStore.GetMostActiveNotificationRecipients(ctx, filter)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("failed to get active notification recipients", "error", err)
|
||||||
|
// return domain.NotificationReport{}, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return domain.NotificationReport{
|
||||||
|
// CountsByType: countsByType,
|
||||||
|
// DeliveryStats: deliveryStats,
|
||||||
|
// ActiveRecipients: activeRecipients,
|
||||||
|
// }, nil
|
||||||
|
// }
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
func validateTimeRange(filter domain.ReportFilter) error {
|
func validateTimeRange(filter domain.ReportFilter) error {
|
||||||
if filter.StartTime.Valid && filter.EndTime.Valid {
|
if filter.StartTime.Valid && filter.EndTime.Valid {
|
||||||
|
|
@ -498,7 +627,7 @@ func calculateActivityLevel(totalBets int64, totalStakes domain.Currency) string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculatePerformanceScore(perf BranchPerformance) float64 {
|
func calculatePerformanceScore(perf domain.BranchPerformance) float64 {
|
||||||
// Simple scoring algorithm - can be enhanced based on business rules
|
// Simple scoring algorithm - can be enhanced based on business rules
|
||||||
profitScore := float64(perf.Profit) / 1000
|
profitScore := float64(perf.Profit) / 1000
|
||||||
customerScore := float64(perf.CustomerCount) * 0.1
|
customerScore := float64(perf.CustomerCount) * 0.1
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,10 @@ type UserStore interface {
|
||||||
SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error)
|
SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error)
|
||||||
UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error // identifier verified email or phone
|
UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error // identifier verified email or phone
|
||||||
|
|
||||||
GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active int64, err error)
|
GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
|
||||||
GetCustomerDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerDetail, error)
|
GetCustomerDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerDetail, error)
|
||||||
GetBranchCustomerCounts(ctx context.Context, filter domain.ReportFilter) (map[int64]int64, error)
|
GetBranchCustomerCounts(ctx context.Context, filter domain.ReportFilter) (map[int64]int64, error)
|
||||||
|
GetRoleCounts(ctx context.Context, role string, filter domain.ReportFilter) (total, active, inactive int64, err error)
|
||||||
}
|
}
|
||||||
type SmsGateway interface {
|
type SmsGateway interface {
|
||||||
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
|
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,6 @@ import (
|
||||||
type VirtualGameService interface {
|
type VirtualGameService interface {
|
||||||
GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error)
|
GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error)
|
||||||
HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error
|
HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error
|
||||||
}
|
|
||||||
|
|
||||||
|
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ type service struct {
|
||||||
repo repository.VirtualGameRepository
|
repo repository.VirtualGameRepository
|
||||||
walletSvc wallet.Service
|
walletSvc wallet.Service
|
||||||
store *repository.Store
|
store *repository.Store
|
||||||
|
// virtualGameStore repository.VirtualGameRepository
|
||||||
config *config.Config
|
config *config.Config
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
@ -166,3 +167,7 @@ func (s *service) verifySignature(callback *domain.PopOKCallback) bool {
|
||||||
expected := hex.EncodeToString(h.Sum(nil))
|
expected := hex.EncodeToString(h.Sum(nil))
|
||||||
return expected == callback.Signature
|
return expected == callback.Signature
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
|
||||||
|
return s.repo.GetGameCounts(ctx, filter)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,14 @@ type WalletStore interface {
|
||||||
UpdateWalletActive(ctx context.Context, id int64, isActive bool) error
|
UpdateWalletActive(ctx context.Context, id int64, isActive bool) error
|
||||||
|
|
||||||
GetBalanceSummary(ctx context.Context, filter domain.ReportFilter) (domain.BalanceSummary, error)
|
GetBalanceSummary(ctx context.Context, filter domain.ReportFilter) (domain.BalanceSummary, error)
|
||||||
|
GetTotalWallets(ctx context.Context, filter domain.ReportFilter) (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransferStore interface {
|
type TransferStore interface {
|
||||||
CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error)
|
CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error)
|
||||||
GetAllTransfers(ctx context.Context) ([]domain.Transfer, error)
|
GetAllTransfers(ctx context.Context) ([]domain.Transfer, error)
|
||||||
GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error)
|
GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error)
|
||||||
|
GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error)
|
||||||
GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error)
|
GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error)
|
||||||
UpdateTransferVerification(ctx context.Context, id int64, verified bool) error
|
UpdateTransferVerification(ctx context.Context, id int64, verified bool) error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
|
func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
|
||||||
senderWallet, err := s.walletStore.GetWalletByID(ctx, transfer.SenderWalletID.Value)
|
senderWallet, err := s.walletStore.GetWalletByID(ctx, transfer.SenderWalletID)
|
||||||
receiverWallet, err := s.walletStore.GetWalletByID(ctx, transfer.ReceiverWalletID)
|
receiverWallet, err := s.walletStore.GetWalletByID(ctx, transfer.ReceiverWalletID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Transfer{}, fmt.Errorf("failed to get sender wallet: %w", err)
|
return domain.Transfer{}, fmt.Errorf("failed to get sender wallet: %w", err)
|
||||||
|
|
@ -39,7 +39,7 @@ func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTran
|
||||||
"current_balance": %d,
|
"current_balance": %d,
|
||||||
"wallet_id": %d,
|
"wallet_id": %d,
|
||||||
"notification_type": "customer_facing"
|
"notification_type": "customer_facing"
|
||||||
}`, transfer.Amount, senderWallet.Balance, transfer.SenderWalletID.Value)),
|
}`, transfer.Amount, senderWallet.Balance, transfer.SenderWalletID)),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send notification to admin team
|
// Send notification to admin team
|
||||||
|
|
@ -53,7 +53,7 @@ func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTran
|
||||||
Headline: "CREDIT WARNING: System Running Out of Funds",
|
Headline: "CREDIT WARNING: System Running Out of Funds",
|
||||||
Message: fmt.Sprintf(
|
Message: fmt.Sprintf(
|
||||||
"Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f",
|
"Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f",
|
||||||
transfer.SenderWalletID.Value,
|
transfer.SenderWalletID,
|
||||||
float64(senderWallet.Balance)/100,
|
float64(senderWallet.Balance)/100,
|
||||||
float64(transfer.Amount)/100,
|
float64(transfer.Amount)/100,
|
||||||
),
|
),
|
||||||
|
|
@ -64,7 +64,7 @@ func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTran
|
||||||
"balance": %d,
|
"balance": %d,
|
||||||
"required_amount": %d,
|
"required_amount": %d,
|
||||||
"notification_type": "admin_alert"
|
"notification_type": "admin_alert"
|
||||||
}`, transfer.SenderWalletID.Value, senderWallet.Balance, transfer.Amount),
|
}`, transfer.SenderWalletID, senderWallet.Balance, transfer.Amount),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send both notifications
|
// Send both notifications
|
||||||
|
|
@ -100,6 +100,10 @@ func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error
|
||||||
return s.transferStore.GetAllTransfers(ctx)
|
return s.transferStore.GetAllTransfers(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error) {
|
||||||
|
return s.transferStore.GetTransferByReference(ctx, reference)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) {
|
func (s *Service) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) {
|
||||||
return s.transferStore.GetTransferByID(ctx, id)
|
return s.transferStore.GetTransferByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +123,7 @@ func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransf
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to receiver
|
// Add to receiver
|
||||||
senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID.Value)
|
senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Transfer{}, err
|
return domain.Transfer{}, err
|
||||||
} else if senderWallet.Balance < transfer.Amount {
|
} else if senderWallet.Balance < transfer.Amount {
|
||||||
|
|
@ -188,10 +192,7 @@ func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiver
|
||||||
|
|
||||||
// Log the transfer so that if there is a mistake, it can be reverted
|
// Log the transfer so that if there is a mistake, it can be reverted
|
||||||
transfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
|
transfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
|
||||||
SenderWalletID: domain.ValidInt64{
|
SenderWalletID: senderID,
|
||||||
Value: senderID,
|
|
||||||
Valid: true,
|
|
||||||
},
|
|
||||||
CashierID: cashierID,
|
CashierID: cashierID,
|
||||||
ReceiverWalletID: receiverID,
|
ReceiverWalletID: receiverID,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
|
|
|
||||||
|
|
@ -1,464 +1,163 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// "bytes"
|
|
||||||
// "encoding/json"
|
|
||||||
// "fmt"
|
|
||||||
// "io"
|
|
||||||
// "net/http"
|
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// // GetBanks godoc
|
// InitiateDeposit godoc
|
||||||
// // @Summary Get list of banks
|
// @Summary Initiate a deposit
|
||||||
// // @Description Fetch all supported banks from Chapa
|
// @Description Starts a new deposit process using Chapa payment gateway
|
||||||
// // @Tags Chapa
|
|
||||||
// // @Accept json
|
|
||||||
// // @Produce json
|
|
||||||
// // @Success 200 {object} domain.ChapaSupportedBanksResponse
|
|
||||||
// // @Router /api/v1/chapa/banks [get]
|
|
||||||
// func (h *Handler) GetBanks(c *fiber.Ctx) error {
|
|
||||||
// httpReq, err := http.NewRequest("GET", h.Cfg.CHAPA_BASE_URL+"/banks", nil)
|
|
||||||
// // log.Printf("\n\nbase url is: %v\n\n", h.Cfg.CHAPA_BASE_URL)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(500).JSON(fiber.Map{"error": "Failed to create request", "details": err.Error()})
|
|
||||||
// }
|
|
||||||
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
|
|
||||||
|
|
||||||
// resp, err := http.DefaultClient.Do(httpReq)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(500).JSON(fiber.Map{"error": "Failed to fetch banks", "details": err.Error()})
|
|
||||||
// }
|
|
||||||
// defer resp.Body.Close()
|
|
||||||
|
|
||||||
// body, err := io.ReadAll(resp.Body)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(500).JSON(fiber.Map{"error": "Failed to read response", "details": err.Error()})
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return c.Status(resp.StatusCode).Type("json").Send(body)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // InitializePayment godoc
|
|
||||||
// // @Summary Initialize a payment transaction
|
|
||||||
// // @Description Initiate a payment through Chapa
|
|
||||||
// // @Tags Chapa
|
|
||||||
// // @Accept json
|
|
||||||
// // @Produce json
|
|
||||||
// // @Param payload body domain.InitPaymentRequest true "Payment initialization request"
|
|
||||||
// // @Success 200 {object} domain.InitPaymentResponse
|
|
||||||
// // @Router /api/v1/chapa/payments/initialize [post]
|
|
||||||
// func (h *Handler) InitializePayment(c *fiber.Ctx) error {
|
|
||||||
// var req InitPaymentRequest
|
|
||||||
// if err := c.BodyParser(&req); err != nil {
|
|
||||||
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
||||||
// "error": "Invalid request body",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Generate and assign a unique transaction reference
|
|
||||||
// req.TxRef = uuid.New().String()
|
|
||||||
|
|
||||||
// payload, err := json.Marshal(req)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(500).JSON(fiber.Map{
|
|
||||||
// "error": "Failed to serialize request",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transaction/initialize", bytes.NewBuffer(payload))
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(500).JSON(fiber.Map{
|
|
||||||
// "error": "Failed to create request",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
|
|
||||||
// httpReq.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
// resp, err := http.DefaultClient.Do(httpReq)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(500).JSON(fiber.Map{
|
|
||||||
// "error": "Failed to initialize payment",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// defer resp.Body.Close()
|
|
||||||
|
|
||||||
// body, err := io.ReadAll(resp.Body)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(500).JSON(fiber.Map{
|
|
||||||
// "error": "Failed to read response",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return c.Status(resp.StatusCode).Type("json").Send(body)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // VerifyTransaction godoc
|
|
||||||
// // @Summary Verify a payment transaction
|
|
||||||
// // @Description Verify the transaction status from Chapa using tx_ref
|
|
||||||
// // @Tags Chapa
|
|
||||||
// // @Accept json
|
|
||||||
// // @Produce json
|
|
||||||
// // @Param tx_ref path string true "Transaction Reference"
|
|
||||||
// // @Success 200 {object} domain.VerifyTransactionResponse
|
|
||||||
// // @Router /api/v1/chapa/payments/verify/{tx_ref} [get]
|
|
||||||
// func (h *Handler) VerifyTransaction(c *fiber.Ctx) error {
|
|
||||||
// txRef := c.Params("tx_ref")
|
|
||||||
// if txRef == "" {
|
|
||||||
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
||||||
// "error": "Missing transaction reference",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// url := fmt.Sprintf("%s/transaction/verify/%s", h.Cfg.CHAPA_BASE_URL, txRef)
|
|
||||||
|
|
||||||
// httpReq, err := http.NewRequest("GET", url, nil)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(500).JSON(fiber.Map{
|
|
||||||
// "error": "Failed to create request",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
|
|
||||||
|
|
||||||
// resp, err := http.DefaultClient.Do(httpReq)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(500).JSON(fiber.Map{
|
|
||||||
// "error": "Failed to verify transaction",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// defer resp.Body.Close()
|
|
||||||
|
|
||||||
// body, err := io.ReadAll(resp.Body)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(500).JSON(fiber.Map{
|
|
||||||
// "error": "Failed to read response",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return c.Status(resp.StatusCode).Type("json").Send(body)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // ReceiveWebhook godoc
|
|
||||||
// // @Summary Receive Chapa webhook
|
|
||||||
// // @Description Endpoint to receive webhook payloads from Chapa
|
|
||||||
// // @Tags Chapa
|
|
||||||
// // @Accept json
|
|
||||||
// // @Produce json
|
|
||||||
// // @Param payload body object true "Webhook Payload (dynamic)"
|
|
||||||
// // @Success 200 {string} string "ok"
|
|
||||||
// // @Router /api/v1/chapa/payments/callback [post]
|
|
||||||
// func (h *Handler) ReceiveWebhook(c *fiber.Ctx) error {
|
|
||||||
// var payload map[string]interface{}
|
|
||||||
// if err := c.BodyParser(&payload); err != nil {
|
|
||||||
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
||||||
// "error": "Invalid webhook data",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// h.logger.Info("Chapa webhook received", "payload", payload)
|
|
||||||
|
|
||||||
// // Optional: you can verify tx_ref here again if needed
|
|
||||||
|
|
||||||
// return c.SendStatus(fiber.StatusOK)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // CreateTransfer godoc
|
|
||||||
// // @Summary Create a money transfer
|
|
||||||
// // @Description Initiate a transfer request via Chapa
|
|
||||||
// // @Tags Chapa
|
|
||||||
// // @Accept json
|
|
||||||
// // @Produce json
|
|
||||||
// // @Param payload body domain.TransferRequest true "Transfer request body"
|
|
||||||
// // @Success 200 {object} domain.CreateTransferResponse
|
|
||||||
// // @Router /api/v1/chapa/transfers [post]
|
|
||||||
// func (h *Handler) CreateTransfer(c *fiber.Ctx) error {
|
|
||||||
// var req TransferRequest
|
|
||||||
// if err := c.BodyParser(&req); err != nil {
|
|
||||||
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
||||||
// "error": "Invalid request",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Inject unique transaction reference
|
|
||||||
// req.Reference = uuid.New().String()
|
|
||||||
|
|
||||||
// payload, err := json.Marshal(req)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
||||||
// "error": "Failed to serialize request",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transfers", bytes.NewBuffer(payload))
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
||||||
// "error": "Failed to create HTTP request",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
|
|
||||||
// httpReq.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
// resp, err := http.DefaultClient.Do(httpReq)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
||||||
// "error": "Transfer request failed",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// defer resp.Body.Close()
|
|
||||||
|
|
||||||
// body, err := io.ReadAll(resp.Body)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
||||||
// "error": "Failed to read response",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return c.Status(resp.StatusCode).Type("json").Send(body)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // VerifyTransfer godoc
|
|
||||||
// // @Summary Verify a transfer
|
|
||||||
// // @Description Check the status of a money transfer via reference
|
|
||||||
// // @Tags Chapa
|
|
||||||
// // @Accept json
|
|
||||||
// // @Produce json
|
|
||||||
// // @Param transfer_ref path string true "Transfer Reference"
|
|
||||||
// // @Success 200 {object} domain.VerifyTransferResponse
|
|
||||||
// // @Router /api/v1/chapa/transfers/verify/{transfer_ref} [get]
|
|
||||||
// func (h *Handler) VerifyTransfer(c *fiber.Ctx) error {
|
|
||||||
// transferRef := c.Params("transfer_ref")
|
|
||||||
// if transferRef == "" {
|
|
||||||
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
||||||
// "error": "Missing transfer reference in URL",
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// url := fmt.Sprintf("%s/transfers/verify/%s", h.Cfg.CHAPA_BASE_URL, transferRef)
|
|
||||||
|
|
||||||
// httpReq, err := http.NewRequest("GET", url, nil)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
||||||
// "error": "Failed to create HTTP request",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
|
|
||||||
|
|
||||||
// resp, err := http.DefaultClient.Do(httpReq)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
||||||
// "error": "Verification request failed",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// defer resp.Body.Close()
|
|
||||||
|
|
||||||
// body, err := io.ReadAll(resp.Body)
|
|
||||||
// if err != nil {
|
|
||||||
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
||||||
// "error": "Failed to read response body",
|
|
||||||
// "details": err.Error(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return c.Status(resp.StatusCode).Type("json").Send(body)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// VerifyChapaPayment godoc
|
|
||||||
// @Summary Verifies Chapa webhook transaction
|
|
||||||
// @Tags Chapa
|
// @Tags Chapa
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param payload body domain.ChapaTransactionType true "Webhook Payload"
|
// @Param request body domain.ChapaDepositRequestPayload true "Deposit request"
|
||||||
// @Success 200 {object} domain.Response
|
// @Success 200 {object} domain.ChapaDepositResponse
|
||||||
// @Router /api/v1/chapa/payments/verify [post]
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
func (h *Handler) VerifyChapaPayment(c *fiber.Ctx) error {
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
var txType domain.ChapaTransactionType
|
|
||||||
if err := c.BodyParser(&txType); err != nil {
|
|
||||||
return domain.UnProcessableEntityResponse(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch txType.Type {
|
|
||||||
case "Payout":
|
|
||||||
var payload domain.ChapaWebHookTransfer
|
|
||||||
if err := c.BodyParser(&payload); err != nil {
|
|
||||||
return domain.UnProcessableEntityResponse(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.chapaSvc.HandleChapaTransferWebhook(c.Context(), payload); err != nil {
|
|
||||||
return domain.FiberErrorResponse(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
||||||
Message: "Chapa transfer verified successfully",
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
|
|
||||||
case "API":
|
|
||||||
var payload domain.ChapaWebHookPayment
|
|
||||||
if err := c.BodyParser(&payload); err != nil {
|
|
||||||
return domain.UnProcessableEntityResponse(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.chapaSvc.HandleChapaPaymentWebhook(c.Context(), payload); err != nil {
|
|
||||||
return domain.FiberErrorResponse(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
||||||
Message: "Chapa payment verified successfully",
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
|
|
||||||
default:
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.Response{
|
|
||||||
Message: "Invalid Chapa transaction type",
|
|
||||||
Success: false,
|
|
||||||
StatusCode: fiber.StatusBadRequest,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithdrawUsingChapa godoc
|
|
||||||
// @Summary Withdraw using Chapa
|
|
||||||
// @Description Initiates a withdrawal transaction using Chapa for the authenticated user.
|
|
||||||
// @Tags Chapa
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param request body domain.ChapaWithdrawRequest true "Chapa Withdraw Request"
|
|
||||||
// @Success 200 {object} domain.Response{data=string} "Withdrawal requested successfully"
|
|
||||||
// @Failure 400 {object} domain.Response "Invalid request"
|
|
||||||
// @Failure 401 {object} domain.Response "Unauthorized"
|
|
||||||
// @Failure 422 {object} domain.Response "Unprocessable Entity"
|
|
||||||
// @Failure 500 {object} domain.Response "Internal Server Error"
|
|
||||||
// @Router /api/v1/chapa/payments/withdraw [post]
|
|
||||||
func (h *Handler) WithdrawUsingChapa(c *fiber.Ctx) error {
|
|
||||||
var req domain.ChapaWithdrawRequest
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return domain.UnProcessableEntityResponse(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, ok := c.Locals("user_id").(int64)
|
|
||||||
if !ok || userID == 0 {
|
|
||||||
return c.Status(fiber.StatusUnauthorized).JSON(domain.Response{
|
|
||||||
Message: "Unauthorized",
|
|
||||||
Success: false,
|
|
||||||
StatusCode: fiber.StatusUnauthorized,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.chapaSvc.WithdrawUsingChapa(c.Context(), userID, req); err != nil {
|
|
||||||
return domain.FiberErrorResponse(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
||||||
Message: "Withdrawal requested successfully",
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DepositUsingChapa godoc
|
|
||||||
// @Summary Deposit money into user wallet using Chapa
|
|
||||||
// @Description Deposits money into user wallet from user account using Chapa
|
|
||||||
// @Tags Chapa
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param payload body domain.ChapaDepositRequest true "Deposit request payload"
|
|
||||||
// @Success 200 {object} domain.ChapaPaymentUrlResponseWrapper
|
|
||||||
// @Failure 400 {object} domain.Response "Invalid request"
|
|
||||||
// @Failure 422 {object} domain.Response "Validation error"
|
|
||||||
// @Failure 500 {object} domain.Response "Internal server error"
|
|
||||||
// @Router /api/v1/chapa/payments/deposit [post]
|
// @Router /api/v1/chapa/payments/deposit [post]
|
||||||
func (h *Handler) DepositUsingChapa(c *fiber.Ctx) error {
|
func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
|
||||||
// Extract user info from token (adjust as per your auth middleware)
|
// Get user ID from context (set by your auth middleware)
|
||||||
userID, ok := c.Locals("user_id").(int64)
|
userID, ok := c.Locals("user_id").(int64)
|
||||||
if !ok || userID == 0 {
|
if !ok {
|
||||||
return c.Status(fiber.StatusUnauthorized).JSON(domain.Response{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Unauthorized",
|
Error: "invalid user ID",
|
||||||
Success: false,
|
|
||||||
StatusCode: fiber.StatusUnauthorized,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var req domain.ChapaDepositRequest
|
var req domain.ChapaDepositRequestPayload
|
||||||
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
return domain.UnProcessableEntityResponse(c)
|
fmt.Sprintln("We first first are here init Chapa payment")
|
||||||
}
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Error: err.Error(),
|
||||||
// Validate input in domain/domain (you may have a Validate method)
|
|
||||||
if err := req.Validate(); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.Response{
|
|
||||||
Message: err.Error(),
|
|
||||||
Success: false,
|
|
||||||
StatusCode: fiber.StatusBadRequest,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call service to handle the deposit logic and get payment URL
|
amount := domain.Currency(req.Amount * 100)
|
||||||
paymentUrl, svcErr := h.chapaSvc.DepositUsingChapa(c.Context(), userID, req)
|
|
||||||
if svcErr != nil {
|
|
||||||
return domain.FiberErrorResponse(c, svcErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.ResponseWDataFactory[domain.ChapaPaymentUrlResponse]{
|
fmt.Sprintln("We are here init Chapa payment")
|
||||||
Data: domain.ChapaPaymentUrlResponse{
|
|
||||||
PaymentURL: paymentUrl,
|
checkoutURL, err := h.chapaSvc.InitiateDeposit(c.Context(), userID, amount)
|
||||||
},
|
if err != nil {
|
||||||
Response: domain.Response{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Deposit process started on wallet, fulfill payment using the URL provided",
|
Error: err.Error(),
|
||||||
Success: true,
|
Message: checkoutURL,
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadChapaBanks godoc
|
return c.Status(fiber.StatusOK).JSON(domain.ChapaDepositResponse{
|
||||||
// @Summary fetches chapa supported banks
|
CheckoutURL: checkoutURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookCallback godoc
|
||||||
|
// @Summary Chapa payment webhook callback (used by Chapa)
|
||||||
|
// @Description Handles payment notifications from Chapa
|
||||||
// @Tags Chapa
|
// @Tags Chapa
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} domain.ChapaSupportedBanksResponseWrapper
|
// @Param request body domain.ChapaWebhookPayload true "Webhook payload"
|
||||||
// @Failure 400,401,404,422,500 {object} domain.Response
|
// @Success 200 {object} map[string]interface{}
|
||||||
// @Router /api/v1/chapa/banks [get]
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
func (h *Handler) ReadChapaBanks(c *fiber.Ctx) error {
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
banks, err := h.chapaSvc.GetSupportedBanks()
|
// @Router /api/v1/chapa/payments/webhook/verify [post]
|
||||||
fmt.Printf("\n\nhandler fetched banks: %+v\n\n", banks)
|
func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
|
||||||
if err != nil {
|
// Verify webhook signature first
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.Response{
|
// signature := c.Get("Chapa-Signature")
|
||||||
Message: "Internal server error",
|
// if !verifySignature(signature, c.Body()) {
|
||||||
Success: false,
|
// return c.Status(fiber.StatusUnauthorized).JSON(ErrorResponse{
|
||||||
StatusCode: fiber.StatusInternalServerError,
|
// Error: "invalid signature",
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
TxRef string `json:"tx_ref"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BodyParser(&payload); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.ResponseWDataFactory[[]domain.ChapaSupportedBank]{
|
if err := h.chapaSvc.VerifyDeposit(c.Context(), payload.TxRef); err != nil {
|
||||||
Data: banks,
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Response: domain.Response{
|
Error: err.Error(),
|
||||||
Message: "read successful on chapa supported banks",
|
})
|
||||||
Success: true,
|
}
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
},
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
|
StatusCode: 200,
|
||||||
|
Message: "payment verified successfully",
|
||||||
|
Data: payload.TxRef,
|
||||||
|
Success: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyPayment godoc
|
||||||
|
// @Summary Verify a payment manually
|
||||||
|
// @Description Manually verify a payment using Chapa's API
|
||||||
|
// @Tags Chapa
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param tx_ref path string true "Transaction Reference"
|
||||||
|
// @Success 200 {object} domain.ChapaVerificationResponse
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/chapa/payments/manual/verify/{tx_ref} [get]
|
||||||
|
func (h *Handler) ManualVerifyPayment(c *fiber.Ctx) error {
|
||||||
|
txRef := c.Params("tx_ref")
|
||||||
|
if txRef == "" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to verify Chapa transaction",
|
||||||
|
Error: "Transaction reference is required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
verification, err := h.chapaSvc.ManualVerifyPayment(c.Context(), txRef)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to verify Chapa transaction",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(domain.ChapaVerificationResponse{
|
||||||
|
Status: string(verification.Status),
|
||||||
|
Amount: verification.Amount,
|
||||||
|
Currency: verification.Currency,
|
||||||
|
TxRef: txRef,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSupportedBanks godoc
|
||||||
|
// @Summary Get supported banks
|
||||||
|
// @Description Get list of banks supported by Chapa
|
||||||
|
// @Tags Chapa
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} domain.Bank
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /banks [get]
|
||||||
|
func (h *Handler) GetSupportedBanks(c *fiber.Ctx) error {
|
||||||
|
banks, err := h.chapaSvc.GetSupportedBanks(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Error: err.Error(),
|
||||||
|
Message: "Failed to fetch banks",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
|
Message: "Banks fetched successfully",
|
||||||
|
StatusCode: 200,
|
||||||
|
Success: true,
|
||||||
|
Data: banks,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ type Handler struct {
|
||||||
userSvc *user.Service
|
userSvc *user.Service
|
||||||
referralSvc referralservice.ReferralStore
|
referralSvc referralservice.ReferralStore
|
||||||
reportSvc report.ReportStore
|
reportSvc report.ReportStore
|
||||||
chapaSvc chapa.ChapaPort
|
chapaSvc *chapa.Service
|
||||||
walletSvc *wallet.Service
|
walletSvc *wallet.Service
|
||||||
transactionSvc *transaction.Service
|
transactionSvc *transaction.Service
|
||||||
ticketSvc *ticket.Service
|
ticketSvc *ticket.Service
|
||||||
|
|
@ -58,7 +58,7 @@ func New(
|
||||||
notificationSvc *notificationservice.Service,
|
notificationSvc *notificationservice.Service,
|
||||||
validator *customvalidator.CustomValidator,
|
validator *customvalidator.CustomValidator,
|
||||||
reportSvc report.ReportStore,
|
reportSvc report.ReportStore,
|
||||||
chapaSvc chapa.ChapaPort,
|
chapaSvc *chapa.Service,
|
||||||
walletSvc *wallet.Service,
|
walletSvc *wallet.Service,
|
||||||
referralSvc referralservice.ReferralStore,
|
referralSvc referralservice.ReferralStore,
|
||||||
virtualGameSvc virtualgameservice.VirtualGameService,
|
virtualGameSvc virtualgameservice.VirtualGameService,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
func convertTransfer(transfer domain.Transfer) TransferWalletRes {
|
||||||
var senderWalletID *int64
|
var senderWalletID *int64
|
||||||
if transfer.SenderWalletID.Valid {
|
senderWalletID = &transfer.SenderWalletID
|
||||||
senderWalletID = &transfer.SenderWalletID.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
var cashierID *int64
|
var cashierID *int64
|
||||||
if transfer.CashierID.Valid {
|
if transfer.CashierID.Valid {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
_ "github.com/SamuelTariku/FortuneBet-Backend/docs"
|
_ "github.com/SamuelTariku/FortuneBet-Backend/docs"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
|
||||||
// "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
// "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
||||||
|
|
||||||
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor"
|
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor"
|
||||||
|
|
@ -192,13 +193,13 @@ func (a *App) initAppRoutes() {
|
||||||
a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet)
|
a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet)
|
||||||
|
|
||||||
//Chapa Routes
|
//Chapa Routes
|
||||||
group.Post("/chapa/payments/verify", a.authMiddleware, h.VerifyChapaPayment)
|
group.Post("/chapa/payments/webhook/verify", h.WebhookCallback)
|
||||||
group.Post("/chapa/payments/withdraw", a.authMiddleware, h.WithdrawUsingChapa)
|
group.Get("/chapa/payments/manual/verify/:tx_ref", h.ManualVerifyPayment)
|
||||||
group.Post("/chapa/payments/deposit", a.authMiddleware, h.DepositUsingChapa)
|
group.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit)
|
||||||
group.Get("/chapa/banks", a.authMiddleware, h.ReadChapaBanks)
|
group.Get("/chapa/banks", h.GetSupportedBanks)
|
||||||
|
|
||||||
//Report Routes
|
//Report Routes
|
||||||
group.Get("/reports/dashboard", a.authMiddleware, h.GetDashboardReport)
|
group.Get("/reports/dashboard", h.GetDashboardReport)
|
||||||
|
|
||||||
//Wallet Monitor Service
|
//Wallet Monitor Service
|
||||||
// group.Get("/debug/wallet-monitor/status", func(c *fiber.Ctx) error {
|
// group.Get("/debug/wallet-monitor/status", func(c *fiber.Ctx) error {
|
||||||
|
|
@ -230,7 +231,7 @@ func (a *App) initAppRoutes() {
|
||||||
|
|
||||||
//mongoDB logs
|
//mongoDB logs
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
group.Get("/logs", handlers.GetLogsHandler(ctx))
|
group.Get("/logs", a.authMiddleware, a.SuperAdminOnly, handlers.GetLogsHandler(ctx))
|
||||||
|
|
||||||
// Recommendation Routes
|
// Recommendation Routes
|
||||||
group.Get("/virtual-games/recommendations/:userID", h.GetRecommendations)
|
group.Get("/virtual-games/recommendations/:userID", h.GetRecommendations)
|
||||||
|
|
@ -247,7 +248,7 @@ func (a *App) initAppRoutes() {
|
||||||
a.fiber.Get("/notifications/all", a.authMiddleware, h.GetAllNotifications)
|
a.fiber.Get("/notifications/all", a.authMiddleware, h.GetAllNotifications)
|
||||||
a.fiber.Post("/notifications/mark-as-read", a.authMiddleware, h.MarkNotificationAsRead)
|
a.fiber.Post("/notifications/mark-as-read", a.authMiddleware, h.MarkNotificationAsRead)
|
||||||
a.fiber.Get("/notifications/unread", a.authMiddleware, h.CountUnreadNotifications)
|
a.fiber.Get("/notifications/unread", a.authMiddleware, h.CountUnreadNotifications)
|
||||||
a.fiber.Post("/notifications/create", h.CreateAndSendNotification)
|
a.fiber.Post("/notifications/create", a.authMiddleware, h.CreateAndSendNotification)
|
||||||
|
|
||||||
// Virtual Game Routes
|
// Virtual Game Routes
|
||||||
a.fiber.Post("/virtual-game/launch", a.authMiddleware, h.LaunchVirtualGame)
|
a.fiber.Post("/virtual-game/launch", a.authMiddleware, h.LaunchVirtualGame)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user