Merge branch 'main' into ticket-bet

This commit is contained in:
Samuel Tariku 2025-06-16 21:34:34 +03:00
commit cfced1951c
53 changed files with 3193 additions and 1050 deletions

7
.env
View File

@ -27,7 +27,14 @@ POPOK_BASE_URL=https://st.pokgaming.com/ #Staging
POPOK_CALLBACK_URL=1 POPOK_CALLBACK_URL=1
#Muli-currency Support
FIXER_API_KEY=3b0f1eb30d-63c875026d-sxy9pl
BASE_CURRENCY=ETB
FIXER_BASE_URL=https://api.apilayer.com/fixer
# Chapa API Configuration # Chapa API Configuration
CHAPA_TRANSFER_TYPE="Payout"
CHAPA_PAYMENT_TYPE="API"
CHAPA_BASE_URL="https://api.chapa.co/v1" CHAPA_BASE_URL="https://api.chapa.co/v1"
CHAPA_ENCRYPTION_KEY=zLdYrjnBCknMvFikmP5jBfen CHAPA_ENCRYPTION_KEY=zLdYrjnBCknMvFikmP5jBfen
CHAPA_PUBLIC_KEY=CHAPUBK_TEST-HJR0qhQRPLTkauNy9Q8UrmskPTOR31aC CHAPA_PUBLIC_KEY=CHAPUBK_TEST-HJR0qhQRPLTkauNy9Q8UrmskPTOR31aC

View File

@ -4,6 +4,7 @@ import (
// "context" // "context"
// "context" // "context"
"context"
"fmt" "fmt"
"log" "log"
"log/slog" "log/slog"
@ -33,6 +34,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
@ -134,13 +136,18 @@ func main() {
chapaSvc := chapa.NewService( chapaSvc := chapa.NewService(
wallet.TransferStore(store), wallet.TransferStore(store),
wallet.WalletStore(store), *walletSvc,
user.UserStore(store), user.UserStore(store),
chapaClient, chapaClient,
) )
// Initialize reporting components
reportRepo := repository.NewReportRepo(store) reportRepo := repository.NewReportRepo(store)
currRepo := repository.NewCurrencyPostgresRepository(store)
fixerFertcherSvc := currency.NewFixerFetcher(
cfg.FIXER_API_KEY,
cfg.FIXER_BASE_URL,
)
reportSvc := report.NewService( reportSvc := report.NewService(
bet.BetStore(store), bet.BetStore(store),
@ -175,7 +182,17 @@ func main() {
logger, logger,
5*time.Minute, 5*time.Minute,
) )
walletMonitorSvc.Start()
currSvc := currency.NewService(
currRepo,
cfg.BASE_CURRENCY,
fixerFertcherSvc,
)
exchangeWorker := currency.NewExchangeRateWorker(fixerFertcherSvc, logger, cfg)
exchangeWorker.Start(context.Background())
defer exchangeWorker.Stop()
go walletMonitorSvc.Start()
httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc) httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc)
httpserver.StartTicketCrons(*ticketSvc) httpserver.StartTicketCrons(*ticketSvc)
@ -183,6 +200,7 @@ func main() {
// Initialize and start HTTP server // Initialize and start HTTP server
app := httpserver.NewApp( app := httpserver.NewApp(
currSvc,
cfg.Port, cfg.Port,
v, v,
authSvc, authSvc,

View File

@ -69,6 +69,15 @@ CREATE TABLE IF NOT EXISTS tickets (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
CREATE TABLE exchange_rates (
id SERIAL PRIMARY KEY,
from_currency VARCHAR(3) NOT NULL,
to_currency VARCHAR(3) NOT NULL,
rate DECIMAL(19, 6) NOT NULL,
valid_until TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (from_currency, to_currency)
);
CREATE TABLE IF NOT EXISTS bet_outcomes ( CREATE TABLE IF NOT EXISTS bet_outcomes (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
bet_id BIGINT NOT NULL, bet_id BIGINT NOT NULL,
@ -123,14 +132,15 @@ CREATE TABLE IF NOT EXISTS customer_wallets (
); );
CREATE TABLE IF NOT EXISTS wallet_transfer ( CREATE TABLE IF NOT EXISTS wallet_transfer (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
amount BIGINT NOT NULL, amount BIGINT,
type VARCHAR(255) NOT NULL, type VARCHAR(255),
receiver_wallet_id BIGINT, receiver_wallet_id BIGINT,
sender_wallet_id BIGINT, sender_wallet_id BIGINT,
cashier_id BIGINT, cashier_id BIGINT,
verified BOOLEAN NOT NULL DEFAULT false, verified BOOLEAN DEFAULT false,
reference_number VARCHAR(255) NOT NULL, reference_number VARCHAR(255),
payment_method VARCHAR(255) NOT NULL, status VARCHAR(255),
payment_method VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
@ -298,7 +308,8 @@ ALTER TABLE bets
ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id), ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id); ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id);
ALTER TABLE wallets ALTER TABLE wallets
ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id); ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id);
ADD COLUMN currency VARCHAR(3) NOT NULL DEFAULT 'ETB';
ALTER TABLE customer_wallets ALTER TABLE customer_wallets
ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id), ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id),
ADD CONSTRAINT fk_customer_wallets_regular_wallet FOREIGN KEY (regular_wallet_id) REFERENCES wallets(id), ADD CONSTRAINT fk_customer_wallets_regular_wallet FOREIGN KEY (regular_wallet_id) REFERENCES wallets(id),

View File

@ -1,3 +1,5 @@
DROP TABLE IF EXISTS vitrual_games;
DROP TABLE IF EXISTS virtual_game_transactions; DROP TABLE IF EXISTS virtual_game_transactions;
DROP TABLE IF EXISTS virtual_game_sessions; DROP TABLE IF EXISTS virtual_game_sessions;

View File

@ -7,9 +7,10 @@ INSERT INTO wallet_transfer (
cashier_id, cashier_id,
verified, verified,
reference_number, reference_number,
status,
payment_method payment_method
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *; RETURNING *;
-- name: GetAllTransfers :many -- name: GetAllTransfers :many
SELECT * SELECT *
@ -31,4 +32,10 @@ WHERE reference_number = $1;
UPDATE wallet_transfer UPDATE wallet_transfer
SET verified = $1, SET verified = $1,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $2;
-- name: UpdateTransferStatus :exec
UPDATE wallet_transfer
SET status = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2; WHERE id = $2;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -31,39 +31,6 @@ 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:
@ -193,6 +160,22 @@ definitions:
tx_ref: tx_ref:
type: string type: string
type: object type: object
domain.ChapaWithdrawalRequest:
properties:
account_name:
type: string
account_number:
type: string
amount:
description: string because Chapa API uses string for monetary values
type: string
bank_code:
type: integer
currency:
type: string
reference:
type: string
type: object
domain.CreateBetOutcomeReq: domain.CreateBetOutcomeReq:
properties: properties:
event_id: event_id:
@ -228,6 +211,81 @@ definitions:
- $ref: '#/definitions/domain.OutcomeStatus' - $ref: '#/definitions/domain.OutcomeStatus'
example: 1 example: 1
type: object type: object
domain.DashboardSummary:
properties:
active_admins:
type: integer
active_bets:
type: integer
active_branches:
type: integer
active_cashiers:
type: integer
active_companies:
type: integer
active_customers:
type: integer
active_games:
type: integer
active_managers:
type: integer
average_stake:
type: integer
branches_count:
type: integer
customer_count:
type: integer
inactive_admins:
type: integer
inactive_branches:
type: integer
inactive_cashiers:
type: integer
inactive_companies:
type: integer
inactive_customers:
type: integer
inactive_games:
type: integer
inactive_managers:
type: integer
profit:
type: integer
read_notifications:
type: integer
total_admins:
type: integer
total_bets:
type: integer
total_cashiers:
type: integer
total_companies:
type: integer
total_deposits:
type: integer
total_games:
type: integer
total_losses:
type: integer
total_managers:
type: integer
total_notifications:
type: integer
total_stakes:
type: integer
total_wallets:
type: integer
total_wins:
type: integer
total_withdrawals:
type: integer
unread_notifications:
type: integer
win_balance:
type: integer
win_rate:
type: number
type: object
domain.ErrorResponse: domain.ErrorResponse:
properties: properties:
error: error:
@ -235,6 +293,57 @@ definitions:
message: message:
type: string type: string
type: object type: object
domain.EventStatus:
enum:
- upcoming
- in_play
- to_be_fixed
- ended
- postponed
- cancelled
- walkover
- interrupted
- abandoned
- retired
- suspended
- decided_by_fa
- removed
type: string
x-enum-varnames:
- STATUS_PENDING
- STATUS_IN_PLAY
- STATUS_TO_BE_FIXED
- STATUS_ENDED
- STATUS_POSTPONED
- STATUS_CANCELLED
- STATUS_WALKOVER
- STATUS_INTERRUPTED
- STATUS_ABANDONED
- STATUS_RETIRED
- STATUS_SUSPENDED
- STATUS_DECIDED_BY_FA
- STATUS_REMOVED
domain.League:
properties:
bet365_id:
example: 1121
type: integer
cc:
example: uk
type: string
id:
example: 1
type: integer
is_active:
example: false
type: boolean
name:
example: BPL
type: string
sport_id:
example: 1
type: integer
type: object
domain.Odd: domain.Odd:
properties: properties:
category: category:
@ -404,6 +513,16 @@ 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
@ -466,48 +585,52 @@ definitions:
type: object type: object
domain.UpcomingEvent: domain.UpcomingEvent:
properties: properties:
awayKitImage: away_kit_image:
description: Kit or image for away team (optional) description: Kit or image for away team (optional)
type: string type: string
awayTeam: away_team:
description: Away team name (can be empty/null) description: Away team name (can be empty/null)
type: string type: string
awayTeamID: away_team_id:
description: Away team ID (can be empty/null) description: Away team ID (can be empty/null)
type: integer type: integer
homeKitImage: home_kit_image:
description: Kit or image for home team (optional) description: Kit or image for home team (optional)
type: string type: string
homeTeam: home_team:
description: Home team name (if available) description: Home team name (if available)
type: string type: string
homeTeamID: home_team_id:
description: Home team ID description: Home team ID
type: integer type: integer
id: id:
description: Event ID description: Event ID
type: string type: string
leagueCC: league_cc:
description: League country code description: League country code
type: string type: string
leagueID: league_id:
description: League ID description: League ID
type: integer type: integer
leagueName: league_name:
description: League name description: League name
type: string type: string
matchName: match_name:
description: Match or event name description: Match or event name
type: string type: string
source: source:
description: bet api provider (bet365, betfair) description: bet api provider (bet365, betfair)
type: string type: string
sportID: sport_id:
description: Sport ID description: Sport ID
type: integer type: integer
startTime: start_time:
description: Converted from "time" field in UNIX format description: Converted from "time" field in UNIX format
type: string type: string
status:
allOf:
- $ref: '#/definitions/domain.EventStatus'
description: Match Status for event
type: object type: object
domain.VeliCallback: domain.VeliCallback:
properties: properties:
@ -550,6 +673,8 @@ definitions:
type: string type: string
id: id:
type: integer type: integer
is_active:
type: boolean
is_featured: is_featured:
type: boolean type: boolean
max_bet: max_bet:
@ -602,6 +727,9 @@ definitions:
type: object type: object
handlers.BranchDetailRes: handlers.BranchDetailRes:
properties: properties:
balance:
example: 100.5
type: number
branch_manager_id: branch_manager_id:
example: 1 example: 1
type: integer type: integer
@ -928,6 +1056,12 @@ definitions:
properties: properties:
branch_id: branch_id:
type: integer type: integer
branch_location:
type: string
branch_name:
type: string
branch_wallet:
type: integer
created_at: created_at:
type: string type: string
email: email:
@ -1045,6 +1179,13 @@ definitions:
- otp - otp
- password - password
type: object type: object
handlers.ResultRes:
properties:
outcomes:
items:
$ref: '#/definitions/domain.BetOutcome'
type: array
type: object
handlers.SearchUserByNameOrPhoneReq: handlers.SearchUserByNameOrPhoneReq:
properties: properties:
query: query:
@ -1296,9 +1437,9 @@ definitions:
type: string type: string
mode: mode:
enum: enum:
- REAL - fun
- DEMO - real
example: REAL example: real
type: string type: string
required: required:
- currency - currency
@ -1395,39 +1536,6 @@ definitions:
example: false example: false
type: boolean type: boolean
type: object type: object
report.DashboardSummary:
properties:
active_bets:
type: integer
active_branches:
type: integer
active_customers:
type: integer
average_stake:
type: integer
branches_count:
type: integer
customer_count:
type: integer
profit:
type: integer
total_bets:
type: integer
total_deposits:
type: integer
total_losses:
type: integer
total_stakes:
type: integer
total_wins:
type: integer
total_withdrawals:
type: integer
win_balance:
type: integer
win_rate:
type: number
type: object
response.APIResponse: response.APIResponse:
properties: properties:
data: {} data: {}
@ -1646,6 +1754,25 @@ 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
description: Get list of banks supported by Chapa
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get supported banks
tags:
- Chapa
/api/v1/chapa/payments/deposit: /api/v1/chapa/payments/deposit:
post: post:
consumes: consumes:
@ -1673,6 +1800,8 @@ paths:
description: Internal Server Error description: Internal Server Error
schema: schema:
$ref: '#/definitions/domain.ErrorResponse' $ref: '#/definitions/domain.ErrorResponse'
security:
- ApiKeyAuth: []
summary: Initiate a deposit summary: Initiate a deposit
tags: tags:
- Chapa - Chapa
@ -1736,6 +1865,105 @@ paths:
summary: Chapa payment webhook callback (used by Chapa) summary: Chapa payment webhook callback (used by Chapa)
tags: tags:
- Chapa - Chapa
/api/v1/chapa/payments/withdraw:
post:
consumes:
- application/json
description: Initiates a withdrawal request to transfer funds to a bank account
via Chapa
parameters:
- description: Withdrawal request details
in: body
name: request
required: true
schema:
$ref: '#/definitions/domain.ChapaWithdrawalRequest'
produces:
- application/json
responses:
"201":
description: Chapa withdrawal process initiated successfully
schema:
$ref: '#/definitions/domain.Response'
"400":
description: Invalid request body
schema:
$ref: '#/definitions/domain.ErrorResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/domain.ErrorResponse'
"422":
description: Unprocessable entity
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/domain.ErrorResponse'
security:
- ApiKeyAuth: []
summary: Initiate a withdrawal
tags:
- Chapa
/api/v1/currencies:
get:
description: Returns list of supported currencies
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
items:
type: integer
type: array
type: object
summary: Get supported currencies
tags:
- Multi-Currency
/api/v1/currencies/convert:
get:
description: Converts amount from one currency to another
parameters:
- description: Source currency code (e.g., USD)
in: query
name: from
required: true
type: string
- description: Target currency code (e.g., ETB)
in: query
name: to
required: true
type: string
- description: Amount to convert
in: query
name: amount
required: true
type: number
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
type: number
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Convert currency
tags:
- Multi-Currency
/api/v1/reports/dashboard: /api/v1/reports/dashboard:
get: get:
consumes: consumes:
@ -1776,7 +2004,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/report.DashboardSummary' $ref: '#/definitions/domain.DashboardSummary'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1997,27 +2225,6 @@ 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:
@ -2458,6 +2665,35 @@ paths:
summary: Delete the branch operation summary: Delete the branch operation
tags: tags:
- branch - branch
/branchCashier:
get:
consumes:
- application/json
description: Gets branch for cahier
parameters:
- description: Branch ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.BranchDetailRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Gets branch for cahier
tags:
- branch
/branchWallet: /branchWallet:
get: get:
consumes: consumes:
@ -2516,6 +2752,39 @@ paths:
summary: Get cashier by id summary: Get cashier by id
tags: tags:
- cashier - cashier
/cashierWallet:
get:
consumes:
- application/json
description: Get wallet for cashier
parameters:
- description: User ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.UserProfileRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Get wallet for cashier
tags:
- cashier
/cashiers: /cashiers:
get: get:
consumes: consumes:
@ -2800,6 +3069,138 @@ paths:
summary: Gets branches by company id summary: Gets branches by company id
tags: tags:
- branch - branch
/events:
get:
consumes:
- application/json
description: Retrieve all upcoming events from the database
parameters:
- description: Page number
in: query
name: page
type: integer
- description: Page size
in: query
name: page_size
type: integer
- description: League ID Filter
in: query
name: league_id
type: string
- description: Sport ID Filter
in: query
name: sport_id
type: string
- description: Country Code Filter
in: query
name: cc
type: string
- description: Start Time
in: query
name: first_start_time
type: string
- description: End Time
in: query
name: last_start_time
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.UpcomingEvent'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Retrieve all upcoming events
tags:
- prematch
/events/{id}:
delete:
consumes:
- application/json
description: Set the event status to removed
parameters:
- description: Event 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: Set the event status to removed
tags:
- event
get:
consumes:
- application/json
description: Retrieve an upcoming event by ID
parameters:
- description: ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.UpcomingEvent'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Retrieve an upcoming by ID
tags:
- prematch
/leagues:
get:
consumes:
- application/json
description: Gets all leagues
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.League'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Gets all leagues
tags:
- leagues
/manager/{id}/branch: /manager/{id}/branch:
get: get:
consumes: consumes:
@ -2966,112 +3367,7 @@ paths:
summary: Update Managers summary: Update Managers
tags: tags:
- manager - manager
/operation: /odds:
post:
consumes:
- application/json
description: Creates a operation
parameters:
- description: Creates operation
in: body
name: createBranchOperation
required: true
schema:
$ref: '#/definitions/handlers.CreateBranchOperationReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.BranchOperationRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Create a operation
tags:
- branch
/prematch/events:
get:
consumes:
- application/json
description: Retrieve all upcoming events from the database
parameters:
- description: Page number
in: query
name: page
type: integer
- description: Page size
in: query
name: page_size
type: integer
- description: League ID Filter
in: query
name: league_id
type: string
- description: Sport ID Filter
in: query
name: sport_id
type: string
- description: Start Time
in: query
name: first_start_time
type: string
- description: End Time
in: query
name: last_start_time
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.UpcomingEvent'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Retrieve all upcoming events
tags:
- prematch
/prematch/events/{id}:
get:
consumes:
- application/json
description: Retrieve an upcoming event by ID
parameters:
- description: ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.UpcomingEvent'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Retrieve an upcoming by ID
tags:
- prematch
/prematch/odds:
get: get:
consumes: consumes:
- application/json - application/json
@ -3092,38 +3388,7 @@ paths:
summary: Retrieve all prematch odds summary: Retrieve all prematch odds
tags: tags:
- prematch - prematch
/prematch/odds/{event_id}: /odds/upcoming/{upcoming_id}:
get:
consumes:
- application/json
description: Retrieve prematch odds for a specific event by event ID
parameters:
- description: Event ID
in: path
name: event_id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.Odd'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Retrieve prematch odds for an event
tags:
- prematch
/prematch/odds/upcoming/{upcoming_id}:
get: get:
consumes: consumes:
- application/json - application/json
@ -3163,7 +3428,7 @@ paths:
summary: Retrieve prematch odds by upcoming ID (FI) summary: Retrieve prematch odds by upcoming ID (FI)
tags: tags:
- prematch - prematch
/prematch/odds/upcoming/{upcoming_id}/market/{market_id}: /odds/upcoming/{upcoming_id}/market/{market_id}:
get: get:
consumes: consumes:
- application/json - application/json
@ -3199,6 +3464,36 @@ paths:
summary: Retrieve raw odds by Market ID summary: Retrieve raw odds by Market ID
tags: tags:
- prematch - prematch
/operation:
post:
consumes:
- application/json
description: Creates a operation
parameters:
- description: Creates operation
in: body
name: createBranchOperation
required: true
schema:
$ref: '#/definitions/handlers.CreateBranchOperationReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.BranchOperationRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Create a operation
tags:
- branch
/random/bet: /random/bet:
post: post:
consumes: consumes:
@ -3336,7 +3631,7 @@ paths:
description: OK description: OK
schema: schema:
items: items:
$ref: '#/definitions/domain.BetOutcome' $ref: '#/definitions/handlers.ResultRes'
type: array type: array
"400": "400":
description: Bad Request description: Bad Request

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: auth.sql // source: auth.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: bet.sql // source: bet.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: branch.sql // source: branch.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: cashier.sql // source: cashier.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: company.sql // source: company.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: copyfrom.go // source: copyfrom.go
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: events.sql // source: events.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: leagues.sql // source: leagues.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
package dbgen package dbgen
@ -482,14 +482,15 @@ type WalletThresholdNotification struct {
type WalletTransfer struct { type WalletTransfer struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Amount int64 `json:"amount"` Amount pgtype.Int8 `json:"amount"`
Type string `json:"type"` Type pgtype.Text `json:"type"`
ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"`
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 pgtype.Bool `json:"verified"`
ReferenceNumber string `json:"reference_number"` ReferenceNumber pgtype.Text `json:"reference_number"`
PaymentMethod string `json:"payment_method"` Status pgtype.Text `json:"status"`
PaymentMethod pgtype.Text `json:"payment_method"`
CreatedAt pgtype.Timestamp `json:"created_at"` CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"`
} }

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: monitor.sql // source: monitor.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: notification.sql // source: notification.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: odds.sql // source: odds.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: otp.sql // source: otp.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: referal.sql // source: referal.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: result.sql // source: result.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: ticket.sql // source: ticket.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: transactions.sql // source: transactions.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: transfer.sql // source: transfer.sql
package dbgen package dbgen
@ -20,21 +20,23 @@ INSERT INTO wallet_transfer (
cashier_id, cashier_id,
verified, verified,
reference_number, reference_number,
status,
payment_method payment_method
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at RETURNING id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, status, payment_method, created_at, updated_at
` `
type CreateTransferParams struct { type CreateTransferParams struct {
Amount int64 `json:"amount"` Amount pgtype.Int8 `json:"amount"`
Type string `json:"type"` Type pgtype.Text `json:"type"`
ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"`
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 pgtype.Bool `json:"verified"`
ReferenceNumber string `json:"reference_number"` ReferenceNumber pgtype.Text `json:"reference_number"`
PaymentMethod string `json:"payment_method"` Status pgtype.Text `json:"status"`
PaymentMethod pgtype.Text `json:"payment_method"`
} }
func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) (WalletTransfer, error) { func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) (WalletTransfer, error) {
@ -46,6 +48,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
arg.CashierID, arg.CashierID,
arg.Verified, arg.Verified,
arg.ReferenceNumber, arg.ReferenceNumber,
arg.Status,
arg.PaymentMethod, arg.PaymentMethod,
) )
var i WalletTransfer var i WalletTransfer
@ -58,6 +61,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber, &i.ReferenceNumber,
&i.Status,
&i.PaymentMethod, &i.PaymentMethod,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -66,7 +70,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, reference_number, payment_method, created_at, updated_at SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, status, payment_method, created_at, updated_at
FROM wallet_transfer FROM wallet_transfer
` `
@ -88,6 +92,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error)
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber, &i.ReferenceNumber,
&i.Status,
&i.PaymentMethod, &i.PaymentMethod,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -103,7 +108,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, reference_number, payment_method, created_at, updated_at SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, status, payment_method, created_at, updated_at
FROM wallet_transfer FROM wallet_transfer
WHERE id = $1 WHERE id = $1
` `
@ -120,6 +125,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber, &i.ReferenceNumber,
&i.Status,
&i.PaymentMethod, &i.PaymentMethod,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -128,12 +134,12 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer
} }
const GetTransferByReference = `-- name: GetTransferByReference :one 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 SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, status, payment_method, created_at, updated_at
FROM wallet_transfer FROM wallet_transfer
WHERE reference_number = $1 WHERE reference_number = $1
` `
func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber string) (WalletTransfer, error) { func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber pgtype.Text) (WalletTransfer, error) {
row := q.db.QueryRow(ctx, GetTransferByReference, referenceNumber) row := q.db.QueryRow(ctx, GetTransferByReference, referenceNumber)
var i WalletTransfer var i WalletTransfer
err := row.Scan( err := row.Scan(
@ -145,6 +151,7 @@ func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber st
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber, &i.ReferenceNumber,
&i.Status,
&i.PaymentMethod, &i.PaymentMethod,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -153,7 +160,7 @@ func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber st
} }
const GetTransfersByWallet = `-- name: GetTransfersByWallet :many const GetTransfersByWallet = `-- name: GetTransfersByWallet :many
SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, payment_method, created_at, updated_at SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, status, 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
@ -177,6 +184,7 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID pgt
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber, &i.ReferenceNumber,
&i.Status,
&i.PaymentMethod, &i.PaymentMethod,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -191,6 +199,23 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID pgt
return items, nil return items, nil
} }
const UpdateTransferStatus = `-- name: UpdateTransferStatus :exec
UPDATE wallet_transfer
SET status = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2
`
type UpdateTransferStatusParams struct {
Status pgtype.Text `json:"status"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateTransferStatus(ctx context.Context, arg UpdateTransferStatusParams) error {
_, err := q.db.Exec(ctx, UpdateTransferStatus, arg.Status, arg.ID)
return err
}
const UpdateTransferVerification = `-- name: UpdateTransferVerification :exec const UpdateTransferVerification = `-- name: UpdateTransferVerification :exec
UPDATE wallet_transfer UPDATE wallet_transfer
SET verified = $1, SET verified = $1,
@ -199,8 +224,8 @@ WHERE id = $2
` `
type UpdateTransferVerificationParams struct { type UpdateTransferVerificationParams struct {
Verified bool `json:"verified"` Verified pgtype.Bool `json:"verified"`
ID int64 `json:"id"` ID int64 `json:"id"`
} }
func (q *Queries) UpdateTransferVerification(ctx context.Context, arg UpdateTransferVerificationParams) error { func (q *Queries) UpdateTransferVerification(ctx context.Context, arg UpdateTransferVerificationParams) error {

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: user.sql // source: user.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: virtual_games.sql // source: virtual_games.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: wallet.sql // source: wallet.sql
package dbgen package dbgen

View File

@ -56,6 +56,9 @@ type VeliGamesConfig struct {
} }
type Config struct { type Config struct {
FIXER_API_KEY string
FIXER_BASE_URL string
BASE_CURRENCY domain.IntCurrency
Port int Port int
DbUrl string DbUrl string
RefreshExpiry int RefreshExpiry int
@ -68,6 +71,8 @@ type Config struct {
AFRO_SMS_SENDER_NAME string AFRO_SMS_SENDER_NAME string
AFRO_SMS_RECEIVER_PHONE_NUMBER string AFRO_SMS_RECEIVER_PHONE_NUMBER string
ADRO_SMS_HOST_URL string ADRO_SMS_HOST_URL string
CHAPA_TRANSFER_TYPE string
CHAPA_PAYMENT_TYPE string
CHAPA_SECRET_KEY string CHAPA_SECRET_KEY string
CHAPA_PUBLIC_KEY string CHAPA_PUBLIC_KEY string
CHAPA_BASE_URL string CHAPA_BASE_URL string
@ -104,6 +109,13 @@ func (c *Config) loadEnv() error {
c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH") c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH")
c.CHAPA_TRANSFER_TYPE = os.Getenv("CHAPA_TRANSFER_TYPE")
c.CHAPA_PAYMENT_TYPE = os.Getenv("CHAPA_PAYMENT_TYPE")
c.FIXER_API_KEY = os.Getenv("FIXER_API_KEY")
c.BASE_CURRENCY = domain.IntCurrency(os.Getenv("BASE_CURRENCY"))
c.FIXER_BASE_URL = os.Getenv("FIXER_BASE_URL")
portStr := os.Getenv("PORT") portStr := os.Getenv("PORT")
if portStr == "" { if portStr == "" {
return ErrInvalidPort return ErrInvalidPort

View File

@ -1,9 +1,27 @@
package domain package domain
import "time" import (
"errors"
"time"
)
var (
ErrInsufficientBalance = errors.New("insufficient balance")
ErrInvalidWithdrawalAmount = errors.New("invalid withdrawal amount")
ErrWithdrawalNotFound = errors.New("withdrawal not found")
)
type PaymentStatus string type PaymentStatus string
type WithdrawalStatus string
const (
WithdrawalStatusPending WithdrawalStatus = "pending"
WithdrawalStatusProcessing WithdrawalStatus = "processing"
WithdrawalStatusCompleted WithdrawalStatus = "completed"
WithdrawalStatusFailed WithdrawalStatus = "failed"
)
const ( const (
PaymentStatusPending PaymentStatus = "pending" PaymentStatusPending PaymentStatus = "pending"
PaymentStatusCompleted PaymentStatus = "completed" PaymentStatusCompleted PaymentStatus = "completed"
@ -91,3 +109,86 @@ type BankData struct {
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
Currency string `json:"currency"` Currency string `json:"currency"`
} }
type ChapaWithdrawal struct {
ID string
UserID int64
Amount Currency
AccountNumber string
BankCode string
Status WithdrawalStatus
Reference string
CreatedAt time.Time
UpdatedAt time.Time
}
type ChapaWithdrawalRequest struct {
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
Amount string `json:"amount"` // string because Chapa API uses string for monetary values
Currency string `json:"currency"`
Reference string `json:"reference"`
BankCode int `json:"bank_code"`
}
// type ChapaWithdrawalRequest struct {
// AccountName string `json:"account_name"`
// AccountNumber string `json:"account_number"`
// Amount Currency `json:"amount"`
// Currency string `json:"currency"`
// BeneficiaryName string `json:"beneficiary_name"`
// BankCode string `json:"bank_code"`
// PhoneNumber string `json:"phone_number"`
// }
type ChapaWithdrawalResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Data struct {
Reference string `json:"reference"`
} `json:"data"`
}
type ChapaTransactionType struct {
Type string `json:"type"`
}
type ChapaWebHookTransfer struct {
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
BankId string `json:"bank_id"`
BankName string `json:"bank_name"`
Currency string `json:"currency"`
Amount string `json:"amount"`
Type string `json:"type"`
Status string `json:"status"`
Reference string `json:"reference"`
TxRef string `json:"tx_ref"`
ChapaReference string `json:"chapa_reference"`
CreatedAt time.Time `json:"created_at"`
}
type ChapaWebHookPayment struct {
Event string `json:"event"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Mobile interface{} `json:"mobile"`
Currency string `json:"currency"`
Amount string `json:"amount"`
Charge string `json:"charge"`
Status string `json:"status"`
Mode string `json:"mode"`
Reference string `json:"reference"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Type string `json:"type"`
TxRef string `json:"tx_ref"`
PaymentMethod string `json:"payment_method"`
Customization struct {
Title interface{} `json:"title"`
Description interface{} `json:"description"`
Logo interface{} `json:"logo"`
} `json:"customization"`
Meta string `json:"meta"`
}

View File

@ -0,0 +1,88 @@
package domain
import (
"errors"
"fmt"
"time"
)
type IntCurrency string
const (
ETB IntCurrency = "ETB" // Ethiopian Birr
NGN IntCurrency = "NGN" // Nigerian Naira
ZAR IntCurrency = "ZAR" // South African Rand
EGP IntCurrency = "EGP" // Egyptian Pound
KES IntCurrency = "KES" // Kenyan Shilling
UGX IntCurrency = "UGX" // Ugandan Shilling
TZS IntCurrency = "TZS" // Tanzanian Shilling
RWF IntCurrency = "RWF" // Rwandan Franc
BIF IntCurrency = "BIF" // Burundian Franc
XOF IntCurrency = "XOF" // West African CFA Franc (BCEAO)
XAF IntCurrency = "XAF" // Central African CFA Franc (BEAC)
GHS IntCurrency = "GHS" // Ghanaian Cedi
SDG IntCurrency = "SDG" // Sudanese Pound
SSP IntCurrency = "SSP" // South Sudanese Pound
DZD IntCurrency = "DZD" // Algerian Dinar
MAD IntCurrency = "MAD" // Moroccan Dirham
TND IntCurrency = "TND" // Tunisian Dinar
LYD IntCurrency = "LYD" // Libyan Dinar
MZN IntCurrency = "MZN" // Mozambican Metical
AOA IntCurrency = "AOA" // Angolan Kwanza
BWP IntCurrency = "BWP" // Botswana Pula
ZMW IntCurrency = "ZMW" // Zambian Kwacha
MWK IntCurrency = "MWK" // Malawian Kwacha
LSL IntCurrency = "LSL" // Lesotho Loti
NAD IntCurrency = "NAD" // Namibian Dollar
SZL IntCurrency = "SZL" // Swazi Lilangeni
CVE IntCurrency = "CVE" // Cape Verdean Escudo
GMD IntCurrency = "GMD" // Gambian Dalasi
SLL IntCurrency = "SLL" // Sierra Leonean Leone
LRD IntCurrency = "LRD" // Liberian Dollar
GNF IntCurrency = "GNF" // Guinean Franc
XCD IntCurrency = "XCD" // Eastern Caribbean Dollar (used in Saint Lucia)
MRU IntCurrency = "MRU" // Mauritanian Ouguiya
KMF IntCurrency = "KMF" // Comorian Franc
DJF IntCurrency = "DJF" // Djiboutian Franc
SOS IntCurrency = "SOS" // Somali Shilling
ERN IntCurrency = "ERN" // Eritrean Nakfa
MGA IntCurrency = "MGA" // Malagasy Ariary
SCR IntCurrency = "SCR" // Seychellois Rupee
MUR IntCurrency = "MUR" // Mauritian Rupee
// International currencies (already listed)
USD IntCurrency = "USD" // US Dollar
EUR IntCurrency = "EUR" // Euro
GBP IntCurrency = "GBP" // British Pound
)
var (
ErrUnsupportedIntCurrency = errors.New("unsupported IntCurrency")
ErrIntCurrencyConversion = errors.New("IntCurrency conversion failed")
)
// IntCurrencyRate represents exchange rate between two currencies
type IntCurrencyRate struct {
From IntCurrency
To IntCurrency
Rate float64
ValidUntil time.Time
}
// Convert converts amount from one IntCurrency to another
func (cr IntCurrencyRate) Convert(amount float64) (float64, error) {
if time.Now().After(cr.ValidUntil) {
return 0, fmt.Errorf("%w: rate expired", ErrIntCurrencyConversion)
}
return amount * cr.Rate, nil
}
// ValidateIntCurrency checks if IntCurrency is supported
func ValidateIntCurrency(c IntCurrency) error {
switch c {
case ETB, USD, EUR, GBP:
return nil
default:
return fmt.Errorf("%w: %s", ErrUnsupportedIntCurrency, c)
}
}

View File

@ -14,6 +14,14 @@ func UnProcessableEntityResponse(c *fiber.Ctx) error {
}) })
} }
func UnExpectedErrorResponse(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(Response{
Message: "Unexpected internal error",
StatusCode: fiber.StatusInternalServerError,
Success: false,
})
}
func FiberErrorResponse(c *fiber.Ctx, err error) error { func FiberErrorResponse(c *fiber.Ctx, err error) error {
var statusCode int var statusCode int
var message string var message string

View File

@ -41,6 +41,7 @@ type Transfer struct {
ReceiverWalletID ValidInt64 ReceiverWalletID ValidInt64
SenderWalletID ValidInt64 SenderWalletID ValidInt64
ReferenceNumber string ReferenceNumber string
Status string
CashierID ValidInt64 CashierID ValidInt64
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
@ -50,6 +51,7 @@ type CreateTransfer struct {
Amount Currency Amount Currency
Verified bool Verified bool
ReferenceNumber string ReferenceNumber string
Status string
ReceiverWalletID ValidInt64 ReceiverWalletID ValidInt64
SenderWalletID ValidInt64 SenderWalletID ValidInt64
CashierID ValidInt64 CashierID ValidInt64

View File

@ -5,6 +5,7 @@ import "time"
type Wallet struct { type Wallet struct {
ID int64 ID int64
Balance Currency Balance Currency
Currency IntCurrency
IsWithdraw bool IsWithdraw bool
IsBettable bool IsBettable bool
IsTransferable bool IsTransferable bool

View File

@ -0,0 +1,96 @@
package repository
import (
"context"
"database/sql"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type CurrencyRepository interface {
GetExchangeRate(ctx context.Context, from, to domain.IntCurrency) (domain.IntCurrencyRate, error)
StoreExchangeRate(ctx context.Context, rate domain.IntCurrencyRate) error
GetSupportedCurrencies(ctx context.Context) ([]domain.IntCurrency, error)
}
type CurrencyPostgresRepository struct {
store *Store
}
func NewCurrencyPostgresRepository(store *Store) *CurrencyPostgresRepository {
return &CurrencyPostgresRepository{store: store}
}
func (r *CurrencyPostgresRepository) GetExchangeRate(ctx context.Context, from, to domain.IntCurrency) (domain.IntCurrencyRate, error) {
const query = `
SELECT from_currency, to_currency, rate, precision, valid_until
FROM exchange_rates
WHERE from_currency = $1 AND to_currency = $2 AND valid_until > NOW()
ORDER BY created_at DESC
LIMIT 1`
var rate domain.IntCurrencyRate
err := r.store.conn.QueryRow(ctx, query, from, to).Scan(
&rate.From,
&rate.To,
&rate.Rate,
&rate.ValidUntil,
)
if err != nil {
if err == sql.ErrNoRows {
return domain.IntCurrencyRate{}, fmt.Errorf("%w: no rate found for %s to %s",
domain.ErrIntCurrencyConversion, from, to)
}
return domain.IntCurrencyRate{}, fmt.Errorf("failed to get exchange rate: %w", err)
}
return rate, nil
}
func (r *CurrencyPostgresRepository) StoreExchangeRate(ctx context.Context, rate domain.IntCurrencyRate) error {
const query = `
INSERT INTO exchange_rates (from_currency, to_currency, rate, precision, valid_until)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (from_currency, to_currency)
DO UPDATE SET
rate = EXCLUDED.rate,
precision = EXCLUDED.precision,
valid_until = EXCLUDED.valid_until,
created_at = NOW()`
_, err := r.store.conn.Exec(ctx, query,
rate.From,
rate.To,
rate.Rate,
rate.ValidUntil)
if err != nil {
return fmt.Errorf("failed to store exchange rate: %w", err)
}
return nil
}
func (r *CurrencyPostgresRepository) GetSupportedCurrencies(ctx context.Context) ([]domain.IntCurrency, error) {
const query = `SELECT DISTINCT currency FROM supported_currencies ORDER BY currency`
var currencies []domain.IntCurrency
rows, err := r.store.conn.Query(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to get supported currencies: %w", err)
}
defer rows.Close()
for rows.Next() {
var currency domain.IntCurrency
if err := rows.Scan(&currency); err != nil {
return nil, fmt.Errorf("failed to scan currency: %w", err)
}
currencies = append(currencies, currency)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("row iteration error: %w", err)
}
return currencies, nil
}

View File

@ -22,21 +22,27 @@ func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer {
Value: transfer.SenderWalletID.Int64, Value: transfer.SenderWalletID.Int64,
Valid: transfer.SenderWalletID.Valid, Valid: transfer.SenderWalletID.Valid,
}, },
ID: transfer.ID,
Amount: domain.Currency(transfer.Amount.Int64),
Type: domain.TransferType(transfer.Type.String),
Verified: transfer.Verified.Bool,
ReceiverWalletID: transfer.ReceiverWalletID.Int64,
SenderWalletID: transfer.SenderWalletID.Int64,
CashierID: domain.ValidInt64{ CashierID: domain.ValidInt64{
Value: transfer.CashierID.Int64, Value: transfer.CashierID.Int64,
Valid: transfer.CashierID.Valid, Valid: transfer.CashierID.Valid,
}, },
PaymentMethod: domain.PaymentMethod(transfer.PaymentMethod), PaymentMethod: domain.PaymentMethod(transfer.PaymentMethod.String),
} }
} }
func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferParams { func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferParams {
return dbgen.CreateTransferParams{ return dbgen.CreateTransferParams{
Amount: int64(transfer.Amount), Amount: pgtype.Int8{Int64: int64(transfer.Amount), Valid: true},
Type: string(transfer.Type), Type: pgtype.Text{String: string(transfer.Type), Valid: true},
ReceiverWalletID: pgtype.Int8{ ReceiverWalletID: pgtype.Int8{
Int64: transfer.ReceiverWalletID.Value, Int64: transfer.ReceiverWalletID,
Valid: transfer.ReceiverWalletID.Valid, Valid: true,
}, },
SenderWalletID: pgtype.Int8{ SenderWalletID: pgtype.Int8{
Int64: transfer.SenderWalletID.Value, Int64: transfer.SenderWalletID.Value,
@ -46,7 +52,7 @@ func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferP
Int64: transfer.CashierID.Value, Int64: transfer.CashierID.Value,
Valid: transfer.CashierID.Valid, Valid: transfer.CashierID.Valid,
}, },
PaymentMethod: string(transfer.PaymentMethod), PaymentMethod: pgtype.Text{String: string(transfer.PaymentMethod), Valid: true},
} }
} }
@ -71,10 +77,7 @@ func (s *Store) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error)
return result, nil return result, nil
} }
func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) { func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) {
transfers, err := s.queries.GetTransfersByWallet(ctx, pgtype.Int8{ transfers, err := s.queries.GetTransfersByWallet(ctx, pgtype.Int8{Int64: walletID, Valid: true})
Int64: walletID,
Valid: true,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -88,7 +91,7 @@ func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]dom
} }
func (s *Store) GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error) { func (s *Store) GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error) {
transfer, err := s.queries.GetTransferByReference(ctx, reference) transfer, err := s.queries.GetTransferByReference(ctx, pgtype.Text{String: reference, Valid: true})
if err != nil { if err != nil {
return domain.Transfer{}, nil return domain.Transfer{}, nil
} }
@ -106,7 +109,16 @@ func (s *Store) GetTransferByID(ctx context.Context, id int64) (domain.Transfer,
func (s *Store) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error { func (s *Store) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error {
err := s.queries.UpdateTransferVerification(ctx, dbgen.UpdateTransferVerificationParams{ err := s.queries.UpdateTransferVerification(ctx, dbgen.UpdateTransferVerificationParams{
ID: id, ID: id,
Verified: verified, Verified: pgtype.Bool{Bool: verified, Valid: true},
})
return err
}
func (s *Store) UpdateTransferStatus(ctx context.Context, id int64, status string) error {
err := s.queries.UpdateTransferStatus(ctx, dbgen.UpdateTransferStatusParams{
ID: id,
Status: pgtype.Text{String: status, Valid: true},
}) })
return err return err

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -211,24 +212,78 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error)
return banks, nil return banks, nil
} }
// Helper method to generate account regex based on bank type func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) {
// func GetAccountRegex(bank domain.Bank) string { // base, err := url.Parse(c.baseURL)
// if bank.IsMobileMoney != nil && bank.IsMobileMoney == 1 { // if err != nil {
// return `^09[0-9]{8}$` // Ethiopian mobile money pattern // return false, fmt.Errorf("invalid base URL: %w", err)
// } // }
// return fmt.Sprintf(`^[0-9]{%d}$`, bank.AcctLength) endpoint := c.baseURL + "/transfers"
// } fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint)
// // Helper method to generate example account number reqBody, err := json.Marshal(req)
// func GetExampleAccount(bank domain.Bank) string { if err != nil {
// if bank.IsMobileMoney != nil && *bank.IsMobileMoney { return false, fmt.Errorf("failed to marshal request: %w", err)
// return "0912345678" // Ethiopian mobile number example }
// }
// // Generate example based on length httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(reqBody))
// example := "1" if err != nil {
// for i := 1; i < bank.AcctLength; i++ { return false, fmt.Errorf("failed to create request: %w", err)
// example += fmt.Sprintf("%d", i%10) }
// }
// return example c.setHeaders(httpReq)
// }
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return false, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("chapa api returned status: %d", resp.StatusCode)
}
var response domain.ChapaWithdrawalResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return false, fmt.Errorf("failed to decode response: %w", err)
}
return response.Status == string(domain.WithdrawalStatusProcessing), nil
}
func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaVerificationResponse, error) {
base, err := url.Parse(c.baseURL)
if err != nil {
return nil, fmt.Errorf("invalid base URL: %w", err)
}
endpoint := base.ResolveReference(&url.URL{Path: fmt.Sprintf("/v1/transfers/%s/verify", reference)})
httpReq, err := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
c.setHeaders(httpReq)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("chapa api returned status: %d", resp.StatusCode)
}
var verification domain.ChapaVerificationResponse
if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &verification, nil
}
func (c *Client) setHeaders(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+c.secretKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
}

View File

@ -16,7 +16,10 @@ import (
type ChapaStore interface { type ChapaStore interface {
InitializePayment(request domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) InitializePayment(request domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error)
VerifyPayment(reference string) (domain.ChapaDepositVerification, error) // VerifyPayment(reference string) (domain.ChapaDepositVerification, error)
ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error)
FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error)
CreateWithdrawal(userID string, amount float64, accountNumber, bankCode string) (*domain.ChapaWithdrawal, error)
HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error
HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"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"
@ -20,7 +21,7 @@ var (
type Service struct { type Service struct {
transferStore wallet.TransferStore transferStore wallet.TransferStore
walletStore wallet.WalletStore walletStore wallet.Service
userStore user.UserStore userStore user.UserStore
cfg *config.Config cfg *config.Config
chapaClient *Client chapaClient *Client
@ -28,7 +29,7 @@ type Service struct {
func NewService( func NewService(
transferStore wallet.TransferStore, transferStore wallet.TransferStore,
walletStore wallet.WalletStore, walletStore wallet.Service,
userStore user.UserStore, userStore user.UserStore,
chapaClient *Client, chapaClient *Client,
@ -113,47 +114,100 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
return response.CheckoutURL, nil return response.CheckoutURL, nil
} }
func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) {
// Parse and validate amount
amount, err := strconv.ParseInt(req.Amount, 10, 64)
if err != nil || amount <= 0 {
return nil, domain.ErrInvalidWithdrawalAmount
}
// VerifyDeposit handles payment verification from webhook // Get user details
func (s *Service) VerifyDeposit(ctx context.Context, reference string) error { // user, err := s.userStore.GetUserByID(ctx, userID)
// Find payment by reference // if err != nil {
payment, err := s.transferStore.GetTransferByReference(ctx, reference) // return nil, fmt.Errorf("failed to get user: %w", err)
// }
// Get user's wallet
wallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
if err != nil { if err != nil {
return ErrPaymentNotFound return nil, fmt.Errorf("failed to get user wallets: %w", err)
} }
// just making sure that the sender id is valid var withdrawWallet domain.Wallet
if !payment.SenderWalletID.Valid { for _, wallet := range wallets {
return fmt.Errorf("sender wallet is invalid %v \n", payment.SenderWalletID) if wallet.IsWithdraw {
} withdrawWallet = wallet
break
// Skip if already completed
if payment.Verified {
return nil
}
// Verify payment with Chapa
verification, err := s.chapaClient.VerifyPayment(ctx, reference)
if err != nil {
return fmt.Errorf("failed to verify payment: %w", err)
}
// Update payment status
if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
return fmt.Errorf("failed to update payment status: %w", err)
}
// If payment is completed, credit user's wallet
if verification.Status == domain.PaymentStatusCompleted {
if err := s.walletStore.UpdateBalance(ctx, payment.SenderWalletID.Value, payment.Amount); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err)
} }
} }
return nil if withdrawWallet.ID == 0 {
return nil, errors.New("withdrawal wallet not found")
}
// Check balance
if withdrawWallet.Balance < domain.Currency(amount) {
return nil, domain.ErrInsufficientBalance
}
// Generate unique reference
reference := uuid.New().String()
createTransfer := domain.CreateTransfer{
Amount: domain.Currency(amount),
Type: domain.WITHDRAW,
ReceiverWalletID: 1,
SenderWalletID: withdrawWallet.ID,
Status: string(domain.PaymentStatusPending),
Verified: false,
ReferenceNumber: reference,
PaymentMethod: domain.TRANSFER_CHAPA,
}
transfer, err := s.transferStore.CreateTransfer(ctx, createTransfer)
if err != nil {
return nil, fmt.Errorf("failed to create transfer record: %w", err)
}
// Initiate transfer with Chapa
transferReq := domain.ChapaWithdrawalRequest{
AccountName: req.AccountName,
AccountNumber: req.AccountNumber,
Amount: fmt.Sprintf("%d", amount),
Currency: req.Currency,
Reference: reference,
// BeneficiaryName: fmt.Sprintf("%s %s", user.FirstName, user.LastName),
BankCode: req.BankCode,
}
success, err := s.chapaClient.InitiateTransfer(ctx, transferReq)
if err != nil || !success {
// Update withdrawal status to failed
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
return nil, fmt.Errorf("failed to initiate transfer: %w", err)
}
// Update withdrawal status to processing
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil {
return nil, fmt.Errorf("failed to update withdrawal status: %w", err)
}
// Deduct from wallet (or wait for webhook confirmation depending on your flow)
newBalance := withdrawWallet.Balance - domain.Currency(amount)
if err := s.walletStore.UpdateBalance(ctx, withdrawWallet.ID, newBalance); err != nil {
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
}
return &transfer, nil
} }
func (s *Service) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
banks, err := s.chapaClient.FetchSupportedBanks(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch banks: %w", err)
}
return banks, nil
}
func (s *Service) ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
// First check if we already have a verified record // First check if we already have a verified record
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef) transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
if err == nil && transfer.Verified { if err == nil && transfer.Verified {
@ -196,10 +250,76 @@ func (s *Service) ManualVerifyPayment(ctx context.Context, txRef string) (*domai
}, nil }, nil
} }
func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) { func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error {
banks, err := s.chapaClient.FetchSupportedBanks(ctx) // Find payment by reference
payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch banks: %w", err) return ErrPaymentNotFound
} }
return banks, nil
if payment.Verified {
return nil
}
// Verify payment with Chapa
// verification, err := s.chapaClient.VerifyPayment(ctx, transfer.Reference)
// if err != nil {
// return fmt.Errorf("failed to verify payment: %w", err)
// }
// Update payment status
// verified := false
// if transfer.Status == string(domain.PaymentStatusCompleted) {
// verified = true
// }
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 transfer.Status == string(domain.PaymentStatusCompleted) {
if err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID, payment.Amount); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err)
}
}
return nil
}
func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error {
// Find payment by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference)
if err != nil {
return ErrPaymentNotFound
}
if transfer.Verified {
return nil
}
// Verify payment with Chapa
// verification, err := s.chapaClient.VerifyPayment(ctx, payment.Reference)
// if err != nil {
// return fmt.Errorf("failed to verify payment: %w", err)
// }
// Update payment status
// verified := false
// if transfer.Status == string(domain.PaymentStatusCompleted) {
// verified = true
// }
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return fmt.Errorf("failed to update payment status: %w", err)
}
// If payment is completed, credit user's wallet
if payment.Status == string(domain.PaymentStatusFailed) {
if err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID, transfer.Amount); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err)
}
}
return nil
} }

