direct depost and veli games fix

This commit is contained in:
Yared Yemane 2025-11-17 18:45:18 +03:00
parent fe7d9ad3b3
commit 1c7e076be5
26 changed files with 3271 additions and 1137 deletions

View File

@ -37,6 +37,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency"
directdeposit "github.com/SamuelTariku/FortuneBet-Backend/internal/services/direct_deposit"
enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
@ -182,7 +183,7 @@ func main() {
walletSvc := wallet.NewService(
repository.NewWalletStore(store),
repository.NewTransferStore(store),
repository.NewDirectDepositStore(store),
// repository.NewDirectDepositStore(store),
notificationSvc,
userSvc,
domain.MongoDBLogger,
@ -326,6 +327,14 @@ func main() {
// Start cron jobs for automated reporting
directdeposit := directdeposit.NewService(
*walletSvc,
repository.NewTransferStore(store),
repository.NewDirectDepositRepository(store),
notificationSvc,
userSvc,
)
enetPulseSvc := enetpulse.New(
*cfg,
store,
@ -373,6 +382,7 @@ func main() {
// Initialize and start HTTP server
app := httpserver.NewApp(
directdeposit,
enetPulseSvc,
atlasVirtualGameService,
veliVirtualGameService,

View File

@ -26,9 +26,7 @@ CREATE TABLE IF NOT EXISTS virtual_game_providers (
provider_id VARCHAR(100) UNIQUE NOT NULL,
-- providerId from Veli Games
provider_name VARCHAR(255) NOT NULL,
-- providerName
logo_dark TEXT,
-- logoForDark (URL)
logo_light TEXT,
-- logoForLight (URL)
enabled BOOLEAN NOT NULL DEFAULT TRUE,
@ -618,20 +616,23 @@ CREATE TABLE flags (
);
CREATE TABLE direct_deposits (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES users (id),
wallet_id BIGINT NOT NULL REFERENCES wallets (id),
amount NUMERIC(15, 2) NOT NULL,
bank_reference TEXT NOT NULL,
sender_account TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('pending', 'completed', 'rejected')),
created_at TIMESTAMP NOT NULL DEFAULT NOW (),
verified_by BIGINT REFERENCES users (id),
verification_notes TEXT,
verified_at TIMESTAMP
customer_id BIGINT REFERENCES users(id),
wallet_id BIGINT REFERENCES wallets(id),
bank_name TEXT,
account_number TEXT,
account_holder TEXT,
amount NUMERIC(18,2),
reference_number TEXT,
transfer_screenshot TEXT,
status TEXT CHECK(status IN ('PENDING', 'APPROVED', 'REJECTED')),
created_at TIMESTAMPTZ,
approved_by BIGINT NULL REFERENCES users(id),
approved_at TIMESTAMPTZ NULL,
rejection_reason TEXT NULL
);
CREATE INDEX idx_direct_deposits_status ON direct_deposits (status);
CREATE INDEX idx_direct_deposits_customer ON direct_deposits (customer_id);
CREATE INDEX idx_direct_deposits_reference ON direct_deposits (bank_reference);
-- CREATE INDEX idx_direct_deposits_status ON direct_deposits (status);
-- CREATE INDEX idx_direct_deposits_customer ON direct_deposits (customer_id);
-- CREATE INDEX idx_direct_deposits_reference ON direct_deposits (bank_reference);
CREATE TABLE IF NOT EXISTS raffles (
id SERIAL PRIMARY KEY,
company_id INT NOT NULL,

View File

@ -9,7 +9,7 @@ CREATE TABLE virtual_game_sessions (
CREATE TABLE virtual_game_transactions (
id BIGSERIAL PRIMARY KEY,
session_id BIGINT NOT NULL REFERENCES virtual_game_sessions(id),
-- session_id BIGINT NOT NULL REFERENCES virtual_game_sessions(id),
user_id BIGINT NOT NULL REFERENCES users(id),
company_id BIGINT,
provider VARCHAR(100),
@ -26,7 +26,7 @@ CREATE TABLE virtual_game_transactions (
CREATE TABLE virtual_game_histories (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(100), -- nullable
-- session_id VARCHAR(100), -- nullable
user_id BIGINT NOT NULL,
company_id BIGINT,
provider VARCHAR(100),
@ -56,7 +56,7 @@ CREATE INDEX idx_virtual_game_game_id ON virtual_game_histories(game_id);
CREATE INDEX idx_virtual_game_external_transaction_id ON virtual_game_histories(external_transaction_id);
CREATE INDEX idx_virtual_game_sessions_user_id ON virtual_game_sessions(user_id);
CREATE INDEX idx_virtual_game_transactions_session_id ON virtual_game_transactions(session_id);
-- CREATE INDEX idx_virtual_game_transactions_session_id ON virtual_game_transactions(session_id);
CREATE INDEX idx_virtual_game_transactions_user_id ON virtual_game_transactions(user_id);
ALTER TABLE favorite_games

View File

@ -1,30 +1,64 @@
-- name: CreateDirectDeposit :one
INSERT INTO direct_deposits (
customer_id,
wallet_id,
amount,
bank_reference,
sender_account,
status
) VALUES (
$1, $2, $3, $4, $5, $6
) RETURNING *;
-- name: GetDirectDeposit :one
SELECT * FROM direct_deposits WHERE id = $1;
-- name: UpdateDirectDeposit :one
UPDATE direct_deposits
SET
status = $2,
verified_by = $3,
verification_notes = $4,
verified_at = $5
WHERE id = $1
customer_id, wallet_id, bank_name, account_number,
account_holder, amount, reference_number,
transfer_screenshot, status
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'PENDING')
RETURNING *;
-- name: GetDirectDepositsByStatus :many
SELECT * FROM direct_deposits WHERE status = $1 ORDER BY created_at DESC;
-- name: GetDirectDepositByID :one
SELECT *
FROM direct_deposits
WHERE id = $1;
-- name: DeleteDirectDeposit :exec
DELETE FROM direct_deposits
WHERE id = $1;
-- name: GetDirectDepositsByStatus :many
SELECT
id,
customer_id,
wallet_id,
bank_name,
account_number,
account_holder,
amount,
reference_number,
transfer_screenshot,
status,
created_at,
approved_by,
approved_at,
rejection_reason
FROM direct_deposits
WHERE status = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3;
-- name: CountDirectDepositsByStatus :one
SELECT COUNT(*)
FROM direct_deposits
WHERE status = $1;
-- name: ApproveDirectDeposit :exec
UPDATE direct_deposits
SET
status = 'APPROVED',
approved_by = $2,
approved_at = NOW()
WHERE
id = $1
AND status = 'PENDING';
-- name: RejectDirectDeposit :exec
UPDATE direct_deposits
SET
status = 'REJECTED',
approved_by = $2, -- still track the admin who took final action
approved_at = NOW(),
rejection_reason = $3
WHERE
id = $1
AND status = 'PENDING';
-- name: GetCustomerDirectDeposits :many
SELECT * FROM direct_deposits WHERE customer_id = $1 ORDER BY created_at DESC;

View File

@ -97,7 +97,7 @@ WHERE session_token = $1;
-- name: CreateVirtualGameTransaction :one
INSERT INTO virtual_game_transactions (
session_id,
-- session_id,
user_id,
company_id,
provider,
@ -108,9 +108,9 @@ INSERT INTO virtual_game_transactions (
external_transaction_id,
status
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id,
session_id,
-- session_id,
user_id,
company_id,
provider,
@ -124,7 +124,7 @@ RETURNING id,
updated_at;
-- name: CreateVirtualGameHistory :one
INSERT INTO virtual_game_histories (
session_id,
-- session_id,
user_id,
company_id,
provider,
@ -148,11 +148,11 @@ VALUES (
$8,
$9,
$10,
$11,
$12
$11
-- $12
)
RETURNING id,
session_id,
-- session_id,
user_id,
company_id,
provider,
@ -169,7 +169,7 @@ RETURNING id,
-- name: GetVirtualGameTransactionByExternalID :one
SELECT id,
session_id,
-- session_id,
user_id,
wallet_id,
transaction_type,
@ -193,7 +193,7 @@ SELECT c.name AS company_name,
COUNT(vgt.id) AS number_of_bets,
COALESCE(SUM(vgt.amount), 0) AS total_transaction_sum
FROM virtual_game_transactions vgt
JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id
-- JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id
JOIN virtual_games vg ON vgs.game_id = vg.id
JOIN companies c ON vgt.company_id = c.id
WHERE vgt.transaction_type = 'BET'

View File

@ -26,7 +26,7 @@ services:
image: mongo:7.0.11
restart: always
ports:
- "27020:27017"
- "27021:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: secret

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -174,6 +174,8 @@ definitions:
properties:
addedTime:
$ref: '#/definitions/domain.ValidInt'
avgBetAmount:
type: integer
awayTeam:
type: string
awayTeamID:
@ -212,6 +214,8 @@ definitions:
type: string
matchPeriod:
$ref: '#/definitions/domain.ValidInt'
numberOfBets:
type: integer
score:
$ref: '#/definitions/domain.ValidString'
source:
@ -226,8 +230,12 @@ definitions:
$ref: '#/definitions/domain.EventStatus'
timerStatus:
$ref: '#/definitions/domain.ValidString'
totalAmount:
type: integer
totalOddOutcomes:
type: integer
totalPotentialWinnings:
type: integer
type: object
domain.BaseLeague:
properties:
@ -349,6 +357,11 @@ definitions:
company_id:
example: 1
type: integer
company_name:
example: fortune
type: string
deducted_stake:
type: number
id:
example: 1
type: integer
@ -373,9 +386,25 @@ definitions:
name:
example: 4-kilo Branch
type: string
number_of_unsettled:
type: integer
profit_percentage:
example: 0.1
type: number
stats_updated_at:
type: string
total_bets:
type: integer
total_cash_backs:
type: number
total_cash_out:
type: number
total_cashiers:
type: integer
total_stake:
type: number
total_unsettled_amount:
type: number
wallet_id:
example: 1
type: integer
@ -608,6 +637,19 @@ definitions:
- customerEmail
- customerPhone
type: object
domain.CompanyMarketSettings:
properties:
companyID:
type: integer
isActive:
$ref: '#/definitions/domain.ValidBool'
marketID:
type: integer
marketName:
type: string
updatedAt:
type: string
type: object
domain.CompanyRes:
properties:
admin_id:
@ -712,6 +754,17 @@ definitions:
- name
- operations
type: object
domain.CreateCompanyMarketSettings:
properties:
companyID:
type: integer
isActive:
$ref: '#/definitions/domain.ValidBool'
marketID:
type: integer
marketName:
type: string
type: object
domain.CreateCompanyReq:
properties:
admin_id:
@ -728,6 +781,25 @@ definitions:
slug:
type: string
type: object
domain.CreateDirectDeposit:
properties:
accountHolder:
type: string
accountNumber:
type: string
amount:
type: number
bankName:
type: string
customerID:
type: integer
referenceNumber:
type: string
transferScreenshot:
type: string
walletID:
type: integer
type: object
domain.CreateSupportedOperationReq:
properties:
description:
@ -857,8 +929,6 @@ definitions:
properties:
brandId:
type: string
country:
type: string
deviceType:
type: string
gameId:
@ -867,26 +937,39 @@ definitions:
type: string
language:
type: string
playerId:
type: string
providerId:
type: string
type: object
domain.DirectDepositRequest:
domain.DirectDeposit:
properties:
accountHolder:
type: string
accountNumber:
type: string
amount:
type: integer
bank_reference:
type: number
approvedAt:
type: string
customer_id:
approvedBy:
type: integer
sender_account:
bankName:
type: string
required:
- amount
- bank_reference
- customer_id
- sender_account
createdAt:
type: string
customerID:
type: integer
id:
type: integer
referenceNumber:
type: string
rejectionReason:
type: string
status:
type: string
transferScreenshot:
type: string
walletID:
type: integer
type: object
domain.EnetpulseFixture:
properties:
@ -1272,6 +1355,8 @@ definitions:
properties:
added_time:
type: integer
average_bet_amount:
type: number
away_team:
type: string
away_team_id:
@ -1314,6 +1399,8 @@ definitions:
type: string
match_period:
type: integer
number_of_bets:
type: integer
score:
type: string
source:
@ -1328,8 +1415,12 @@ definitions:
$ref: '#/definitions/domain.EventStatus'
timer_status:
type: string
total_amount:
type: number
total_odd_outcomes:
type: integer
total_potential_winnings:
type: number
updated_at:
type: string
winning_upper_limit:
@ -1422,8 +1513,6 @@ definitions:
properties:
brandId:
type: string
cashierUrl:
type: string
country:
type: string
currency:
@ -1436,18 +1525,12 @@ definitions:
type: string
language:
type: string
lobbyUrl:
type: string
playerId:
type: string
playerName:
type: string
providerId:
type: string
sessionId:
type: string
userAgent:
type: string
type: object
domain.GameStartResponse:
properties:
@ -1569,6 +1652,8 @@ definitions:
deducted_percentage:
example: 0.1
type: number
deducted_stake:
type: number
id:
example: 1
type: integer
@ -1581,9 +1666,35 @@ definitions:
name:
example: CompanyName
type: string
number_of_unsettled:
type: integer
slug:
example: slug
type: string
stats_updated_at:
type: string
total_admins:
type: integer
total_approvers:
type: integer
total_bets:
type: integer
total_branches:
type: integer
total_cash_backs:
type: number
total_cash_out:
type: number
total_cashiers:
type: integer
total_customers:
type: integer
total_managers:
type: integer
total_stake:
type: number
total_unsettled_amount:
type: number
wallet_id:
example: 1
type: integer
@ -1730,6 +1841,17 @@ definitions:
pagination:
$ref: '#/definitions/domain.Pagination'
type: object
domain.MarketSettings:
properties:
isActive:
type: boolean
marketID:
type: integer
marketName:
type: string
updatedAt:
type: string
type: object
domain.OddMarketFilter:
properties:
limit:
@ -1758,7 +1880,7 @@ definitions:
- 5
type: integer
x-enum-comments:
OUTCOME_STATUS_ERROR: Half Win and Half Given Back
OUTCOME_STATUS_ERROR: Error (Unsettled Bet)
OUTCOME_STATUS_HALF: Half Win and Half Given Back
OUTCOME_STATUS_VOID: Give Back
x-enum-varnames:
@ -1776,6 +1898,7 @@ definitions:
type: array
message:
type: string
metadata: {}
pagination:
$ref: '#/definitions/domain.Pagination'
status_code:
@ -1946,6 +2069,7 @@ definitions:
data: {}
message:
type: string
metadata: {}
status_code:
type: integer
success:
@ -2497,18 +2621,6 @@ definitions:
value:
type: string
type: object
domain.VerifyDirectDepositRequest:
properties:
deposit_id:
type: integer
is_verified:
type: boolean
notes:
type: string
required:
- deposit_id
- is_verified
type: object
domain.VirtualGameProvider:
properties:
created_at:
@ -2795,6 +2907,14 @@ definitions:
last_name:
example: Smith
type: string
number_of_deposits:
type: integer
number_of_transactions:
type: integer
number_of_transfers:
type: integer
number_of_withdraws:
type: integer
phone_number:
example: "0911111111"
type: string
@ -2820,6 +2940,16 @@ definitions:
type: boolean
static_updated_at:
type: string
total_deposits_amount:
type: number
total_transactions:
type: number
total_transfers_amount:
type: number
total_withdraws_amount:
type: number
updated_at:
type: string
type: object
handlers.CustomersRes:
properties:
@ -3568,6 +3698,120 @@ paths:
summary: Set the league to active
tags:
- leagues
/api/v1/{tenant_slug}/market-settings:
delete:
consumes:
- application/json
description: Remove all overridden market settings for a specific tenant
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Delete all market settings for a tenant
tags:
- market_settings
get:
consumes:
- application/json
description: Get all market settings overridden for a specific tenant
parameters:
- description: Number of results to return (default 10)
in: query
name: limit
type: integer
- description: Number of results to skip (default 0)
in: query
name: offset
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.CompanyMarketSettings'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Retrieve all market settings for a tenant
tags:
- market_settings
post:
consumes:
- application/json
description: Insert new market settings for a specific tenant/company
parameters:
- description: Market Settings
in: body
name: body
required: true
schema:
$ref: '#/definitions/domain.CreateCompanyMarketSettings'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Insert company-specific market settings
tags:
- market_settings
/api/v1/{tenant_slug}/market-settings/{id}:
delete:
consumes:
- application/json
description: Remove a specific overridden market setting for a tenant
parameters:
- description: Market ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Delete a specific market setting for a tenant
tags:
- market_settings
/api/v1/{tenant_slug}/odds:
get:
consumes:
@ -6385,87 +6629,235 @@ paths:
summary: Get all customer wallets
tags:
- wallet
/api/v1/direct_deposit:
post:
consumes:
- application/json
description: Customer initiates a direct deposit from mobile banking
parameters:
- description: Deposit details
in: body
name: request
required: true
schema:
$ref: '#/definitions/domain.DirectDepositRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Initiate a direct deposit
tags:
- Direct Deposits
/api/v1/direct_deposit/pending:
/api/v1/direct-deposits:
get:
description: Get list of direct deposits needing verification
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get pending direct deposits
tags:
- Direct Deposits
/api/v1/direct_deposit/verify:
post:
consumes:
- application/json
description: Cashier verifies a direct deposit transaction
description: Fetches direct deposits filtered by status with pagination
parameters:
- description: Verification details
in: body
name: request
- description: Deposit status (e.g., PENDING, APPROVED, REJECTED)
in: query
name: status
required: true
schema:
$ref: '#/definitions/domain.VerifyDirectDepositRequest'
type: string
- description: Page number
in: query
name: page
type: integer
- description: Page size
in: query
name: pageSize
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
items:
$ref: '#/definitions/domain.DirectDeposit'
type: array
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"401":
description: Unauthorized
"502":
description: Bad Gateway
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Verify a direct deposit
summary: Get direct deposits by status
tags:
- Direct Deposits
- DirectDeposit
post:
consumes:
- application/json
description: Creates a direct deposit for a customer and notifies both the customer
and admins
parameters:
- description: Direct deposit details
in: body
name: body
required: true
schema:
$ref: '#/definitions/domain.CreateDirectDeposit'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
$ref: '#/definitions/domain.DirectDeposit'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"502":
description: Bad Gateway
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Create a new direct deposit
tags:
- DirectDeposit
/api/v1/direct-deposits/{depositID}:
delete:
consumes:
- application/json
description: Deletes a direct deposit by its ID
parameters:
- description: Deposit ID
in: path
name: depositID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
type: string
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"502":
description: Bad Gateway
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Delete a direct deposit
tags:
- DirectDeposit
get:
consumes:
- application/json
description: Fetches a single direct deposit by its ID
parameters:
- description: Deposit ID
in: path
name: depositID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
$ref: '#/definitions/domain.DirectDeposit'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"502":
description: Bad Gateway
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get a direct deposit by ID
tags:
- DirectDeposit
/api/v1/direct-deposits/{depositID}/approve:
post:
consumes:
- application/json
description: Approves a direct deposit by admin and credits customer wallet
parameters:
- description: Deposit ID
in: path
name: depositID
required: true
type: integer
- description: Admin ID performing the approval
in: query
name: adminID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
type: string
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"502":
description: Bad Gateway
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Approve a direct deposit
tags:
- DirectDeposit
/api/v1/direct-deposits/{depositID}/reject:
post:
consumes:
- application/json
description: Rejects a direct deposit by admin and notifies the customer
parameters:
- description: Deposit ID
in: path
name: depositID
required: true
type: integer
- description: Admin ID performing the rejection
in: query
name: adminID
required: true
type: integer
- description: Reason for rejection
in: query
name: reason
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
type: string
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"502":
description: Bad Gateway
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Reject a direct deposit
tags:
- DirectDeposit
/api/v1/enetpulse/betting-offers:
get:
consumes:
@ -7303,6 +7695,40 @@ paths:
summary: Update Managers
tags:
- manager
/api/v1/market-settings:
get:
consumes:
- application/json
description: Get all market settings that apply globally
parameters:
- description: Number of results to return (default 10)
in: query
name: limit
type: integer
- description: Number of results to skip (default 0)
in: query
name: offset
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.MarketSettings'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Retrieve all global market settings
tags:
- market_settings
/api/v1/odds:
get:
consumes:
@ -9643,37 +10069,6 @@ paths:
summary: Process Alea Play game callback
tags:
- Alea Virtual Games
/api/v1/win:
post:
consumes:
- application/json
description: Processes win callbacks from either Veli or PopOK providers by
auto-detecting the format
produces:
- application/json
responses:
"200":
description: Win processing result
schema: {}
"400":
description: Invalid request format
schema:
$ref: '#/definitions/domain.ErrorResponse'
"401":
description: Authentication failed
schema:
$ref: '#/definitions/domain.ErrorResponse'
"409":
description: Duplicate transaction
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Handle win callback (Veli or PopOK)
tags:
- Wins
/betwin:
post:
consumes:
@ -9942,6 +10337,37 @@ paths:
summary: Launch a PopOK virtual game
tags:
- Virtual Games - PopOK
/win:
post:
consumes:
- application/json
description: Processes win callbacks from either Veli or PopOK providers by
auto-detecting the format
produces:
- application/json
responses:
"200":
description: Win processing result
schema: {}
"400":
description: Invalid request format
schema:
$ref: '#/definitions/domain.ErrorResponse'
"401":
description: Authentication failed
schema:
$ref: '#/definitions/domain.ErrorResponse'
"409":
description: Duplicate transaction
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Handle win callback (Veli or PopOK)
tags:
- Wins
securityDefinitions:
Bearer:
in: header

View File

@ -11,119 +11,159 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const ApproveDirectDeposit = `-- name: ApproveDirectDeposit :exec
UPDATE direct_deposits
SET
status = 'APPROVED',
approved_by = $2,
approved_at = NOW()
WHERE
id = $1
AND status = 'PENDING'
`
type ApproveDirectDepositParams struct {
ID int64 `json:"id"`
ApprovedBy pgtype.Int8 `json:"approved_by"`
}
func (q *Queries) ApproveDirectDeposit(ctx context.Context, arg ApproveDirectDepositParams) error {
_, err := q.db.Exec(ctx, ApproveDirectDeposit, arg.ID, arg.ApprovedBy)
return err
}
const CountDirectDepositsByStatus = `-- name: CountDirectDepositsByStatus :one
SELECT COUNT(*)
FROM direct_deposits
WHERE status = $1
`
func (q *Queries) CountDirectDepositsByStatus(ctx context.Context, status pgtype.Text) (int64, error) {
row := q.db.QueryRow(ctx, CountDirectDepositsByStatus, status)
var count int64
err := row.Scan(&count)
return count, err
}
const CreateDirectDeposit = `-- name: CreateDirectDeposit :one
INSERT INTO direct_deposits (
customer_id,
wallet_id,
amount,
bank_reference,
sender_account,
status
) VALUES (
$1, $2, $3, $4, $5, $6
) RETURNING id, customer_id, wallet_id, amount, bank_reference, sender_account, status, created_at, verified_by, verification_notes, verified_at
customer_id, wallet_id, bank_name, account_number,
account_holder, amount, reference_number,
transfer_screenshot, status
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'PENDING')
RETURNING id, customer_id, wallet_id, bank_name, account_number, account_holder, amount, reference_number, transfer_screenshot, status, created_at, approved_by, approved_at, rejection_reason
`
type CreateDirectDepositParams struct {
CustomerID int64 `json:"customer_id"`
WalletID int64 `json:"wallet_id"`
CustomerID pgtype.Int8 `json:"customer_id"`
WalletID pgtype.Int8 `json:"wallet_id"`
BankName pgtype.Text `json:"bank_name"`
AccountNumber pgtype.Text `json:"account_number"`
AccountHolder pgtype.Text `json:"account_holder"`
Amount pgtype.Numeric `json:"amount"`
BankReference string `json:"bank_reference"`
SenderAccount string `json:"sender_account"`
Status string `json:"status"`
ReferenceNumber pgtype.Text `json:"reference_number"`
TransferScreenshot pgtype.Text `json:"transfer_screenshot"`
}
func (q *Queries) CreateDirectDeposit(ctx context.Context, arg CreateDirectDepositParams) (DirectDeposit, error) {
row := q.db.QueryRow(ctx, CreateDirectDeposit,
arg.CustomerID,
arg.WalletID,
arg.BankName,
arg.AccountNumber,
arg.AccountHolder,
arg.Amount,
arg.BankReference,
arg.SenderAccount,
arg.Status,
arg.ReferenceNumber,
arg.TransferScreenshot,
)
var i DirectDeposit
err := row.Scan(
&i.ID,
&i.CustomerID,
&i.WalletID,
&i.BankName,
&i.AccountNumber,
&i.AccountHolder,
&i.Amount,
&i.BankReference,
&i.SenderAccount,
&i.ReferenceNumber,
&i.TransferScreenshot,
&i.Status,
&i.CreatedAt,
&i.VerifiedBy,
&i.VerificationNotes,
&i.VerifiedAt,
&i.ApprovedBy,
&i.ApprovedAt,
&i.RejectionReason,
)
return i, err
}
const GetCustomerDirectDeposits = `-- name: GetCustomerDirectDeposits :many
SELECT id, customer_id, wallet_id, amount, bank_reference, sender_account, status, created_at, verified_by, verification_notes, verified_at FROM direct_deposits WHERE customer_id = $1 ORDER BY created_at DESC
const DeleteDirectDeposit = `-- name: DeleteDirectDeposit :exec
DELETE FROM direct_deposits
WHERE id = $1
`
func (q *Queries) GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]DirectDeposit, error) {
rows, err := q.db.Query(ctx, GetCustomerDirectDeposits, customerID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []DirectDeposit
for rows.Next() {
var i DirectDeposit
if err := rows.Scan(
&i.ID,
&i.CustomerID,
&i.WalletID,
&i.Amount,
&i.BankReference,
&i.SenderAccount,
&i.Status,
&i.CreatedAt,
&i.VerifiedBy,
&i.VerificationNotes,
&i.VerifiedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
func (q *Queries) DeleteDirectDeposit(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteDirectDeposit, id)
return err
}
const GetDirectDeposit = `-- name: GetDirectDeposit :one
SELECT id, customer_id, wallet_id, amount, bank_reference, sender_account, status, created_at, verified_by, verification_notes, verified_at FROM direct_deposits WHERE id = $1
const GetDirectDepositByID = `-- name: GetDirectDepositByID :one
SELECT id, customer_id, wallet_id, bank_name, account_number, account_holder, amount, reference_number, transfer_screenshot, status, created_at, approved_by, approved_at, rejection_reason
FROM direct_deposits
WHERE id = $1
`
func (q *Queries) GetDirectDeposit(ctx context.Context, id int64) (DirectDeposit, error) {
row := q.db.QueryRow(ctx, GetDirectDeposit, id)
func (q *Queries) GetDirectDepositByID(ctx context.Context, id int64) (DirectDeposit, error) {
row := q.db.QueryRow(ctx, GetDirectDepositByID, id)
var i DirectDeposit
err := row.Scan(
&i.ID,
&i.CustomerID,
&i.WalletID,
&i.BankName,
&i.AccountNumber,
&i.AccountHolder,
&i.Amount,
&i.BankReference,
&i.SenderAccount,
&i.ReferenceNumber,
&i.TransferScreenshot,
&i.Status,
&i.CreatedAt,
&i.VerifiedBy,
&i.VerificationNotes,
&i.VerifiedAt,
&i.ApprovedBy,
&i.ApprovedAt,
&i.RejectionReason,
)
return i, err
}
const GetDirectDepositsByStatus = `-- name: GetDirectDepositsByStatus :many
SELECT id, customer_id, wallet_id, amount, bank_reference, sender_account, status, created_at, verified_by, verification_notes, verified_at FROM direct_deposits WHERE status = $1 ORDER BY created_at DESC
SELECT
id,
customer_id,
wallet_id,
bank_name,
account_number,
account_holder,
amount,
reference_number,
transfer_screenshot,
status,
created_at,
approved_by,
approved_at,
rejection_reason
FROM direct_deposits
WHERE status = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
`
func (q *Queries) GetDirectDepositsByStatus(ctx context.Context, status string) ([]DirectDeposit, error) {
rows, err := q.db.Query(ctx, GetDirectDepositsByStatus, status)
type GetDirectDepositsByStatusParams struct {
Status pgtype.Text `json:"status"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) GetDirectDepositsByStatus(ctx context.Context, arg GetDirectDepositsByStatusParams) ([]DirectDeposit, error) {
rows, err := q.db.Query(ctx, GetDirectDepositsByStatus, arg.Status, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
@ -135,14 +175,17 @@ func (q *Queries) GetDirectDepositsByStatus(ctx context.Context, status string)
&i.ID,
&i.CustomerID,
&i.WalletID,
&i.BankName,
&i.AccountNumber,
&i.AccountHolder,
&i.Amount,
&i.BankReference,
&i.SenderAccount,
&i.ReferenceNumber,
&i.TransferScreenshot,
&i.Status,
&i.CreatedAt,
&i.VerifiedBy,
&i.VerificationNotes,
&i.VerifiedAt,
&i.ApprovedBy,
&i.ApprovedAt,
&i.RejectionReason,
); err != nil {
return nil, err
}
@ -154,46 +197,25 @@ func (q *Queries) GetDirectDepositsByStatus(ctx context.Context, status string)
return items, nil
}
const UpdateDirectDeposit = `-- name: UpdateDirectDeposit :one
const RejectDirectDeposit = `-- name: RejectDirectDeposit :exec
UPDATE direct_deposits
SET
status = $2,
verified_by = $3,
verification_notes = $4,
verified_at = $5
WHERE id = $1
RETURNING id, customer_id, wallet_id, amount, bank_reference, sender_account, status, created_at, verified_by, verification_notes, verified_at
status = 'REJECTED',
approved_by = $2, -- still track the admin who took final action
approved_at = NOW(),
rejection_reason = $3
WHERE
id = $1
AND status = 'PENDING'
`
type UpdateDirectDepositParams struct {
type RejectDirectDepositParams struct {
ID int64 `json:"id"`
Status string `json:"status"`
VerifiedBy pgtype.Int8 `json:"verified_by"`
VerificationNotes pgtype.Text `json:"verification_notes"`
VerifiedAt pgtype.Timestamp `json:"verified_at"`
ApprovedBy pgtype.Int8 `json:"approved_by"`
RejectionReason pgtype.Text `json:"rejection_reason"`
}
func (q *Queries) UpdateDirectDeposit(ctx context.Context, arg UpdateDirectDepositParams) (DirectDeposit, error) {
row := q.db.QueryRow(ctx, UpdateDirectDeposit,
arg.ID,
arg.Status,
arg.VerifiedBy,
arg.VerificationNotes,
arg.VerifiedAt,
)
var i DirectDeposit
err := row.Scan(
&i.ID,
&i.CustomerID,
&i.WalletID,
&i.Amount,
&i.BankReference,
&i.SenderAccount,
&i.Status,
&i.CreatedAt,
&i.VerifiedBy,
&i.VerificationNotes,
&i.VerifiedAt,
)
return i, err
func (q *Queries) RejectDirectDeposit(ctx context.Context, arg RejectDirectDepositParams) error {
_, err := q.db.Exec(ctx, RejectDirectDeposit, arg.ID, arg.ApprovedBy, arg.RejectionReason)
return err
}

View File

@ -313,16 +313,19 @@ type CustomerWalletDetail struct {
type DirectDeposit struct {
ID int64 `json:"id"`
CustomerID int64 `json:"customer_id"`
WalletID int64 `json:"wallet_id"`
CustomerID pgtype.Int8 `json:"customer_id"`
WalletID pgtype.Int8 `json:"wallet_id"`
BankName pgtype.Text `json:"bank_name"`
AccountNumber pgtype.Text `json:"account_number"`
AccountHolder pgtype.Text `json:"account_holder"`
Amount pgtype.Numeric `json:"amount"`
BankReference string `json:"bank_reference"`
SenderAccount string `json:"sender_account"`
Status string `json:"status"`
CreatedAt pgtype.Timestamp `json:"created_at"`
VerifiedBy pgtype.Int8 `json:"verified_by"`
VerificationNotes pgtype.Text `json:"verification_notes"`
VerifiedAt pgtype.Timestamp `json:"verified_at"`
ReferenceNumber pgtype.Text `json:"reference_number"`
TransferScreenshot pgtype.Text `json:"transfer_screenshot"`
Status pgtype.Text `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
ApprovedBy pgtype.Int8 `json:"approved_by"`
ApprovedAt pgtype.Timestamptz `json:"approved_at"`
RejectionReason pgtype.Text `json:"rejection_reason"`
}
type EnetpulseFixture struct {
@ -1223,7 +1226,6 @@ type VirtualGameSession struct {
type VirtualGameTransaction struct {
ID int64 `json:"id"`
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`

View File

@ -445,7 +445,7 @@ func (q *Queries) CreateVirtualGameSession(ctx context.Context, arg CreateVirtua
const CreateVirtualGameTransaction = `-- name: CreateVirtualGameTransaction :one
INSERT INTO virtual_game_transactions (
session_id,
-- session_id,
user_id,
company_id,
provider,
@ -456,9 +456,9 @@ INSERT INTO virtual_game_transactions (
external_transaction_id,
status
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id,
session_id,
-- session_id,
user_id,
company_id,
provider,
@ -473,7 +473,6 @@ RETURNING id,
`
type CreateVirtualGameTransactionParams struct {
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
@ -487,7 +486,6 @@ type CreateVirtualGameTransactionParams struct {
type CreateVirtualGameTransactionRow struct {
ID int64 `json:"id"`
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
@ -503,7 +501,6 @@ type CreateVirtualGameTransactionRow struct {
func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVirtualGameTransactionParams) (CreateVirtualGameTransactionRow, error) {
row := q.db.QueryRow(ctx, CreateVirtualGameTransaction,
arg.SessionID,
arg.UserID,
arg.CompanyID,
arg.Provider,
@ -517,7 +514,6 @@ func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVi
var i CreateVirtualGameTransactionRow
err := row.Scan(
&i.ID,
&i.SessionID,
&i.UserID,
&i.CompanyID,
&i.Provider,
@ -785,7 +781,7 @@ SELECT c.name AS company_name,
COUNT(vgt.id) AS number_of_bets,
COALESCE(SUM(vgt.amount), 0) AS total_transaction_sum
FROM virtual_game_transactions vgt
JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id
-- JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id
JOIN virtual_games vg ON vgs.game_id = vg.id
JOIN companies c ON vgt.company_id = c.id
WHERE vgt.transaction_type = 'BET'
@ -833,7 +829,7 @@ func (q *Queries) GetVirtualGameSummaryInRange(ctx context.Context, arg GetVirtu
const GetVirtualGameTransactionByExternalID = `-- name: GetVirtualGameTransactionByExternalID :one
SELECT id,
session_id,
-- session_id,
user_id,
wallet_id,
transaction_type,
@ -849,7 +845,6 @@ WHERE external_transaction_id = $1
type GetVirtualGameTransactionByExternalIDRow struct {
ID int64 `json:"id"`
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"`
@ -866,7 +861,6 @@ func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, ext
var i GetVirtualGameTransactionByExternalIDRow
err := row.Scan(
&i.ID,
&i.SessionID,
&i.UserID,
&i.WalletID,
&i.TransactionType,

View File

@ -16,6 +16,11 @@ type Response struct {
Data interface{} `json:"data,omitempty"`
Success bool `json:"success"`
StatusCode int `json:"status_code"`
MetaData interface{} `json:"metadata"`
}
type CallbackErrorResponse struct {
Error string `json:"error,omitempty"`
}
func CalculateWinnings(amount Currency, totalOdds float32) Currency {

View File

@ -1,113 +1,39 @@
package domain
import (
"time"
"math/big"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/jackc/pgx/v5/pgtype"
)
import "time"
type DirectDepositStatus string
const (
DepositStatusPending DirectDepositStatus = "pending"
DepositStatusCompleted DirectDepositStatus = "completed"
DepositStatusRejected DirectDepositStatus = "rejected"
DepositStatusPending DirectDepositStatus = "PENDING"
DepositStatusCompleted DirectDepositStatus = "COMPLETED"
DepositStatusRejected DirectDepositStatus = "REJECTED"
)
type DirectDeposit struct {
ID int64 `json:"id"`
CustomerID int64 `json:"customer_id"`
WalletID int64 `json:"wallet_id"`
Wallet Wallet `json:"wallet"`
Amount Currency `json:"amount"`
BankReference string `json:"bank_reference"`
SenderAccount string `json:"sender_account"`
Status DirectDepositStatus `json:"status"`
CreatedAt time.Time `json:"created_at"`
VerifiedBy *int64 `json:"verified_by"`
VerificationNotes string `json:"verification_notes"`
VerifiedAt *time.Time `json:"verified_at"`
ID int
CustomerID int
WalletID int
BankName string
AccountNumber string
AccountHolder string
Amount float64
ReferenceNumber string
TransferScreenshot string
Status string
CreatedAt time.Time
ApprovedBy *int
ApprovedAt *time.Time
RejectionReason *string
}
type CreateDirectDeposit struct {
CustomerID int64
WalletID int64
Amount Currency
BankReference string
SenderAccount string
Status DirectDepositStatus
}
type UpdateDirectDeposit struct {
ID int64
Status DirectDepositStatus
VerifiedBy int64
VerificationNotes string
VerifiedAt time.Time
}
type DirectDepositRequest struct {
CustomerID int64 `json:"customer_id" binding:"required"`
Amount Currency `json:"amount" binding:"required,gt=0"`
BankReference string `json:"bank_reference" binding:"required"`
SenderAccount string `json:"sender_account" binding:"required"`
}
type VerifyDirectDepositRequest struct {
DepositID int64 `json:"deposit_id" binding:"required"`
IsVerified bool `json:"is_verified" binding:"required"`
Notes string `json:"notes"`
}
func ConvertDBDirectDeposit(deposit dbgen.DirectDeposit) DirectDeposit {
return DirectDeposit{
ID: deposit.ID,
CustomerID: deposit.CustomerID,
WalletID: deposit.WalletID,
Amount: Currency(deposit.Amount.Int.Int64()),
BankReference: deposit.BankReference,
SenderAccount: deposit.SenderAccount,
Status: DirectDepositStatus(deposit.Status),
CreatedAt: deposit.CreatedAt.Time,
VerifiedBy: convertPgInt64ToPtr(deposit.VerifiedBy),
VerificationNotes: deposit.VerificationNotes.String,
VerifiedAt: convertPgTimeToPtr(deposit.VerifiedAt),
}
}
func ConvertCreateDirectDeposit(deposit CreateDirectDeposit) dbgen.CreateDirectDepositParams {
return dbgen.CreateDirectDepositParams{
CustomerID: deposit.CustomerID,
WalletID: deposit.WalletID,
Amount: pgtype.Numeric{Int: big.NewInt(int64(deposit.Amount)), Valid: true},
BankReference: deposit.BankReference,
SenderAccount: deposit.SenderAccount,
Status: string(deposit.Status),
}
}
func ConvertUpdateDirectDeposit(deposit UpdateDirectDeposit) dbgen.UpdateDirectDepositParams {
return dbgen.UpdateDirectDepositParams{
ID: deposit.ID,
Status: string(deposit.Status),
VerifiedBy: pgtype.Int8{Int64: deposit.VerifiedBy, Valid: true},
VerificationNotes: pgtype.Text{String: deposit.VerificationNotes, Valid: deposit.VerificationNotes != ""},
VerifiedAt: pgtype.Timestamp{Time: deposit.VerifiedAt, Valid: true},
}
}
func convertPgInt64ToPtr(i pgtype.Int8) *int64 {
if i.Valid {
return &i.Int64
}
return nil
}
func convertPgTimeToPtr(t pgtype.Timestamp) *time.Time {
if t.Valid {
return &t.Time
}
return nil
CustomerID int
WalletID int
BankName string
AccountNumber string
AccountHolder string
Amount float64
ReferenceNumber string
TransferScreenshot string
}

View File

@ -47,13 +47,13 @@ type ApprovalStore interface {
GetPendingApprovals(ctx context.Context) ([]domain.TransferDetail, error)
}
type DirectDepositStore interface {
CreateDirectDeposit(ctx context.Context, deposit domain.CreateDirectDeposit) (domain.DirectDeposit, error)
GetDirectDeposit(ctx context.Context, id int64) (domain.DirectDeposit, error)
UpdateDirectDeposit(ctx context.Context, deposit domain.UpdateDirectDeposit) (domain.DirectDeposit, error)
GetDirectDepositsByStatus(ctx context.Context, status domain.DirectDepositStatus) ([]domain.DirectDeposit, error)
GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]domain.DirectDeposit, error)
}
// type DirectDepositStore interface {
// CreateDirectDeposit(ctx context.Context, deposit domain.CreateDirectDeposit) (domain.DirectDeposit, error)
// GetDirectDeposit(ctx context.Context, id int64) (domain.DirectDeposit, error)
// UpdateDirectDeposit(ctx context.Context, deposit domain.UpdateDirectDeposit) (domain.DirectDeposit, error)
// GetDirectDepositsByStatus(ctx context.Context, status domain.DirectDepositStatus) ([]domain.DirectDeposit, error)
// GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]domain.DirectDeposit, error)
// }
type WalletStatStore interface {
UpdateWalletStats(ctx context.Context) error

View File

@ -2,61 +2,195 @@ package repository
import (
"context"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/ports"
"github.com/jackc/pgx/v5/pgtype"
)
// Interface for creating new wallet store
func NewDirectDepositStore(s *Store) ports.DirectDepositStore { return s }
type DirectDepositRepository interface {
CreateDirectDeposit(ctx context.Context, deposit *domain.DirectDeposit) error
GetDirectDepositsByStatus(ctx context.Context, status string, page int, pageSize int) ([]domain.DirectDeposit, int64, error)
ApproveDirectDeposit(ctx context.Context, depositID int, adminID int) error
RejectDirectDeposit(ctx context.Context, depositID int, adminID int, reason string) error
DeleteDirectDeposit(ctx context.Context, id int) error
GetDirectDepositByID(ctx context.Context, id int) (*domain.DirectDeposit, error)
}
type DirectDepositRepo struct {
store *Store
}
func (s *Store) CreateDirectDeposit(ctx context.Context, deposit domain.CreateDirectDeposit) (domain.DirectDeposit, error) {
newDeposit, err := s.queries.CreateDirectDeposit(ctx, domain.ConvertCreateDirectDeposit(deposit))
func NewDirectDepositRepository(store *Store) DirectDepositRepository {
return &DirectDepositRepo{store: store}
}
func (r *DirectDepositRepo) CreateDirectDeposit(ctx context.Context, deposit *domain.DirectDeposit) error {
params := dbgen.CreateDirectDepositParams{
CustomerID: pgtype.Int8{Int64: int64(deposit.CustomerID)},
WalletID: pgtype.Int8{Int64: int64(deposit.WalletID)},
BankName: pgtype.Text{String: deposit.BankName},
AccountNumber: pgtype.Text{String: deposit.AccountNumber},
AccountHolder: pgtype.Text{String: deposit.AccountHolder},
Amount: pgtype.Numeric{Exp: int32(deposit.Amount)},
ReferenceNumber: pgtype.Text{String: deposit.ReferenceNumber},
TransferScreenshot: pgtype.Text{String: deposit.TransferScreenshot},
}
dbDeposit, err := r.store.queries.CreateDirectDeposit(ctx, params)
if err != nil {
return domain.DirectDeposit{}, err
}
return domain.ConvertDBDirectDeposit(newDeposit), nil
return err
}
func (s *Store) GetDirectDeposit(ctx context.Context, id int64) (domain.DirectDeposit, error) {
deposit, err := s.queries.GetDirectDeposit(ctx, id)
// Map back to domain struct
deposit.ID = int(dbDeposit.ID)
deposit.Status = dbDeposit.Status.String
deposit.CreatedAt = dbDeposit.CreatedAt.Time
return nil
}
func (r *DirectDepositRepo) GetDirectDepositsByStatus(
ctx context.Context,
status string,
page int,
pageSize int,
) ([]domain.DirectDeposit, int64, error) {
// Default pagination rules
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 50
}
offset := (page - 1) * pageSize
params := dbgen.GetDirectDepositsByStatusParams{
Status: pgtype.Text{String: status},
Limit: int32(pageSize),
Offset: int32(offset),
}
dbItems, err := r.store.queries.GetDirectDepositsByStatus(ctx, params)
if err != nil {
return domain.DirectDeposit{}, err
}
return domain.ConvertDBDirectDeposit(deposit), nil
return nil, 0, err
}
func (s *Store) UpdateDirectDeposit(ctx context.Context, deposit domain.UpdateDirectDeposit) (domain.DirectDeposit, error) {
updatedDeposit, err := s.queries.UpdateDirectDeposit(ctx, domain.ConvertUpdateDirectDeposit(deposit))
total, err := r.store.queries.CountDirectDepositsByStatus(ctx, pgtype.Text{String: status})
if err != nil {
return domain.DirectDeposit{}, err
}
return domain.ConvertDBDirectDeposit(updatedDeposit), nil
return nil, 0, err
}
func (s *Store) GetDirectDepositsByStatus(ctx context.Context, status domain.DirectDepositStatus) ([]domain.DirectDeposit, error) {
deposits, err := s.queries.GetDirectDepositsByStatus(ctx, string(status))
deposits := make([]domain.DirectDeposit, len(dbItems))
for i, d := range dbItems {
deposits[i] = *mapDBDirectDepositToDomain(&d)
}
return deposits, total, nil
}
func (r *DirectDepositRepo) ApproveDirectDeposit(
ctx context.Context,
depositID int,
adminID int,
) error {
params := dbgen.ApproveDirectDepositParams{
ID: int64(depositID),
ApprovedBy: pgtype.Int8{Int64: int64(adminID)},
}
err := r.store.queries.ApproveDirectDeposit(ctx, params)
if err != nil {
return err
}
return nil
}
func (r *DirectDepositRepo) GetDirectDepositByID(
ctx context.Context,
id int,
) (*domain.DirectDeposit, error) {
dbDeposit, err := r.store.queries.GetDirectDepositByID(ctx, int64(id))
if err != nil {
return nil, err
}
result := make([]domain.DirectDeposit, 0, len(deposits))
for _, deposit := range deposits {
result = append(result, domain.ConvertDBDirectDeposit(deposit))
}
return result, nil
deposit := mapDBDirectDepositToDomain(&dbDeposit)
return deposit, nil
}
func (s *Store) GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]domain.DirectDeposit, error) {
deposits, err := s.queries.GetCustomerDirectDeposits(ctx, customerID)
func (r *DirectDepositRepo) DeleteDirectDeposit(
ctx context.Context,
id int,
) error {
err := r.store.queries.DeleteDirectDeposit(ctx, int64(id))
if err != nil {
return nil, err
return err
}
result := make([]domain.DirectDeposit, 0, len(deposits))
for _, deposit := range deposits {
result = append(result, domain.ConvertDBDirectDeposit(deposit))
return nil
}
func (r *DirectDepositRepo) RejectDirectDeposit(
ctx context.Context,
depositID int,
adminID int,
reason string,
) error {
params := dbgen.RejectDirectDepositParams{
ID: int64(depositID),
ApprovedBy: pgtype.Int8{Int64: int64(adminID)},
RejectionReason: pgtype.Text{String: reason},
}
err := r.store.queries.RejectDirectDeposit(ctx, params)
if err != nil {
return err
}
return nil
}
func mapDBDirectDepositToDomain(d *dbgen.DirectDeposit) *domain.DirectDeposit {
var approvedBy *int
if d.ApprovedBy.Valid {
v := int(d.ApprovedBy.Int64)
approvedBy = &v
}
var approvedAt *time.Time
if d.ApprovedAt.Valid {
t := d.ApprovedAt.Time
approvedAt = &t
}
var rejectionReason *string
if d.RejectionReason.Valid {
r := d.RejectionReason.String
rejectionReason = &r
}
return &domain.DirectDeposit{
ID: int(d.ID),
CustomerID: int(d.CustomerID.Int64),
WalletID: int(d.WalletID.Int64),
BankName: d.BankName.String,
AccountNumber: d.AccountNumber.String,
AccountHolder: d.AccountHolder.String,
Amount: float64(d.Amount.Exp),
ReferenceNumber: d.ReferenceNumber.String,
TransferScreenshot: d.TransferScreenshot.String,
Status: d.Status.String,
CreatedAt: d.CreatedAt.Time,
ApprovedBy: approvedBy,
ApprovedAt: approvedAt,
RejectionReason: rejectionReason,
}
return result, nil
}

View File

@ -224,7 +224,7 @@ func (r *VirtualGameRepo) GetVirtualGameSessionByToken(ctx context.Context, toke
func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error {
params := dbgen.CreateVirtualGameTransactionParams{
SessionID: tx.SessionID,
// SessionID: tx.SessionID,
UserID: tx.UserID,
WalletID: tx.WalletID,
TransactionType: tx.TransactionType,
@ -239,7 +239,7 @@ func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx *
func (r *VirtualGameRepo) CreateVirtualGameHistory(ctx context.Context, his *domain.VirtualGameHistory) error {
params := dbgen.CreateVirtualGameHistoryParams{
SessionID: pgtype.Text{String: his.SessionID, Valid: true},
// SessionID: pgtype.Text{String: his.SessionID, Valid: true},
UserID: his.UserID,
// WalletID: pgtype.Int8{Int64: *his.WalletID, Valid: true},
TransactionType: his.TransactionType,
@ -262,7 +262,7 @@ func (r *VirtualGameRepo) GetVirtualGameTransactionByExternalID(ctx context.Cont
}
return &domain.VirtualGameTransaction{
ID: dbTx.ID,
SessionID: dbTx.SessionID,
// SessionID: dbTx.SessionID,
UserID: dbTx.UserID,
WalletID: dbTx.WalletID,
TransactionType: dbTx.TransactionType,

View File

@ -0,0 +1,305 @@
package directdeposit
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/ports"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
)
type Service struct {
walletSvc wallet.Service
transferStore ports.TransferStore
directDepositStore repository.DirectDepositRepository
notificationSvc *notificationservice.Service
userSvc *user.Service
// mongoLogger *zap.Logger
// logger *slog.Logger
}
func NewService(
walletSvc wallet.Service,
transferStore ports.TransferStore,
directDepositStore repository.DirectDepositRepository,
notificationSvc *notificationservice.Service,
userSvc *user.Service,
// mongoLogger *zap.Logger,
// logger *slog.Logger,
) *Service {
return &Service{
walletSvc: walletSvc,
transferStore: transferStore,
directDepositStore: directDepositStore,
notificationSvc: notificationSvc,
userSvc: userSvc,
// mongoLogger: mongoLogger,
// logger: logger,
}
}
func (s *Service) CreateDirectDeposit(
ctx context.Context,
req domain.CreateDirectDeposit,
) (domain.DirectDeposit, error) {
deposit := domain.DirectDeposit{
CustomerID: req.CustomerID,
WalletID: req.WalletID,
BankName: req.BankName,
AccountNumber: req.AccountNumber,
AccountHolder: req.AccountHolder,
Amount: req.Amount,
ReferenceNumber: req.ReferenceNumber,
TransferScreenshot: req.TransferScreenshot,
Status: "PENDING",
}
// Step 1: create the deposit in DB
if err := s.directDepositStore.CreateDirectDeposit(ctx, &deposit); err != nil {
return domain.DirectDeposit{}, err
}
// Step 2: prepare common notification metadata
raw, _ := json.Marshal(map[string]any{
"deposit_id": deposit.ID,
"customer_id": deposit.CustomerID,
"amount": deposit.Amount,
"status": deposit.Status,
"timestamp": time.Now(),
})
// -------------------------------
// Step 3a: notify the customer
customerNotification := &domain.Notification{
RecipientID: int64(deposit.CustomerID),
DeliveryChannel: domain.DeliveryChannelInApp,
Reciever: domain.NotificationRecieverSideCustomer,
Type: domain.NOTIFICATION_TYPE_WALLET,
DeliveryStatus: domain.DeliveryStatusPending,
IsRead: false,
Level: domain.NotificationLevelInfo,
Priority: 2,
Metadata: raw,
Payload: domain.NotificationPayload{
Headline: "Direct Deposit Created",
Message: fmt.Sprintf("Your direct deposit of %.2f is now pending approval.", deposit.Amount),
},
}
if err := s.notificationSvc.SendNotification(ctx, customerNotification); err != nil {
return domain.DirectDeposit{}, err
}
// -------------------------------
// Step 3b: notify admins
adminNotification := &domain.Notification{
RecipientID: 0, // 0 or special ID for admin-wide notifications
DeliveryChannel: domain.DeliveryChannelInApp,
Reciever: domain.NotificationRecieverSideAdmin,
Type: domain.NOTIFICATION_TYPE_APPROVAL_REQUIRED,
DeliveryStatus: domain.DeliveryStatusPending,
IsRead: false,
Level: domain.NotificationLevelInfo,
Priority: 2,
Metadata: raw,
Payload: domain.NotificationPayload{
Headline: "New Direct Deposit Pending Approval",
Message: fmt.Sprintf("Customer #%d has created a direct deposit of %.2f that requires your approval.", deposit.CustomerID, deposit.Amount),
},
}
if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil {
return domain.DirectDeposit{}, err
}
return deposit, nil
}
func (s *Service) GetDirectDepositsByStatus(
ctx context.Context,
status string,
page int,
pageSize int,
) ([]domain.DirectDeposit, int64, error) {
deposits, total, err := s.directDepositStore.GetDirectDepositsByStatus(
ctx,
status,
page,
pageSize,
)
if err != nil {
return nil, 0, err
}
return deposits, total, nil
}
func (s *Service) ApproveDirectDeposit(
ctx context.Context,
depositID int,
adminID int,
) error {
// Step 1: fetch deposit (ensure it exists)
deposit, err := s.directDepositStore.GetDirectDepositByID(ctx, depositID)
if err != nil {
return err
}
// Step 2: approve in DB
if err := s.directDepositStore.ApproveDirectDeposit(ctx, depositID, adminID); err != nil {
return err
}
// Step 3: credit wallet balance
wallet, err := s.walletSvc.GetCustomerWallet(ctx, int64(deposit.CustomerID))
if err != nil {
return err
}
if _, err := s.walletSvc.AddToWallet(ctx,
wallet.RegularID,
domain.Currency(deposit.Amount),
domain.ValidInt64{},
domain.TRANSFER_DIRECT,
domain.PaymentDetails{},
"",
); err != nil {
return err
}
// Step 4: record transfer
transfer := domain.CreateTransfer{
Amount: domain.Currency(deposit.Amount),
Verified: true,
Message: "Direct deposit approved and credited",
Type: domain.DEPOSIT,
PaymentMethod: domain.TRANSFER_DIRECT,
ReceiverWalletID: domain.ValidInt64{Valid: true, Value: wallet.RegularID},
ReferenceNumber: deposit.ReferenceNumber,
Status: string(domain.DepositStatusCompleted),
}
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
return err
}
// Step 5: send customer notification
raw, _ := json.Marshal(map[string]any{
"deposit_id": deposit.ID,
"amount": deposit.Amount,
"status": "APPROVED",
"timestamp": time.Now(),
})
notification := &domain.Notification{
RecipientID: int64(deposit.CustomerID),
DeliveryChannel: domain.DeliveryChannelInApp,
Reciever: domain.NotificationRecieverSideCustomer,
Type: domain.NOTIFICATION_TYPE_TRANSFER_SUCCESS,
DeliveryStatus: domain.DeliveryStatusPending,
IsRead: false,
Level: domain.NotificationLevelInfo,
Priority: 2,
Metadata: raw,
Payload: domain.NotificationPayload{
Headline: "Direct Deposit Approved",
Message: fmt.Sprintf("Your direct deposit of %.2f has been approved and credited to your wallet.", deposit.Amount),
},
}
if err := s.notificationSvc.SendNotification(ctx, notification); err != nil {
return err
}
return nil
}
func (s *Service) RejectDirectDeposit(
ctx context.Context,
depositID int,
adminID int,
reason string,
) error {
// Step 1: fetch deposit to ensure it exists
deposit, err := s.directDepositStore.GetDirectDepositByID(ctx, depositID)
if err != nil {
return err
}
// Step 2: reject operation
if err := s.directDepositStore.RejectDirectDeposit(ctx, depositID, adminID, reason); err != nil {
return err
}
// Step 3: send customer notification
raw, _ := json.Marshal(map[string]any{
"deposit_id": deposit.ID,
"amount": deposit.Amount,
"status": "REJECTED",
"reason": reason,
"timestamp": time.Now(),
})
notification := &domain.Notification{
RecipientID: int64(deposit.CustomerID),
DeliveryChannel: domain.DeliveryChannelInApp,
Reciever: domain.NotificationRecieverSideCustomer,
Type: domain.NOTIFICATION_TYPE_TRANSFER_REJECTED,
DeliveryStatus: domain.DeliveryStatusPending,
IsRead: false,
Level: domain.NotificationLevelWarning,
Priority: 2,
Metadata: raw,
Payload: domain.NotificationPayload{
Headline: "Direct Deposit Rejected",
Message: fmt.Sprintf("Your direct deposit of %.2f was rejected. Reason: %s", deposit.Amount, reason),
},
}
if err := s.notificationSvc.SendNotification(ctx, notification); err != nil {
return err
}
return nil
}
func (s *Service) GetDirectDepositByID(
ctx context.Context,
depositID int,
) (*domain.DirectDeposit, error) {
deposit, err := s.directDepositStore.GetDirectDepositByID(ctx, depositID)
if err != nil {
return nil, err
}
return deposit, nil
}
func (s *Service) DeleteDirectDeposit(
ctx context.Context,
depositID int,
) error {
// Optional: fetch first to ensure deposit exists
_, err := s.directDepositStore.GetDirectDepositByID(ctx, depositID)
if err != nil {
return err
}
// Perform deletion
if err := s.directDepositStore.DeleteDirectDeposit(ctx, depositID); err != nil {
return err
}
return nil
}

View File

@ -71,7 +71,7 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
// 3. Record virtual game history (optional but recommended)
history := &domain.VirtualGameHistory{
SessionID: sessionID,
// SessionID: sessionID,
UserID: userID,
CompanyID: user.CompanyID.Value,
Provider: string(domain.PROVIDER_POPOK),

View File

@ -1,216 +0,0 @@
package wallet
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
// InitiateDirectDeposit creates a pending deposit request
func (s *Service) InitiateDirectDeposit(
ctx context.Context,
customerID int64,
amount domain.Currency,
bankRef string, // Mobile banking transaction reference
senderAccount string, // Customer's account number
) (domain.DirectDeposit, error) {
// Get customer's betting wallet
customerWallet, err := s.GetCustomerWallet(ctx, customerID)
if err != nil {
return domain.DirectDeposit{}, fmt.Errorf("failed to get customer wallet: %w", err)
}
// Create pending deposit record
deposit, err := s.directDepositStore.CreateDirectDeposit(ctx, domain.CreateDirectDeposit{
CustomerID: customerID,
WalletID: customerWallet.ID,
Amount: amount,
BankReference: bankRef,
SenderAccount: senderAccount,
Status: domain.DepositStatusPending,
})
if err != nil {
return domain.DirectDeposit{}, fmt.Errorf("failed to create deposit record: %w", err)
}
// Notify cashiers for manual verification
go s.notifyCashiersForVerification(ctx, deposit.ID, customerID, amount)
return deposit, nil
}
// VerifyDirectDeposit verifies and processes the deposit
func (s *Service) VerifyDirectDeposit(
ctx context.Context,
depositID int64,
cashierID int64,
isVerified bool,
verificationNotes string,
) (domain.DirectDeposit, error) {
// Get the deposit record
deposit, err := s.directDepositStore.GetDirectDeposit(ctx, depositID)
if err != nil {
return domain.DirectDeposit{}, fmt.Errorf("failed to get deposit: %w", err)
}
// Validate deposit status
if deposit.Status != domain.DepositStatusPending {
return domain.DirectDeposit{}, errors.New("only pending deposits can be verified")
}
// Update based on verification result
if isVerified {
// Credit the wallet
err = s.walletStore.UpdateBalance(ctx, deposit.WalletID,
deposit.Wallet.Balance+deposit.Amount)
if err != nil {
return domain.DirectDeposit{}, fmt.Errorf("failed to update wallet balance: %w", err)
}
// Publish wallet update event
// go s.publishWalletUpdate(ctx, deposit.WalletID, deposit.Wallet.UserID,
// deposit.Wallet.Balance+deposit.Amount, "direct_deposit_verified")
// Update deposit status
deposit.Status = domain.DepositStatusCompleted
} else {
deposit.Status = domain.DepositStatusRejected
}
// Update deposit record
updatedDeposit, err := s.directDepositStore.UpdateDirectDeposit(ctx, domain.UpdateDirectDeposit{
ID: depositID,
Status: deposit.Status,
VerifiedBy: cashierID,
VerificationNotes: verificationNotes,
VerifiedAt: time.Now(),
})
if err != nil {
return domain.DirectDeposit{}, fmt.Errorf("failed to update deposit: %w", err)
}
// Notify customer of verification result
go s.notifyCustomerVerificationResult(ctx, updatedDeposit)
return updatedDeposit, nil
}
// GetPendingDirectDeposits returns deposits needing verification
func (s *Service) GetPendingDirectDeposits(ctx context.Context) ([]domain.DirectDeposit, error) {
return s.directDepositStore.GetDirectDepositsByStatus(ctx, domain.DepositStatusPending)
}
// Helper functions
func (s *Service) notifyCashiersForVerification(ctx context.Context, depositID, customerID int64, amount domain.Currency) {
cashiers, _, err := s.userSvc.GetAllCashiers(ctx, domain.UserFilter{Role: string(domain.RoleCashier)})
if err != nil {
s.logger.Error("failed to get cashiers for notification",
"error", err,
"deposit_id", depositID)
return
}
customer, err := s.userSvc.GetUserByID(ctx, customerID)
if err != nil {
s.logger.Error("failed to get customer details",
"error", err,
"customer_id", customerID)
return
}
for _, cashier := range cashiers {
metadataMap := map[string]interface{}{
"deposit_id": depositID,
"customer_id": customerID,
"amount": amount.Float32(),
}
metadataJSON, err := json.Marshal(metadataMap)
if err != nil {
s.logger.Error("failed to marshal notification metadata",
"error", err,
"deposit_id", depositID)
continue
}
notification := &domain.Notification{
RecipientID: cashier.ID,
Type: domain.NotificationTypeDepositVerification,
Level: domain.NotificationLevelInfo,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: "Direct Deposit Requires Verification",
Message: fmt.Sprintf("Customer %s deposited %.2f - please verify", customer.FirstName+" "+customer.LastName, amount.Float32()),
},
Metadata: metadataJSON,
}
if err := s.notificationSvc.SendNotification(ctx, notification); err != nil {
s.logger.Error("failed to send verification notification",
"cashier_id", cashier.ID,
"error", err)
}
}
}
func (s *Service) notifyCustomerVerificationResult(ctx context.Context, deposit domain.DirectDeposit) {
var (
headline string
message string
level domain.NotificationLevel
)
if deposit.Status == domain.DepositStatusCompleted {
headline = "Deposit Verified"
message = fmt.Sprintf("Your deposit of %.2f has been credited to your wallet", deposit.Amount.Float32())
level = domain.NotificationLevelSuccess
} else {
headline = "Deposit Rejected"
message = fmt.Sprintf("Your deposit of %.2f was not verified. Reason: %s",
deposit.Amount.Float32(), deposit.VerificationNotes)
level = domain.NotificationLevelError
}
metadataMap := map[string]interface{}{
"deposit_id": deposit.ID,
"amount": deposit.Amount.Float32(),
"status": string(deposit.Status),
}
metadataJSON, err := json.Marshal(metadataMap)
if err != nil {
s.logger.Error("failed to marshal notification metadata",
"error", err,
"deposit_id", deposit.ID)
return
}
notification := &domain.Notification{
RecipientID: deposit.CustomerID,
Type: domain.NotificationTypeDepositResult,
Level: level,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: headline,
Message: message,
},
Metadata: metadataJSON,
}
if err := s.notificationSvc.SendNotification(ctx, notification); err != nil {
s.logger.Error("failed to send deposit result notification",
"customer_id", deposit.CustomerID,
"error", err)
}
}
// func (s *Service) publishWalletUpdate(ctx context.Context, walletID, userID int64, newBalance domain.Currency, trigger string) {
// s.kafkaProducer.Publish(ctx, fmt.Sprint(walletID), event.WalletEvent{
// EventType: event.WalletBalanceUpdated,
// WalletID: walletID,
// UserID: userID,
// Balance: newBalance,
// Trigger: trigger,
// })
// }

View File

@ -14,7 +14,7 @@ type Service struct {
// approvalStore ApprovalStore
walletStore ports.WalletStore
transferStore ports.TransferStore
directDepositStore ports.DirectDepositStore
// directDepositStore ports.DirectDepositStore
notificationSvc *notificationservice.Service
userSvc *user.Service
mongoLogger *zap.Logger
@ -24,7 +24,7 @@ type Service struct {
func NewService(
walletStore ports.WalletStore,
transferStore ports.TransferStore,
directDepositStore ports.DirectDepositStore,
// directDepositStore ports.DirectDepositStore,
notificationSvc *notificationservice.Service,
userSvc *user.Service,
mongoLogger *zap.Logger,
@ -33,7 +33,7 @@ func NewService(
return &Service{
walletStore: walletStore,
transferStore: transferStore,
directDepositStore: directDepositStore,
// directDepositStore: directDepositStore,
// approvalStore: approvalStore,
notificationSvc: notificationSvc,
userSvc: userSvc,

View File

@ -13,6 +13,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency"
directdeposit "github.com/SamuelTariku/FortuneBet-Backend/internal/services/direct_deposit"
enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
@ -48,6 +49,7 @@ import (
)
type App struct {
directDepositSvc *directdeposit.Service
enetPulseSvc *enetpulse.Service
atlasVirtualGameService atlas.AtlasVirtualGameService
veliVirtualGameService *veli.Service
@ -92,6 +94,7 @@ type App struct {
}
func NewApp(
directDepositSvc *directdeposit.Service,
enetPulseSvc *enetpulse.Service,
atlasVirtualGameService atlas.AtlasVirtualGameService,
veliVirtualGameService *veli.Service,
@ -149,6 +152,7 @@ func NewApp(
app.Static("/static", "./static")
s := &App{
directDepositSvc: directDepositSvc,
enetPulseSvc: enetPulseSvc,
atlasVirtualGameService: atlasVirtualGameService,
veliVirtualGameService: veliVirtualGameService,

View File

@ -1,124 +1,267 @@
package handlers
import (
"fmt"
"math"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
)
// InitiateDirectDeposit godoc
// @Summary Initiate a direct deposit
// @Description Customer initiates a direct deposit from mobile banking
// @Tags Direct Deposits
// CreateDirectDeposit godoc
// @Summary Create a new direct deposit
// @Description Creates a direct deposit for a customer and notifies both the customer and admins
// @Tags DirectDeposit
// @Accept json
// @Produce json
// @Param request body domain.DirectDepositRequest true "Deposit details"
// @Success 201 {object} domain.Response
// @Param body body domain.CreateDirectDeposit true "Direct deposit details"
// @Success 200 {object} domain.Response{data=domain.DirectDeposit}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/direct_deposit [post]
func (h *Handler) InitiateDirectDeposit(c *fiber.Ctx) error {
var req domain.DirectDepositRequest
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/direct-deposits [post]
func (h *Handler) CreateDirectDeposit(c *fiber.Ctx) error {
var req domain.CreateDirectDeposit
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Invalid request payload",
})
}
deposit, err := h.walletSvc.InitiateDirectDeposit(
c.Context(),
req.CustomerID,
req.Amount,
req.BankReference,
req.SenderAccount,
)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Failed to initiate direct deposit",
})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Direct deposit initiated successfully",
// Call service
deposit, err := h.directDepositSvc.CreateDirectDeposit(c.Context(), req)
if err != nil {
h.logger.Error("CreateDirectDeposit error", err.Error())
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to create direct deposit",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Direct deposit created successfully",
Data: deposit,
StatusCode: fiber.StatusOK,
Success: true,
StatusCode: fiber.StatusCreated,
})
}
// VerifyDirectDeposit godoc
// @Summary Verify a direct deposit
// @Description Cashier verifies a direct deposit transaction
// @Tags Direct Deposits
// GetDirectDepositsByStatus godoc
// @Summary Get direct deposits by status
// @Description Fetches direct deposits filtered by status with pagination
// @Tags DirectDeposit
// @Accept json
// @Produce json
// @Param request body domain.VerifyDirectDepositRequest true "Verification details"
// @Success 200 {object} domain.Response
// @Param status query string true "Deposit status (e.g., PENDING, APPROVED, REJECTED)"
// @Param page query int false "Page number"
// @Param pageSize query int false "Page size"
// @Success 200 {object} domain.Response{data=[]domain.DirectDeposit}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 401 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/direct_deposit/verify [post]
func (h *Handler) VerifyDirectDeposit(c *fiber.Ctx) error {
var req domain.VerifyDirectDepositRequest
if err := c.BodyParser(&req); err != nil {
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/direct-deposits [get]
func (h *Handler) GetDirectDepositsByStatus(c *fiber.Ctx) error {
status := c.Query("status")
if status == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Invalid verification request",
Message: "status query parameter is required",
})
}
cashierID := c.Locals("user_id")
if cashierID == nil {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Error: "missing user_id in context",
Message: "Unauthorized access",
})
}
page, _ := strconv.Atoi(c.Query("page", "1"))
pageSize, _ := strconv.Atoi(c.Query("pageSize", "50"))
deposit, err := h.walletSvc.VerifyDirectDeposit(
c.Context(),
req.DepositID,
cashierID.(int64),
req.IsVerified,
req.Notes,
)
deposits, total, err := h.directDepositSvc.GetDirectDepositsByStatus(c.Context(), status, page, pageSize)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
h.logger.Error("GetDirectDepositsByStatus error", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch direct deposits",
Error: err.Error(),
Message: "Failed to verify deposit",
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Deposit verification processed successfully",
Data: deposit,
Success: true,
StatusCode: fiber.StatusOK,
})
}
// GetPendingDeposits godoc
// @Summary Get pending direct deposits
// @Description Get list of direct deposits needing verification
// @Tags Direct Deposits
// @Produce json
// @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/direct_deposit/pending [get]
func (h *Handler) GetPendingDirectDeposits(c *fiber.Ctx) error {
deposits, err := h.walletSvc.GetPendingDirectDeposits(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Failed to retrieve pending deposits",
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Pending deposits retrieved successfully",
Message: fmt.Sprintf("Direct deposits with status '%s' fetched successfully", status),
Data: deposits,
Success: true,
StatusCode: fiber.StatusOK,
Success: true,
// Optional: include pagination info
MetaData: map[string]any{
"page": page,
"pageSize": pageSize,
"total": total,
"totalPage": int(math.Ceil(float64(total) / float64(pageSize))),
},
})
}
// ApproveDirectDeposit godoc
// @Summary Approve a direct deposit
// @Description Approves a direct deposit by admin and credits customer wallet
// @Tags DirectDeposit
// @Accept json
// @Produce json
// @Param depositID path int true "Deposit ID"
// @Param adminID query int true "Admin ID performing the approval"
// @Success 200 {object} domain.Response{data=string}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/direct-deposits/{depositID}/approve [post]
func (h *Handler) ApproveDirectDeposit(c *fiber.Ctx) error {
depositID, err := strconv.Atoi(c.Params("depositID"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid deposit ID",
Error: err.Error(),
})
}
adminID, err := strconv.Atoi(c.Query("adminID"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid admin ID",
Error: err.Error(),
})
}
if err := h.directDepositSvc.ApproveDirectDeposit(c.Context(), depositID, adminID); err != nil {
h.logger.Error("ApproveDirectDeposit error", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to approve direct deposit",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: fmt.Sprintf("Direct deposit #%d approved successfully", depositID),
Data: fmt.Sprintf("Deposit #%d approved", depositID),
StatusCode: fiber.StatusOK,
Success: true,
})
}
// RejectDirectDeposit godoc
// @Summary Reject a direct deposit
// @Description Rejects a direct deposit by admin and notifies the customer
// @Tags DirectDeposit
// @Accept json
// @Produce json
// @Param depositID path int true "Deposit ID"
// @Param adminID query int true "Admin ID performing the rejection"
// @Param reason query string true "Reason for rejection"
// @Success 200 {object} domain.Response{data=string}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/direct-deposits/{depositID}/reject [post]
func (h *Handler) RejectDirectDeposit(c *fiber.Ctx) error {
depositID, err := strconv.Atoi(c.Params("depositID"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid deposit ID",
Error: err.Error(),
})
}
adminID, err := strconv.Atoi(c.Query("adminID"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid admin ID",
Error: err.Error(),
})
}
reason := c.Query("reason")
if reason == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Rejection reason is required",
})
}
if err := h.directDepositSvc.RejectDirectDeposit(c.Context(), depositID, adminID, reason); err != nil {
h.logger.Error("RejectDirectDeposit error", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to reject direct deposit",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: fmt.Sprintf("Direct deposit #%d rejected successfully", depositID),
Data: fmt.Sprintf("Deposit #%d rejected", depositID),
StatusCode: fiber.StatusOK,
Success: true,
})
}
// GetDirectDepositByID godoc
// @Summary Get a direct deposit by ID
// @Description Fetches a single direct deposit by its ID
// @Tags DirectDeposit
// @Accept json
// @Produce json
// @Param depositID path int true "Deposit ID"
// @Success 200 {object} domain.Response{data=domain.DirectDeposit}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/direct-deposits/{depositID} [get]
func (h *Handler) GetDirectDepositByID(c *fiber.Ctx) error {
depositID, err := strconv.Atoi(c.Params("depositID"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid deposit ID",
Error: err.Error(),
})
}
deposit, err := h.directDepositSvc.GetDirectDepositByID(c.Context(), depositID)
if err != nil {
h.logger.Error("GetDirectDepositByID error", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch direct deposit",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: fmt.Sprintf("Direct deposit #%d fetched successfully", depositID),
Data: deposit,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// DeleteDirectDeposit godoc
// @Summary Delete a direct deposit
// @Description Deletes a direct deposit by its ID
// @Tags DirectDeposit
// @Accept json
// @Produce json
// @Param depositID path int true "Deposit ID"
// @Success 200 {object} domain.Response{data=string}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/direct-deposits/{depositID} [delete]
func (h *Handler) DeleteDirectDeposit(c *fiber.Ctx) error {
depositID, err := strconv.Atoi(c.Params("depositID"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid deposit ID",
Error: err.Error(),
})
}
if err := h.directDepositSvc.DeleteDirectDeposit(c.Context(), depositID); err != nil {
h.logger.Error("DeleteDirectDeposit error", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to delete direct deposit",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: fmt.Sprintf("Direct deposit #%d deleted successfully", depositID),
Data: fmt.Sprintf("Deposit #%d deleted", depositID),
StatusCode: fiber.StatusOK,
Success: true,
})
}

View File

@ -12,6 +12,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency"
directdeposit "github.com/SamuelTariku/FortuneBet-Backend/internal/services/direct_deposit"
enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
@ -43,6 +44,7 @@ import (
)
type Handler struct {
directDepositSvc *directdeposit.Service
orchestrationSvc *orchestration.Service
enetPulseSvc *enetpulse.Service
telebirrSvc *telebirr.TelebirrService
@ -84,6 +86,7 @@ type Handler struct {
}
func New(
directDepositSvc *directdeposit.Service,
orchestrationSvc *orchestration.Service,
enetPulseSvc *enetpulse.Service,
telebirrSvc *telebirr.TelebirrService,
@ -124,6 +127,7 @@ func New(
mongoLoggerSvc *zap.Logger,
) *Handler {
return &Handler{
directDepositSvc: directDepositSvc,
orchestrationSvc: orchestrationSvc,
enetPulseSvc: enetPulseSvc,
telebirrSvc: telebirrSvc,

View File

@ -346,17 +346,17 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error {
res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req)
if err != nil {
if strings.Contains(err.Error(), veli.ErrDuplicateTransaction.Error()) {
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{
// Message: "Duplicate transaction",
Error: veli.ErrDuplicateTransaction.Error(),
})
} else if strings.Contains(err.Error(), veli.ErrInsufficientBalance.Error()) {
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{
// Message: "Wallet balance is insufficient",
Error: veli.ErrInsufficientBalance.Error(),
})
} else if strings.Contains(err.Error(), veli.ErrPlayerNotFound.Error()) {
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{
// Message: "User not found",
Error: veli.ErrPlayerNotFound.Error(),
})
@ -473,12 +473,12 @@ func (h *Handler) HandleWin(c *fiber.Ctx) error {
res, err := h.veliVirtualGameSvc.ProcessWin(c.Context(), req)
if err != nil {
if errors.Is(err, veli.ErrDuplicateTransaction) {
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{
// Message: "Duplicate transaction",
Error: veli.ErrDuplicateTransaction.Error(),
})
} else if errors.Is(err, veli.ErrPlayerNotFound) {
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{
// Message: "Duplicate transaction",
Error: veli.ErrPlayerNotFound.Error(),
})
@ -550,12 +550,12 @@ func (h *Handler) HandleCancel(c *fiber.Ctx) error {
res, err := h.veliVirtualGameSvc.ProcessCancel(c.Context(), req)
if err != nil {
if strings.Contains(err.Error(), veli.ErrDuplicateTransaction.Error()) {
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{
// Message: "Duplicate transaction",
Error: veli.ErrDuplicateTransaction.Error(),
})
} else if strings.Contains(err.Error(), veli.ErrPlayerNotFound.Error()) {
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{
// Message: "User not found",
Error: veli.ErrPlayerNotFound.Error(),
})

View File

@ -20,6 +20,7 @@ import (
func (a *App) initAppRoutes() {
h := handlers.New(
a.directDepositSvc,
a.orchestrationSvc,
a.enetPulseSvc,
a.telebirrSvc,
@ -103,9 +104,12 @@ func (a *App) initAppRoutes() {
groupV1.Get("/tenant", a.authMiddleware, h.GetTenantSlugByToken)
//Direct_deposit
groupV1.Post("/direct_deposit", a.authMiddleware, h.InitiateDirectDeposit)
groupV1.Post("/direct_deposit/verify", a.authMiddleware, h.VerifyDirectDeposit)
groupV1.Get("/direct_deposit/pending", a.authMiddleware, h.GetPendingDirectDeposits)
groupV1.Post("/direct-deposits", a.authMiddleware, h.CreateDirectDeposit)
groupV1.Post("/direct-deposits/:depositID/approve", a.authMiddleware, h.ApproveDirectDeposit)
groupV1.Post("/direct-deposits/:depositID/reject", a.authMiddleware, h.RejectDirectDeposit)
groupV1.Get("/direct-deposits", a.authMiddleware, h.GetDirectDepositsByStatus)
groupV1.Get("/direct-deposits/:depositID", a.authMiddleware, h.GetDirectDepositByID)
groupV1.Delete("/direct-deposits/:depositID", a.authMiddleware, h.DeleteDirectDeposit)
// Swagger
a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler())