chapa minor fixes

This commit is contained in:
Yared Yemane 2025-11-03 17:20:35 +03:00
parent 1e39d75239
commit 46d70d7c8c
19 changed files with 1069 additions and 318 deletions

View File

@ -253,6 +253,7 @@ CREATE TABLE IF NOT EXISTS wallet_transfer (
cashier_id BIGINT,
verified BOOLEAN DEFAULT false,
reference_number VARCHAR(255) NOT NULL,
ext_reference_number VARCHAR(255),
session_id VARCHAR(255),
status VARCHAR(255),
payment_method VARCHAR(255),

View File

@ -448,6 +448,46 @@ SELECT *
FROM enetpulse_preodds_bettingoffers
ORDER BY created_at DESC;
-- name: GetAllEnetpulsePreoddsWithBettingOffers :many
SELECT
p.id AS preodds_db_id,
p.preodds_id,
p.event_fk,
p.outcome_type_fk,
p.outcome_scope_fk,
p.outcome_subtype_fk,
p.event_participant_number,
p.iparam,
p.iparam2,
p.dparam,
p.dparam2,
p.sparam,
p.updates_count AS preodds_updates_count,
p.last_updated_at AS preodds_last_updated_at,
p.created_at AS preodds_created_at,
p.updated_at AS preodds_updated_at,
-- Betting offer fields
bo.id AS bettingoffer_db_id,
bo.bettingoffer_id,
bo.preodds_fk, -- ✅ ensure alias matches struct field
bo.bettingoffer_status_fk,
bo.odds_provider_fk,
bo.odds,
bo.odds_old,
bo.active,
bo.coupon_key,
bo.updates_count AS bettingoffer_updates_count,
bo.last_updated_at AS bettingoffer_last_updated_at,
bo.created_at AS bettingoffer_created_at,
bo.updated_at AS bettingoffer_updated_at
FROM enetpulse_preodds p
LEFT JOIN enetpulse_preodds_bettingoffers bo
ON bo.preodds_fk = p.preodds_id
ORDER BY p.created_at DESC, bo.created_at DESC;
-- name: GetFixturesWithPreodds :many
SELECT
f.fixture_id AS id,

View File

@ -8,11 +8,12 @@ INSERT INTO wallet_transfer (
cashier_id,
verified,
reference_number,
ext_reference_number,
session_id,
status,
payment_method
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING *;
-- name: GetAllTransfers :many
SELECT *

View File

@ -1081,6 +1081,128 @@ func (q *Queries) GetAllEnetpulsePreoddsBettingOffers(ctx context.Context) ([]En
return items, nil
}
const GetAllEnetpulsePreoddsWithBettingOffers = `-- name: GetAllEnetpulsePreoddsWithBettingOffers :many
SELECT
p.id AS preodds_db_id,
p.preodds_id,
p.event_fk,
p.outcome_type_fk,
p.outcome_scope_fk,
p.outcome_subtype_fk,
p.event_participant_number,
p.iparam,
p.iparam2,
p.dparam,
p.dparam2,
p.sparam,
p.updates_count AS preodds_updates_count,
p.last_updated_at AS preodds_last_updated_at,
p.created_at AS preodds_created_at,
p.updated_at AS preodds_updated_at,
-- Betting offer fields
bo.id AS bettingoffer_db_id,
bo.bettingoffer_id,
bo.preodds_fk, -- ensure alias matches struct field
bo.bettingoffer_status_fk,
bo.odds_provider_fk,
bo.odds,
bo.odds_old,
bo.active,
bo.coupon_key,
bo.updates_count AS bettingoffer_updates_count,
bo.last_updated_at AS bettingoffer_last_updated_at,
bo.created_at AS bettingoffer_created_at,
bo.updated_at AS bettingoffer_updated_at
FROM enetpulse_preodds p
LEFT JOIN enetpulse_preodds_bettingoffers bo
ON bo.preodds_fk = p.preodds_id
ORDER BY p.created_at DESC, bo.created_at DESC
`
type GetAllEnetpulsePreoddsWithBettingOffersRow struct {
PreoddsDbID int64 `json:"preodds_db_id"`
PreoddsID string `json:"preodds_id"`
EventFk int64 `json:"event_fk"`
OutcomeTypeFk pgtype.Int4 `json:"outcome_type_fk"`
OutcomeScopeFk pgtype.Int4 `json:"outcome_scope_fk"`
OutcomeSubtypeFk pgtype.Int4 `json:"outcome_subtype_fk"`
EventParticipantNumber pgtype.Int4 `json:"event_participant_number"`
Iparam pgtype.Text `json:"iparam"`
Iparam2 pgtype.Text `json:"iparam2"`
Dparam pgtype.Text `json:"dparam"`
Dparam2 pgtype.Text `json:"dparam2"`
Sparam pgtype.Text `json:"sparam"`
PreoddsUpdatesCount pgtype.Int4 `json:"preodds_updates_count"`
PreoddsLastUpdatedAt pgtype.Timestamptz `json:"preodds_last_updated_at"`
PreoddsCreatedAt pgtype.Timestamptz `json:"preodds_created_at"`
PreoddsUpdatedAt pgtype.Timestamptz `json:"preodds_updated_at"`
BettingofferDbID pgtype.Int8 `json:"bettingoffer_db_id"`
BettingofferID pgtype.Text `json:"bettingoffer_id"`
PreoddsFk pgtype.Text `json:"preodds_fk"`
BettingofferStatusFk pgtype.Int4 `json:"bettingoffer_status_fk"`
OddsProviderFk pgtype.Int4 `json:"odds_provider_fk"`
Odds pgtype.Numeric `json:"odds"`
OddsOld pgtype.Numeric `json:"odds_old"`
Active pgtype.Bool `json:"active"`
CouponKey pgtype.Text `json:"coupon_key"`
BettingofferUpdatesCount pgtype.Int4 `json:"bettingoffer_updates_count"`
BettingofferLastUpdatedAt pgtype.Timestamptz `json:"bettingoffer_last_updated_at"`
BettingofferCreatedAt pgtype.Timestamptz `json:"bettingoffer_created_at"`
BettingofferUpdatedAt pgtype.Timestamptz `json:"bettingoffer_updated_at"`
}
func (q *Queries) GetAllEnetpulsePreoddsWithBettingOffers(ctx context.Context) ([]GetAllEnetpulsePreoddsWithBettingOffersRow, error) {
rows, err := q.db.Query(ctx, GetAllEnetpulsePreoddsWithBettingOffers)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllEnetpulsePreoddsWithBettingOffersRow
for rows.Next() {
var i GetAllEnetpulsePreoddsWithBettingOffersRow
if err := rows.Scan(
&i.PreoddsDbID,
&i.PreoddsID,
&i.EventFk,
&i.OutcomeTypeFk,
&i.OutcomeScopeFk,
&i.OutcomeSubtypeFk,
&i.EventParticipantNumber,
&i.Iparam,
&i.Iparam2,
&i.Dparam,
&i.Dparam2,
&i.Sparam,
&i.PreoddsUpdatesCount,
&i.PreoddsLastUpdatedAt,
&i.PreoddsCreatedAt,
&i.PreoddsUpdatedAt,
&i.BettingofferDbID,
&i.BettingofferID,
&i.PreoddsFk,
&i.BettingofferStatusFk,
&i.OddsProviderFk,
&i.Odds,
&i.OddsOld,
&i.Active,
&i.CouponKey,
&i.BettingofferUpdatesCount,
&i.BettingofferLastUpdatedAt,
&i.BettingofferCreatedAt,
&i.BettingofferUpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetAllEnetpulseResults = `-- name: GetAllEnetpulseResults :many
SELECT id, result_id, name, sport_fk, tournament_fk, tournament_template_fk, tournament_name, tournament_template_name, sport_name, start_date, status_type, status_desc_fk, round_type_fk, updates_count, last_updated_at, round, live, venue_name, livestats_plus, livestats_type, commentary, lineup_confirmed, verified, spectators, game_started, first_half_ended, second_half_started, second_half_ended, game_ended, created_at, updated_at
FROM enetpulse_results

View File

@ -1147,6 +1147,7 @@ type WalletTransfer struct {
CashierID pgtype.Int8 `json:"cashier_id"`
Verified pgtype.Bool `json:"verified"`
ReferenceNumber string `json:"reference_number"`
ExtReferenceNumber pgtype.Text `json:"ext_reference_number"`
SessionID pgtype.Text `json:"session_id"`
Status pgtype.Text `json:"status"`
PaymentMethod pgtype.Text `json:"payment_method"`
@ -1164,6 +1165,7 @@ type WalletTransferDetail struct {
CashierID pgtype.Int8 `json:"cashier_id"`
Verified pgtype.Bool `json:"verified"`
ReferenceNumber string `json:"reference_number"`
ExtReferenceNumber pgtype.Text `json:"ext_reference_number"`
SessionID pgtype.Text `json:"session_id"`
Status pgtype.Text `json:"status"`
PaymentMethod pgtype.Text `json:"payment_method"`

View File

@ -21,12 +21,13 @@ INSERT INTO wallet_transfer (
cashier_id,
verified,
reference_number,
ext_reference_number,
session_id,
status,
payment_method
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at
`
type CreateTransferParams struct {
@ -38,6 +39,7 @@ type CreateTransferParams struct {
CashierID pgtype.Int8 `json:"cashier_id"`
Verified pgtype.Bool `json:"verified"`
ReferenceNumber string `json:"reference_number"`
ExtReferenceNumber pgtype.Text `json:"ext_reference_number"`
SessionID pgtype.Text `json:"session_id"`
Status pgtype.Text `json:"status"`
PaymentMethod pgtype.Text `json:"payment_method"`
@ -53,6 +55,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
arg.CashierID,
arg.Verified,
arg.ReferenceNumber,
arg.ExtReferenceNumber,
arg.SessionID,
arg.Status,
arg.PaymentMethod,
@ -68,6 +71,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
&i.CashierID,
&i.Verified,
&i.ReferenceNumber,
&i.ExtReferenceNumber,
&i.SessionID,
&i.Status,
&i.PaymentMethod,
@ -78,7 +82,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
}
const GetAllTransfers = `-- name: GetAllTransfers :many
SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number
SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number
FROM wallet_transfer_details
`
@ -101,6 +105,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransferDetail,
&i.CashierID,
&i.Verified,
&i.ReferenceNumber,
&i.ExtReferenceNumber,
&i.SessionID,
&i.Status,
&i.PaymentMethod,
@ -121,7 +126,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransferDetail,
}
const GetTransferByID = `-- name: GetTransferByID :one
SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number
SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number
FROM wallet_transfer_details
WHERE id = $1
`
@ -139,6 +144,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer
&i.CashierID,
&i.Verified,
&i.ReferenceNumber,
&i.ExtReferenceNumber,
&i.SessionID,
&i.Status,
&i.PaymentMethod,
@ -152,7 +158,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer
}
const GetTransferByReference = `-- name: GetTransferByReference :one
SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number
SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number
FROM wallet_transfer_details
WHERE reference_number = $1
`
@ -170,6 +176,7 @@ func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber st
&i.CashierID,
&i.Verified,
&i.ReferenceNumber,
&i.ExtReferenceNumber,
&i.SessionID,
&i.Status,
&i.PaymentMethod,
@ -217,7 +224,7 @@ func (q *Queries) GetTransferStats(ctx context.Context, senderWalletID pgtype.In
}
const GetTransfersByWallet = `-- name: GetTransfersByWallet :many
SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number
SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number
FROM wallet_transfer_details
WHERE receiver_wallet_id = $1
OR sender_wallet_id = $1
@ -242,6 +249,7 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID pgt
&i.CashierID,
&i.Verified,
&i.ReferenceNumber,
&i.ExtReferenceNumber,
&i.SessionID,
&i.Status,
&i.PaymentMethod,

View File

@ -144,6 +144,7 @@ type Config struct {
CHAPA_ENCRYPTION_KEY string
CHAPA_CALLBACK_URL string
CHAPA_RETURN_URL string
CHAPA_RECEIPT_URL string
Bet365Token string
EnetPulseConfig EnetPulseConfig
PopOK domain.PopOKConfig
@ -262,6 +263,7 @@ func (c *Config) loadEnv() error {
c.CHAPA_PUBLIC_KEY = os.Getenv("CHAPA_PUBLIC_KEY")
c.CHAPA_ENCRYPTION_KEY = os.Getenv("CHAPA_ENCRYPTION_KEY")
c.CHAPA_BASE_URL = os.Getenv("CHAPA_BASE_URL")
c.CHAPA_RECEIPT_URL = os.Getenv("CHAPA_RECEIPT_URL")
if c.CHAPA_BASE_URL == "" {
c.CHAPA_BASE_URL = "https://api.chapa.co/v1"
}

View File

@ -33,7 +33,7 @@ const (
PaymentStatusFailed PaymentStatus = "failed"
)
type ChapaDepositRequest struct {
type ChapaInitDepositRequest struct {
Amount Currency `json:"amount"`
Currency string `json:"currency"`
Email string `json:"email"`
@ -42,6 +42,8 @@ type ChapaDepositRequest struct {
TxRef string `json:"tx_ref"`
CallbackURL string `json:"callback_url"`
ReturnURL string `json:"return_url"`
PhoneNumber string `json:"phone_number"`
// PhoneNumber string `json:"phone_number"`
}
type ChapaDepositRequestPayload struct {
@ -49,9 +51,15 @@ type ChapaDepositRequestPayload struct {
}
type ChapaWebhookPayload struct {
TxRef string `json:"tx_ref"`
TxRef string `json:"trx_ref"`
Amount Currency `json:"amount"`
Currency string `json:"currency"`
// Currency string `json:"currency"`
Status PaymentStatus `json:"status"`
}
type ChapaPaymentWebhookRequest struct {
TxRef string `json:"trx_ref"`
RefId string `json:"ref_id"`
Status PaymentStatus `json:"status"`
}
@ -69,10 +77,91 @@ type ChapaDepositVerification struct {
}
type ChapaVerificationResponse struct {
Message string `json:"message"`
Status string `json:"status"`
Amount float64 `json:"amount"`
Data struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Currency string `json:"currency"`
Amount float64 `json:"amount"`
Charge float64 `json:"charge"`
Mode string `json:"mode"`
Method string `json:"method"`
Type string `json:"type"`
Status string `json:"status"`
Reference string `json:"reference"`
TxRef string `json:"tx_ref"`
Customization struct {
Title string `json:"title"`
Description string `json:"description"`
Logo interface{} `json:"logo"`
} `json:"customization"`
Meta interface{} `json:"meta"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} `json:"data"`
}
type ChapaAllTransactionsResponse struct {
Message string `json:"message"`
Status string `json:"status"`
Data struct {
Transactions []struct {
Status string `json:"status"`
RefID string `json:"ref_id"`
Type string `json:"type"`
CreatedAt string `json:"created_at"`
Currency string `json:"currency"`
Amount string `json:"amount"`
Charge string `json:"charge"`
TransID *string `json:"trans_id"`
PaymentMethod string `json:"payment_method"`
Customer struct {
ID int64 `json:"id"`
Email *string `json:"email"`
FirstName *string `json:"first_name"`
LastName *string `json:"last_name"`
Mobile *string `json:"mobile"`
} `json:"customer"`
} `json:"transactions"`
Pagination struct {
PerPage int `json:"per_page"`
CurrentPage int `json:"current_page"`
FirstPageURL string `json:"first_page_url"`
NextPageURL *string `json:"next_page_url"`
PrevPageURL *string `json:"prev_page_url"`
} `json:"pagination"`
} `json:"data"`
}
type ChapaTransactionEvent struct {
Item int64 `json:"item"`
Message string `json:"message"`
Type string `json:"type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ChapaTransaction struct {
Status string `json:"status"`
RefID string `json:"ref_id"`
Type string `json:"type"`
CreatedAt string `json:"created_at"`
Currency string `json:"currency"`
Amount string `json:"amount"`
Charge string `json:"charge"`
TransID *string `json:"trans_id"`
PaymentMethod string `json:"payment_method"`
Customer ChapaCustomer `json:"customer"`
}
type ChapaCustomer struct {
ID int64 `json:"id"`
Email *string `json:"email"`
FirstName *string `json:"first_name"`
LastName *string `json:"last_name"`
Mobile *string `json:"mobile"`
}
// type Bank struct {
@ -221,3 +310,13 @@ type SwapResponse struct {
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type ChapaCancelResponse struct {
Message string `json:"message"`
Status string `json:"status"`
TxRef string `json:"tx_ref"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}

View File

@ -703,6 +703,7 @@ type EnetpulsePreodds struct {
LastUpdatedAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
BettingOffers []EnetpulsePreoddsBettingOffer
}
type EnetpulseResultParticipant struct {

View File

@ -302,6 +302,71 @@ func (s *Store) GetAllEnetpulsePreoddsBettingOffers(ctx context.Context) ([]doma
return offers, nil
}
func (s *Store) GetAllEnetpulsePreoddsWithBettingOffers(ctx context.Context) ([]domain.EnetpulsePreodds, error) {
rows, err := s.queries.GetAllEnetpulsePreoddsWithBettingOffers(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch preodds with betting offers: %w", err)
}
// Map for grouping betting offers under each Preodd
preoddsMap := make(map[string]*domain.EnetpulsePreodds)
for _, row := range rows {
pid := row.PreoddsID
preodd, exists := preoddsMap[pid]
if !exists {
// Create the base Preodd entry
preodd = &domain.EnetpulsePreodds{
ID: row.PreoddsDbID,
PreoddsID: row.PreoddsID,
EventFK: row.EventFk,
OutcomeTypeFK: row.OutcomeTypeFk.Int32,
OutcomeScopeFK: row.OutcomeScopeFk.Int32,
OutcomeSubtypeFK: row.OutcomeSubtypeFk.Int32,
EventParticipantNumber: row.EventParticipantNumber.Int32,
IParam: row.Iparam.String,
IParam2: row.Iparam2.String,
DParam: row.Dparam.String,
DParam2: row.Dparam2.String,
SParam: row.Sparam.String,
UpdatesCount: row.PreoddsUpdatesCount.Int32,
LastUpdatedAt: row.PreoddsLastUpdatedAt.Time,
CreatedAt: row.PreoddsCreatedAt.Time,
UpdatedAt: row.PreoddsUpdatedAt.Time,
BettingOffers: []domain.EnetpulsePreoddsBettingOffer{},
}
preoddsMap[pid] = preodd
}
// Append BettingOffer only if exists
if row.BettingofferID.Valid && row.BettingofferID.String != "" {
offer := domain.EnetpulsePreoddsBettingOffer{
ID: row.BettingofferDbID.Int64,
BettingOfferID: row.BettingofferID.String,
BettingOfferStatusFK: row.BettingofferStatusFk.Int32,
OddsProviderFK: row.OddsProviderFk.Int32,
Odds: float64(row.Odds.Exp),
OddsOld: float64(row.OddsOld.Exp),
Active: fmt.Sprintf("%v", row.Active),
CouponKey: row.CouponKey.String,
UpdatesCount: int(row.BettingofferUpdatesCount.Int32),
LastUpdatedAt: row.BettingofferLastUpdatedAt.Time,
CreatedAt: row.BettingofferCreatedAt.Time,
UpdatedAt: row.BettingofferUpdatedAt.Time,
}
preodd.BettingOffers = append(preodd.BettingOffers, offer)
}
}
// Convert map to slice
result := make([]domain.EnetpulsePreodds, 0, len(preoddsMap))
for _, p := range preoddsMap {
result = append(result, *p)
}
return result, nil
}
func (s *Store) GetFixturesWithPreodds(ctx context.Context) ([]domain.EnetpulseFixtureWithPreodds, error) {
dbRows, err := s.queries.GetFixturesWithPreodds(ctx)
if err != nil {

View File

@ -893,6 +893,7 @@ func (s *Service) GetBetOutcomeByBetID(ctx context.Context, UserID int64) ([]dom
func (s *Service) GetBetOutcomeViewByEventID(ctx context.Context, eventID int64, filter domain.BetOutcomeViewFilter) ([]domain.BetOutcomeViewRes, int64, error) {
return s.betStore.GetBetOutcomeViewByEventID(ctx, eventID, filter)
}
func (s *Service) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) {
return s.betStore.GetBetOutcomeByEventID(ctx, eventID, is_filtered)
}

View File

@ -29,16 +29,17 @@ 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.ChapaInitDepositRequest) (domain.ChapaDepositResponse, error) {
payload := map[string]interface{}{
"amount": fmt.Sprintf("%.2f", float64(req.Amount)),
"currency": req.Currency,
// "email": req.Email,
"email": req.Email,
"first_name": req.FirstName,
"last_name": req.LastName,
"tx_ref": req.TxRef,
"callback_url": req.CallbackURL,
"return_url": req.ReturnURL,
"phone_number": req.PhoneNumber,
}
fmt.Printf("\n\nChapa Payload: %+v\n\n", payload)
@ -69,9 +70,9 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositR
return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body)) // <-- Log it
}
if resp.StatusCode != http.StatusOK {
return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
// if resp.StatusCode != http.StatusOK {
// return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
// }
var response struct {
Message string `json:"message"`
@ -133,12 +134,13 @@ func (c *Client) VerifyPayment(ctx context.Context, reference string) (domain.Ch
func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
url := fmt.Sprintf("%s/transaction/verify/%s", c.baseURL, txRef)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.secretKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
@ -147,32 +149,24 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body))
}
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 {
var verification domain.ChapaVerificationResponse
if err := json.NewDecoder(resp.Body).Decode(&verification); 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
}
// Normalize payment status for internal use
// switch strings.ToLower(verification.Data.Status) {
// case "success":
// verification.Status = string(domain.PaymentStatusCompleted)
// default:
// verification.Status = string(domain.PaymentStatusFailed)
// }
return &domain.ChapaVerificationResponse{
Status: string(status),
Amount: response.Amount,
Currency: response.Currency,
}, nil
return &verification, nil
}
func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
@ -215,11 +209,74 @@ func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domai
return &domain.ChapaVerificationResponse{
Status: string(status),
Amount: response.Amount,
Currency: response.Currency,
// Amount: response.Amount,
// Currency: response.Currency,
}, nil
}
func (c *Client) GetAllTransactions(ctx context.Context) (domain.ChapaAllTransactionsResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/transactions", nil)
if err != nil {
return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body))
}
var response domain.ChapaAllTransactionsResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("failed to decode response: %w", err)
}
return response, nil
}
func (c *Client) GetTransactionEvents(ctx context.Context, refId string) ([]domain.ChapaTransactionEvent, error) {
url := fmt.Sprintf("%s/transaction/events/%s", c.baseURL, refId)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.secretKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body))
}
var response struct {
Message string `json:"message"`
Status string `json:"status"`
Data []domain.ChapaTransactionEvent `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return response.Data, nil
}
func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil)
if err != nil {
@ -336,6 +393,62 @@ func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.
return &verification, nil
}
func (c *Client) CancelTransaction(ctx context.Context, txRef string) (domain.ChapaCancelResponse, error) {
// Construct URL for the cancel transaction endpoint
url := fmt.Sprintf("%s/transaction/cancel/%s", c.baseURL, txRef)
// Create HTTP request with context
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, url, nil)
if err != nil {
return domain.ChapaCancelResponse{}, fmt.Errorf("failed to create request: %w", err)
}
// Set authorization header
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
httpReq.Header.Set("Content-Type", "application/json")
// Execute the HTTP request
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return domain.ChapaCancelResponse{}, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
// Handle non-OK responses
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return domain.ChapaCancelResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body))
}
// Decode successful response
var response struct {
Message string `json:"message"`
Status string `json:"status"`
Data struct {
TxRef string `json:"tx_ref"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return domain.ChapaCancelResponse{}, fmt.Errorf("failed to decode response: %w", err)
}
// Return mapped domain response
return domain.ChapaCancelResponse{
Message: response.Message,
Status: response.Status,
TxRef: response.Data.TxRef,
Amount: response.Data.Amount,
Currency: response.Data.Currency,
CreatedAt: response.Data.CreatedAt,
UpdatedAt: response.Data.UpdatedAt,
}, nil
}
func (c *Client) setHeaders(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+c.secretKey)
req.Header.Set("Content-Type", "application/json")

View File

@ -15,7 +15,7 @@ import (
// }
type ChapaStore interface {
InitializePayment(request domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error)
InitializePayment(request domain.ChapaInitDepositRequest) (domain.ChapaDepositResponse, error)
ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error)
FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error)
CreateWithdrawal(userID string, amount float64, accountNumber, bankCode string) (*domain.ChapaWithdrawal, error)

View File

@ -56,22 +56,22 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
return "", fmt.Errorf("failed to get user: %w", err)
}
var senderWallet domain.Wallet
// var senderWallet domain.Wallet
// Generate unique reference
// reference := uuid.New().String()
reference := fmt.Sprintf("chapa-deposit-%d-%s", userID, uuid.New().String())
senderWallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
senderWallet, err := s.walletStore.GetCustomerWallet(ctx, userID)
if err != nil {
return "", fmt.Errorf("failed to get sender wallets: %w", err)
}
for _, wallet := range senderWallets {
if wallet.IsTransferable {
senderWallet = wallet
break
}
return "", fmt.Errorf("failed to get sender wallet: %w", err)
}
// for _, wallet := range senderWallets {
// if wallet.IsTransferable {
// senderWallet = wallet
// break
// }
// }
// Check if payment with this reference already exists
// if transfer, err := s.transferStore.GetTransferByReference(ctx, reference); err == nil {
@ -92,9 +92,16 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
Valid: true,
},
Verified: false,
Status: string(domain.STATUS_PENDING),
}
payload := domain.ChapaDepositRequest{
userPhoneNum := user.PhoneNumber[len(user.PhoneNumber)-9:]
if len(user.PhoneNumber) >= 9 {
userPhoneNum = "0" + userPhoneNum
}
payload := domain.ChapaInitDepositRequest{
Amount: amount,
Currency: "ETB",
Email: user.Email,
@ -103,6 +110,7 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
TxRef: reference,
CallbackURL: s.cfg.CHAPA_CALLBACK_URL,
ReturnURL: s.cfg.CHAPA_RETURN_URL,
PhoneNumber: userPhoneNum,
}
// Initialize payment with Chapa
@ -127,6 +135,157 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
return response.CheckoutURL, nil
}
func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaPaymentWebhookRequest) error {
// Find payment by reference
payment, err := s.transferStore.GetTransferByReference(ctx, transfer.TxRef)
if err != nil {
return domain.ErrPaymentNotFound
}
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 payment is completed, credit user's wallet
if transfer.Status == domain.PaymentStatusSuccessful {
if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
return fmt.Errorf("failed to update is payment verified value: %w", err)
}
if err := s.transferStore.UpdateTransferStatus(ctx, payment.ID, string(domain.DepositStatusCompleted)); 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{
ReferenceNumber: domain.ValidString{
Value: transfer.TxRef,
},
}, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err)
}
}
return nil
}
func (s *Service) CancelDeposit(ctx context.Context, userID int64, txRef string) (domain.ChapaCancelResponse, error) {
// Validate input
if txRef == "" {
return domain.ChapaCancelResponse{}, fmt.Errorf("transaction reference is required")
}
// Retrieve user to verify ownership / context (optional but good practice)
user, err := s.userStore.GetUserByID(ctx, userID)
if err != nil {
return domain.ChapaCancelResponse{}, fmt.Errorf("failed to get user: %w", err)
}
fmt.Printf("\n\nAttempting to cancel Chapa transaction: %s for user %s (%d)\n\n", txRef, user.Email, userID)
// Call Chapa API to cancel transaction
cancelResp, err := s.chapaClient.CancelTransaction(ctx, txRef)
if err != nil {
return domain.ChapaCancelResponse{}, fmt.Errorf("failed to cancel transaction via Chapa: %w", err)
}
// Update transfer/payment status locally
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
if err != nil {
// Log but do not block cancellation if remote succeeded
fmt.Printf("Warning: unable to find local transfer for txRef %s: %v\n", txRef, err)
} else {
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.STATUS_CANCELLED)); err != nil {
fmt.Printf("Warning: failed to update transfer status for txRef %s: %v\n", txRef, err)
}
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, false); err != nil {
fmt.Printf("Warning: failed to update transfer status for txRef %s: %v\n", txRef, err)
}
}
fmt.Printf("\n\nChapa cancellation response: %+v\n\n", cancelResp)
return cancelResp, nil
}
func (s *Service) FetchAllTransactions(ctx context.Context) ([]domain.ChapaTransaction, error) {
// Call Chapa API to get all transactions
resp, err := s.chapaClient.GetAllTransactions(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch transactions from Chapa: %w", err)
}
if resp.Status != "success" {
return nil, fmt.Errorf("chapa API returned non-success status: %s", resp.Status)
}
transactions := make([]domain.ChapaTransaction, 0, len(resp.Data.Transactions))
// Map API transactions to domain transactions
for _, t := range resp.Data.Transactions {
tx := domain.ChapaTransaction{
Status: t.Status,
RefID: t.RefID,
Type: t.Type,
CreatedAt: t.CreatedAt,
Currency: t.Currency,
Amount: t.Amount,
Charge: t.Charge,
TransID: t.TransID,
PaymentMethod: t.PaymentMethod,
Customer: domain.ChapaCustomer{
ID: t.Customer.ID,
Email: t.Customer.Email,
FirstName: t.Customer.FirstName,
LastName: t.Customer.LastName,
Mobile: t.Customer.Mobile,
},
}
transactions = append(transactions, tx)
}
return transactions, nil
}
func (s *Service) FetchTransactionEvents(ctx context.Context, refID string) ([]domain.ChapaTransactionEvent, error) {
if refID == "" {
return nil, fmt.Errorf("transaction reference ID is required")
}
// Call Chapa client to fetch transaction events
events, err := s.chapaClient.GetTransactionEvents(ctx, refID)
if err != nil {
return nil, fmt.Errorf("failed to fetch transaction events from Chapa: %w", err)
}
// Optional: Transform or filter events if needed
transformedEvents := make([]domain.ChapaTransactionEvent, 0, len(events))
for _, e := range events {
transformedEvents = append(transformedEvents, domain.ChapaTransactionEvent{
Item: e.Item,
Message: e.Message,
Type: e.Type,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
})
}
return transformedEvents, 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)
@ -213,124 +372,7 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma
return &transfer, nil
}
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) ManuallyVerify(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
// Lookup transfer by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("transfer not found for reference %s: %w", txRef, err)
}
if transfer.Verified {
return &domain.ChapaVerificationResponse{
Status: string(domain.PaymentStatusCompleted),
Amount: float64(transfer.Amount) / 100,
Currency: "ETB",
}, nil
}
// Validate sender wallet
if !transfer.SenderWalletID.Valid {
return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID)
}
var verification *domain.ChapaVerificationResponse
// Decide verification method based on type
switch strings.ToLower(string(transfer.Type)) {
case "deposit":
// Use Chapa Payment Verification
verification, err = s.chapaClient.ManualVerifyPayment(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err)
}
if verification.Status == string(domain.PaymentStatusSuccessful) {
// Mark verified
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return nil, fmt.Errorf("failed to mark deposit transfer as verified: %w", err)
}
// Credit wallet
_, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value,
transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{},
fmt.Sprintf("Added %v to wallet using Chapa", transfer.Amount.Float32()))
if 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 verification, nil
}
func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error {
// Find payment by reference
payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference)
if err != nil {
return domain.ErrPaymentNotFound
}
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 payment is completed, credit user's wallet
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{
ReferenceNumber: domain.ValidString{
Value: transfer.Reference,
},
}, fmt.Sprintf("Added %v to wallet using Chapa", 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 {
func (s *Service) ProcessVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error {
// Find payment by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference)
if err != nil {
@ -369,15 +411,111 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai
return nil
}
func (s *Service) GetPaymentReceiptURL(ctx context.Context, chapaRef string) (string, error) {
if chapaRef == "" {
return "", fmt.Errorf("chapa reference ID is required")
func (s *Service) GetPaymentReceiptURL(refId string) (string, error) {
if refId == "" {
return "", fmt.Errorf("reference ID cannot be empty")
}
receiptURL := fmt.Sprintf("https://chapa.link/payment-receipt/%s", chapaRef)
receiptURL := s.cfg.CHAPA_RECEIPT_URL + refId
return receiptURL, nil
}
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) ManuallyVerify(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
// Lookup transfer by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("transfer not found for reference %s: %w", txRef, err)
}
// If already verified, just return a completed response
if transfer.Verified {
return &domain.ChapaVerificationResponse{}, errors.New("transfer already verified")
}
// Validate sender wallet
if !transfer.SenderWalletID.Valid {
return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID)
}
var verification *domain.ChapaVerificationResponse
switch strings.ToLower(string(transfer.Type)) {
case string(domain.DEPOSIT):
// Verify Chapa payment
verification, err = s.chapaClient.ManualVerifyPayment(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err)
}
if strings.ToLower(verification.Data.Status) == "success" ||
verification.Status == string(domain.PaymentStatusCompleted) {
// Credit wallet
_, err := s.walletStore.AddToWallet(ctx,
transfer.SenderWalletID.Value,
transfer.Amount,
domain.ValidInt64{},
domain.TRANSFER_CHAPA,
domain.PaymentDetails{},
fmt.Sprintf("Added %.2f ETB to wallet using Chapa", transfer.Amount.Float32()))
if err != nil {
return nil, fmt.Errorf("failed to credit wallet: %w", err)
}
// Mark verified in DB
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return nil, fmt.Errorf("failed to mark deposit transfer as verified: %w", err)
}
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.DepositStatusCompleted)); err != nil {
return nil, fmt.Errorf("failed to update deposit transfer status: %w", err)
}
}
case string(domain.WITHDRAW):
// Verify Chapa transfer
verification, err = s.chapaClient.ManualVerifyTransfer(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err)
}
if strings.ToLower(verification.Data.Status) == "success" ||
verification.Status == string(domain.PaymentStatusCompleted) {
// Deduct wallet
_, err := s.walletStore.DeductFromWallet(ctx,
transfer.SenderWalletID.Value,
transfer.Amount,
domain.ValidInt64{},
domain.TRANSFER_CHAPA,
fmt.Sprintf("Deducted %.2f ETB from wallet using Chapa", transfer.Amount.Float32()))
if err != nil {
return nil, fmt.Errorf("failed to debit wallet: %w", err)
}
// Mark verified in DB
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return nil, fmt.Errorf("failed to mark withdraw transfer as verified: %w", err)
}
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusCompleted)); err != nil {
return nil, fmt.Errorf("failed to update withdraw transfer status: %w", err)
}
}
default:
return nil, fmt.Errorf("unsupported transfer type: %s", transfer.Type)
}
return verification, nil
}
func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.chapa.co/v1/transfers", nil)
if err != nil {

View File

@ -14,4 +14,5 @@ type EnetPulseService interface {
FetchTournamentParticipants(ctx context.Context, tournamentID string) error
FetchPreMatchOdds(ctx context.Context, params domain.PreMatchOddsRequest) (*domain.PreMatchOddsResponse, error)
FetchCountryFlag(ctx context.Context, countryFK int64) (*domain.ImageResponse, error)
GetAllPreoddsWithBettingOffers(ctx context.Context) ([]domain.EnetpulsePreodds, error)
}

View File

@ -872,6 +872,7 @@ func (s *Service) FetchAndStorePreodds(ctx context.Context) error {
for _, fixture := range fixtures {
// 4⃣ Loop through each outcome type
for _, outcome := range outcomeTypes {
url := fmt.Sprintf(
"http://eapi.enetpulse.com/preodds/event/?objectFK=%s&odds_providerFK=%s&outcome_typeFK=%s&username=%s&token=%s",
fixture.FixtureID,
@ -896,6 +897,7 @@ func (s *Service) FetchAndStorePreodds(ctx context.Context) error {
continue
}
// Struct adjusted exactly to match JSON structure
var preoddsResp struct {
Preodds map[string]struct {
ID string `json:"id"`
@ -910,17 +912,18 @@ func (s *Service) FetchAndStorePreodds(ctx context.Context) error {
Sparam string `json:"sparam"`
N string `json:"n"`
UT string `json:"ut"`
BettingOffers []struct {
PreoddsBettingOffers map[string]struct {
ID string `json:"id"`
BettingOfferStatusFK int32 `json:"bettingoffer_status_fk"`
OddsProviderFK int32 `json:"odds_provider_fk"`
Odds float64 `json:"odds"`
OddsOld float64 `json:"odds_old"`
BettingOfferStatusFK string `json:"bettingoffer_statusFK"`
OddsProviderFK string `json:"odds_providerFK"`
Odds string `json:"odds"`
OddsOld string `json:"odds_old"`
Active string `json:"active"`
CouponKey string `json:"coupon_key"`
CouponKey string `json:"couponKey"`
N string `json:"n"`
UT string `json:"ut"`
} `json:"bettingoffers"`
} `json:"preodds_bettingoffers"`
} `json:"preodds"`
}
@ -929,65 +932,53 @@ func (s *Service) FetchAndStorePreodds(ctx context.Context) error {
}
for _, p := range preoddsResp.Preodds {
updatesCount := 0
if p.N != "" {
if n, err := strconv.Atoi(p.N); err == nil {
updatesCount = n
}
}
lastUpdatedAt, _ := time.Parse(time.RFC3339, p.UT)
eventParticipantNumber := int32(0)
if p.EventParticipantNumber != "" {
if epn, err := strconv.Atoi(p.EventParticipantNumber); err == nil {
eventParticipantNumber = int32(epn)
}
}
// Convert numeric/string fields safely
updatesCount, _ := strconv.Atoi(defaultIfEmpty(p.N, "0"))
eventParticipantNumber, _ := strconv.Atoi(defaultIfEmpty(p.EventParticipantNumber, "0"))
lastUpdatedAt := parseTimeOrNow(p.UT)
createPreodds := domain.CreateEnetpulsePreodds{
PreoddsID: p.ID,
EventFK: fixture.FixtureID,
OutcomeTypeFK: outcome.OutcomeTypeID,
OutcomeScopeFK: string(p.OutcomeScopeFK),
OutcomeSubtypeFK: string(p.OutcomeSubtypeFK),
EventParticipantNumber: int(eventParticipantNumber),
OutcomeTypeFK: p.OutcomeTypeFK,
OutcomeScopeFK: p.OutcomeScopeFK,
OutcomeSubtypeFK: p.OutcomeSubtypeFK,
EventParticipantNumber: eventParticipantNumber,
IParam: p.Iparam,
IParam2: p.Iparam2,
DParam: p.Dparam,
DParam2: p.Dparam2,
SParam: p.Sparam,
UpdatesCount: int(updatesCount),
UpdatesCount: updatesCount,
LastUpdatedAt: lastUpdatedAt,
}
fmt.Printf("\n\nPreodds are:%v\n\n", createPreodds)
storedPreodds, err := s.store.CreateEnetpulsePreodds(ctx, createPreodds)
// Store preodds in DB
_, err := s.store.CreateEnetpulsePreodds(ctx, createPreodds)
if err != nil {
continue
}
for _, o := range p.BettingOffers {
bettingUpdates := 0
if o.N != "" {
if n, err := strconv.Atoi(o.N); err == nil {
bettingUpdates = n
}
}
// 5⃣ Loop through betting offers map
for _, o := range p.PreoddsBettingOffers {
bettingUpdates, _ := strconv.Atoi(defaultIfEmpty(o.N, "0"))
bettingLastUpdatedAt := parseTimeOrNow(o.UT)
bettingLastUpdatedAt, _ := time.Parse(time.RFC3339, o.UT)
odds, _ := strconv.ParseFloat(defaultIfEmpty(o.Odds, "0"), 64)
oddsOld, _ := strconv.ParseFloat(defaultIfEmpty(o.OddsOld, "0"), 64)
bettingOfferStatusFK, _ := strconv.Atoi(defaultIfEmpty(o.BettingOfferStatusFK, "0"))
oddsProviderFK, _ := strconv.Atoi(defaultIfEmpty(o.OddsProviderFK, "0"))
createOffer := domain.CreateEnetpulsePreoddsBettingOffer{
BettingOfferID: o.ID,
PreoddsFK: storedPreodds.PreoddsID,
BettingOfferStatusFK: o.BettingOfferStatusFK,
OddsProviderFK: o.OddsProviderFK,
Odds: o.Odds,
OddsOld: o.OddsOld,
PreoddsFK: createPreodds.PreoddsID,
BettingOfferStatusFK: int32(bettingOfferStatusFK),
OddsProviderFK: int32(oddsProviderFK),
Odds: odds,
OddsOld: oddsOld,
Active: o.Active,
CouponKey: o.CouponKey,
UpdatesCount: int(bettingUpdates),
UpdatesCount: bettingUpdates,
LastUpdatedAt: bettingLastUpdatedAt,
}
@ -1000,6 +991,23 @@ func (s *Service) FetchAndStorePreodds(ctx context.Context) error {
return nil
}
// Utility helpers
func defaultIfEmpty(val, def string) string {
if val == "" {
return def
}
return val
}
func parseTimeOrNow(t string) time.Time {
parsed, err := time.Parse(time.RFC3339, t)
if err != nil {
return time.Now().UTC()
}
return parsed
}
// helper function to parse string to int32 safely
func ParseStringToInt32(s string) int32 {
if s == "" {
@ -1117,6 +1125,15 @@ func (s *Service) GetAllBettingOffers(ctx context.Context) ([]domain.EnetpulsePr
return offers, nil
}
func (s *Service) GetAllPreoddsWithBettingOffers(ctx context.Context) ([]domain.EnetpulsePreodds, error) {
preodds, err := s.store.GetAllEnetpulsePreoddsWithBettingOffers(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch preodds with betting offers from DB: %w", err)
}
return preodds, nil
}
func (s *Service) GetFixturesWithPreodds(ctx context.Context) ([]domain.EnetpulseFixtureWithPreodds, error) {
// 1⃣ Fetch fixtures and their associated preodds from the repository
fixtures, err := s.store.GetFixturesWithPreodds(ctx)

View File

@ -39,7 +39,7 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
})
}
amount := domain.Currency(req.Amount * 100)
amount := domain.Currency(req.Amount)
fmt.Println("We are here init Chapa payment")
@ -51,40 +51,6 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
})
}
// get static wallet of user
// wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), userID)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
// Error: err.Error(),
// Message: "Failed to initiate Chapa deposit",
// })
// }
// var multiplier float32 = 1
// bonusMultiplier, err := h.bonusSvc.GetBonusMultiplier(c.Context())
// if err == nil {
// multiplier = bonusMultiplier[0].Multiplier
// }
// var balanceCap int64 = 0
// bonusBalanceCap, err := h.bonusSvc.GetBonusBalanceCap(c.Context())
// if err == nil {
// balanceCap = bonusBalanceCap[0].BalanceCap
// }
// capedBalanceAmount := domain.Currency((math.Min(req.Amount, float64(balanceCap)) * float64(multiplier)) * 100)
// _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, capedBalanceAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{},
// fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", capedBalanceAmount, multiplier),
// )
// if err != nil {
// h.logger.Error("Failed to add bonus to static wallet", "walletID", wallet.StaticID, "user id", userID, "error", err)
// return err
// }
// if err := h.bonusSvc.ProcessWelcomeBonus(c.Context(), domain.ToCurrency(float32(req.Amount)), 0, userID); err != nil {
// return err
// }
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa deposit process initiated successfully",
Data: checkoutURL,
@ -114,16 +80,16 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
switch chapaTransactionType.Type {
case h.Cfg.CHAPA_PAYMENT_TYPE:
chapaTransferVerificationRequest := new(domain.ChapaWebHookTransfer)
chapaTransferVerificationRequest := new(domain.ChapaPaymentWebhookRequest)
if err := c.BodyParser(chapaTransferVerificationRequest); err != nil {
return domain.UnProcessableEntityResponse(c)
}
err := h.chapaSvc.HandleVerifyDepositWebhook(c.Context(), *chapaTransferVerificationRequest)
err := h.chapaSvc.ProcessVerifyDepositWebhook(c.Context(), *chapaTransferVerificationRequest)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to verify Chapa depposit",
Message: "Failed to verify Chapa deposit",
Error: err.Error(),
})
}
@ -140,7 +106,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
return domain.UnProcessableEntityResponse(c)
}
err := h.chapaSvc.HandleVerifyWithdrawWebhook(c.Context(), *chapaPaymentVerificationRequest)
err := h.chapaSvc.ProcessVerifyWithdrawWebhook(c.Context(), *chapaPaymentVerificationRequest)
if err != nil {
return domain.UnExpectedErrorResponse(c)
}
@ -161,6 +127,120 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
})
}
// CancelDeposit godoc
// @Summary Cancel a Chapa deposit transaction
// @Description Cancels an active Chapa transaction using its transaction reference
// @Tags Chapa
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param tx_ref path string true "Transaction Reference"
// @Success 200 {object} domain.ChapaCancelResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transaction/cancel/{tx_ref} [put]
func (h *Handler) CancelDeposit(c *fiber.Ctx) error {
// Get user ID from context (set by your auth middleware)
userID, ok := c.Locals("user_id").(int64)
if !ok {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: "invalid user ID",
Message: "User ID is required to cancel a deposit",
})
}
// Extract tx_ref from URL path
txRef := c.Params("tx_ref")
if txRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: "missing transaction reference",
Message: "Transaction reference is required in the path",
})
}
fmt.Printf("\n\nReceived request to cancel Chapa transaction: %s (User ID: %d)\n\n", txRef, userID)
// Call the service layer to cancel deposit
cancelResp, err := h.chapaSvc.CancelDeposit(c.Context(), userID, txRef)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Failed to cancel Chapa deposit",
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa transaction cancelled successfully",
Data: cancelResp,
StatusCode: 200,
Success: true,
})
}
// FetchAllTransactions godoc
// @Summary Get all Chapa transactions
// @Description Retrieves all transactions from Chapa payment gateway
// @Tags Chapa
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {array} domain.ChapaTransaction
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transactions [get]
func (h *Handler) FetchAllTransactions(c *fiber.Ctx) error {
transactions, err := h.chapaSvc.FetchAllTransactions(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Failed to fetch Chapa transactions",
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa transactions retrieved successfully",
Data: transactions,
StatusCode: 200,
Success: true,
})
}
// GetTransactionEvents godoc
// @Summary Fetch transaction events
// @Description Retrieve the timeline of events for a specific Chapa transaction
// @Tags Chapa
// @Accept json
// @Produce json
// @Param ref_id path string true "Transaction Reference"
// @Success 200 {array} domain.ChapaTransactionEvent
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transaction/events/{ref_id} [get]
func (h *Handler) GetTransactionEvents(c *fiber.Ctx) error {
refID := c.Params("ref_id")
if refID == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to fetch transaction events",
Error: "Transaction reference is required",
})
}
events, err := h.chapaSvc.FetchTransactionEvents(c.Context(), refID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch transaction events",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Transaction events fetched successfully",
Data: events,
StatusCode: 200,
Success: true,
})
}
// VerifyPayment godoc
// @Summary Verify a payment manually
// @Description Manually verify a payment using Chapa's API
@ -171,7 +251,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
// @Success 200 {object} domain.ChapaVerificationResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/payments/manual/verify/{tx_ref} [get]
// @Router /api/v1/chapa/transaction/manual/verify/{tx_ref} [get]
func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error {
txRef := c.Params("tx_ref")
if txRef == "" {
@ -189,11 +269,11 @@ func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error {
})
}
return c.Status(fiber.StatusOK).JSON(domain.ChapaVerificationResponse{
Status: string(verification.Status),
Amount: verification.Amount,
Currency: verification.Currency,
TxRef: txRef,
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa transaction verified successfully",
Data: verification,
StatusCode: 200,
Success: true,
})
}
@ -284,7 +364,7 @@ func (h *Handler) GetPaymentReceipt(c *fiber.Ctx) error {
})
}
receiptURL, err := h.chapaSvc.GetPaymentReceiptURL(c.Context(), chapaRef)
receiptURL, err := h.chapaSvc.GetPaymentReceiptURL(chapaRef)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get Chapa payment receipt",

View File

@ -205,6 +205,62 @@ func (h *Handler) GetAllPreodds(c *fiber.Ctx) error {
})
}
// GetAllBettingOffers godoc
// @Summary Get all betting offers
// @Description Fetches all EnetPulse preodds betting offers stored in the database
// @Tags EnetPulse
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response{data=[]domain.EnetpulsePreoddsBettingOffer}
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/enetpulse/betting-offers [get]
func (h *Handler) GetAllBettingOffers(c *fiber.Ctx) error {
// Call service
offers, err := h.enetPulseSvc.GetAllBettingOffers(c.Context())
if err != nil {
log.Println("GetAllBettingOffers error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch EnetPulse betting offers",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "EnetPulse betting offers fetched successfully",
Data: offers,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// GetAllPreoddsWithBettingOffers godoc
// @Summary Get all preodds with betting offers
// @Description Fetches all EnetPulse pre-match odds along with their associated betting offers stored in the database
// @Tags EnetPulse
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response{data=[]domain.EnetpulsePreodds}
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/enetpulse/preodds-with-offers [get]
func (h *Handler) GetAllPreoddsWithBettingOffers(c *fiber.Ctx) error {
// Call service
preodds, err := h.enetPulseSvc.GetAllPreoddsWithBettingOffers(c.Context())
if err != nil {
log.Println("GetAllPreoddsWithBettingOffers error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch EnetPulse preodds with betting offers",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "EnetPulse preodds with betting offers fetched successfully",
Data: preodds,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// GetFixturesWithPreodds godoc
// @Summary Get fixtures with preodds
// @Description Fetches all EnetPulse fixtures along with their associated pre-match odds

View File

@ -292,7 +292,8 @@ func (a *App) initAppRoutes() {
groupV1.Get("/tournament_stages", h.GetAllTournamentStages)
groupV1.Get("/fixtures", h.GetFixturesByDate)
groupV1.Get("/results", h.GetAllResults)
groupV1.Get("/preodds", h.GetAllPreodds)
groupV1.Get("/preodds", h.GetAllPreoddsWithBettingOffers)
groupV1.Get("/bettingoffers", h.GetAllBettingOffers)
groupV1.Get("/fixtures/preodds", h.GetFixturesWithPreodds)
// Leagues
@ -380,7 +381,10 @@ func (a *App) initAppRoutes() {
//Chapa Routes
groupV1.Post("/chapa/payments/webhook/verify", h.WebhookCallback)
groupV1.Get("/chapa/payments/manual/verify/:tx_ref", h.ManualVerifyTransaction)
groupV1.Get("/chapa/transaction/manual/verify/:tx_ref", h.ManualVerifyTransaction)
groupV1.Put("/chapa/transaction/cancel/:tx_ref", a.authMiddleware, h.CancelDeposit)
groupV1.Get("/chapa/transactions", h.FetchAllTransactions)
groupV1.Get("/chapa/transaction/events/:ref_id", h.GetTransactionEvents)
groupV1.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit)
groupV1.Post("/chapa/payments/withdraw", a.authMiddleware, h.InitiateWithdrawal)
groupV1.Get("/chapa/banks", h.GetSupportedBanks)