View File

@ -0,0 +1,69 @@
package currency
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type FixerFetcher struct {
apiKey string
baseURL string
httpClient *http.Client
}
func NewFixerFetcher(apiKey string, baseURL string) *FixerFetcher {
return &FixerFetcher{
apiKey: apiKey,
baseURL: baseURL,
httpClient: &http.Client{Timeout: 10 * time.Second},
}
}
type fixerResponse struct {
Success bool `json:"success"`
Base string `json:"base"`
Date string `json:"date"`
Rates map[string]float64 `json:"rates"`
}
func (f *FixerFetcher) FetchLatestRates(ctx context.Context, baseCurrency domain.IntCurrency) (map[domain.IntCurrency]float64, error) {
url := fmt.Sprintf("%s/latest?base=%s", f.baseURL, baseCurrency)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("apikey", f.apiKey)
resp, err := f.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch rates: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var result fixerResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if !result.Success {
return nil, fmt.Errorf("api returned unsuccessful response")
}
rates := make(map[domain.IntCurrency]float64)
for currency, rate := range result.Rates {
rates[domain.IntCurrency(currency)] = rate
}
return rates, nil
}

View File

@ -0,0 +1,125 @@
package currency
import (
"context"
"fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
)
type Service struct {
repo repository.CurrencyRepository
baseCurrency domain.IntCurrency
fixerFetcher *FixerFetcher
}
func NewService(repo repository.CurrencyRepository, baseCurrency domain.IntCurrency, fixerFetcher *FixerFetcher) *Service {
return &Service{repo: repo}
}
func (s *Service) Convert(ctx context.Context, amount float64, from, to domain.IntCurrency) (float64, error) {
if from == to {
return amount, nil
}
rate, err := s.repo.GetExchangeRate(ctx, from, to)
if err != nil {
return 0, err
}
return rate.Convert(amount)
}
func (s *Service) GetSupportedCurrencies(ctx context.Context) ([]domain.IntCurrency, error) {
return s.repo.GetSupportedCurrencies(ctx)
}
func (s *Service) UpdateRates(ctx context.Context) error {
// Implement fetching from external API (e.g., Fixer, Open Exchange Rates)
rates := map[domain.IntCurrency]map[domain.IntCurrency]float64{
domain.ETB: {
domain.USD: 0.018,
domain.EUR: 0.016,
domain.GBP: 0.014,
},
// Add other currencies...
}
for from, toRates := range rates {
for to, rate := range toRates {
err := s.repo.StoreExchangeRate(ctx, domain.IntCurrencyRate{
From: from,
To: to,
Rate: rate,
ValidUntil: time.Now().Add(24 * time.Hour), // Refresh daily
})
if err != nil {
return err
}
}
}
return nil
}
func (s *Service) FetchAndStoreRates(ctx context.Context) error {
// s.logger.Info("Starting exchange rate update")
rates, err := s.fixerFetcher.FetchLatestRates(ctx, s.baseCurrency)
if err != nil {
// s.logger.Error("Failed to fetch rates", "error", err)
return fmt.Errorf("failed to fetch rates: %w", err)
}
// Convert to integer rates with precision
const precision = 6 // 1.000000
for currency, rate := range rates {
if currency == s.baseCurrency {
continue
}
intRate := domain.IntCurrencyRate{
From: s.baseCurrency,
To: currency,
Rate: rate * float64(pow10(precision)),
ValidUntil: time.Now().Add(24 * time.Hour), // Rates valid for 24 hours
}
if err := s.repo.StoreExchangeRate(ctx, intRate); err != nil {
// s.logger.Error("Failed to store rate",
// "from", s.baseCurrency,
// "to", currency,
// "error", err)
continue // Try to store other rates even if one fails
}
// Also store the inverse rate
inverseRate := domain.IntCurrencyRate{
From: currency,
To: s.baseCurrency,
Rate: (1 / rate) * float64(pow10(precision)),
ValidUntil: time.Now().Add(24 * time.Hour),
}
if err := s.repo.StoreExchangeRate(ctx, inverseRate); err != nil {
// s.logger.Error("Failed to store inverse rate",
// "from", currency,
// "to", s.baseCurrency,
// "error", err)
return fmt.Errorf("Error storing exchange rates")
}
}
// s.logger.Info("Exchange rates updated successfully")
return nil
}
func pow10(n int) int64 {
result := int64(1)
for i := 0; i < n; i++ {
result *= 10
}
return result
}

