institution service + more PopOK callback

This commit is contained in:
Yared Yemane 2025-06-22 21:49:16 +03:00
parent 036d598ebe
commit bdf057e01d
30 changed files with 2696 additions and 1298 deletions

View File

@ -35,6 +35,7 @@ import (
"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/currency"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
"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"
@ -162,6 +163,8 @@ func main() {
go httpserver.SetupReportCronJobs(context.Background(), reportSvc) go httpserver.SetupReportCronJobs(context.Background(), reportSvc)
bankRepository := repository.NewBankRepository(store)
instSvc := institutions.New(bankRepository)
// Initialize report worker with CSV exporter // Initialize report worker with CSV exporter
// csvExporter := infrastructure.CSVExporter{ // csvExporter := infrastructure.CSVExporter{
// ExportPath: cfg.ReportExportPath, // Make sure to add this to your config // ExportPath: cfg.ReportExportPath, // Make sure to add this to your config
@ -200,6 +203,7 @@ func main() {
// Initialize and start HTTP server // Initialize and start HTTP server
app := httpserver.NewApp( app := httpserver.NewApp(
instSvc,
currSvc, currSvc,
cfg.Port, cfg.Port,
v, v,

View File

@ -112,6 +112,23 @@ CREATE TABLE IF NOT EXISTS ticket_outcomes (
status INT NOT NULL DEFAULT 0, status INT NOT NULL DEFAULT 0,
expires TIMESTAMP NOT NULL expires TIMESTAMP NOT NULL
); );
CREATE TABLE IF NOT EXISTS banks (
id BIGSERIAL PRIMARY KEY,
slug VARCHAR(255) NOT NULL UNIQUE,
swift VARCHAR(20) NOT NULL,
name VARCHAR(255) NOT NULL,
acct_length INT NOT NULL,
country_id INT NOT NULL,
is_mobilemoney INT, -- nullable integer (0 or 1)
is_active INT NOT NULL, -- 0 or 1
is_rtgs INT NOT NULL, -- 0 or 1
active INT NOT NULL, -- 0 or 1
is_24hrs INT, -- nullable integer (0 or 1)
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
currency VARCHAR(10) NOT NULL,
bank_logo TEXT -- URL or base64 string
);
CREATE TABLE IF NOT EXISTS wallets ( CREATE TABLE IF NOT EXISTS wallets (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
balance BIGINT NOT NULL DEFAULT 0, balance BIGINT NOT NULL DEFAULT 0,

60
db/query/institutions.sql Normal file
View File

@ -0,0 +1,60 @@
-- name: CreateBank :one
INSERT INTO banks (
slug,
swift,
name,
acct_length,
country_id,
is_mobilemoney,
is_active,
is_rtgs,
active,
is_24hrs,
created_at,
updated_at,
currency,
bank_logo
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, $11, $12
)
RETURNING *;
-- name: GetBankByID :one
SELECT *
FROM banks
WHERE id = $1;
-- name: GetAllBanks :many
SELECT *
FROM banks
WHERE (
country_id = sqlc.narg('country_id')
OR sqlc.narg('country_id') IS NULL
)
AND (
is_active = sqlc.narg('is_active')
OR sqlc.narg('is_active') IS NULL
);
-- name: UpdateBank :one
UPDATE banks
SET slug = COALESCE(sqlc.narg(slug), slug),
swift = COALESCE(sqlc.narg(swift), swift),
name = COALESCE(sqlc.narg(name), name),
acct_length = COALESCE(sqlc.narg(acct_length), acct_length),
country_id = COALESCE(sqlc.narg(country_id), country_id),
is_mobilemoney = COALESCE(sqlc.narg(is_mobilemoney), is_mobilemoney),
is_active = COALESCE(sqlc.narg(is_active), is_active),
is_rtgs = COALESCE(sqlc.narg(is_rtgs), is_rtgs),
active = COALESCE(sqlc.narg(active), active),
is_24hrs = COALESCE(sqlc.narg(is_24hrs), is_24hrs),
updated_at = CURRENT_TIMESTAMP,
currency = COALESCE(sqlc.narg(currency), currency),
bank_logo = COALESCE(sqlc.narg(bank_logo), bank_logo)
WHERE id = $1
RETURNING *;
-- name: DeleteBank :exec
DELETE FROM banks
WHERE id = $1;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,42 @@ definitions:
user_id: user_id:
type: string type: string
type: object type: object
domain.Bank:
properties:
acct_length:
type: integer
active:
type: integer
bank_logo:
description: URL or base64
type: string
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:
@ -491,11 +527,13 @@ definitions:
- BANK - BANK
domain.PaymentStatus: domain.PaymentStatus:
enum: enum:
- success
- pending - pending
- completed - completed
- failed - failed
type: string type: string
x-enum-varnames: x-enum-varnames:
- PaymentStatusSuccessful
- PaymentStatusPending - PaymentStatusPending
- PaymentStatusCompleted - PaymentStatusCompleted
- PaymentStatusFailed - PaymentStatusFailed
@ -559,20 +597,6 @@ definitions:
items: {} items: {}
type: array type: array
type: object type: object
domain.RecommendationErrorResponse:
properties:
message:
type: string
type: object
domain.RecommendationSuccessfulResponse:
properties:
message:
type: string
recommended_games:
items:
$ref: '#/definitions/domain.VirtualGame'
type: array
type: object
domain.ReferralSettings: domain.ReferralSettings:
properties: properties:
betReferralBonusPercentage: betReferralBonusPercentage:
@ -742,37 +766,6 @@ definitions:
- $ref: '#/definitions/domain.EventStatus' - $ref: '#/definitions/domain.EventStatus'
description: Match Status for event description: Match Status for event
type: object type: object
domain.VirtualGame:
properties:
category:
type: string
created_at:
type: string
id:
type: integer
is_active:
type: boolean
is_featured:
type: boolean
max_bet:
type: number
min_bet:
type: number
name:
type: string
popularity_score:
type: integer
provider:
type: string
rtp:
type: number
thumbnail_url:
type: string
updated_at:
type: string
volatility:
type: string
type: object
handlers.AdminRes: handlers.AdminRes:
properties: properties:
created_at: created_at:
@ -1340,34 +1333,27 @@ definitions:
handlers.TransferWalletRes: handlers.TransferWalletRes:
properties: properties:
amount: amount:
example: 100
type: number type: number
cashier_id: cashier_id:
example: 789
type: integer type: integer
created_at: created_at:
example: "2025-04-08T12:00:00Z"
type: string type: string
id: id:
example: 1
type: integer type: integer
payment_method: payment_method:
example: bank
type: string type: string
receiver_wallet_id: receiver_wallet_id:
example: 1
type: integer type: integer
reference_number:
description: ← Add this
type: string
sender_wallet_id: sender_wallet_id:
example: 1
type: integer type: integer
type: type:
example: transfer
type: string type: string
updated_at: updated_at:
example: "2025-04-08T12:30:00Z"
type: string type: string
verified: verified:
example: true
type: boolean type: boolean
type: object type: object
handlers.UpdateCashOutReq: handlers.UpdateCashOutReq:
@ -1801,6 +1787,144 @@ 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/banks:
get:
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: List all banks
tags:
- Institutions - Banks
post:
consumes:
- application/json
parameters:
- description: Bank Info
in: body
name: bank
required: true
schema:
$ref: '#/definitions/domain.Bank'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/domain.Bank'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Create a new bank
tags:
- Institutions - Banks
/api/v1/banks/{id}:
delete:
parameters:
- description: Bank ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"204":
description: Deleted successfully
schema:
type: string
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Delete a bank
tags:
- Institutions - Banks
get:
parameters:
- description: Bank ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Bank'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get a bank by ID
tags:
- Institutions - Banks
put:
consumes:
- application/json
parameters:
- description: Bank ID
in: path
name: id
required: true
type: integer
- description: Bank Info
in: body
name: bank
required: true
schema:
$ref: '#/definitions/domain.Bank'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Bank'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Update a bank
tags:
- Institutions - Banks
/api/v1/chapa/banks: /api/v1/chapa/banks:
get: get:
consumes: consumes:
@ -2144,31 +2268,6 @@ paths:
summary: Get dashboard report summary: Get dashboard report
tags: tags:
- Reports - Reports
/api/v1/virtual-games/recommendations/{userID}:
get:
consumes:
- application/json
description: Returns a list of recommended virtual games for a specific user
parameters:
- description: User ID
in: path
name: userID
required: true
type: string
produces:
- application/json
responses:
"200":
description: Recommended games fetched successfully
schema:
$ref: '#/definitions/domain.RecommendationSuccessfulResponse'
"500":
description: Failed to fetch recommendations
schema:
$ref: '#/definitions/domain.RecommendationErrorResponse'
summary: Get virtual game recommendations
tags:
- Recommendations
/api/v1/webhooks/alea: /api/v1/webhooks/alea:
post: post:
consumes: consumes:
@ -2301,180 +2400,6 @@ paths:
summary: Refresh token summary: Refresh token
tags: tags:
- auth - auth
/bet:
get:
consumes:
- application/json
description: Gets all the bets
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.BetRes'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Gets all bets
tags:
- bet
post:
consumes:
- application/json
description: Creates a bet
parameters:
- description: Creates bet
in: body
name: createBet
required: true
schema:
$ref: '#/definitions/domain.CreateBetReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.BetRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Create a bet
tags:
- bet
/bet/{id}:
delete:
consumes:
- application/json
description: Deletes bet by id
parameters:
- description: Bet 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: Deletes bet by id
tags:
- bet
get:
consumes:
- application/json
description: Gets a single bet by id
parameters:
- description: Bet ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.BetRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Gets bet by id
tags:
- bet
patch:
consumes:
- application/json
description: Updates the cashed out field
parameters:
- description: Bet ID
in: path
name: id
required: true
type: integer
- description: Updates Cashed Out
in: body
name: updateCashOut
required: true
schema:
$ref: '#/definitions/handlers.UpdateCashOutReq'
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: Updates the cashed out field
tags:
- bet
/bet/cashout/{id}:
get:
consumes:
- application/json
description: Gets a single bet by cashout id
parameters:
- description: cashout ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.BetRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Gets bet by cashout id
tags:
- bet
/branch: /branch:
get: get:
consumes: consumes:
@ -3622,36 +3547,6 @@ paths:
summary: Recommend virtual games summary: Recommend virtual games
tags: tags:
- Virtual Games - PopOK - Virtual Games - PopOK
/random/bet:
post:
consumes:
- application/json
description: Generate a random bet
parameters:
- description: Create Random bet
in: body
name: createBet
required: true
schema:
$ref: '#/definitions/domain.RandomBetReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.BetRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Generate a random bet
tags:
- bet
/referral/settings: /referral/settings:
get: get:
consumes: consumes:
@ -3828,6 +3723,210 @@ paths:
summary: Gets all companies summary: Gets all companies
tags: tags:
- company - company
/sport/bet:
get:
consumes:
- application/json
description: Gets all the bets
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.BetRes'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Gets all bets
tags:
- bet
post:
consumes:
- application/json
description: Creates a bet
parameters:
- description: Creates bet
in: body
name: createBet
required: true
schema:
$ref: '#/definitions/domain.CreateBetReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.BetRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Create a bet
tags:
- bet
/sport/bet/{id}:
delete:
consumes:
- application/json
description: Deletes bet by id
parameters:
- description: Bet 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: Deletes bet by id
tags:
- bet
get:
consumes:
- application/json
description: Gets a single bet by id
parameters:
- description: Bet ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.BetRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Gets bet by id
tags:
- bet
patch:
consumes:
- application/json
description: Updates the cashed out field
parameters:
- description: Bet ID
in: path
name: id
required: true
type: integer
- description: Updates Cashed Out
in: body
name: updateCashOut
required: true
schema:
$ref: '#/definitions/handlers.UpdateCashOutReq'
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: Updates the cashed out field
tags:
- bet
/sport/bet/cashout/{id}:
get:
consumes:
- application/json
description: Gets a single bet by cashout id
parameters:
- description: cashout ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.BetRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Gets bet by cashout id
tags:
- bet
/sport/random/bet:
post:
consumes:
- application/json
description: Generate a random bet
parameters:
- description: Create Random bet
in: body
name: createBet
required: true
schema:
$ref: '#/definitions/domain.RandomBetReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.BetRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Generate a random bet
tags:
- bet
/supportedOperation: /supportedOperation:
get: get:
consumes: consumes:

251
gen/db/institutions.sql.go Normal file
View File

@ -0,0 +1,251 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: institutions.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CreateBank = `-- name: CreateBank :one
INSERT INTO banks (
slug,
swift,
name,
acct_length,
country_id,
is_mobilemoney,
is_active,
is_rtgs,
active,
is_24hrs,
created_at,
updated_at,
currency,
bank_logo
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, $11, $12
)
RETURNING id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo
`
type CreateBankParams struct {
Slug string `json:"slug"`
Swift string `json:"swift"`
Name string `json:"name"`
AcctLength int32 `json:"acct_length"`
CountryID int32 `json:"country_id"`
IsMobilemoney pgtype.Int4 `json:"is_mobilemoney"`
IsActive int32 `json:"is_active"`
IsRtgs int32 `json:"is_rtgs"`
Active int32 `json:"active"`
Is24hrs pgtype.Int4 `json:"is_24hrs"`
Currency string `json:"currency"`
BankLogo pgtype.Text `json:"bank_logo"`
}
func (q *Queries) CreateBank(ctx context.Context, arg CreateBankParams) (Bank, error) {
row := q.db.QueryRow(ctx, CreateBank,
arg.Slug,
arg.Swift,
arg.Name,
arg.AcctLength,
arg.CountryID,
arg.IsMobilemoney,
arg.IsActive,
arg.IsRtgs,
arg.Active,
arg.Is24hrs,
arg.Currency,
arg.BankLogo,
)
var i Bank
err := row.Scan(
&i.ID,
&i.Slug,
&i.Swift,
&i.Name,
&i.AcctLength,
&i.CountryID,
&i.IsMobilemoney,
&i.IsActive,
&i.IsRtgs,
&i.Active,
&i.Is24hrs,
&i.CreatedAt,
&i.UpdatedAt,
&i.Currency,
&i.BankLogo,
)
return i, err
}
const DeleteBank = `-- name: DeleteBank :exec
DELETE FROM banks
WHERE id = $1
`
func (q *Queries) DeleteBank(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteBank, id)
return err
}
const GetAllBanks = `-- name: GetAllBanks :many
SELECT id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo
FROM banks
WHERE (
country_id = $1
OR $1 IS NULL
)
AND (
is_active = $2
OR $2 IS NULL
)
`
type GetAllBanksParams struct {
CountryID pgtype.Int4 `json:"country_id"`
IsActive pgtype.Int4 `json:"is_active"`
}
func (q *Queries) GetAllBanks(ctx context.Context, arg GetAllBanksParams) ([]Bank, error) {
rows, err := q.db.Query(ctx, GetAllBanks, arg.CountryID, arg.IsActive)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Bank
for rows.Next() {
var i Bank
if err := rows.Scan(
&i.ID,
&i.Slug,
&i.Swift,
&i.Name,
&i.AcctLength,
&i.CountryID,
&i.IsMobilemoney,
&i.IsActive,
&i.IsRtgs,
&i.Active,
&i.Is24hrs,
&i.CreatedAt,
&i.UpdatedAt,
&i.Currency,
&i.BankLogo,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetBankByID = `-- name: GetBankByID :one
SELECT id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo
FROM banks
WHERE id = $1
`
func (q *Queries) GetBankByID(ctx context.Context, id int64) (Bank, error) {
row := q.db.QueryRow(ctx, GetBankByID, id)
var i Bank
err := row.Scan(
&i.ID,
&i.Slug,
&i.Swift,
&i.Name,
&i.AcctLength,
&i.CountryID,
&i.IsMobilemoney,
&i.IsActive,
&i.IsRtgs,
&i.Active,
&i.Is24hrs,
&i.CreatedAt,
&i.UpdatedAt,
&i.Currency,
&i.BankLogo,
)
return i, err
}
const UpdateBank = `-- name: UpdateBank :one
UPDATE banks
SET slug = COALESCE($2, slug),
swift = COALESCE($3, swift),
name = COALESCE($4, name),
acct_length = COALESCE($5, acct_length),
country_id = COALESCE($6, country_id),
is_mobilemoney = COALESCE($7, is_mobilemoney),
is_active = COALESCE($8, is_active),
is_rtgs = COALESCE($9, is_rtgs),
active = COALESCE($10, active),
is_24hrs = COALESCE($11, is_24hrs),
updated_at = CURRENT_TIMESTAMP,
currency = COALESCE($12, currency),
bank_logo = COALESCE($13, bank_logo)
WHERE id = $1
RETURNING id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo
`
type UpdateBankParams struct {
ID int64 `json:"id"`
Slug pgtype.Text `json:"slug"`
Swift pgtype.Text `json:"swift"`
Name pgtype.Text `json:"name"`
AcctLength pgtype.Int4 `json:"acct_length"`
CountryID pgtype.Int4 `json:"country_id"`
IsMobilemoney pgtype.Int4 `json:"is_mobilemoney"`
IsActive pgtype.Int4 `json:"is_active"`
IsRtgs pgtype.Int4 `json:"is_rtgs"`
Active pgtype.Int4 `json:"active"`
Is24hrs pgtype.Int4 `json:"is_24hrs"`
Currency pgtype.Text `json:"currency"`
BankLogo pgtype.Text `json:"bank_logo"`
}
func (q *Queries) UpdateBank(ctx context.Context, arg UpdateBankParams) (Bank, error) {
row := q.db.QueryRow(ctx, UpdateBank,
arg.ID,
arg.Slug,
arg.Swift,
arg.Name,
arg.AcctLength,
arg.CountryID,
arg.IsMobilemoney,
arg.IsActive,
arg.IsRtgs,
arg.Active,
arg.Is24hrs,
arg.Currency,
arg.BankLogo,
)
var i Bank
err := row.Scan(
&i.ID,
&i.Slug,
&i.Swift,
&i.Name,
&i.AcctLength,
&i.CountryID,
&i.IsMobilemoney,
&i.IsActive,
&i.IsRtgs,
&i.Active,
&i.Is24hrs,
&i.CreatedAt,
&i.UpdatedAt,
&i.Currency,
&i.BankLogo,
)
return i, err
}

View File

@ -55,6 +55,24 @@ func (ns NullReferralstatus) Value() (driver.Value, error) {
return string(ns.Referralstatus), nil return string(ns.Referralstatus), nil
} }
type Bank struct {
ID int64 `json:"id"`
Slug string `json:"slug"`
Swift string `json:"swift"`
Name string `json:"name"`
AcctLength int32 `json:"acct_length"`
CountryID int32 `json:"country_id"`
IsMobilemoney pgtype.Int4 `json:"is_mobilemoney"`
IsActive int32 `json:"is_active"`
IsRtgs int32 `json:"is_rtgs"`
Active int32 `json:"active"`
Is24hrs pgtype.Int4 `json:"is_24hrs"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Currency string `json:"currency"`
BankLogo pgtype.Text `json:"bank_logo"`
}
type Bet struct { type Bet struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Amount int64 `json:"amount"` Amount int64 `json:"amount"`

View File

@ -16,6 +16,7 @@ type PaymentStatus string
type WithdrawalStatus string type WithdrawalStatus string
const ( const (
WithdrawalStatusSuccessful WithdrawalStatus = "success"
WithdrawalStatusPending WithdrawalStatus = "pending" WithdrawalStatusPending WithdrawalStatus = "pending"
WithdrawalStatusProcessing WithdrawalStatus = "processing" WithdrawalStatusProcessing WithdrawalStatus = "processing"
WithdrawalStatusCompleted WithdrawalStatus = "completed" WithdrawalStatusCompleted WithdrawalStatus = "completed"
@ -23,9 +24,10 @@ const (
) )
const ( const (
PaymentStatusPending PaymentStatus = "pending" PaymentStatusSuccessful PaymentStatus = "success"
PaymentStatusCompleted PaymentStatus = "completed" PaymentStatusPending PaymentStatus = "pending"
PaymentStatusFailed PaymentStatus = "failed" PaymentStatusCompleted PaymentStatus = "completed"
PaymentStatusFailed PaymentStatus = "failed"
) )
type ChapaDepositRequest struct { type ChapaDepositRequest struct {
@ -70,22 +72,23 @@ type ChapaVerificationResponse struct {
TxRef string `json:"tx_ref"` TxRef string `json:"tx_ref"`
} }
type Bank struct { // type Bank struct {
ID int `json:"id"` // ID int `json:"id"`
Slug string `json:"slug"` // Slug string `json:"slug"`
Swift string `json:"swift"` // Swift string `json:"swift"`
Name string `json:"name"` // Name string `json:"name"`
AcctLength int `json:"acct_length"` // AcctLength int `json:"acct_length"`
CountryID int `json:"country_id"` // CountryID int `json:"country_id"`
IsMobileMoney int `json:"is_mobilemoney"` // nullable // IsMobileMoney int `json:"is_mobilemoney"` // nullable
IsActive int `json:"is_active"` // IsActive int `json:"is_active"`
IsRTGS int `json:"is_rtgs"` // IsRTGS int `json:"is_rtgs"`
Active int `json:"active"` // Active int `json:"active"`
Is24Hrs int `json:"is_24hrs"` // nullable // Is24Hrs int `json:"is_24hrs"` // nullable
CreatedAt time.Time `json:"created_at"` // CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` // UpdatedAt time.Time `json:"updated_at"`
Currency string `json:"currency"` // Currency string `json:"currency"`
} // BankLogo string `json:"bank_logo"` // URL or base64
// }
type BankResponse struct { type BankResponse struct {
Message string `json:"message"` Message string `json:"message"`
@ -142,11 +145,9 @@ type ChapaWithdrawalRequest struct {
// } // }
type ChapaWithdrawalResponse struct { type ChapaWithdrawalResponse struct {
Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
Data struct { Status string `json:"status"`
Reference string `json:"reference"` Data string `json:"data"` // Accepts string instead of struct
} `json:"data"`
} }
type ChapaTransactionType struct { type ChapaTransactionType struct {

View File

@ -0,0 +1,21 @@
package domain
import "time"
type Bank struct {
ID int `json:"id"`
Slug string `json:"slug"`
Swift string `json:"swift"`
Name string `json:"name"`
AcctLength int `json:"acct_length"`
CountryID int `json:"country_id"`
IsMobileMoney int `json:"is_mobilemoney"` // nullable
IsActive int `json:"is_active"`
IsRTGS int `json:"is_rtgs"`
Active int `json:"active"`
Is24Hrs int `json:"is_24hrs"` // nullable
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Currency string `json:"currency"`
BankLogo string `json:"bank_logo"` // URL or base64
}

View File

@ -97,15 +97,16 @@ func FromJSON(data []byte) (*Notification, error) {
func ReceiverFromRole(role Role) NotificationRecieverSide { func ReceiverFromRole(role Role) NotificationRecieverSide {
if role == RoleAdmin { switch role {
case RoleAdmin:
return NotificationRecieverSideAdmin return NotificationRecieverSideAdmin
} else if role == RoleCashier { case RoleCashier:
return NotificationRecieverSideCashier return NotificationRecieverSideCashier
} else if role == RoleBranchManager { case RoleBranchManager:
return NotificationRecieverSideBranchManager return NotificationRecieverSideBranchManager
} else if role == RoleCustomer { case RoleCustomer:
return NotificationRecieverSideCustomer return NotificationRecieverSideCustomer
} else { default:
return "" return ""
} }
} }

View File

@ -159,6 +159,11 @@ type PopOKWinResponse struct {
Balance float64 `json:"balance"` Balance float64 `json:"balance"`
} }
type PopOKGenerateTokenRequest struct {
GameID string `json:"newGameId"`
Token string `json:"token"`
}
type PopOKCancelRequest struct { type PopOKCancelRequest struct {
ExternalToken string `json:"externalToken"` ExternalToken string `json:"externalToken"`
PlayerID string `json:"playerId"` PlayerID string `json:"playerId"`
@ -172,6 +177,10 @@ type PopOKCancelResponse struct {
Balance float64 `json:"balance"` Balance float64 `json:"balance"`
} }
type PopOKGenerateTokenResponse struct {
NewToken string `json:"newToken"`
}
type AleaPlayCallback struct { type AleaPlayCallback struct {
EventID string `json:"event_id"` EventID string `json:"event_id"`
TransactionID string `json:"transaction_id"` TransactionID string `json:"transaction_id"`

View File

@ -0,0 +1,139 @@
package repository
import (
"context"
"database/sql"
"errors"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
)
type BankRepository interface {
CreateBank(ctx context.Context, bank *domain.Bank) error
GetBankByID(ctx context.Context, id int) (*domain.Bank, error)
GetAllBanks(ctx context.Context, countryID *int, isActive *int) ([]domain.Bank, error)
UpdateBank(ctx context.Context, bank *domain.Bank) error
DeleteBank(ctx context.Context, id int) error
}
type BankRepo struct {
store *Store
}
func NewBankRepository(store *Store) BankRepository {
return &BankRepo{store: store}
}
func (r *BankRepo) CreateBank(ctx context.Context, bank *domain.Bank) error {
params := dbgen.CreateBankParams{
Slug: bank.Slug,
Swift: bank.Swift,
Name: bank.Name,
AcctLength: int32(bank.AcctLength),
CountryID: int32(bank.CountryID),
IsMobilemoney: pgtype.Int4{Int32: int32(bank.IsMobileMoney), Valid: true},
IsActive: int32(bank.IsActive),
IsRtgs: int32(bank.IsRTGS),
Active: int32(bank.Active),
Is24hrs: pgtype.Int4{Int32: int32(bank.Is24Hrs), Valid: true},
Currency: bank.Currency,
BankLogo: pgtype.Text{String: bank.BankLogo, Valid: true},
}
createdBank, err := r.store.queries.CreateBank(ctx, params)
if err != nil {
return err
}
// Update the ID and timestamps on the passed struct
bank.ID = int(createdBank.ID)
bank.CreatedAt = createdBank.CreatedAt.Time
bank.UpdatedAt = createdBank.UpdatedAt.Time
return nil
}
func (r *BankRepo) GetBankByID(ctx context.Context, id int) (*domain.Bank, error) {
dbBank, err := r.store.queries.GetBankByID(ctx, int64(id))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return mapDBBankToDomain(&dbBank), nil
}
func (r *BankRepo) GetAllBanks(ctx context.Context, countryID *int, isActive *int) ([]domain.Bank, error) {
params := dbgen.GetAllBanksParams{
CountryID: pgtype.Int4{},
IsActive: pgtype.Int4{},
}
if countryID != nil {
params.CountryID = pgtype.Int4{Int32: int32(*countryID), Valid: true}
}
if isActive != nil {
params.IsActive = pgtype.Int4{Int32: int32(*isActive), Valid: true}
}
dbBanks, err := r.store.queries.GetAllBanks(ctx, params)
if err != nil {
return nil, err
}
banks := make([]domain.Bank, len(dbBanks))
for i, b := range dbBanks {
banks[i] = *mapDBBankToDomain(&b)
}
return banks, nil
}
func (r *BankRepo) UpdateBank(ctx context.Context, bank *domain.Bank) error {
params := dbgen.UpdateBankParams{
ID: int64(bank.ID),
Slug: pgtype.Text{String: bank.Slug, Valid: true},
Swift: pgtype.Text{String: bank.Swift, Valid: true},
Name: pgtype.Text{String: bank.Name, Valid: true},
AcctLength: pgtype.Int4{Int32: int32(bank.AcctLength), Valid: true},
CountryID: pgtype.Int4{Int32: int32(bank.CountryID), Valid: true},
IsMobilemoney: pgtype.Int4{Int32: int32(bank.IsMobileMoney), Valid: true},
IsActive: pgtype.Int4{Int32: int32(bank.IsActive), Valid: true},
IsRtgs: pgtype.Int4{Int32: int32(bank.IsRTGS), Valid: true},
Active: pgtype.Int4{Int32: int32(bank.Active), Valid: true},
Is24hrs: pgtype.Int4{Int32: int32(bank.Is24Hrs), Valid: true},
Currency: pgtype.Text{String: bank.Currency, Valid: true},
BankLogo: pgtype.Text{String: bank.BankLogo, Valid: true},
}
updatedBank, err := r.store.queries.UpdateBank(ctx, params)
if err != nil {
return err
}
// update timestamps in domain struct
bank.UpdatedAt = updatedBank.UpdatedAt.Time
return nil
}
func (r *BankRepo) DeleteBank(ctx context.Context, id int) error {
return r.store.queries.DeleteBank(ctx, int64(id))
}
// Helper to map DB struct to domain
func mapDBBankToDomain(dbBank *dbgen.Bank) *domain.Bank {
return &domain.Bank{
ID: int(dbBank.ID),
Slug: dbBank.Slug,
Swift: dbBank.Swift,
Name: dbBank.Name,
AcctLength: int(dbBank.AcctLength),
CountryID: int(dbBank.CountryID),
IsMobileMoney: int(dbBank.IsMobilemoney.Int32),
IsActive: int(dbBank.IsActive),
IsRTGS: int(dbBank.IsRtgs),
Active: int(dbBank.Active),
Is24Hrs: int(dbBank.Is24hrs.Int32),
CreatedAt: dbBank.CreatedAt.Time,
UpdatedAt: dbBank.UpdatedAt.Time,
Currency: dbBank.Currency,
BankLogo: dbBank.BankLogo.String,
}
}

View File

@ -31,8 +31,8 @@ func NewClient(baseURL, secretKey string) *Client {
func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) { func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) {
payload := map[string]interface{}{ payload := map[string]interface{}{
"amount": fmt.Sprintf("%.2f", float64(req.Amount)/100), "amount": fmt.Sprintf("%.2f", float64(req.Amount)/100),
"currency": req.Currency, "currency": req.Currency,
// "email": req.Email, // "email": req.Email,
"first_name": req.FirstName, "first_name": req.FirstName,
"last_name": req.LastName, "last_name": req.LastName,
@ -175,6 +175,51 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain
}, nil }, nil
} }
func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
url := fmt.Sprintf("%s/transfers/verify/%s", c.baseURL, txRef)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.secretKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var response struct {
Status string `json:"status"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
var status domain.PaymentStatus
switch response.Status {
case "success":
status = domain.PaymentStatusCompleted
default:
status = domain.PaymentStatusFailed
}
return &domain.ChapaVerificationResponse{
Status: string(status),
Amount: response.Amount,
Currency: response.Currency,
}, nil
}
func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) { func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil) req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil)
if err != nil { if err != nil {
@ -223,10 +268,6 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error)
} }
func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) { func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) {
// base, err := url.Parse(c.baseURL)
// if err != nil {
// return false, fmt.Errorf("invalid base URL: %w", err)
// }
endpoint := c.baseURL + "/transfers" endpoint := c.baseURL + "/transfers"
fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint) fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint)
@ -240,7 +281,9 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa
return false, fmt.Errorf("failed to create request: %w", err) return false, fmt.Errorf("failed to create request: %w", err)
} }
c.setHeaders(httpReq) // Set headers here
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq) resp, err := c.httpClient.Do(httpReq)
if err != nil { if err != nil {
@ -249,7 +292,8 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("chapa api returned status: %d", resp.StatusCode) body, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("chapa api returned status: %d - %s", resp.StatusCode, string(body))
} }
var response domain.ChapaWithdrawalResponse var response domain.ChapaWithdrawalResponse
@ -257,7 +301,7 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa
return false, fmt.Errorf("failed to decode response: %w", err) return false, fmt.Errorf("failed to decode response: %w", err)
} }
return response.Status == string(domain.WithdrawalStatusProcessing), nil return response.Status == string(domain.WithdrawalStatusSuccessful), nil
} }
func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaVerificationResponse, error) { func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaVerificationResponse, error) {

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"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"
@ -60,7 +61,9 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
var senderWallet domain.Wallet var senderWallet domain.Wallet
// Generate unique reference // Generate unique reference
reference := uuid.New().String() // reference := uuid.New().String()
reference := fmt.Sprintf("chapa-deposit-%d-%s", userID, uuid.New().String())
senderWallets, err := s.walletStore.GetWalletsByUser(ctx, userID) senderWallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get sender wallets: %w", err) return "", fmt.Errorf("failed to get sender wallets: %w", err)
@ -92,8 +95,7 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
Verified: false, Verified: false,
} }
// Initialize payment with Chapa payload := domain.ChapaDepositRequest{
response, err := s.chapaClient.InitializePayment(ctx, domain.ChapaDepositRequest{
Amount: amount, Amount: amount,
Currency: "ETB", Currency: "ETB",
Email: user.Email, Email: user.Email,
@ -102,7 +104,12 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
TxRef: reference, TxRef: reference,
CallbackURL: s.cfg.CHAPA_CALLBACK_URL, CallbackURL: s.cfg.CHAPA_CALLBACK_URL,
ReturnURL: s.cfg.CHAPA_RETURN_URL, ReturnURL: s.cfg.CHAPA_RETURN_URL,
}) }
// Initialize payment with Chapa
response, err := s.chapaClient.InitializePayment(ctx, payload)
fmt.Printf("\n\nChapa payload is: %+v\n\n", payload)
if err != nil { if err != nil {
// Update payment status to failed // Update payment status to failed
@ -189,12 +196,16 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma
} }
success, err := s.chapaClient.InitiateTransfer(ctx, transferReq) success, err := s.chapaClient.InitiateTransfer(ctx, transferReq)
if err != nil || !success { if err != nil {
// Update withdrawal status to failed
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
return nil, fmt.Errorf("failed to initiate transfer: %w", err) return nil, fmt.Errorf("failed to initiate transfer: %w", err)
} }
if !success {
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
return nil, errors.New("chapa rejected the transfer request")
}
// Update withdrawal status to processing // Update withdrawal status to processing
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil { if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil {
return nil, fmt.Errorf("failed to update withdrawal status: %w", err) return nil, fmt.Errorf("failed to update withdrawal status: %w", err)
@ -216,50 +227,68 @@ func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error)
return banks, nil return banks, nil
} }
func (s *Service) ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
// First check if we already have a verified record // Lookup transfer by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef) transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
if err == nil && transfer.Verified { if err != nil {
return nil, fmt.Errorf("transfer not found for reference %s: %w", txRef, err)
}
if transfer.Verified {
return &domain.ChapaVerificationResponse{ return &domain.ChapaVerificationResponse{
Status: string(domain.PaymentStatusCompleted), Status: string(domain.PaymentStatusCompleted),
Amount: float64(transfer.Amount) / 100, // Convert from cents/kobo Amount: float64(transfer.Amount) / 100,
Currency: "ETB", Currency: "ETB",
}, nil }, nil
} }
fmt.Printf("\n\nSender wallet ID is:%v\n\n", transfer.SenderWalletID.Value) // Validate sender wallet
fmt.Printf("\n\nTransfer is:%v\n\n", transfer)
// just making sure that the sender id is valid
if !transfer.SenderWalletID.Valid { if !transfer.SenderWalletID.Valid {
return nil, fmt.Errorf("sender wallet id is invalid: %v \n", transfer.SenderWalletID) return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID)
} }
// If not verified or not found, verify with Chapa var verification *domain.ChapaVerificationResponse
verification, err := s.chapaClient.VerifyPayment(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("failed to verify payment: %w", err)
}
// Update our records if payment is successful // Decide verification method based on type
if verification.Status == domain.PaymentStatusCompleted { switch strings.ToLower(string(transfer.Type)) {
err = s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true) case "deposit":
// Use Chapa Payment Verification
verification, err = s.chapaClient.ManualVerifyPayment(ctx, txRef)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to update verification status: %w", err) return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err)
} }
// Credit user's wallet if verification.Status == string(domain.PaymentStatusSuccessful) {
err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID.Value, transfer.Amount) // Mark verified
if err != nil { if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return nil, fmt.Errorf("failed to update wallet balance: %w", err) return nil, fmt.Errorf("failed to mark deposit transfer as verified: %w", err)
}
// Credit wallet
if _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{}); err != nil {
return nil, fmt.Errorf("failed to credit wallet: %w", err)
}
} }
case "withdraw":
// Use Chapa Transfer Verification
verification, err = s.chapaClient.ManualVerifyTransfer(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err)
}
if verification.Status == string(domain.PaymentStatusSuccessful) {
// Mark verified (withdraw doesn't affect balance)
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return nil, fmt.Errorf("failed to mark withdrawal transfer as verified: %w", err)
}
}
default:
return nil, fmt.Errorf("unsupported transfer type: %s", transfer.Type)
} }
return &domain.ChapaVerificationResponse{ return verification, nil
Status: string(verification.Status),
Amount: float64(verification.Amount),
Currency: verification.Currency,
}, nil
} }
func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error { func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error {
@ -285,12 +314,13 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai
// verified = true // 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 payment is completed, credit user's wallet
if transfer.Status == string(domain.PaymentStatusCompleted) { if transfer.Status == string(domain.PaymentStatusSuccessful) {
if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
return fmt.Errorf("failed to update payment status: %w", err)
}
if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{ if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{
ReferenceNumber: domain.ValidString{ ReferenceNumber: domain.ValidString{
Value: transfer.Reference, Value: transfer.Reference,
@ -326,12 +356,11 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai
// verified = true // verified = true
// } // }
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { if payment.Status == string(domain.PaymentStatusSuccessful) {
return fmt.Errorf("failed to update payment status: %w", err) 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 walle
// If payment is completed, credit user's wallet } else {
if payment.Status == string(domain.PaymentStatusFailed) {
if _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil { if _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err) return fmt.Errorf("failed to credit user wallet: %w", err)
} }

View File

@ -0,0 +1 @@
package institutions

View File

@ -0,0 +1,44 @@
package institutions
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
)
type Service struct {
repo repository.BankRepository
}
func New(repo repository.BankRepository) *Service {
return &Service{repo: repo}
}
func (s *Service) Create(ctx context.Context, bank *domain.Bank) error {
return s.repo.CreateBank(ctx, bank)
}
func (s *Service) Update(ctx context.Context, bank *domain.Bank) error {
return s.repo.UpdateBank(ctx, bank)
}
func (s *Service) GetByID(ctx context.Context, id int64) (*domain.Bank, error) {
return s.repo.GetBankByID(ctx, int(id))
}
func (s *Service) Delete(ctx context.Context, id int64) error {
return s.repo.DeleteBank(ctx, int(id))
}
func (s *Service) List(ctx context.Context) ([]*domain.Bank, error) {
banks, err := s.repo.GetAllBanks(ctx, nil, nil)
if err != nil {
return nil, err
}
result := make([]*domain.Bank, len(banks))
for i := range banks {
result[i] = &banks[i]
}
return result, nil
}

View File

@ -0,0 +1 @@
package issues

View File

@ -0,0 +1 @@
package issues

View File

@ -13,6 +13,8 @@ type VirtualGameService interface {
GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfoRequest) (*domain.PopOKPlayerInfoResponse, error) GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfoRequest) (*domain.PopOKPlayerInfoResponse, error)
ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error)
ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error)
ProcessTournamentWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error)
ProcessPromoWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error)
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error)

View File

@ -299,6 +299,167 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
}, nil }, nil
} }
func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) {
// 1. Validate token and get user ID
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
if err != nil {
s.logger.Error("Invalid token in tournament win request", "error", err)
return nil, fmt.Errorf("invalid token")
}
// 2. Check for duplicate tournament win transaction
existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID)
if err != nil {
s.logger.Error("Failed to check existing tournament transaction", "error", err)
return nil, fmt.Errorf("transaction check failed")
}
if existingTx != nil && existingTx.TransactionType == "TOURNAMENT_WIN" {
s.logger.Warn("Duplicate tournament win", "transactionID", req.TransactionID)
wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
balance := 0.0
if len(wallets) > 0 {
balance = float64(wallets[0].Balance) / 100
}
return &domain.PopOKWinResponse{
TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", existingTx.ID),
Balance: balance,
}, nil
}
// 3. Convert amount to cents
amountCents := int64(req.Amount * 100)
// 4. Credit user wallet
if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
s.logger.Error("Failed to credit wallet for tournament", "userID", claims.UserID, "error", err)
return nil, fmt.Errorf("wallet credit failed")
}
// 5. Log tournament win transaction
tx := &domain.VirtualGameTransaction{
UserID: claims.UserID,
TransactionType: "TOURNAMENT_WIN",
Amount: amountCents,
Currency: req.Currency,
ExternalTransactionID: req.TransactionID,
Status: "COMPLETED",
CreatedAt: time.Now(),
}
if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil {
s.logger.Error("Failed to record tournament win transaction", "error", err)
return nil, fmt.Errorf("transaction recording failed")
}
// 6. Fetch updated balance
wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
if err != nil {
return nil, fmt.Errorf("Failed to get wallet balance")
}
return &domain.PopOKWinResponse{
TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", tx.ID),
Balance: float64(wallets[0].Balance) / 100,
}, nil
}
func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) {
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
if err != nil {
s.logger.Error("Invalid token in promo win request", "error", err)
return nil, fmt.Errorf("invalid token")
}
existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID)
if err != nil {
s.logger.Error("Failed to check existing promo transaction", "error", err)
return nil, fmt.Errorf("transaction check failed")
}
if existingTx != nil && existingTx.TransactionType == "PROMO_WIN" {
s.logger.Warn("Duplicate promo win", "transactionID", req.TransactionID)
wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
balance := 0.0
if len(wallets) > 0 {
balance = float64(wallets[0].Balance) / 100
}
return &domain.PopOKWinResponse{
TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", existingTx.ID),
Balance: balance,
}, nil
}
amountCents := int64(req.Amount * 100)
if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
s.logger.Error("Failed to credit wallet for promo", "userID", claims.UserID, "error", err)
return nil, fmt.Errorf("wallet credit failed")
}
tx := &domain.VirtualGameTransaction{
UserID: claims.UserID,
TransactionType: "PROMO_WIN",
Amount: amountCents,
Currency: req.Currency,
ExternalTransactionID: req.TransactionID,
Status: "COMPLETED",
CreatedAt: time.Now(),
}
if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil {
s.logger.Error("Failed to create promo win transaction", "error", err)
return nil, fmt.Errorf("transaction recording failed")
}
wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
if err != nil {
return nil, fmt.Errorf("failed to read wallets")
}
return &domain.PopOKWinResponse{
TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", tx.ID),
Balance: float64(wallets[0].Balance) / 100,
}, nil
}
// func (s *service) GenerateNewToken(ctx context.Context, req *domain.PopOKGenerateTokenRequest) (*domain.PopOKGenerateTokenResponse, error) {
// userID, err := strconv.ParseInt(req.PlayerID, 10, 64)
// if err != nil {
// s.logger.Error("Invalid player ID", "playerID", req.PlayerID, "error", err)
// return nil, fmt.Errorf("invalid player ID")
// }
// user, err := s.store.GetUserByID(ctx, userID)
// if err != nil {
// s.logger.Error("Failed to find user for token refresh", "userID", userID, "error", err)
// return nil, fmt.Errorf("user not found")
// }
// newSessionID := fmt.Sprintf("%d-%s-%d", userID, req.GameID, time.Now().UnixNano())
// token, err := jwtutil.CreatePopOKJwt(
// userID,
// user.FirstName,
// req.Currency,
// "en",
// req.Mode,
// newSessionID,
// s.config.PopOK.SecretKey,
// 24*time.Hour,
// )
// if err != nil {
// s.logger.Error("Failed to generate new token", "userID", userID, "error", err)
// return nil, fmt.Errorf("token generation failed")
// }
// return &domain.PopOKGenerateTokenResponse{
// NewToken: token,
// }, nil
// }
func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error) { func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error) {
// 1. Validate token and get user ID // 1. Validate token and get user ID
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey) claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)

View File

@ -12,6 +12,7 @@ import (
"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/currency"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
"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"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
@ -36,6 +37,7 @@ import (
) )
type App struct { type App struct {
instSvc *institutions.Service
currSvc *currency.Service currSvc *currency.Service
fiber *fiber.App fiber *fiber.App
aleaVirtualGameService alea.AleaVirtualGameService aleaVirtualGameService alea.AleaVirtualGameService
@ -68,6 +70,7 @@ type App struct {
} }
func NewApp( func NewApp(
instSvc *institutions.Service,
currSvc *currency.Service, currSvc *currency.Service,
port int, validator *customvalidator.CustomValidator, port int, validator *customvalidator.CustomValidator,
authSvc *authentication.Service, authSvc *authentication.Service,
@ -110,6 +113,7 @@ func NewApp(
})) }))
s := &App{ s := &App{
instSvc: instSvc,
currSvc: currSvc, currSvc: currSvc,
fiber: app, fiber: app,
port: port, port: port,

View File

@ -22,7 +22,7 @@ import (
// @Success 200 {object} domain.BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [post] // @Router /sport/bet [post]
func (h *Handler) CreateBet(c *fiber.Ctx) error { func (h *Handler) CreateBet(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64) userID := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role) role := c.Locals("role").(domain.Role)
@ -82,7 +82,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
// @Success 200 {object} domain.BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /random/bet [post] // @Router /sport/random/bet [post]
func (h *Handler) RandomBet(c *fiber.Ctx) error { func (h *Handler) RandomBet(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64) userID := c.Locals("user_id").(int64)
@ -207,7 +207,7 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
// @Success 200 {array} domain.BetRes // @Success 200 {array} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [get] // @Router /sport/bet [get]
func (h *Handler) GetAllBet(c *fiber.Ctx) error { func (h *Handler) GetAllBet(c *fiber.Ctx) error {
companyID := c.Locals("company_id").(domain.ValidInt64) companyID := c.Locals("company_id").(domain.ValidInt64)
branchID := c.Locals("branch_id").(domain.ValidInt64) branchID := c.Locals("branch_id").(domain.ValidInt64)
@ -268,7 +268,7 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error {
// @Success 200 {object} domain.BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [get] // @Router /sport/bet/{id} [get]
func (h *Handler) GetBetByID(c *fiber.Ctx) error { func (h *Handler) GetBetByID(c *fiber.Ctx) error {
betID := c.Params("id") betID := c.Params("id")
id, err := strconv.ParseInt(betID, 10, 64) id, err := strconv.ParseInt(betID, 10, 64)
@ -314,7 +314,7 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error {
// @Success 200 {object} domain.BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/cashout/{id} [get] // @Router /sport/bet/cashout/{id} [get]
func (h *Handler) GetBetByCashoutID(c *fiber.Ctx) error { func (h *Handler) GetBetByCashoutID(c *fiber.Ctx) error {
cashoutID := c.Params("id") cashoutID := c.Params("id")
@ -355,7 +355,7 @@ type UpdateCashOutReq struct {
// @Success 200 {object} response.APIResponse // @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [patch] // @Router /sport/bet/{id} [patch]
func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { func (h *Handler) UpdateCashOut(c *fiber.Ctx) error {
type UpdateCashOutReq struct { type UpdateCashOutReq struct {
CashedOut bool `json:"cashed_out" validate:"required" example:"true"` CashedOut bool `json:"cashed_out" validate:"required" example:"true"`
@ -418,7 +418,7 @@ func (h *Handler) UpdateCashOut(c *fiber.Ctx) error {
// @Success 200 {object} response.APIResponse // @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [delete] // @Router /sport/bet/{id} [delete]
func (h *Handler) DeleteBet(c *fiber.Ctx) error { func (h *Handler) DeleteBet(c *fiber.Ctx) error {
betID := c.Params("id") betID := c.Params("id")
id, err := strconv.ParseInt(betID, 10, 64) id, err := strconv.ParseInt(betID, 10, 64)

View File

@ -32,7 +32,7 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
var req domain.ChapaDepositRequestPayload var req domain.ChapaDepositRequestPayload
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
fmt.Sprintln("We first first are here init Chapa payment") // fmt.Println("We first first are here init Chapa payment")
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: err.Error(), Error: err.Error(),
Message: "Failed to parse request body", Message: "Failed to parse request body",
@ -41,7 +41,7 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
amount := domain.Currency(req.Amount * 100) amount := domain.Currency(req.Amount * 100)
fmt.Sprintln("We are here init Chapa payment") fmt.Println("We are here init Chapa payment")
checkoutURL, err := h.chapaSvc.InitiateDeposit(c.Context(), userID, amount) checkoutURL, err := h.chapaSvc.InitiateDeposit(c.Context(), userID, amount)
if err != nil { if err != nil {
@ -79,7 +79,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
} }
switch chapaTransactionType.Type { switch chapaTransactionType.Type {
case h.Cfg.CHAPA_TRANSFER_TYPE: case h.Cfg.CHAPA_PAYMENT_TYPE:
chapaTransferVerificationRequest := new(domain.ChapaWebHookTransfer) chapaTransferVerificationRequest := new(domain.ChapaWebHookTransfer)
if err := c.BodyParser(chapaTransferVerificationRequest); err != nil { if err := c.BodyParser(chapaTransferVerificationRequest); err != nil {
@ -100,7 +100,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
Data: chapaTransferVerificationRequest, Data: chapaTransferVerificationRequest,
Success: true, Success: true,
}) })
case h.Cfg.CHAPA_PAYMENT_TYPE: case h.Cfg.CHAPA_TRANSFER_TYPE:
chapaPaymentVerificationRequest := new(domain.ChapaWebHookPayment) chapaPaymentVerificationRequest := new(domain.ChapaWebHookPayment)
if err := c.BodyParser(chapaPaymentVerificationRequest); err != nil { if err := c.BodyParser(chapaPaymentVerificationRequest); err != nil {
return domain.UnProcessableEntityResponse(c) return domain.UnProcessableEntityResponse(c)
@ -147,7 +147,7 @@ func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error {
}) })
} }
verification, err := h.chapaSvc.ManualVerifTransaction(c.Context(), txRef) verification, err := h.chapaSvc.ManuallyVerify(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",

View File

@ -11,6 +11,7 @@ import (
"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/currency"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
"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"
@ -30,6 +31,7 @@ import (
) )
type Handler struct { type Handler struct {
instSvc *institutions.Service
currSvc *currency.Service currSvc *currency.Service
logger *slog.Logger logger *slog.Logger
notificationSvc *notificationservice.Service notificationSvc *notificationservice.Service
@ -59,6 +61,7 @@ type Handler struct {
} }
func New( func New(
instSvc *institutions.Service,
currSvc *currency.Service, currSvc *currency.Service,
logger *slog.Logger, logger *slog.Logger,
notificationSvc *notificationservice.Service, notificationSvc *notificationservice.Service,
@ -87,6 +90,7 @@ func New(
mongoLoggerSvc *zap.Logger, mongoLoggerSvc *zap.Logger,
) *Handler { ) *Handler {
return &Handler{ return &Handler{
instSvc: instSvc,
currSvc: currSvc, currSvc: currSvc,
logger: logger, logger: logger,
notificationSvc: notificationSvc, notificationSvc: notificationSvc,

View File

@ -0,0 +1,135 @@
package handlers
import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
)
// @Summary Create a new bank
// @Tags Institutions - Banks
// @Accept json
// @Produce json
// @Param bank body domain.Bank true "Bank Info"
// @Success 201 {object} domain.Bank
// @Failure 400 {object} domain.ErrorResponse
// @Router /api/v1/banks [post]
func (h *Handler) CreateBank(c *fiber.Ctx) error {
var bank domain.Bank
if err := c.BodyParser(&bank); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"})
}
err := h.instSvc.Create(c.Context(), &bank)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(bank)
}
// @Summary Get a bank by ID
// @Tags Institutions - Banks
// @Produce json
// @Param id path int true "Bank ID"
// @Success 200 {object} domain.Bank
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/banks/{id} [get]
func (h *Handler) GetBankByID(c *fiber.Ctx) error {
id, err := c.ParamsInt("id")
if err != nil || id <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid bank ID"})
}
bank, err := h.instSvc.GetByID(c.Context(), int64(id))
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "bank not found"})
}
return c.JSON(bank)
}
// @Summary Update a bank
// @Tags Institutions - Banks
// @Accept json
// @Produce json
// @Param id path int true "Bank ID"
// @Param bank body domain.Bank true "Bank Info"
// @Success 200 {object} domain.Bank
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/banks/{id} [put]
func (h *Handler) UpdateBank(c *fiber.Ctx) error {
id, err := c.ParamsInt("id")
if err != nil || id <= 0 {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid bank ID"})
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to update bank",
Error: err.Error(),
})
}
var bank domain.Bank
if err := c.BodyParser(&bank); err != nil {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"})
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to update bank",
Error: err.Error(),
})
}
bank.ID = id
err = h.instSvc.Update(c.Context(), &bank)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update bank",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Bank updated successfully",
StatusCode: fiber.StatusOK,
Success: true,
Data: bank,
})
// return c.JSON(bank)
}
// @Summary Delete a bank
// @Tags Institutions - Banks
// @Produce json
// @Param id path int true "Bank ID"
// @Success 204 {string} string "Deleted successfully"
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/banks/{id} [delete]
func (h *Handler) DeleteBank(c *fiber.Ctx) error {
id, err := c.ParamsInt("id")
if err != nil || id <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid bank ID"})
}
err = h.instSvc.Delete(c.Context(), int64(id))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.SendStatus(fiber.StatusNoContent)
}
// @Summary List all banks
// @Tags Institutions - Banks
// @Produce json
// @Success 200 {array} domain.Bank
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/banks [get]
func (h *Handler) ListBanks(c *fiber.Ctx) error {
banks, err := h.instSvc.List(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(banks)
}

View File

@ -1,9 +1,5 @@
package handlers package handlers
import (
"github.com/gofiber/fiber/v2"
)
// @Summary Get virtual game recommendations // @Summary Get virtual game recommendations
// @Description Returns a list of recommended virtual games for a specific user // @Description Returns a list of recommended virtual games for a specific user
// @Tags Recommendations // @Tags Recommendations
@ -13,14 +9,15 @@ import (
// @Success 200 {object} domain.RecommendationSuccessfulResponse "Recommended games fetched successfully" // @Success 200 {object} domain.RecommendationSuccessfulResponse "Recommended games fetched successfully"
// @Failure 500 {object} domain.RecommendationErrorResponse "Failed to fetch recommendations" // @Failure 500 {object} domain.RecommendationErrorResponse "Failed to fetch recommendations"
// @Router /api/v1/virtual-games/recommendations/{userID} [get] // @Router /api/v1/virtual-games/recommendations/{userID} [get]
func (h *Handler) GetRecommendations(c *fiber.Ctx) error {
userID := c.Params("userID") // or from JWT // func (h *Handler) GetRecommendations(c *fiber.Ctx) error {
recommendations, err := h.recommendationSvc.GetRecommendations(c.Context(), userID) // userID := c.Params("userID") // or from JWT
if err != nil { // recommendations, err := h.recommendationSvc.GetRecommendations(c.Context(), userID)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch recommendations") // if err != nil {
} // return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch recommendations")
return c.JSON(fiber.Map{ // }
"message": "Recommended games fetched successfully", // return c.JSON(fiber.Map{
"recommended_games": recommendations, // "message": "Recommended games fetched successfully",
}) // "recommended_games": recommendations,
} // })
// }

View File

@ -11,17 +11,19 @@ import (
) )
type TransferWalletRes struct { type TransferWalletRes struct {
ID int64 `json:"id" example:"1"` ID int64 `json:"id"`
Amount float32 `json:"amount" example:"100.0"` Amount float32 `json:"amount"`
Verified bool `json:"verified" example:"true"` Verified bool `json:"verified"`
Type string `json:"type" example:"transfer"` Type string `json:"type"`
PaymentMethod string `json:"payment_method" example:"bank"` PaymentMethod string `json:"payment_method"`
ReceiverWalletID *int64 `json:"receiver_wallet_id" example:"1"` ReceiverWalletID *int64 `json:"receiver_wallet_id,omitempty"`
SenderWalletID *int64 `json:"sender_wallet_id" example:"1"` SenderWalletID *int64 `json:"sender_wallet_id,omitempty"`
CashierID *int64 `json:"cashier_id" example:"789"` CashierID *int64 `json:"cashier_id,omitempty"`
CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"` ReferenceNumber string `json:"reference_number"` // ← Add this
UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
type RefillRes struct { type RefillRes struct {
ID int64 `json:"id" example:"1"` ID int64 `json:"id" example:"1"`
Amount float32 `json:"amount" example:"100.0"` Amount float32 `json:"amount" example:"100.0"`
@ -35,33 +37,34 @@ type RefillRes struct {
UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"` UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"`
} }
func convertTransfer(transfer domain.Transfer) TransferWalletRes { func convertTransfer(t domain.Transfer) TransferWalletRes {
var receiverID *int64
if t.ReceiverWalletID.Valid {
receiverID = &t.ReceiverWalletID.Value
}
var senderID *int64
if t.SenderWalletID.Valid {
senderID = &t.SenderWalletID.Value
}
var cashierID *int64 var cashierID *int64
if transfer.CashierID.Valid { if t.CashierID.Valid {
cashierID = &transfer.CashierID.Value cashierID = &t.CashierID.Value
}
var receiverID *int64
if transfer.ReceiverWalletID.Valid {
receiverID = &transfer.ReceiverWalletID.Value
}
var senderId *int64
if transfer.SenderWalletID.Valid {
senderId = &transfer.SenderWalletID.Value
} }
return TransferWalletRes{ return TransferWalletRes{
ID: transfer.ID, ID: t.ID,
Amount: transfer.Amount.Float32(), Amount: float32(t.Amount),
Verified: transfer.Verified, Verified: t.Verified,
Type: string(transfer.Type), Type: string(t.Type),
PaymentMethod: string(transfer.PaymentMethod), PaymentMethod: string(t.PaymentMethod),
ReceiverWalletID: receiverID, ReceiverWalletID: receiverID,
SenderWalletID: senderId, SenderWalletID: senderID,
CashierID: cashierID, CashierID: cashierID,
CreatedAt: transfer.CreatedAt, ReferenceNumber: t.ReferenceNumber,
UpdatedAt: transfer.UpdatedAt, CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
} }
} }
@ -142,10 +145,11 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error {
var senderID int64 var senderID int64
//TODO: check to make sure that the cashiers aren't transferring TO branch wallet //TODO: check to make sure that the cashiers aren't transferring TO branch wallet
if role == domain.RoleCustomer { switch role {
case domain.RoleCustomer:
h.logger.Error("Unauthorized access", "userID", userID, "role", role) h.logger.Error("Unauthorized access", "userID", userID, "role", role)
return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil)
} else if role == domain.RoleBranchManager || role == domain.RoleAdmin || role == domain.RoleSuperAdmin { case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin:
company, err := h.companySvc.GetCompanyByID(c.Context(), companyID.Value) company, err := h.companySvc.GetCompanyByID(c.Context(), companyID.Value)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
@ -156,7 +160,7 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error {
} }
senderID = company.WalletID senderID = company.WalletID
h.logger.Error("Will", "userID", userID, "role", role) h.logger.Error("Will", "userID", userID, "role", role)
} else { default:
cashierBranch, err := h.branchSvc.GetBranchByCashier(c.Context(), userID) cashierBranch, err := h.branchSvc.GetBranchByCashier(c.Context(), userID)
if err != nil { if err != nil {
h.logger.Error("Failed to get branch", "user ID", userID, "error", err) h.logger.Error("Failed to get branch", "user ID", userID, "error", err)

View File

@ -199,3 +199,45 @@ func (h *Handler) RecommendGames(c *fiber.Ctx) error {
return c.JSON(recommendations) return c.JSON(recommendations)
} }
func (h *Handler) HandleTournamentWin(c *fiber.Ctx) error {
var req domain.PopOKWinRequest
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Invalid tournament win request body", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request body",
})
}
resp, err := h.virtualGameSvc.ProcessTournamentWin(c.Context(), &req)
if err != nil {
h.logger.Error("Failed to process tournament win", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.JSON(resp)
}
func (h *Handler) HandlePromoWin(c *fiber.Ctx) error {
var req domain.PopOKWinRequest
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Invalid promo win request body", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request body",
})
}
resp, err := h.virtualGameSvc.ProcessPromoWin(c.Context(), &req)
if err != nil {
h.logger.Error("Failed to process promo win", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.JSON(resp)
}

View File

@ -20,6 +20,7 @@ import (
func (a *App) initAppRoutes() { func (a *App) initAppRoutes() {
h := handlers.New( h := handlers.New(
a.instSvc,
a.currSvc, a.currSvc,
a.logger, a.logger,
a.NotidicationStore, a.NotidicationStore,
@ -187,7 +188,7 @@ func (a *App) initAppRoutes() {
a.fiber.Patch("/sport/bet/:id", a.authMiddleware, h.UpdateCashOut) a.fiber.Patch("/sport/bet/:id", a.authMiddleware, h.UpdateCashOut)
a.fiber.Delete("/sport/bet/:id", a.authMiddleware, h.DeleteBet) a.fiber.Delete("/sport/bet/:id", a.authMiddleware, h.DeleteBet)
a.fiber.Post("/random/bet", a.authMiddleware, h.RandomBet) a.fiber.Post("/sport/random/bet", a.authMiddleware, h.RandomBet)
// Wallet // Wallet
a.fiber.Get("/wallet", h.GetAllWallets) a.fiber.Get("/wallet", h.GetAllWallets)
@ -274,6 +275,8 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/bet", h.HandleBet) a.fiber.Post("/bet", h.HandleBet)
a.fiber.Post("/win", h.HandleWin) a.fiber.Post("/win", h.HandleWin)
a.fiber.Post("/cancel", h.HandleCancel) a.fiber.Post("/cancel", h.HandleCancel)
a.fiber.Post("/promoWin ", h.HandlePromoWin)
a.fiber.Post("/tournamentWin ", h.HandleTournamentWin)
a.fiber.Get("/popok/games", h.GetGameList) a.fiber.Get("/popok/games", h.GetGameList)
a.fiber.Get("/popok/games/recommend", a.authMiddleware, h.RecommendGames) a.fiber.Get("/popok/games/recommend", a.authMiddleware, h.RecommendGames)