From 46d70d7c8ce96e51b2ed4953a3f78cdc529486a2 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Mon, 3 Nov 2025 17:20:35 +0300 Subject: [PATCH] chapa minor fixes --- db/migrations/000001_fortune.up.sql | 1 + db/query/enet_pulse.sql | 40 ++ db/query/transfer.sql | 3 +- gen/db/enet_pulse.sql.go | 122 +++++++ gen/db/models.go | 64 ++-- gen/db/transfer.sql.go | 42 ++- internal/config/config.go | 2 + internal/domain/chapa.go | 117 +++++- internal/domain/enet_pulse.go | 1 + internal/repository/enet_pulse.go | 67 +++- internal/services/bet/service.go | 1 + internal/services/chapa/client.go | 173 +++++++-- internal/services/chapa/port.go | 2 +- internal/services/chapa/service.go | 402 ++++++++++++++------- internal/services/enet_pulse/port.go | 1 + internal/services/enet_pulse/service.go | 113 +++--- internal/web_server/handlers/chapa.go | 172 ++++++--- internal/web_server/handlers/enet_pulse.go | 56 +++ internal/web_server/routes.go | 8 +- 19 files changed, 1069 insertions(+), 318 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 6f2043c..b911279 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -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), diff --git a/db/query/enet_pulse.sql b/db/query/enet_pulse.sql index 96cf090..f586d46 100644 --- a/db/query/enet_pulse.sql +++ b/db/query/enet_pulse.sql @@ -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, diff --git a/db/query/transfer.sql b/db/query/transfer.sql index 0229d0f..bd44f37 100644 --- a/db/query/transfer.sql +++ b/db/query/transfer.sql @@ -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 * diff --git a/gen/db/enet_pulse.sql.go b/gen/db/enet_pulse.sql.go index aafaad4..f06cd69 100644 --- a/gen/db/enet_pulse.sql.go +++ b/gen/db/enet_pulse.sql.go @@ -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 diff --git a/gen/db/models.go b/gen/db/models.go index dd3f35e..a3ff73c 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1138,38 +1138,40 @@ type WalletThresholdNotification struct { } type WalletTransfer struct { - ID int64 `json:"id"` - Amount pgtype.Int8 `json:"amount"` - Message string `json:"message"` - Type pgtype.Text `json:"type"` - ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` - SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` - CashierID pgtype.Int8 `json:"cashier_id"` - Verified pgtype.Bool `json:"verified"` - ReferenceNumber string `json:"reference_number"` - SessionID pgtype.Text `json:"session_id"` - Status pgtype.Text `json:"status"` - PaymentMethod pgtype.Text `json:"payment_method"` - CreatedAt pgtype.Timestamp `json:"created_at"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` + ID int64 `json:"id"` + Amount pgtype.Int8 `json:"amount"` + Message string `json:"message"` + Type pgtype.Text `json:"type"` + ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` + SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` + 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"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type WalletTransferDetail struct { - ID int64 `json:"id"` - Amount pgtype.Int8 `json:"amount"` - Message string `json:"message"` - Type pgtype.Text `json:"type"` - ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` - SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` - CashierID pgtype.Int8 `json:"cashier_id"` - Verified pgtype.Bool `json:"verified"` - ReferenceNumber string `json:"reference_number"` - SessionID pgtype.Text `json:"session_id"` - Status pgtype.Text `json:"status"` - PaymentMethod pgtype.Text `json:"payment_method"` - CreatedAt pgtype.Timestamp `json:"created_at"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` - FirstName pgtype.Text `json:"first_name"` - LastName pgtype.Text `json:"last_name"` - PhoneNumber pgtype.Text `json:"phone_number"` + ID int64 `json:"id"` + Amount pgtype.Int8 `json:"amount"` + Message string `json:"message"` + Type pgtype.Text `json:"type"` + ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` + SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` + 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"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + FirstName pgtype.Text `json:"first_name"` + LastName pgtype.Text `json:"last_name"` + PhoneNumber pgtype.Text `json:"phone_number"` } diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index b2a1066..185225b 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -21,26 +21,28 @@ 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 { - Amount pgtype.Int8 `json:"amount"` - Message string `json:"message"` - Type pgtype.Text `json:"type"` - ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` - SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` - CashierID pgtype.Int8 `json:"cashier_id"` - Verified pgtype.Bool `json:"verified"` - ReferenceNumber string `json:"reference_number"` - SessionID pgtype.Text `json:"session_id"` - Status pgtype.Text `json:"status"` - PaymentMethod pgtype.Text `json:"payment_method"` + Amount pgtype.Int8 `json:"amount"` + Message string `json:"message"` + Type pgtype.Text `json:"type"` + ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` + SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` + 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"` } func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) (WalletTransfer, error) { @@ -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, diff --git a/internal/config/config.go b/internal/config/config.go index 73031ba..b802be0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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" } diff --git a/internal/domain/chapa.go b/internal/domain/chapa.go index bf6b683..1d814ee 100644 --- a/internal/domain/chapa.go +++ b/internal/domain/chapa.go @@ -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,10 +51,16 @@ type ChapaDepositRequestPayload struct { } type ChapaWebhookPayload struct { - TxRef string `json:"tx_ref"` - Amount Currency `json:"amount"` - Currency string `json:"currency"` - Status PaymentStatus `json:"status"` + TxRef string `json:"trx_ref"` + Amount Currency `json:"amount"` + // 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"` } // PaymentResponse contains the response from payment initialization @@ -69,10 +77,91 @@ type ChapaDepositVerification struct { } type ChapaVerificationResponse struct { - Status string `json:"status"` - Amount float64 `json:"amount"` - Currency string `json:"currency"` - TxRef string `json:"tx_ref"` + Message string `json:"message"` + Status string `json:"status"` + 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"` +} diff --git a/internal/domain/enet_pulse.go b/internal/domain/enet_pulse.go index 78bcf77..00725d9 100644 --- a/internal/domain/enet_pulse.go +++ b/internal/domain/enet_pulse.go @@ -703,6 +703,7 @@ type EnetpulsePreodds struct { LastUpdatedAt time.Time CreatedAt time.Time UpdatedAt time.Time + BettingOffers []EnetpulsePreoddsBettingOffer } type EnetpulseResultParticipant struct { diff --git a/internal/repository/enet_pulse.go b/internal/repository/enet_pulse.go index 2dc99a9..4d3b2df 100644 --- a/internal/repository/enet_pulse.go +++ b/internal/repository/enet_pulse.go @@ -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 { @@ -944,6 +1009,6 @@ func ConvertDBEnetpulseResultReferee(r dbgen.EnetpulseResultReferee) domain.Enet Var1RefereeFk: r.Var1RefereeFk.String, Var2RefereeFk: r.Var2RefereeFk.String, LastUpdatedAt: r.LastUpdatedAt.Time, - CreatedAt: r.CreatedAt.Time , + CreatedAt: r.CreatedAt.Time, } } diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index d6ff26a..eb9ca75 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -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) } diff --git a/internal/services/chapa/client.go b/internal/services/chapa/client.go index 3beed5b..01c0f41 100644 --- a/internal/services/chapa/client.go +++ b/internal/services/chapa/client.go @@ -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, + "amount": fmt.Sprintf("%.2f", float64(req.Amount)), + "currency": req.Currency, + "email": req.Email, "first_name": req.FirstName, "last_name": req.LastName, "tx_ref": req.TxRef, "callback_url": req.CallbackURL, "return_url": req.ReturnURL, + "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") diff --git a/internal/services/chapa/port.go b/internal/services/chapa/port.go index 862e3c4..e2a0667 100644 --- a/internal/services/chapa/port.go +++ b/internal/services/chapa/port.go @@ -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) diff --git a/internal/services/chapa/service.go b/internal/services/chapa/service.go index 95ce795..5ec26fb 100644 --- a/internal/services/chapa/service.go +++ b/internal/services/chapa/service.go @@ -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 { diff --git a/internal/services/enet_pulse/port.go b/internal/services/enet_pulse/port.go index a75ab8e..e9e0b1b 100644 --- a/internal/services/enet_pulse/port.go +++ b/internal/services/enet_pulse/port.go @@ -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) } diff --git a/internal/services/enet_pulse/service.go b/internal/services/enet_pulse/service.go index 2fa4304..fd9b7d2 100644 --- a/internal/services/enet_pulse/service.go +++ b/internal/services/enet_pulse/service.go @@ -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 { - 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"` - Active string `json:"active"` - CouponKey string `json:"coupon_key"` - N string `json:"n"` - UT string `json:"ut"` - } `json:"bettingoffers"` + + PreoddsBettingOffers map[string]struct { + ID string `json:"id"` + 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:"couponKey"` + N string `json:"n"` + UT string `json:"ut"` + } `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) diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index f260bc5..c671dbb 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -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", diff --git a/internal/web_server/handlers/enet_pulse.go b/internal/web_server/handlers/enet_pulse.go index 7bd3616..e89c7b2 100644 --- a/internal/web_server/handlers/enet_pulse.go +++ b/internal/web_server/handlers/enet_pulse.go @@ -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 diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 27f8099..01c70aa 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -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)