View File

@ -0,0 +1,55 @@
package currency
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/go-co-op/gocron"
)
type ExchangeRateWorker struct {
fetcherService *FixerFetcher
scheduler *gocron.Scheduler
logger *slog.Logger
cfg *config.Config
}
func NewExchangeRateWorker(
fetcherService *FixerFetcher, logger *slog.Logger, cfg *config.Config,
) *ExchangeRateWorker {
return &ExchangeRateWorker{
fetcherService: fetcherService,
scheduler: gocron.NewScheduler(time.UTC),
logger: logger,
cfg: cfg,
}
}
func (w *ExchangeRateWorker) Start(ctx context.Context) {
_, err := w.scheduler.Every(6).Hours().Do(w.RunUpdate)
if err != nil {
return
}
// Run immediately on startup
go w.RunUpdate()
w.scheduler.StartAsync()
}
func (w *ExchangeRateWorker) RunUpdate() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if _, err := w.fetcherService.FetchLatestRates(ctx, w.cfg.BASE_CURRENCY); err != nil {
fmt.Println("Exchange rate update failed", "error", err)
}
}
func (w *ExchangeRateWorker) Stop() {
w.scheduler.Stop()
w.logger.Info("Exchange rate worker stopped")
}

View File

@ -28,4 +28,5 @@ type TransferStore interface {
GetTransferByReference(ctx context.Context, reference string) (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
UpdateTransferStatus(ctx context.Context, id int64, status string) error
} }

View File

@ -38,10 +38,17 @@ func (s *Service) UpdateTransferVerification(ctx context.Context, id int64, veri
return s.transferStore.UpdateTransferVerification(ctx, id, verified) return s.transferStore.UpdateTransferVerification(ctx, id, verified)
} }
func (s *Service) UpdateTransferStatus(ctx context.Context, id int64, status string) error {
return s.transferStore.UpdateTransferStatus(ctx, id, status)
}
func (s *Service) UpdateTransferStatus(ctx context.Context, id int64, status string) error {
return s.transferStore.UpdateTransferStatus(ctx, id, status)
}
func (s *Service) TransferToWallet(ctx context.Context,
senderID int64, receiverID int64, func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiverID int64,
amount domain.Currency, paymentMethod domain.PaymentMethod, amount domain.Currency, paymentMethod domain.PaymentMethod,
cashierID domain.ValidInt64) (domain.Transfer, error) { cashierID domain.ValidInt64) (domain.Transfer, error) {

View File

@ -10,6 +10,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
@ -34,6 +35,7 @@ import (
) )
type App struct { type App struct {
currSvc *currency.Service
fiber *fiber.App fiber *fiber.App
aleaVirtualGameService alea.AleaVirtualGameService aleaVirtualGameService alea.AleaVirtualGameService
veliVirtualGameService veli.VeliVirtualGameService veliVirtualGameService veli.VeliVirtualGameService
@ -64,6 +66,7 @@ type App struct {
} }
func NewApp( func NewApp(
currSvc *currency.Service,
port int, validator *customvalidator.CustomValidator, port int, validator *customvalidator.CustomValidator,
authSvc *authentication.Service, authSvc *authentication.Service,
logger *slog.Logger, logger *slog.Logger,
@ -104,6 +107,7 @@ func NewApp(
})) }))
s := &App{ s := &App{
currSvc: currSvc,
fiber: app, fiber: app,
port: port, port: port,
authSvc: authSvc, authSvc: authSvc,

View File

@ -13,6 +13,7 @@ import (
// @Tags Chapa // @Tags Chapa
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Security ApiKeyAuth
// @Param request body domain.ChapaDepositRequestPayload true "Deposit request" // @Param request body domain.ChapaDepositRequestPayload true "Deposit request"
// @Success 200 {object} domain.ChapaDepositResponse // @Success 200 {object} domain.ChapaDepositResponse
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
@ -65,38 +66,59 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/payments/webhook/verify [post] // @Router /api/v1/chapa/payments/webhook/verify [post]
func (h *Handler) WebhookCallback(c *fiber.Ctx) error { func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
// Verify webhook signature first
// signature := c.Get("Chapa-Signature")
// if !verifySignature(signature, c.Body()) {
// return c.Status(fiber.StatusUnauthorized).JSON(ErrorResponse{
// Error: "invalid signature",
// })
// }
var payload struct { chapaTransactionType := new(domain.ChapaTransactionType)
TxRef string `json:"tx_ref"`
Amount float64 `json:"amount"` if parseTypeErr := c.BodyParser(chapaTransactionType); parseTypeErr != nil {
Currency string `json:"currency"` return domain.UnProcessableEntityResponse(c)
Status string `json:"status"`
} }
if err := c.BodyParser(&payload); err != nil { switch chapaTransactionType.Type {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ case h.Cfg.CHAPA_TRANSFER_TYPE:
Error: err.Error(), chapaTransferVerificationRequest := new(domain.ChapaWebHookTransfer)
if err := c.BodyParser(chapaTransferVerificationRequest); err != nil {
return domain.UnProcessableEntityResponse(c)
}
err := h.chapaSvc.HandleVerifyDepositWebhook(c.Context(), *chapaTransferVerificationRequest)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to verify Chapa depposit",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
StatusCode: 200,
Message: "Chapa deposit transaction verified successfully",
Data: chapaTransferVerificationRequest,
Success: true,
}) })
} case h.Cfg.CHAPA_PAYMENT_TYPE:
chapaPaymentVerificationRequest := new(domain.ChapaWebHookPayment)
if err := c.BodyParser(chapaPaymentVerificationRequest); err != nil {
return domain.UnProcessableEntityResponse(c)
}
if err := h.chapaSvc.VerifyDeposit(c.Context(), payload.TxRef); err != nil { err := h.chapaSvc.HandleVerifyWithdrawWebhook(c.Context(), *chapaPaymentVerificationRequest)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ if err != nil {
Error: err.Error(), return domain.UnExpectedErrorResponse(c)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
StatusCode: 200,
Message: "Chapa withdrawal transaction verified successfully",
Data: chapaPaymentVerificationRequest,
Success: true,
}) })
} }
return c.Status(fiber.StatusOK).JSON(domain.Response{ // Return a 400 Bad Request if the type does not match any known case
StatusCode: 200, return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "payment verified successfully", Message: "Invalid Chapa webhook type",
Data: payload.TxRef, Error: "Unknown transaction type",
Success: true,
}) })
} }
@ -111,7 +133,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/payments/manual/verify/{tx_ref} [get] // @Router /api/v1/chapa/payments/manual/verify/{tx_ref} [get]
func (h *Handler) ManualVerifyPayment(c *fiber.Ctx) error { func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error {
txRef := c.Params("tx_ref") txRef := c.Params("tx_ref")
if txRef == "" { if txRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
@ -120,7 +142,7 @@ func (h *Handler) ManualVerifyPayment(c *fiber.Ctx) error {
}) })
} }
verification, err := h.chapaSvc.ManualVerifyPayment(c.Context(), txRef) verification, err := h.chapaSvc.ManualVerifTransaction(c.Context(), txRef)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to verify Chapa transaction", Message: "Failed to verify Chapa transaction",
@ -142,9 +164,9 @@ func (h *Handler) ManualVerifyPayment(c *fiber.Ctx) error {
// @Tags Chapa // @Tags Chapa
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {array} domain.Bank // @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /banks [get] // @Router /api/v1/chapa/banks [get]
func (h *Handler) GetSupportedBanks(c *fiber.Ctx) error { func (h *Handler) GetSupportedBanks(c *fiber.Ctx) error {
banks, err := h.chapaSvc.GetSupportedBanks(c.Context()) banks, err := h.chapaSvc.GetSupportedBanks(c.Context())
if err != nil { if err != nil {
@ -161,3 +183,44 @@ func (h *Handler) GetSupportedBanks(c *fiber.Ctx) error {
Data: banks, Data: banks,
}) })
} }
// InitiateWithdrawal initiates a withdrawal request via Chapa payment gateway
// @Summary Initiate a withdrawal
// @Description Initiates a withdrawal request to transfer funds to a bank account via Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param request body domain.ChapaWithdrawalRequest true "Withdrawal request details"
// @Success 201 {object} domain.Response "Chapa withdrawal process initiated successfully"
// @Failure 400 {object} domain.ErrorResponse "Invalid request body"
// @Failure 401 {object} domain.ErrorResponse "Unauthorized"
// @Failure 422 {object} domain.ErrorResponse "Unprocessable entity"
// @Failure 500 {object} domain.ErrorResponse "Internal server error"
// @Router /api/v1/chapa/payments/withdraw [post]
func (h *Handler) InitiateWithdrawal(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok {
return domain.UnProcessableEntityResponse(c)
}
var req domain.ChapaWithdrawalRequest
if err := c.BodyParser(&req); err != nil {
return domain.UnProcessableEntityResponse(c)
}
withdrawal, err := h.chapaSvc.InitiateWithdrawal(c.Context(), userID, req)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to initiate Chapa withdrawal",
Error: err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Chapa withdrawal process initiated successfully",
StatusCode: 201,
Success: true,
Data: withdrawal,
})
}

View File

@ -0,0 +1,57 @@
package handlers
import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
)
// @Summary Get supported currencies
// @Description Returns list of supported currencies
// @Tags Multi-Currency
// @Produce json
// @Success 200 {object} domain.Response{data=[]domain.Currency}
// @Router /api/v1/currencies [get]
func (h *Handler) GetSupportedCurrencies(c *fiber.Ctx) error {
currencies, err := h.currSvc.GetSupportedCurrencies(c.Context())
if err != nil {
return domain.UnExpectedErrorResponse(c)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Success: true,
Message: "Supported currencies retrieved successfully",
Data: currencies,
StatusCode: fiber.StatusOK,
})
}
// @Summary Convert currency
// @Description Converts amount from one currency to another
// @Tags Multi-Currency
// @Produce json
// @Param from query string true "Source currency code (e.g., USD)"
// @Param to query string true "Target currency code (e.g., ETB)"
// @Param amount query number true "Amount to convert"
// @Success 200 {object} domain.Response{data=float64}
// @Failure 400 {object} domain.ErrorResponse
// @Router /api/v1/currencies/convert [get]
func (h *Handler) ConvertCurrency(c *fiber.Ctx) error {
from := domain.IntCurrency(c.Query("from"))
to := domain.IntCurrency(c.Query("to"))
amount := c.QueryFloat("amount", 0)
// if err != nil {
// return domain.BadRequestResponse(c)
// }
converted, err := h.currSvc.Convert(c.Context(), amount, from, to)
if err != nil {
return domain.UnExpectedErrorResponse(c)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Success: true,
Message: "Currency converted successfully",
Data: converted,
StatusCode: fiber.StatusOK,
})
}

View File

@ -9,14 +9,15 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"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"
@ -29,6 +30,7 @@ import (
) )
type Handler struct { type Handler struct {
currSvc *currency.Service
logger *slog.Logger logger *slog.Logger
notificationSvc *notificationservice.Service notificationSvc *notificationservice.Service
userSvc *user.Service userSvc *user.Service
@ -56,6 +58,7 @@ type Handler struct {
} }
func New( func New(
currSvc *currency.Service,
logger *slog.Logger, logger *slog.Logger,
notificationSvc *notificationservice.Service, notificationSvc *notificationservice.Service,
validator *customvalidator.CustomValidator, validator *customvalidator.CustomValidator,
@ -82,6 +85,7 @@ func New(
cfg *config.Config, cfg *config.Config,
) *Handler { ) *Handler {
return &Handler{ return &Handler{
currSvc: currSvc,
logger: logger, logger: logger,
notificationSvc: notificationSvc, notificationSvc: notificationSvc,
reportSvc: reportSvc, reportSvc: reportSvc,

View File

@ -24,7 +24,7 @@ import (
// @Param sport_id query string false "Sport ID filter" // @Param sport_id query string false "Sport ID filter"
// @Param status query int false "Status filter (0=Pending, 1=Win, 2=Loss, 3=Half, 4=Void, 5=Error)" // @Param status query int false "Status filter (0=Pending, 1=Win, 2=Loss, 3=Half, 4=Void, 5=Error)"
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Success 200 {object} report.DashboardSummary // @Success 200 {object} domain.DashboardSummary
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 401 {object} domain.ErrorResponse // @Failure 401 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse

View File

@ -9,7 +9,7 @@ import (
) )
type ResultRes struct { type ResultRes struct {
ResultData json.RawMessage `json:"result_data"` ResultData json.RawMessage `json:"result_data" swaggerignore:"true"`
Outcomes []domain.BetOutcome `json:"outcomes"` Outcomes []domain.BetOutcome `json:"outcomes"`
} }

View File

@ -20,6 +20,7 @@ import (
func (a *App) initAppRoutes() { func (a *App) initAppRoutes() {
h := handlers.New( h := handlers.New(
a.currSvc,
a.logger, a.logger,
a.NotidicationStore, a.NotidicationStore,
a.validator, a.validator,
@ -199,10 +200,15 @@ a.fiber.Get("/cashierWallet", a.authMiddleware, h.GetWalletForCashier)
//Chapa Routes //Chapa Routes
group.Post("/chapa/payments/webhook/verify", h.WebhookCallback) group.Post("/chapa/payments/webhook/verify", h.WebhookCallback)
group.Get("/chapa/payments/manual/verify/:tx_ref", h.ManualVerifyPayment) group.Get("/chapa/payments/manual/verify/:tx_ref", h.ManualVerifyTransaction)
group.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit) group.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit)
group.Post("/chapa/payments/withdraw", a.authMiddleware, h.InitiateWithdrawal)
group.Get("/chapa/banks", h.GetSupportedBanks) group.Get("/chapa/banks", h.GetSupportedBanks)
// Currencies
group.Get("/currencies", h.GetSupportedCurrencies)
group.Get("/currencies/convert", h.ConvertCurrency)
//Report Routes //Report Routes
group.Get("/reports/dashboard", h.GetDashboardReport) group.Get("/reports/dashboard", h.GetDashboardReport)