From f1a4f5e6f9da1032f01dbd5fc2ec6c6068c3b97e Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Sun, 17 Aug 2025 12:13:29 +0300 Subject: [PATCH 1/4] Arifpay webhook fixes --- cmd/main.go | 4 +- db/migrations/000001_fortune.up.sql | 1 + ...l => 000004_virtual_game_Session.down.sql} | 0 ...sql => 000004_virtual_game_Session.up.sql} | 0 db/query/transfer.sql | 3 +- gen/db/models.go | 2 + gen/db/transfer.sql.go | 20 +- go.mod | 2 +- internal/config/config.go | 37 +- internal/domain/arifpay.go | 90 ++- internal/domain/transfer.go | 3 + internal/repository/transfer.go | 6 + internal/services/arifpay/service.go | 594 +++++++++++++++--- internal/services/santimpay/service.go | 2 +- internal/services/virtualGame/veli/service.go | 55 +- internal/web_server/handlers/arifpay.go | 263 ++++++-- internal/web_server/handlers/veli_games.go | 48 +- internal/web_server/routes.go | 8 +- 18 files changed, 910 insertions(+), 228 deletions(-) rename db/migrations/{000004_virtual_game_Sessios.down.sql => 000004_virtual_game_Session.down.sql} (100%) rename db/migrations/{000004_virtual_game_Sessios.up.sql => 000004_virtual_game_Session.up.sql} (100%) diff --git a/cmd/main.go b/cmd/main.go index c737e71..a9b4970 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -153,7 +153,7 @@ func main() { virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) veliCLient := veli.NewClient(cfg, walletSvc) - veliVirtualGameService := veli.New(veliCLient) + veliVirtualGameService := veli.New(veliCLient, walletSvc) recommendationSvc := recommendation.NewService(recommendationRepo) chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY) @@ -234,7 +234,7 @@ func main() { transferStore := wallet.TransferStore(store) // walletStore := wallet.WalletStore(store) - arifpaySvc := arifpay.NewArifpayService(cfg, transferStore, &http.Client{ + arifpaySvc := arifpay.NewArifpayService(cfg, transferStore, walletSvc, &http.Client{ Timeout: 30 * time.Second}) santimpayClient := santimpay.NewSantimPayClient(cfg) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 9a952a1..71519c4 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -190,6 +190,7 @@ CREATE TABLE IF NOT EXISTS wallet_transfer ( cashier_id BIGINT, verified BOOLEAN DEFAULT false, reference_number VARCHAR(255) NOT NULL, + session_id VARCHAR(255), status VARCHAR(255), payment_method VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, diff --git a/db/migrations/000004_virtual_game_Sessios.down.sql b/db/migrations/000004_virtual_game_Session.down.sql similarity index 100% rename from db/migrations/000004_virtual_game_Sessios.down.sql rename to db/migrations/000004_virtual_game_Session.down.sql diff --git a/db/migrations/000004_virtual_game_Sessios.up.sql b/db/migrations/000004_virtual_game_Session.up.sql similarity index 100% rename from db/migrations/000004_virtual_game_Sessios.up.sql rename to db/migrations/000004_virtual_game_Session.up.sql diff --git a/db/query/transfer.sql b/db/query/transfer.sql index 9219943..dc4c156 100644 --- a/db/query/transfer.sql +++ b/db/query/transfer.sql @@ -8,10 +8,11 @@ INSERT INTO wallet_transfer ( cashier_id, verified, reference_number, + session_id, status, payment_method ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *; -- name: GetAllTransfers :many SELECT * diff --git a/gen/db/models.go b/gen/db/models.go index a414f7f..7138ff5 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -713,6 +713,7 @@ type WalletTransfer struct { 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"` @@ -729,6 +730,7 @@ type WalletTransferDetail struct { 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"` diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index b564b72..35e38d4 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -21,11 +21,12 @@ INSERT INTO wallet_transfer ( cashier_id, verified, reference_number, + session_id, status, payment_method ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) -RETURNING id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, status, payment_method, created_at, updated_at +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 ` type CreateTransferParams struct { @@ -37,6 +38,7 @@ type CreateTransferParams struct { 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"` } @@ -51,6 +53,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) arg.CashierID, arg.Verified, arg.ReferenceNumber, + arg.SessionID, arg.Status, arg.PaymentMethod, ) @@ -65,6 +68,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.SessionID, &i.Status, &i.PaymentMethod, &i.CreatedAt, @@ -74,7 +78,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, 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, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number FROM wallet_transfer_details ` @@ -97,6 +101,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransferDetail, &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.SessionID, &i.Status, &i.PaymentMethod, &i.CreatedAt, @@ -116,7 +121,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, 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, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number FROM wallet_transfer_details WHERE id = $1 ` @@ -134,6 +139,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.SessionID, &i.Status, &i.PaymentMethod, &i.CreatedAt, @@ -146,7 +152,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, 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, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number FROM wallet_transfer_details WHERE reference_number = $1 ` @@ -164,6 +170,7 @@ func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber st &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.SessionID, &i.Status, &i.PaymentMethod, &i.CreatedAt, @@ -176,7 +183,7 @@ func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber st } const GetTransfersByWallet = `-- name: GetTransfersByWallet :many -SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, 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, 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 @@ -201,6 +208,7 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID pgt &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.SessionID, &i.Status, &i.PaymentMethod, &i.CreatedAt, diff --git a/go.mod b/go.mod index 71351a2..9001f12 100644 --- a/go.mod +++ b/go.mod @@ -94,6 +94,6 @@ require ( github.com/segmentio/kafka-go v0.4.48 // direct ) -require github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f +// require github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f // require github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f // direct diff --git a/internal/config/config.go b/internal/config/config.go index a0acbb1..afc4d78 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,7 @@ import ( "log/slog" "os" "strconv" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger" @@ -59,11 +60,25 @@ type VeliConfig struct { } type ARIFPAYConfig struct { - APIKey string `mapstructure:"ARIFPAYAPI_KEY"` - CancelUrl string `mapstructure:"ARIFPAY_BASE_URL"` - ErrorUrl string `mapstructure:"ARIFPAY_SECRET_KEY"` - NotifyUrl string `mapstructure:"ARIFPAY_OPERATOR_ID"` - SuccessUrl string `mapstructure:"ARIFPAY_BRAND_ID"` + APIKey string `mapstructure:"ARIFPAY_API_KEY"` + BaseURL string `mapstructure:"ARIFPAY_BASE_URL"` + + // Default URLs + CancelUrl string `mapstructure:"cancelUrl"` + SuccessUrl string `mapstructure:"successUrl"` + ErrorUrl string `mapstructure:"errorUrl"` + B2CNotifyUrl string `mapstructure:"notifyUrl"` + C2BNotifyUrl string `mapstructure:"notifyUrl"` + + // Default Payment Configs + PaymentMethods []string `mapstructure:"paymentMethods"` + ExpireDate string `mapstructure:"expireDate"` + ItemName string `mapstructure:"name"` + Quantity int `mapstructure:"quantity"` + Description string `mapstructure:"description"` + BeneficiaryAccountNumber string `mapstructure:"accountNumber"` + Bank string `mapstructure:"bank"` + Lang string `mapstructure:"amount"` } type SANTIMPAYConfig struct { @@ -224,8 +239,18 @@ func (c *Config) loadEnv() error { c.ARIFPAY.APIKey = os.Getenv("ARIFPAY_API_KEY") c.ARIFPAY.CancelUrl = os.Getenv("ARIFPAY_CANCEL_URL") c.ARIFPAY.ErrorUrl = os.Getenv("ARIFPAY_ERROR_URL") - c.ARIFPAY.NotifyUrl = os.Getenv("ARIFPAY_NOTIFY_URL") + c.ARIFPAY.C2BNotifyUrl = os.Getenv("ARIFPAY_C2B_NOTIFY_URL") + c.ARIFPAY.B2CNotifyUrl = os.Getenv("ARIFPAY_B2C_NOTIFY_URL") c.ARIFPAY.SuccessUrl = os.Getenv("ARIFPAY_SUCCESS_URL") + c.ARIFPAY.BaseURL = os.Getenv("ARIFPAY_BASE_URL") + c.ARIFPAY.Bank = os.Getenv("ARIFPAY_BANK") + c.ARIFPAY.BeneficiaryAccountNumber = os.Getenv("ARIFPAY_BENEFICIARY_ACCOUNT_NUMBER") + c.ARIFPAY.Description = os.Getenv("ARIFPAY_DESCRIPTION") + c.ARIFPAY.ExpireDate = time.Now().Add(time.Hour * 3).Format("2006-01-02") + c.ARIFPAY.ItemName = os.Getenv("ARIFPAY_ITEM_NAME") + c.ARIFPAY.Lang = "EN" + c.ARIFPAY.Quantity = 1 + c.ARIFPAY.PaymentMethods = []string{"TELEBIRR", "AWAASH", "AWAASH_WALLET", "PSS", "CBE", "AMOLE", "BOA", "KACHA", "TELEBIRR", "HELLOCASH", "MPESSA"} c.SANTIMPAY.SecretKey = os.Getenv("SANTIMPAY_SECRET_KEY") c.SANTIMPAY.MerchantID = os.Getenv("SANTIMPAY_MERCHANT_ID") diff --git a/internal/domain/arifpay.go b/internal/domain/arifpay.go index 372a7b8..94ac010 100644 --- a/internal/domain/arifpay.go +++ b/internal/domain/arifpay.go @@ -1,37 +1,39 @@ package domain -import "time" +type CheckoutSessionRequest struct { + CancelURL string `json:"cancelUrl"` + Phone string `json:"phone"` + Email string `json:"email"` + Nonce string `json:"nonce"` + SuccessURL string `json:"successUrl"` + ErrorURL string `json:"errorUrl"` + NotifyURL string `json:"notifyUrl"` + PaymentMethods []string `json:"paymentMethods"` + ExpireDate string `json:"expireDate"` // could also use time.Time if you parse it -type Item struct { - Name string `json:"name"` - Quantity int `json:"quantity"` - Price float64 `json:"price"` - Description string `json:"description"` - Image string `json:"image"` + Items []struct { + Name string `json:"name"` + Quantity int `json:"quantity"` + Price float64 `json:"price"` + Description string `json:"description"` + } `json:"items"` + + Beneficiaries []struct { + AccountNumber string `json:"accountNumber"` + Bank string `json:"bank"` + Amount float64 `json:"amount"` + } `json:"beneficiaries"` + + Lang string `json:"lang"` } -type Beneficiary struct { - AccountNumber string `json:"accountNumber"` - Bank string `json:"bank"` - Amount float64 `json:"amount"` +type CheckoutSessionClientRequest struct { + Amount float64 `json:"amount" binding:"required"` + CustomerEmail string `json:"customerEmail" binding:"required"` + CustomerPhone string `json:"customerPhone" binding:"required"` } -type CreateCheckoutSessionRequest struct { - CancelUrl string `json:"cancelUrl"` - Phone string `json:"phone"` - Email string `json:"email"` - Nonce string `json:"nonce"` - ErrorUrl string `json:"errorUrl"` - NotifyUrl string `json:"notifyUrl"` - SuccessUrl string `json:"successUrl"` - PaymentMethods []string `json:"paymentMethods"` - ExpireDate time.Time `json:"expireDate"` - Items []Item `json:"items"` - Beneficiaries []Beneficiary `json:"beneficiaries"` - Lang string `json:"lang"` -} - -type ArifPayCheckoutResponse struct { +type CancelCheckoutSessionResponse struct { Error bool `json:"error"` Msg string `json:"msg"` Data struct { @@ -42,12 +44,34 @@ type ArifPayCheckoutResponse struct { } `json:"data"` } -type ArifPayB2CRequest struct { - SessionID string `json:"Sessionid"` - PhoneNumber string `json:"Phonenumber"` +type WebhookRequest struct { + UUID string `json:"uuid"` + Nonce string `json:"nonce"` + Phone string `json:"phone"` + PaymentMethod string `json:"paymentMethod"` + TotalAmount int64 `json:"totalAmount"` + TransactionStatus string `json:"transactionStatus"` + Transaction struct { + TransactionID string `json:"transactionId"` + TransactionStatus string `json:"transactionStatus"` + } `json:"transaction"` + NotificationURL string `json:"notificationUrl"` + SessionID string `json:"sessionId"` } -type ArifpayVerifyByTransactionIDRequest struct { - TransactionID string `json:"transactionId"` - PaymentType int `json:"paymentType"` +type ArifpayB2CRequest struct{ + PhoneNumber string `json:"Phonenumber"` + Amount float64 `json:"amount" binding:"required"` + CustomerEmail string `json:"customerEmail" binding:"required"` + CustomerPhone string `json:"customerPhone" binding:"required"` +} + +type ArifpayVerifyByTransactionIDRequest struct{ + TransactionId string `json:"transactionId"` + PaymentType int `json:"paymentType"` +} + +type ARIFPAYPaymentMethod struct { + ID int + Name string } diff --git a/internal/domain/transfer.go b/internal/domain/transfer.go index 370b8a1..cf629c3 100644 --- a/internal/domain/transfer.go +++ b/internal/domain/transfer.go @@ -66,6 +66,7 @@ type Transfer struct { ReceiverWalletID ValidInt64 `json:"receiver_wallet_id"` SenderWalletID ValidInt64 `json:"sender_wallet_id"` ReferenceNumber string `json:"reference_number"` // <-- needed + SessionID string `json:"session_id"` Status string `json:"status"` DepositorID ValidInt64 `json:"depositor_id"` CreatedAt time.Time `json:"created_at"` @@ -81,6 +82,7 @@ type TransferDetail struct { ReceiverWalletID ValidInt64 `json:"receiver_wallet_id"` SenderWalletID ValidInt64 `json:"sender_wallet_id"` ReferenceNumber string `json:"reference_number"` // <-- needed + SessionID string `json:"session_id"` Status string `json:"status"` DepositorID ValidInt64 `json:"depositor_id"` DepositorFirstName string `json:"depositor_first_name"` @@ -99,6 +101,7 @@ type CreateTransfer struct { ReceiverWalletID ValidInt64 `json:"receiver_wallet_id"` SenderWalletID ValidInt64 `json:"sender_wallet_id"` ReferenceNumber string `json:"reference_number"` // <-- needed + SessionID string `json:"session_id"` Status string `json:"status"` CashierID ValidInt64 `json:"cashier_id"` } diff --git a/internal/repository/transfer.go b/internal/repository/transfer.go index 4324ddf..cad330e 100644 --- a/internal/repository/transfer.go +++ b/internal/repository/transfer.go @@ -32,6 +32,7 @@ func convertDBTransferDetail(transfer dbgen.WalletTransferDetail) domain.Transfe DepositorPhoneNumber: transfer.PhoneNumber.String, PaymentMethod: domain.PaymentMethod(transfer.PaymentMethod.String), ReferenceNumber: transfer.ReferenceNumber, + SessionID: transfer.SessionID.String, Status: transfer.Status.String, CreatedAt: transfer.CreatedAt.Time, UpdatedAt: transfer.UpdatedAt.Time, @@ -58,6 +59,7 @@ func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer { }, PaymentMethod: domain.PaymentMethod(transfer.PaymentMethod.String), ReferenceNumber: transfer.ReferenceNumber, + SessionID: transfer.SessionID.String, Status: transfer.Status.String, CreatedAt: transfer.CreatedAt.Time, UpdatedAt: transfer.UpdatedAt.Time, @@ -82,6 +84,10 @@ func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferP Valid: transfer.CashierID.Valid, }, ReferenceNumber: string(transfer.ReferenceNumber), + SessionID: pgtype.Text{ + String: transfer.SessionID, + Valid: true, + }, PaymentMethod: pgtype.Text{String: string(transfer.PaymentMethod), Valid: true}, Verified: pgtype.Bool{ diff --git a/internal/services/arifpay/service.go b/internal/services/arifpay/service.go index 8573bc5..a074d98 100644 --- a/internal/services/arifpay/service.go +++ b/internal/services/arifpay/service.go @@ -4,12 +4,11 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" - "time" - "github.com/AnaniyaBelew/ArifpayGoPlugin" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" @@ -19,174 +18,567 @@ import ( type ArifpayService struct { cfg *config.Config transferStore wallet.TransferStore + walletSvc *wallet.Service httpClient *http.Client } -func NewArifpayService(cfg *config.Config, transferStore wallet.TransferStore, httpClient *http.Client) *ArifpayService { +func NewArifpayService(cfg *config.Config, transferStore wallet.TransferStore, walletSvc *wallet.Service, httpClient *http.Client) *ArifpayService { return &ArifpayService{ cfg: cfg, transferStore: transferStore, + walletSvc: walletSvc, httpClient: httpClient, } } -func (s *ArifpayService) CreateCheckoutSession(req domain.CreateCheckoutSessionRequest) (string, error) { - // Create SDK-compatible payload - paymentPayload := ArifpayGoPlugin.PaymentRequest{ - CancelUrl: s.cfg.ARIFPAY.CancelUrl, - Phone: req.Phone, - Email: req.Email, - Nonce: req.Nonce, - ErrorUrl: s.cfg.ARIFPAY.ErrorUrl, - NotifyUrl: s.cfg.ARIFPAY.NotifyUrl, - SuccessUrl: s.cfg.ARIFPAY.SuccessUrl, - PaymentMethods: req.PaymentMethods, - Lang: req.Lang, +func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientRequest, isDeposit bool) (map[string]any, error) { + // Generate unique nonce + nonce := uuid.NewString() + + var NotifyURL string + + if isDeposit{ + NotifyURL = s.cfg.ARIFPAY.C2BNotifyUrl + }else{ + NotifyURL = s.cfg.ARIFPAY.B2CNotifyUrl } - // Convert items - for _, item := range req.Items { - paymentPayload.Items = append(paymentPayload.Items, domain.Item{ - Name: item.Name, - Quantity: item.Quantity, - Price: item.Price, - Description: item.Description, - Image: item.Image, - }) + // Construct full checkout request + checkoutReq := domain.CheckoutSessionRequest{ + CancelURL: s.cfg.ARIFPAY.CancelUrl, + Phone: req.CustomerPhone, // must be in format 2519... + Email: req.CustomerEmail, + Nonce: nonce, + SuccessURL: s.cfg.ARIFPAY.SuccessUrl, + ErrorURL: s.cfg.ARIFPAY.ErrorUrl, + NotifyURL: NotifyURL, + PaymentMethods: s.cfg.ARIFPAY.PaymentMethods, + ExpireDate: s.cfg.ARIFPAY.ExpireDate, + Items: []struct { + Name string `json:"name"` + Quantity int `json:"quantity"` + Price float64 `json:"price"` + Description string `json:"description"` + }{ + { + Name: s.cfg.ARIFPAY.ItemName, + Quantity: s.cfg.ARIFPAY.Quantity, + Price: req.Amount, + Description: s.cfg.ARIFPAY.Description, + }, + }, + Beneficiaries: []struct { + AccountNumber string `json:"accountNumber"` + Bank string `json:"bank"` + Amount float64 `json:"amount"` + }{ + { + AccountNumber: s.cfg.ARIFPAY.BeneficiaryAccountNumber, + Bank: s.cfg.ARIFPAY.Bank, + Amount: req.Amount, + }, + }, + Lang: s.cfg.ARIFPAY.Lang, } - // Convert beneficiaries - for _, b := range req.Beneficiaries { - paymentPayload.Beneficiaries = append(paymentPayload.Beneficiaries, domain.Beneficiary{ - AccountNumber: b.AccountNumber, - Bank: b.Bank, - Amount: b.Amount, - }) - } - - // Instantiate payment client - expireDate := time.Now().AddDate(2, 0, 0) // 2 months from now - paymentClient := ArifpayGoPlugin.NewPayment(s.cfg.ARIFPAY.APIKey, expireDate) - - // Create checkout session - response, err := paymentClient.MakePayment(paymentPayload) + // Marshal to JSON + payload, err := json.Marshal(checkoutReq) if err != nil { - return "", err + return nil, fmt.Errorf("failed to marshal checkout request: %w", err) } - transfer := domain.CreateTransfer{ - Amount: domain.Currency(req.Beneficiaries[0].Amount), - Verified: false, - Type: domain.DEPOSIT, - ReferenceNumber: uuid.NewString(), - Status: string(domain.PaymentStatusPending), - } - - if _, err := s.transferStore.CreateTransfer(context.Background(), transfer); err != nil { - return "", err - } - - return response, nil -} - -func (s *ArifpayService) B2CTransfer(ctx context.Context, req domain.ArifPayB2CRequest, endpoint string) (*map[string]interface{}, error) { - // endpoint := c.baseURL + "/api/Telebirr/b2c/transfer" - - payloadBytes, err := json.Marshal(req) + // Send request to Arifpay API + url := fmt.Sprintf("%s/api/checkout/session", s.cfg.ARIFPAY.BaseURL) + httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) if err != nil { - return nil, fmt.Errorf("failed to marshal request payload: %w", err) + return nil, err } - - httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(payloadBytes)) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Accept", "application/json") httpReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) resp, err := s.httpClient.Do(httpReq) if err != nil { - return nil, fmt.Errorf("request to Telebirr B2C failed: %w", err) + return nil, err } defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) + // Read response + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("Telebirr API returned status %d: %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("failed to create checkout session: %s", string(body)) } - var response map[string]interface{} - if err := json.Unmarshal(body, &response); err != nil { - return nil, fmt.Errorf("failed to parse response body: %w", err) + // Optionally unmarshal response to struct + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("invalid response from Arifpay: %w", err) } - return &response, nil + data := result["data"].(map[string]interface{}) + // paymentURL := data["paymentUrl"].(string) + + // Store transfer in DB + transfer := domain.CreateTransfer{ + Amount: domain.Currency(req.Amount), + Verified: false, + Type: domain.DEPOSIT, + ReferenceNumber: nonce, + SessionID: fmt.Sprintf("%v", data["sessionId"]), + Status: string(domain.PaymentStatusPending), + } + + if _, err := s.transferStore.CreateTransfer(context.Background(), transfer); err != nil { + return nil, err + } + + return data, nil } -func (s *ArifpayService) VerifyByTransactionID(transactionID string, paymentType int) ([]byte, error) { - url := "https://gateway.arifpay.org/api/checkout/getSessionByTransactionId" +func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID string) (*domain.CancelCheckoutSessionResponse, error) { + // Build the cancel URL + url := fmt.Sprintf("%s/api/sandbox/checkout/session/%s", s.cfg.ARIFPAY.BaseURL, sessionID) - var payload domain.ArifpayVerifyByTransactionIDRequest - - bodyBytes, err := json.Marshal(payload) - if err != nil { - return nil, fmt.Errorf("failed to marshal request body: %w", err) - } - - req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes)) + // Create the request + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } + // Add headers req.Header.Set("Content-Type", "application/json") req.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) + // Execute request resp, err := s.httpClient.Do(req) if err != nil { - return nil, fmt.Errorf("request failed: %w", err) + return nil, fmt.Errorf("failed to execute cancel request: %w", err) } defer resp.Body.Close() - respBody, err := io.ReadAll(resp.Body) + // Read response body + body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read cancel response: %w", err) } + // Handle non-200 status codes if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("non-200 response from Arifpay: %s", string(respBody)) + return nil, fmt.Errorf("cancel request failed: status=%d, body=%s", resp.StatusCode, string(body)) } - return respBody, nil + // Decode into response struct + var cancelResp domain.CancelCheckoutSessionResponse + if err := json.Unmarshal(body, &cancelResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal cancel response: %w", err) + } + + return &cancelResp, nil } -func (s *ArifpayService) VerifyBySessionID(sessionID string) ([]byte, error) { - url := "https://gateway.arifpay.org/api/ms/transaction/status/" + sessionID +func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRequest, userId int64, isDepost bool) error { + // 1. Get transfer by SessionID + transfer, err := s.transferStore.GetTransferByReference(ctx, req.Transaction.TransactionID) + if err != nil { + return err + } - // Create GET request without body - req, err := http.NewRequest("GET", url, nil) + wallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) + if err != nil { + return err + } + + if transfer.Verified { + return errors.New("transfer already verified") + } + + // 2. Update transfer status + newStatus := req.Transaction.TransactionStatus + // if req.Transaction.TransactionStatus != "" { + // newStatus = req.Transaction.TransactionStatus + // } + + err = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, newStatus) + if err != nil { + return err + } + + err = s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true) + if err != nil { + return err + } + + // 3. If SUCCESS -> update customer wallet balance + if (newStatus == "SUCCESS" && isDepost) || (newStatus == "FAILED" && !isDepost) { + _, err = s.walletSvc.AddToWallet(ctx, wallets[0].ID, domain.Currency(req.TotalAmount), domain.ValidInt64{}, transfer.PaymentMethod, domain.PaymentDetails{ + ReferenceNumber: domain.ValidString{ + Value: req.Transaction.TransactionID, + Valid: true, + }, + BankNumber: domain.ValidString{ + Value: "", + Valid: false, + }, + }, "") + if err != nil { + return err + } + } + + return nil +} + +func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error { + // Step 1: Create Session + referenceNum := uuid.NewString() + + sessionReq := domain.CheckoutSessionClientRequest{ + Amount: req.Amount, + CustomerEmail: req.CustomerEmail, + CustomerPhone: req.CustomerPhone, + } + + sessionResp, err := s.CreateCheckoutSession(sessionReq, false) + if err != nil { + return fmt.Errorf("failed to create session: %w", err) + } + + // Step 2: Execute Transfer + transferURL := fmt.Sprintf("%s/api/Telebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL) + reqBody := map[string]any{ + "Sessionid": sessionResp["sessionId"], + "Phonenumber": req.PhoneNumber, + } + + payload, err := json.Marshal(reqBody) + if err != nil { + return fmt.Errorf("failed to marshal transfer request: %w", err) + } + + transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload)) + if err != nil { + return fmt.Errorf("failed to build transfer request: %w", err) + } + transferReq.Header.Set("Content-Type", "application/json") + transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) + + transferResp, err := s.httpClient.Do(transferReq) + if err != nil { + return fmt.Errorf("failed to execute transfer request: %w", err) + } + defer transferResp.Body.Close() + + if transferResp.StatusCode >= 300 { + body, _ := io.ReadAll(transferResp.Body) + return fmt.Errorf("transfer failed with status %d: %s", transferResp.StatusCode, string(body)) + } + + // Step 3: Store transfer in DB + transfer := domain.CreateTransfer{ + Amount: domain.Currency(req.Amount), + Verified: false, + Type: domain.WITHDRAW, // B2C = payout + ReferenceNumber: referenceNum, + SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]), + Status: string(domain.PaymentStatusPending), + PaymentMethod: domain.TRANSFER_ARIFPAY, + } + if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { + return fmt.Errorf("failed to store transfer: %w", err) + } + + // Step 4: Deduct from wallet + userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) + if err != nil { + return fmt.Errorf("failed to get user wallets: %w", err) + } + if len(userWallets) == 0 { + return fmt.Errorf("no wallet found for user %d", userId) + } + + _, err = s.walletSvc.DeductFromWallet( + ctx, + userWallets[0].ID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + "", + ) + if err != nil { + return fmt.Errorf("failed to deduct from wallet: %w", err) + } + + return nil +} + +func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error { + // Step 1: Create Session + referenceNum := uuid.NewString() + + sessionReq := domain.CheckoutSessionClientRequest{ + Amount: req.Amount, + CustomerEmail: req.CustomerEmail, + CustomerPhone: req.CustomerPhone, + } + + sessionResp, err := s.CreateCheckoutSession(sessionReq, false) + if err != nil { + return fmt.Errorf("cbebirr: failed to create session: %w", err) + } + + // Step 2: Execute Transfer + transferURL := fmt.Sprintf("%s/api/Cbebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL) + reqBody := map[string]any{ + "Sessionid": sessionResp["sessionId"], + "Phonenumber": req.PhoneNumber, + } + + payload, err := json.Marshal(reqBody) + if err != nil { + return fmt.Errorf("cbebirr: failed to marshal transfer request: %w", err) + } + + transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload)) + if err != nil { + return fmt.Errorf("cbebirr: failed to build transfer request: %w", err) + } + transferReq.Header.Set("Content-Type", "application/json") + transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) + + transferResp, err := s.httpClient.Do(transferReq) + if err != nil { + return fmt.Errorf("cbebirr: failed to execute transfer request: %w", err) + } + defer transferResp.Body.Close() + + if transferResp.StatusCode >= 300 { + body, _ := io.ReadAll(transferResp.Body) + return fmt.Errorf("cbebirr: transfer failed with status %d: %s", transferResp.StatusCode, string(body)) + } + + // Step 3: Store transfer in DB + transfer := domain.CreateTransfer{ + Amount: domain.Currency(req.Amount), + Verified: false, + Type: domain.WITHDRAW, // B2C = payout + ReferenceNumber: referenceNum, + SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]), + Status: string(domain.PaymentStatusPending), + PaymentMethod: domain.TRANSFER_ARIFPAY, + } + if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { + return fmt.Errorf("cbebirr: failed to store transfer: %w", err) + } + + // Step 4: Deduct from user wallet + userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) + if err != nil { + return fmt.Errorf("cbebirr: failed to get user wallets: %w", err) + } + if len(userWallets) == 0 { + return fmt.Errorf("cbebirr: no wallet found for user %d", userId) + } + + _, err = s.walletSvc.DeductFromWallet( + ctx, + userWallets[0].ID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + "", + ) + if err != nil { + return fmt.Errorf("cbebirr: failed to deduct from wallet: %w", err) + } + + return nil +} + +func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error { + // Step 1: Create Session + referenceNum := uuid.NewString() + + sessionReq := domain.CheckoutSessionClientRequest{ + Amount: req.Amount, + CustomerEmail: req.CustomerEmail, + CustomerPhone: req.CustomerPhone, + } + + sessionResp, err := s.CreateCheckoutSession(sessionReq, false) + if err != nil { + return fmt.Errorf("Mpesa: failed to create session: %w", err) + } + + // Step 2: Execute Transfer + transferURL := fmt.Sprintf("%s/api/Mpesa/b2c/transfer", s.cfg.ARIFPAY.BaseURL) + reqBody := map[string]any{ + "Sessionid": sessionResp["sessionId"], + "Phonenumber": req.PhoneNumber, + } + + payload, err := json.Marshal(reqBody) + if err != nil { + return fmt.Errorf("Mpesa: failed to marshal transfer request: %w", err) + } + + transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload)) + if err != nil { + return fmt.Errorf("Mpesa: failed to build transfer request: %w", err) + } + transferReq.Header.Set("Content-Type", "application/json") + transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) + + transferResp, err := s.httpClient.Do(transferReq) + if err != nil { + return fmt.Errorf("Mpesa: failed to execute transfer request: %w", err) + } + defer transferResp.Body.Close() + + if transferResp.StatusCode >= 300 { + body, _ := io.ReadAll(transferResp.Body) + return fmt.Errorf("Mpesa: transfer failed with status %d: %s", transferResp.StatusCode, string(body)) + } + + // Step 3: Store transfer in DB + transfer := domain.CreateTransfer{ + Amount: domain.Currency(req.Amount), + Verified: false, + Type: domain.WITHDRAW, // B2C = payout + ReferenceNumber: referenceNum, + SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]), + Status: string(domain.PaymentStatusPending), + PaymentMethod: domain.TRANSFER_ARIFPAY, + } + if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { + return fmt.Errorf("Mpesa: failed to store transfer: %w", err) + } + + // Step 4: Deduct from user wallet + userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) + if err != nil { + return fmt.Errorf("Mpesa: failed to get user wallets: %w", err) + } + if len(userWallets) == 0 { + return fmt.Errorf("Mpesa: no wallet found for user %d", userId) + } + + _, err = s.walletSvc.DeductFromWallet( + ctx, + userWallets[0].ID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + "", + ) + if err != nil { + return fmt.Errorf("Mpesa: failed to deduct from wallet: %w", err) + } + + return nil +} + +func (s *ArifpayService) VerifyTransactionByTransactionID(ctx context.Context, req domain.ArifpayVerifyByTransactionIDRequest) (*domain.WebhookRequest, error) { + endpoint := fmt.Sprintf("%s/api/checkout/getSessionByTransactionId", s.cfg.ARIFPAY.BaseURL) + + // Marshal request payload + bodyBytes, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + // Build HTTP request + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(bodyBytes)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) + httpReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) - resp, err := s.httpClient.Do(req) + // Execute request + resp, err := s.httpClient.Do(httpReq) if err != nil { - return nil, fmt.Errorf("request failed: %w", err) + return nil, fmt.Errorf("failed to call verify transaction API: %w", err) } defer resp.Body.Close() - respBody, err := io.ReadAll(resp.Body) + // Read response body + respBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf("failed to read response: %w", err) } + // Handle non-200 responses if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("non-200 response from Arifpay: %s", string(respBody)) + return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(respBytes)) } - return respBody, nil + // Decode into domain response + var result domain.WebhookRequest + if err := json.Unmarshal(respBytes, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + return &result, nil +} + +func (s *ArifpayService) VerifyTransactionBySessionID(ctx context.Context, sessionID string) (*domain.WebhookRequest, error) { + endpoint := fmt.Sprintf("%s/api/ms/transaction/status/%s", s.cfg.ARIFPAY.BaseURL, sessionID) + + // Create HTTP GET request + httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // Set required headers + httpReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) + + // Execute request + resp, err := s.httpClient.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("failed to call verify transaction API: %w", err) + } + defer resp.Body.Close() + + // Read response body + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + // Handle non-200 responses + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(respBytes)) + } + + // Decode into domain response + var result domain.WebhookRequest + if err := json.Unmarshal(respBytes, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + return &result, nil +} + +func (s *ArifpayService) GetPaymentMethodsMapping() []domain.ARIFPAYPaymentMethod { + return []domain.ARIFPAYPaymentMethod{ + {ID: 1, Name: "ACCOUNT"}, + {ID: 2, Name: "NONYMOUS_ACCOUNT"}, + {ID: 3, Name: "ANONYMOUS_CARD"}, + {ID: 4, Name: "TELEBIRR"}, + {ID: 5, Name: "AWASH"}, + {ID: 6, Name: "AWASH_WALLET"}, + {ID: 7, Name: "PSS"}, + {ID: 8, Name: "CBE"}, + {ID: 9, Name: "AMOLE"}, + {ID: 10, Name: "BOA"}, + {ID: 11, Name: "KACHA"}, + {ID: 12, Name: "ETHSWITCH"}, + {ID: 13, Name: "TELEBIRR_USSD"}, + {ID: 14, Name: "HELLOCASH"}, + {ID: 15, Name: "MPESSA"}, + } } diff --git a/internal/services/santimpay/service.go b/internal/services/santimpay/service.go index eca451e..a557f49 100644 --- a/internal/services/santimpay/service.go +++ b/internal/services/santimpay/service.go @@ -47,7 +47,7 @@ func (s *SantimPayService) GeneratePaymentURL(input domain.GeneratePaymentURLInp SignedToken: token, SuccessRedirectURL: s.cfg.ARIFPAY.SuccessUrl, FailureRedirectURL: s.cfg.ARIFPAY.ErrorUrl, - NotifyURL: s.cfg.ARIFPAY.NotifyUrl, + NotifyURL: s.cfg.ARIFPAY.B2CNotifyUrl, CancelRedirectURL: s.cfg.ARIFPAY.CancelUrl, PhoneNumber: input.PhoneNumber, } diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 3d4638c..1a07114 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" ) var ( @@ -19,11 +20,13 @@ var ( type service struct { client *Client + walletSvc *wallet.Service } -func New(client *Client) VeliVirtualGameService { +func New(client *Client, walletSvc *wallet.Service) VeliVirtualGameService { return &service{ client: client, + walletSvc: walletSvc, } } @@ -87,20 +90,48 @@ func (s *service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) } func (s *service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) { - sigParams := map[string]string{ - "sessionId": req.SessionID, - "providerId": req.ProviderID, - "playerId": req.PlayerID, - "currency": req.Currency, - "brandId": req.BrandID, + // Retrieve player's real balance from wallet service + playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid PlayerID: %w", err) } - if req.GameID != "" { - sigParams["gameId"] = req.GameID + playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + if err != nil { + return nil, fmt.Errorf("failed to get real balance: %w", err) } - var res domain.BalanceResponse - err := s.client.post(ctx, "/balance", req, sigParams, &res) - return &res, err + realBalance := playerWallets[0].Balance + + // Retrieve bonus balance if applicable + var bonusBalance float64 + if len(playerWallets) > 1 { + bonusBalance = float64(playerWallets[1].Balance) + } else { + bonusBalance = 0 + } + + // Build the response + res := &domain.BalanceResponse{ + Real: struct { + Currency string `json:"currency"` + Amount float64 `json:"amount"` + }{ + Currency: string(playerWallets[0].Currency), + Amount: float64(realBalance), + }, + } + + if bonusBalance > 0 { + res.Bonus = &struct { + Currency string `json:"currency"` + Amount float64 `json:"amount"` + }{ + Currency: req.Currency, + Amount: bonusBalance, + } + } + + return res, nil } func (s *service) ProcessBet(ctx context.Context, req domain.BetRequest) (*domain.BetResponse, error) { diff --git a/internal/web_server/handlers/arifpay.go b/internal/web_server/handlers/arifpay.go index ad8b9ce..c61d24e 100644 --- a/internal/web_server/handlers/arifpay.go +++ b/internal/web_server/handlers/arifpay.go @@ -1,8 +1,6 @@ package handlers import ( - "encoding/json" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/gofiber/fiber/v2" ) @@ -14,13 +12,13 @@ import ( // @Tags Arifpay // @Accept json // @Produce json -// @Param request body domain.CreateCheckoutSessionRequest true "Checkout session request payload" +// @Param request body domain.CheckoutSessionClientRequest true "Checkout session request payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/arifpay/checkout [post] func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error { - var req domain.CreateCheckoutSessionRequest + var req domain.CheckoutSessionClientRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Error: err.Error(), @@ -28,7 +26,7 @@ func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error { }) } - paymentURL, err := h.arifpaySvc.CreateCheckoutSession(req) + data, err := h.arifpaySvc.CreateCheckoutSession(req, true) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Error: err.Error(), @@ -38,62 +36,138 @@ func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Checkout session created successfully", - Data: paymentURL, + Data: data, Success: true, StatusCode: fiber.StatusOK, }) } -// B2CTransferHandler handles Arifpay B2C transfers based on the transfer_mode. +// CancelCheckoutSessionHandler cancels an existing Arifpay checkout session. // -// @Summary Initiate B2C Transfer -// @Description Initiates a B2C transfer via Telebirr, CBE, or MPESA through Arifpay +// @Summary Cancel Arifpay Checkout Session +// @Description Cancels a payment session using Arifpay before completion. // @Tags Arifpay // @Accept json // @Produce json -// @Param transfer_mode query string true "Transfer mode (Telebirr, CBE, MPESA)" -// @Param request body domain.ArifPayB2CRequest true "Transfer request payload" -// @Success 200 {object} domain.Response -// @Failure 400 {object} domain.ErrorResponse -// @Failure 502 {object} domain.ErrorResponse -// @Router /api/v1/arifpay/b2c/transfer [post] -func (h *Handler) B2CTransferHandler(c *fiber.Ctx) error { - transferMode := c.Query("transfer_mode") - - var endpoint string - switch transferMode { - case "Telebirr": - endpoint = "https://telebirr-b2c.arifpay.net/api/Telebirr/b2c/transfer" - case "CBE": - endpoint = "https://cbe-b2c.arifpay.net/api/Cbebirr/b2c/transfer" - case "MPESA": - endpoint = "https://mpesa-b2c.arifpay.net/api/Mpesa/b2c/transfer" - default: +// @Param sessionId path string true "Checkout session ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/arifpay/checkout/{sessionId}/cancel [post] +func (h *Handler) CancelCheckoutSessionHandler(c *fiber.Ctx) error { + sessionID := c.Params("sessionId") + if sessionID == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Error: "invalid transfer_mode. Allowed values: Telebirr, CBE, MPESA", - Message: "Failed to process your request", + Error: "missing session ID", + Message: "Session ID is required", }) } - var req domain.ArifPayB2CRequest - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Error: err.Error(), - Message: "Failed to process your request", - }) - } - - resp, err := h.arifpaySvc.B2CTransfer(c.Context(), req, endpoint) + data, err := h.arifpaySvc.CancelCheckoutSession(c.Context(), sessionID) if err != nil { - return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Error: err.Error(), - Message: "Failed to process your request", + Message: "Failed to cancel checkout session", }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Transfer initiated successfully", - Data: resp, + Message: "Checkout session canceled successfully", + Data: data, + Success: true, + StatusCode: fiber.StatusOK, + }) +} + +// HandleWebhook processes Arifpay webhook notifications. +// +// @Summary Handle Arifpay C2B Webhook +// @Description Handles webhook notifications from Arifpay for C2B transfers and updates transfer + wallet status. +// @Tags Arifpay +// @Accept json +// @Produce json +// @Param request body domain.WebhookRequest true "Arifpay webhook payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/arifpay/c2b-webhook [post] +func (h *Handler) HandleArifpayC2BWebhook(c *fiber.Ctx) error { + var req domain.WebhookRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Invalid webhook payload", + }) + } + + // 🚨 Decide how to get userId: + // If you get it from auth context/middleware, extract it here. + // For now, let's assume userId comes from your auth claims: + userId, ok := c.Locals("user_id").(int64) + if !ok { + return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ + Error: "missing user id", + Message: "Unauthorized", + }) + } + + err := h.arifpaySvc.HandleWebhook(c.Context(), req, userId, true) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Failed to process webhook", + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Webhook processed successfully", + Success: true, + StatusCode: fiber.StatusOK, + }) +} + +// HandleWebhook processes Arifpay webhook notifications. +// +// @Summary Handle Arifpay B2C Webhook +// @Description Handles webhook notifications from Arifpay for B2C transfers and updates transfer + wallet status. +// @Tags Arifpay +// @Accept json +// @Produce json +// @Param request body domain.WebhookRequest true "Arifpay webhook payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/arifpay/b2c-webhook [post] +func (h *Handler) HandleArifpayB2CWebhook(c *fiber.Ctx) error { + var req domain.WebhookRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Invalid webhook payload", + }) + } + + // 🚨 Decide how to get userId: + // If you get it from auth context/middleware, extract it here. + // For now, let's assume userId comes from your auth claims: + userId, ok := c.Locals("user_id").(int64) + if !ok { + return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ + Error: "missing user id", + Message: "Unauthorized", + }) + } + + err := h.arifpaySvc.HandleWebhook(c.Context(), req, userId, false) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Failed to process webhook", + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Webhook processed successfully", Success: true, StatusCode: fiber.StatusOK, }) @@ -119,14 +193,7 @@ func (h *Handler) ArifpayVerifyByTransactionIDHandler(c *fiber.Ctx) error { }) } - if req.TransactionID == "" || req.PaymentType == 0 { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Error: "missing transactionId or paymentType", - Message: "transactionId and paymentType are required fields", - }) - } - - resp, err := h.arifpaySvc.VerifyByTransactionID(req.TransactionID, req.PaymentType) + resp, err := h.arifpaySvc.VerifyTransactionByTransactionID(c.Context(), req) if err != nil { return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Error: err.Error(), @@ -136,7 +203,7 @@ func (h *Handler) ArifpayVerifyByTransactionIDHandler(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Transaction verified successfully", - Data: json.RawMessage(resp), + Data: resp, Success: true, StatusCode: fiber.StatusOK, }) @@ -162,18 +229,104 @@ func (h *Handler) ArifpayVerifyBySessionIDHandler(c *fiber.Ctx) error { }) } - resp, err := h.arifpaySvc.VerifyBySessionID(sessionID) + resp, err := h.arifpaySvc.VerifyTransactionBySessionID(c.Context(), sessionID) if err != nil { return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Error: err.Error(), - Message: "Failed to verify session", + Message: "Failed to verify transaction", }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Session verified successfully", - Data: json.RawMessage(resp), + Message: "Transaction verified successfully", + Data: resp, Success: true, StatusCode: fiber.StatusOK, }) } + +// ExecuteTransfer handles B2C transfers via Telebirr, CBE, or MPESA. +// +// @Summary Execute B2C Transfer +// @Description Initiates a B2C transfer using Telebirr, CBE, or MPESA depending on the "type" query parameter +// @Tags Arifpay +// @Accept json +// @Produce json +// @Param type query string true "Transfer type (telebirr, cbe, mpesa)" +// @Param request body domain.ArifpayB2CRequest true "Transfer request payload" +// @Success 200 {object} map[string]string "message: transfer executed successfully" +// @Failure 400 {object} map[string]string "error: invalid request or unsupported transfer type" +// @Failure 500 {object} map[string]string "error: internal server error" +// @Router /api/v1/arifpay/b2c/transfer [post] +func (h *Handler) ExecuteArifpayB2CTransfer(c *fiber.Ctx) error { + transferType := c.Query("type") + if transferType == "" { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to process your withdrawal request", + Error: "missing query parameter: type (telebirr, cbe, mpesa)", + }) + } + + userId, ok := c.Locals("user_id").(int64) + if !ok { + return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ + Error: "missing user id", + Message: "Unauthorized", + }) + } + + var req domain.ArifpayB2CRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to process your withdrawal request", + Error: "invalid request body", + }) + } + + var err error + switch transferType { + case "telebirr": + err = h.arifpaySvc.ExecuteTelebirrB2CTransfer(c.Context(), req, userId) + case "cbe": + err = h.arifpaySvc.ExecuteCBEB2CTransfer(c.Context(), req, userId) + case "mpesa": + err = h.arifpaySvc.ExecuteMPesaB2CTransfer(c.Context(), req, userId) + default: + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to process your withdrawal request", + Error: "unsupported transfer type, must be one of: telebirr, cbe, mpesa", + }) + } + + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to process your withdrawal request", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Withdrawal process initiated successfully", + Success: true, + StatusCode: fiber.StatusOK, + }) +} + +// GetPaymentMethodsHandler returns the list of all Arifpay payment methods +// +// @Summary List Arifpay Payment Methods +// @Description Returns all payment method IDs and names for Arifpay +// @Tags Arifpay +// @Produce json +// @Success 200 {object} []domain.ARIFPAYPaymentMethod +// @Router /api/v1/arifpay/payment-methods [get] +func (h *Handler) GetArifpayPaymentMethodsHandler(c *fiber.Ctx) error { + methods := h.arifpaySvc.GetPaymentMethodsMapping() + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Success: true, + Message: "Arifpay payment methods fetched successfully", + Data: methods, + StatusCode: fiber.StatusOK, + }) +} diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go index 2bdacd6..4378f9b 100644 --- a/internal/web_server/handlers/veli_games.go +++ b/internal/web_server/handlers/veli_games.go @@ -175,14 +175,22 @@ func (h *Handler) StartDemoGame(c *fiber.Ctx) error { func (h *Handler) GetBalance(c *fiber.Ctx) error { var req domain.BalanceRequest if err := c.BodyParser(&req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + // return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) } // Optionally verify signature here... balance, err := h.veliVirtualGameSvc.GetBalance(c.Context(), req) if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + // return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve balance", + Error: err.Error(), + }) } return c.JSON(balance) @@ -191,7 +199,11 @@ func (h *Handler) GetBalance(c *fiber.Ctx) error { func (h *Handler) PlaceBet(c *fiber.Ctx) error { var req domain.BetRequest if err := c.BodyParser(&req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + // return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) } // Signature check optional here @@ -201,7 +213,11 @@ func (h *Handler) PlaceBet(c *fiber.Ctx) error { if errors.Is(err, veli.ErrDuplicateTransaction) { return fiber.NewError(fiber.StatusConflict, "DUPLICATE_TRANSACTION") } - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to process bet", + Error: err.Error(), + }) + // return fiber.NewError(fiber.StatusBadRequest, err.Error()) } return c.JSON(res) @@ -210,7 +226,11 @@ func (h *Handler) PlaceBet(c *fiber.Ctx) error { func (h *Handler) RegisterWin(c *fiber.Ctx) error { var req domain.WinRequest if err := c.BodyParser(&req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + // return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) } res, err := h.veliVirtualGameSvc.ProcessWin(c.Context(), req) @@ -218,7 +238,11 @@ func (h *Handler) RegisterWin(c *fiber.Ctx) error { if errors.Is(err, veli.ErrDuplicateTransaction) { return fiber.NewError(fiber.StatusConflict, "DUPLICATE_TRANSACTION") } - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + // return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to process win", + Error: err.Error(), + }) } return c.JSON(res) @@ -227,7 +251,11 @@ func (h *Handler) RegisterWin(c *fiber.Ctx) error { func (h *Handler) CancelTransaction(c *fiber.Ctx) error { var req domain.CancelRequest if err := c.BodyParser(&req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + // return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) } res, err := h.veliVirtualGameSvc.ProcessCancel(c.Context(), req) @@ -235,7 +263,11 @@ func (h *Handler) CancelTransaction(c *fiber.Ctx) error { if errors.Is(err, veli.ErrDuplicateTransaction) { return fiber.NewError(fiber.StatusConflict, "DUPLICATE_TRANSACTION") } - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + // return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to process cancel", + Error: err.Error(), + }) } return c.JSON(res) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 8132608..d27718f 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -114,9 +114,13 @@ func (a *App) initAppRoutes() { //Arifpay groupV1.Post("/arifpay/checkout", a.authMiddleware, h.CreateCheckoutSessionHandler) - groupV1.Post("/arifpay/b2c/transfer", a.authMiddleware, h.B2CTransferHandler) + groupV1.Post("/arifpay/checkout/cancel/:session_id", a.authMiddleware, h.CancelCheckoutSessionHandler) + groupV1.Post("/api/v1/arifpay/c2b-webhook", a.authMiddleware, h.HandleArifpayC2BWebhook) + groupV1.Post("/api/v1/arifpay/b2c-webhook", a.authMiddleware, h.HandleArifpayB2CWebhook) + groupV1.Post("/arifpay/b2c/transfer", a.authMiddleware, h.ExecuteArifpayB2CTransfer) groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler) groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler) + groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler) //Telebirr groupV1.Post("/telebirr/init-payment", a.authMiddleware, h.CreateTelebirrPaymentHandler) @@ -281,7 +285,7 @@ func (a *App) initAppRoutes() { //Veli Virtual Game Routes groupV1.Post("/veli/providers", h.GetProviders) groupV1.Post("/veli/games-list", h.GetGamesByProvider) - groupV1.Post("/veli/start-game", a.authMiddleware, h.StartGame) + groupV1.Post("/veli/start-game", h.StartGame) groupV1.Post("/veli/start-demo-game", h.StartDemoGame) a.fiber.Post("/balance", h.GetBalance) groupV1.Post("/veli/gaming-activity", h.GetGamingActivity) From 5283efeb1e6212897e0d2903bdeaa1dfa84ca675 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Sun, 17 Aug 2025 12:34:20 +0300 Subject: [PATCH 2/4] swagger fix --- docs/docs.go | 1288 +++++++++++++++++++++++++++++++---- docs/swagger.json | 1288 +++++++++++++++++++++++++++++++---- docs/swagger.yaml | 869 ++++++++++++++++++++--- internal/domain/shop_bet.go | 2 +- 4 files changed, 3048 insertions(+), 399 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 012d2f0..db60542 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -339,9 +339,9 @@ const docTemplate = `{ } } }, - "/api/v1/arifpay/b2c/transfer": { + "/api/v1/arifpay/b2c-webhook": { "post": { - "description": "Initiates a B2C transfer via Telebirr, CBE, or MPESA through Arifpay", + "description": "Handles webhook notifications from Arifpay for B2C transfers and updates transfer + wallet status.", "consumes": [ "application/json" ], @@ -351,22 +351,15 @@ const docTemplate = `{ "tags": [ "Arifpay" ], - "summary": "Initiate B2C Transfer", + "summary": "Handle Arifpay B2C Webhook", "parameters": [ { - "type": "string", - "description": "Transfer mode (Telebirr, CBE, MPESA)", - "name": "transfer_mode", - "in": "query", - "required": true - }, - { - "description": "Transfer request payload", + "description": "Arifpay webhook payload", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.ArifPayB2CRequest" + "$ref": "#/definitions/domain.WebhookRequest" } } ], @@ -383,8 +376,116 @@ const docTemplate = `{ "$ref": "#/definitions/domain.ErrorResponse" } }, - "502": { - "description": "Bad Gateway", + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/arifpay/b2c/transfer": { + "post": { + "description": "Initiates a B2C transfer using Telebirr, CBE, or MPESA depending on the \"type\" query parameter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Arifpay" + ], + "summary": "Execute B2C Transfer", + "parameters": [ + { + "type": "string", + "description": "Transfer type (telebirr, cbe, mpesa)", + "name": "type", + "in": "query", + "required": true + }, + { + "description": "Transfer request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ArifpayB2CRequest" + } + } + ], + "responses": { + "200": { + "description": "message: transfer executed successfully", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "error: invalid request or unsupported transfer type", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "error: internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/arifpay/c2b-webhook": { + "post": { + "description": "Handles webhook notifications from Arifpay for C2B transfers and updates transfer + wallet status.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Arifpay" + ], + "summary": "Handle Arifpay C2B Webhook", + "parameters": [ + { + "description": "Arifpay webhook payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.WebhookRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/domain.ErrorResponse" } @@ -412,7 +513,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.CreateCheckoutSessionRequest" + "$ref": "#/definitions/domain.CheckoutSessionClientRequest" } } ], @@ -438,6 +539,73 @@ const docTemplate = `{ } } }, + "/api/v1/arifpay/checkout/{sessionId}/cancel": { + "post": { + "description": "Cancels a payment session using Arifpay before completion.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Arifpay" + ], + "summary": "Cancel Arifpay Checkout Session", + "parameters": [ + { + "type": "string", + "description": "Checkout session ID", + "name": "sessionId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/arifpay/payment-methods": { + "get": { + "description": "Returns all payment method IDs and names for Arifpay", + "produces": [ + "application/json" + ], + "tags": [ + "Arifpay" + ], + "summary": "List Arifpay Payment Methods", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ARIFPAYPaymentMethod" + } + } + } + } + } + }, "/api/v1/arifpay/session-id/verify-transaction/{session_id}": { "get": { "description": "Verifies an Arifpay transaction using a session ID", @@ -528,7 +696,59 @@ const docTemplate = `{ } } }, - "/api/v1/auth/login": { + "/api/v1/auth/admin-login": { + "post": { + "description": "Login customer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Login customer", + "parameters": [ + { + "description": "Login admin", + "name": "login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.loginAdminReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.loginAdminRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/auth/customer-login": { "post": { "description": "Login customer", "consumes": [ @@ -1379,6 +1599,44 @@ const docTemplate = `{ } } }, + "/api/v1/branchLocation": { + "get": { + "description": "Gets all branch locations", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets all branch locations", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BranchLocation" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/branchWallet": { "get": { "description": "Retrieve all branch wallets", @@ -2473,6 +2731,130 @@ const docTemplate = `{ } } }, + "/api/v1/direct_deposit": { + "post": { + "description": "Customer initiates a direct deposit from mobile banking", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Direct Deposits" + ], + "summary": "Initiate a direct deposit", + "parameters": [ + { + "description": "Deposit details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.DirectDepositRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/direct_deposit/pending": { + "get": { + "description": "Get list of direct deposits needing verification", + "produces": [ + "application/json" + ], + "tags": [ + "Direct Deposits" + ], + "summary": "Get pending direct deposits", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/direct_deposit/verify": { + "post": { + "description": "Cashier verifies a direct deposit transaction", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Direct Deposits" + ], + "summary": "Verify a direct deposit", + "parameters": [ + { + "description": "Verification details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.VerifyDirectDepositRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/events": { "get": { "description": "Retrieve all upcoming events from the database", @@ -2637,7 +3019,7 @@ const docTemplate = `{ }, "/api/v1/events/{id}/flag": { "put": { - "description": "Update the event flagged", + "description": "Update the event featured", "consumes": [ "application/json" ], @@ -2647,7 +3029,7 @@ const docTemplate = `{ "tags": [ "event" ], - "summary": "update the event flagged", + "summary": "update the event featured", "parameters": [ { "type": "integer", @@ -3929,6 +4311,52 @@ const docTemplate = `{ } } }, + "/api/v1/santimpay/payment": { + "post": { + "description": "Generates a payment URL using SantimPay and returns it to the client.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SantimPay" + ], + "summary": "Create SantimPay Payment Session", + "parameters": [ + { + "description": "SantimPay payment request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GeneratePaymentURLInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/search/branch": { "get": { "description": "Search branches by name or location", @@ -4015,6 +4443,42 @@ const docTemplate = `{ } }, "/api/v1/shop/bet": { + "get": { + "description": "Gets all the shop bets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets all shop bets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ShopBetRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, "post": { "description": "Create bet at branch", "consumes": [ @@ -4595,6 +5059,50 @@ const docTemplate = `{ } } }, + "/api/v1/sport/bet/fastcode/{fast_code}": { + "get": { + "description": "Gets a single bet by fast_code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by fast_code", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "fast_code", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/sport/bet/{id}": { "get": { "description": "Gets a single bet by id", @@ -4860,6 +5368,98 @@ const docTemplate = `{ } } }, + "/api/v1/telebirr/callback": { + "post": { + "description": "Processes the Telebirr payment result and updates wallet balance.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Telebirr" + ], + "summary": "Handle Telebirr Payment Callback", + "parameters": [ + { + "description": "Callback payload from Telebirr", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.TelebirrPaymentCallbackPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/telebirr/payment": { + "post": { + "description": "Generates a payment URL using Telebirr and returns it to the client.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Telebirr" + ], + "summary": "Create Telebirr Payment Session", + "parameters": [ + { + "description": "Telebirr payment request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GeneratePaymentURLInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/ticket": { "get": { "description": "Retrieve all tickets", @@ -5156,6 +5756,46 @@ const docTemplate = `{ } } }, + "/api/v1/user/admin-profile": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get user profile", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get user profile", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.AdminProfileRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/user/bets": { "get": { "description": "Gets user bets", @@ -5240,6 +5880,46 @@ const docTemplate = `{ } } }, + "/api/v1/user/customer-profile": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get user profile", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get user profile", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomerProfileRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/user/delete/{id}": { "delete": { "description": "Delete a user by their ID", @@ -5284,46 +5964,6 @@ const docTemplate = `{ } } }, - "/api/v1/user/profile": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Get user profile", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "user" - ], - "summary": "Get user profile", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handlers.UserProfileRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/api/v1/user/register": { "post": { "description": "Register user", @@ -6511,6 +7151,17 @@ const docTemplate = `{ } }, "definitions": { + "domain.ARIFPAYPaymentMethod": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, "domain.AleaPlayCallback": { "type": "object", "properties": { @@ -6559,13 +7210,24 @@ const docTemplate = `{ } } }, - "domain.ArifPayB2CRequest": { + "domain.ArifpayB2CRequest": { "type": "object", + "required": [ + "amount", + "customerEmail", + "customerPhone" + ], "properties": { "Phonenumber": { "type": "string" }, - "Sessionid": { + "amount": { + "type": "number" + }, + "customerEmail": { + "type": "string" + }, + "customerPhone": { "type": "string" } } @@ -6634,20 +7296,6 @@ const docTemplate = `{ } } }, - "domain.Beneficiary": { - "type": "object", - "properties": { - "accountNumber": { - "type": "string" - }, - "amount": { - "type": "number" - }, - "bank": { - "type": "string" - } - } - }, "domain.BetOutcome": { "type": "object", "properties": { @@ -6818,12 +7466,29 @@ const docTemplate = `{ "type": "string", "example": "4-kilo Branch" }, + "profit_percentage": { + "type": "number", + "example": 0.1 + }, "wallet_id": { "type": "integer", "example": 1 } } }, + "domain.BranchLocation": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "addis_ababa" + }, + "name": { + "type": "string", + "example": "Addis Ababa" + } + } + }, "domain.BranchOperationRes": { "type": "object", "properties": { @@ -6868,6 +7533,10 @@ const docTemplate = `{ "type": "string", "example": "4-kilo Branch" }, + "profit_percentage": { + "type": "number", + "example": 0.1 + }, "wallet_id": { "type": "integer", "example": 1 @@ -6990,6 +7659,25 @@ const docTemplate = `{ } } }, + "domain.CheckoutSessionClientRequest": { + "type": "object", + "required": [ + "amount", + "customerEmail", + "customerPhone" + ], + "properties": { + "amount": { + "type": "number" + }, + "customerEmail": { + "type": "string" + }, + "customerPhone": { + "type": "string" + } + } + }, "domain.CompanyRes": { "type": "object", "properties": { @@ -7040,7 +7728,6 @@ const docTemplate = `{ "type": "object", "required": [ "amount", - "branch_id", "outcomes" ], "properties": { @@ -7125,56 +7812,10 @@ const docTemplate = `{ "items": { "type": "integer" } - } - } - }, - "domain.CreateCheckoutSessionRequest": { - "type": "object", - "properties": { - "beneficiaries": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Beneficiary" - } }, - "cancelUrl": { - "type": "string" - }, - "email": { - "type": "string" - }, - "errorUrl": { - "type": "string" - }, - "expireDate": { - "type": "string" - }, - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Item" - } - }, - "lang": { - "type": "string" - }, - "nonce": { - "type": "string" - }, - "notifyUrl": { - "type": "string" - }, - "paymentMethods": { - "type": "array", - "items": { - "type": "string" - } - }, - "phone": { - "type": "string" - }, - "successUrl": { - "type": "string" + "profit_percentage": { + "type": "number", + "example": 0.1 } } }, @@ -7185,6 +7826,10 @@ const docTemplate = `{ "type": "integer", "example": 1 }, + "deducted_percentage": { + "type": "number", + "example": 0.1 + }, "name": { "type": "string", "example": "CompanyName" @@ -7392,6 +8037,29 @@ const docTemplate = `{ } } }, + "domain.DirectDepositRequest": { + "type": "object", + "required": [ + "amount", + "bank_reference", + "customer_id", + "sender_account" + ], + "properties": { + "amount": { + "type": "integer" + }, + "bank_reference": { + "type": "string" + }, + "customer_id": { + "type": "integer" + }, + "sender_account": { + "type": "string" + } + } + }, "domain.ErrorResponse": { "type": "object", "properties": { @@ -7667,6 +8335,23 @@ const docTemplate = `{ } } }, + "domain.GeneratePaymentURLInput": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "phoneNumber": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + }, "domain.GetCompanyRes": { "type": "object", "properties": { @@ -7738,26 +8423,6 @@ const docTemplate = `{ } } }, - "domain.Item": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "image": { - "type": "string" - }, - "name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "quantity": { - "type": "integer" - } - } - }, "domain.League": { "type": "object", "properties": { @@ -8308,6 +8973,82 @@ const docTemplate = `{ } } }, + "domain.ShopBetRes": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "bet_id": { + "type": "integer", + "example": 1 + }, + "branch_id": { + "type": "integer", + "example": 2 + }, + "cashed_out": { + "type": "boolean", + "example": false + }, + "cashout_id": { + "type": "string", + "example": "21234" + }, + "company_id": { + "type": "integer", + "example": 2 + }, + "created_at": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + }, + "full_name": { + "type": "string", + "example": "John" + }, + "id": { + "type": "integer" + }, + "number_of_outcomes": { + "type": "integer", + "example": 1 + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetOutcome" + } + }, + "phone_number": { + "type": "string", + "example": "1234567890" + }, + "shop_transaction_id": { + "type": "integer" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.OutcomeStatus" + } + ], + "example": 1 + }, + "total_odds": { + "type": "number", + "example": 4.22 + }, + "transaction_verified": { + "type": "boolean", + "example": true + }, + "updated_at": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + } + } + }, "domain.ShopDepositReq": { "type": "object", "properties": { @@ -8493,6 +9234,67 @@ const docTemplate = `{ } } }, + "domain.TelebirrPaymentCallbackPayload": { + "type": "object", + "properties": { + "appid": { + "description": "App ID provided by Telebirr", + "type": "string" + }, + "callback_info": { + "description": "Optional merchant-defined callback data", + "type": "string" + }, + "merch_code": { + "description": "Merchant short code", + "type": "string" + }, + "merch_order_id": { + "description": "Order ID from merchant system", + "type": "string" + }, + "notify_time": { + "description": "Notification timestamp (UTC, in seconds)", + "type": "string" + }, + "notify_url": { + "description": "Optional callback URL", + "type": "string" + }, + "payment_order_id": { + "description": "Order ID from Telebirr system", + "type": "string" + }, + "sign": { + "description": "Signature of the payload", + "type": "string" + }, + "sign_type": { + "description": "Signature type, e.g., SHA256WithRSA", + "type": "string" + }, + "total_amount": { + "description": "Payment amount", + "type": "string" + }, + "trade_status": { + "description": "Payment status (e.g., Completed, Failure)", + "type": "string" + }, + "trans_currency": { + "description": "Currency type (e.g., ETB)", + "type": "string" + }, + "trans_end_time": { + "description": "Transaction end time (UTC seconds)", + "type": "string" + }, + "trans_id": { + "description": "Transaction ID", + "type": "string" + } + } + }, "domain.TicketOutcome": { "type": "object", "properties": { @@ -8596,10 +9398,6 @@ const docTemplate = `{ "description": "Away team ID (can be empty/null)", "type": "integer" }, - "flagged": { - "description": "Whether the event is flagged or not", - "type": "boolean" - }, "home_kit_image": { "description": "Kit or image for home team (optional)", "type": "string" @@ -8616,6 +9414,14 @@ const docTemplate = `{ "description": "Event ID", "type": "string" }, + "is_active": { + "description": "Whether the event is featured or not", + "type": "boolean" + }, + "is_featured": { + "description": "Whether the event is featured or not", + "type": "boolean" + }, "league_cc": { "description": "League country code", "type": "string" @@ -8684,6 +9490,108 @@ const docTemplate = `{ } } }, + "domain.VerifyDirectDepositRequest": { + "type": "object", + "required": [ + "deposit_id", + "is_verified" + ], + "properties": { + "deposit_id": { + "type": "integer" + }, + "is_verified": { + "type": "boolean" + }, + "notes": { + "type": "string" + } + } + }, + "domain.WebhookRequest": { + "type": "object", + "properties": { + "nonce": { + "type": "string" + }, + "notificationUrl": { + "type": "string" + }, + "paymentMethod": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "sessionId": { + "type": "string" + }, + "totalAmount": { + "type": "integer" + }, + "transaction": { + "type": "object", + "properties": { + "transactionId": { + "type": "string" + }, + "transactionStatus": { + "type": "string" + } + } + }, + "transactionStatus": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "handlers.AdminProfileRes": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_verified": { + "type": "boolean" + }, + "role": { + "$ref": "#/definitions/domain.Role" + }, + "suspended": { + "type": "boolean" + }, + "suspended_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handlers.AdminRes": { "type": "object", "properties": { @@ -8856,6 +9764,53 @@ const docTemplate = `{ } } }, + "handlers.CustomerProfileRes": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_verified": { + "type": "boolean" + }, + "referral_code": { + "type": "string" + }, + "role": { + "$ref": "#/definitions/domain.Role" + }, + "suspended": { + "type": "boolean" + }, + "suspended_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handlers.CustomerWalletRes": { "type": "object", "properties": { @@ -9333,6 +10288,9 @@ const docTemplate = `{ "phone_verified": { "type": "boolean" }, + "referral_code": { + "type": "string" + }, "role": { "$ref": "#/definitions/domain.Role" }, @@ -9420,6 +10378,40 @@ const docTemplate = `{ } } }, + "handlers.loginAdminReq": { + "type": "object", + "required": [ + "password" + ], + "properties": { + "email": { + "type": "string", + "example": "john.doe@example.com" + }, + "password": { + "type": "string", + "example": "password123" + }, + "phone_number": { + "type": "string", + "example": "1234567890" + } + } + }, + "handlers.loginAdminRes": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, "handlers.loginCustomerReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index f65862a..efe4771 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -331,9 +331,9 @@ } } }, - "/api/v1/arifpay/b2c/transfer": { + "/api/v1/arifpay/b2c-webhook": { "post": { - "description": "Initiates a B2C transfer via Telebirr, CBE, or MPESA through Arifpay", + "description": "Handles webhook notifications from Arifpay for B2C transfers and updates transfer + wallet status.", "consumes": [ "application/json" ], @@ -343,22 +343,15 @@ "tags": [ "Arifpay" ], - "summary": "Initiate B2C Transfer", + "summary": "Handle Arifpay B2C Webhook", "parameters": [ { - "type": "string", - "description": "Transfer mode (Telebirr, CBE, MPESA)", - "name": "transfer_mode", - "in": "query", - "required": true - }, - { - "description": "Transfer request payload", + "description": "Arifpay webhook payload", "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.ArifPayB2CRequest" + "$ref": "#/definitions/domain.WebhookRequest" } } ], @@ -375,8 +368,116 @@ "$ref": "#/definitions/domain.ErrorResponse" } }, - "502": { - "description": "Bad Gateway", + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/arifpay/b2c/transfer": { + "post": { + "description": "Initiates a B2C transfer using Telebirr, CBE, or MPESA depending on the \"type\" query parameter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Arifpay" + ], + "summary": "Execute B2C Transfer", + "parameters": [ + { + "type": "string", + "description": "Transfer type (telebirr, cbe, mpesa)", + "name": "type", + "in": "query", + "required": true + }, + { + "description": "Transfer request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ArifpayB2CRequest" + } + } + ], + "responses": { + "200": { + "description": "message: transfer executed successfully", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "error: invalid request or unsupported transfer type", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "error: internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/arifpay/c2b-webhook": { + "post": { + "description": "Handles webhook notifications from Arifpay for C2B transfers and updates transfer + wallet status.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Arifpay" + ], + "summary": "Handle Arifpay C2B Webhook", + "parameters": [ + { + "description": "Arifpay webhook payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.WebhookRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/domain.ErrorResponse" } @@ -404,7 +505,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.CreateCheckoutSessionRequest" + "$ref": "#/definitions/domain.CheckoutSessionClientRequest" } } ], @@ -430,6 +531,73 @@ } } }, + "/api/v1/arifpay/checkout/{sessionId}/cancel": { + "post": { + "description": "Cancels a payment session using Arifpay before completion.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Arifpay" + ], + "summary": "Cancel Arifpay Checkout Session", + "parameters": [ + { + "type": "string", + "description": "Checkout session ID", + "name": "sessionId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/arifpay/payment-methods": { + "get": { + "description": "Returns all payment method IDs and names for Arifpay", + "produces": [ + "application/json" + ], + "tags": [ + "Arifpay" + ], + "summary": "List Arifpay Payment Methods", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ARIFPAYPaymentMethod" + } + } + } + } + } + }, "/api/v1/arifpay/session-id/verify-transaction/{session_id}": { "get": { "description": "Verifies an Arifpay transaction using a session ID", @@ -520,7 +688,59 @@ } } }, - "/api/v1/auth/login": { + "/api/v1/auth/admin-login": { + "post": { + "description": "Login customer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Login customer", + "parameters": [ + { + "description": "Login admin", + "name": "login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.loginAdminReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.loginAdminRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/auth/customer-login": { "post": { "description": "Login customer", "consumes": [ @@ -1371,6 +1591,44 @@ } } }, + "/api/v1/branchLocation": { + "get": { + "description": "Gets all branch locations", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets all branch locations", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BranchLocation" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/branchWallet": { "get": { "description": "Retrieve all branch wallets", @@ -2465,6 +2723,130 @@ } } }, + "/api/v1/direct_deposit": { + "post": { + "description": "Customer initiates a direct deposit from mobile banking", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Direct Deposits" + ], + "summary": "Initiate a direct deposit", + "parameters": [ + { + "description": "Deposit details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.DirectDepositRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/direct_deposit/pending": { + "get": { + "description": "Get list of direct deposits needing verification", + "produces": [ + "application/json" + ], + "tags": [ + "Direct Deposits" + ], + "summary": "Get pending direct deposits", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/direct_deposit/verify": { + "post": { + "description": "Cashier verifies a direct deposit transaction", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Direct Deposits" + ], + "summary": "Verify a direct deposit", + "parameters": [ + { + "description": "Verification details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.VerifyDirectDepositRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/events": { "get": { "description": "Retrieve all upcoming events from the database", @@ -2629,7 +3011,7 @@ }, "/api/v1/events/{id}/flag": { "put": { - "description": "Update the event flagged", + "description": "Update the event featured", "consumes": [ "application/json" ], @@ -2639,7 +3021,7 @@ "tags": [ "event" ], - "summary": "update the event flagged", + "summary": "update the event featured", "parameters": [ { "type": "integer", @@ -3921,6 +4303,52 @@ } } }, + "/api/v1/santimpay/payment": { + "post": { + "description": "Generates a payment URL using SantimPay and returns it to the client.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SantimPay" + ], + "summary": "Create SantimPay Payment Session", + "parameters": [ + { + "description": "SantimPay payment request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GeneratePaymentURLInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/search/branch": { "get": { "description": "Search branches by name or location", @@ -4007,6 +4435,42 @@ } }, "/api/v1/shop/bet": { + "get": { + "description": "Gets all the shop bets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets all shop bets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ShopBetRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, "post": { "description": "Create bet at branch", "consumes": [ @@ -4587,6 +5051,50 @@ } } }, + "/api/v1/sport/bet/fastcode/{fast_code}": { + "get": { + "description": "Gets a single bet by fast_code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by fast_code", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "fast_code", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/sport/bet/{id}": { "get": { "description": "Gets a single bet by id", @@ -4852,6 +5360,98 @@ } } }, + "/api/v1/telebirr/callback": { + "post": { + "description": "Processes the Telebirr payment result and updates wallet balance.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Telebirr" + ], + "summary": "Handle Telebirr Payment Callback", + "parameters": [ + { + "description": "Callback payload from Telebirr", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.TelebirrPaymentCallbackPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/telebirr/payment": { + "post": { + "description": "Generates a payment URL using Telebirr and returns it to the client.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Telebirr" + ], + "summary": "Create Telebirr Payment Session", + "parameters": [ + { + "description": "Telebirr payment request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GeneratePaymentURLInput" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/ticket": { "get": { "description": "Retrieve all tickets", @@ -5148,6 +5748,46 @@ } } }, + "/api/v1/user/admin-profile": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get user profile", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get user profile", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.AdminProfileRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/user/bets": { "get": { "description": "Gets user bets", @@ -5232,6 +5872,46 @@ } } }, + "/api/v1/user/customer-profile": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get user profile", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get user profile", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CustomerProfileRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/user/delete/{id}": { "delete": { "description": "Delete a user by their ID", @@ -5276,46 +5956,6 @@ } } }, - "/api/v1/user/profile": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Get user profile", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "user" - ], - "summary": "Get user profile", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handlers.UserProfileRes" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/api/v1/user/register": { "post": { "description": "Register user", @@ -6503,6 +7143,17 @@ } }, "definitions": { + "domain.ARIFPAYPaymentMethod": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, "domain.AleaPlayCallback": { "type": "object", "properties": { @@ -6551,13 +7202,24 @@ } } }, - "domain.ArifPayB2CRequest": { + "domain.ArifpayB2CRequest": { "type": "object", + "required": [ + "amount", + "customerEmail", + "customerPhone" + ], "properties": { "Phonenumber": { "type": "string" }, - "Sessionid": { + "amount": { + "type": "number" + }, + "customerEmail": { + "type": "string" + }, + "customerPhone": { "type": "string" } } @@ -6626,20 +7288,6 @@ } } }, - "domain.Beneficiary": { - "type": "object", - "properties": { - "accountNumber": { - "type": "string" - }, - "amount": { - "type": "number" - }, - "bank": { - "type": "string" - } - } - }, "domain.BetOutcome": { "type": "object", "properties": { @@ -6810,12 +7458,29 @@ "type": "string", "example": "4-kilo Branch" }, + "profit_percentage": { + "type": "number", + "example": 0.1 + }, "wallet_id": { "type": "integer", "example": 1 } } }, + "domain.BranchLocation": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "addis_ababa" + }, + "name": { + "type": "string", + "example": "Addis Ababa" + } + } + }, "domain.BranchOperationRes": { "type": "object", "properties": { @@ -6860,6 +7525,10 @@ "type": "string", "example": "4-kilo Branch" }, + "profit_percentage": { + "type": "number", + "example": 0.1 + }, "wallet_id": { "type": "integer", "example": 1 @@ -6982,6 +7651,25 @@ } } }, + "domain.CheckoutSessionClientRequest": { + "type": "object", + "required": [ + "amount", + "customerEmail", + "customerPhone" + ], + "properties": { + "amount": { + "type": "number" + }, + "customerEmail": { + "type": "string" + }, + "customerPhone": { + "type": "string" + } + } + }, "domain.CompanyRes": { "type": "object", "properties": { @@ -7032,7 +7720,6 @@ "type": "object", "required": [ "amount", - "branch_id", "outcomes" ], "properties": { @@ -7117,56 +7804,10 @@ "items": { "type": "integer" } - } - } - }, - "domain.CreateCheckoutSessionRequest": { - "type": "object", - "properties": { - "beneficiaries": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Beneficiary" - } }, - "cancelUrl": { - "type": "string" - }, - "email": { - "type": "string" - }, - "errorUrl": { - "type": "string" - }, - "expireDate": { - "type": "string" - }, - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Item" - } - }, - "lang": { - "type": "string" - }, - "nonce": { - "type": "string" - }, - "notifyUrl": { - "type": "string" - }, - "paymentMethods": { - "type": "array", - "items": { - "type": "string" - } - }, - "phone": { - "type": "string" - }, - "successUrl": { - "type": "string" + "profit_percentage": { + "type": "number", + "example": 0.1 } } }, @@ -7177,6 +7818,10 @@ "type": "integer", "example": 1 }, + "deducted_percentage": { + "type": "number", + "example": 0.1 + }, "name": { "type": "string", "example": "CompanyName" @@ -7384,6 +8029,29 @@ } } }, + "domain.DirectDepositRequest": { + "type": "object", + "required": [ + "amount", + "bank_reference", + "customer_id", + "sender_account" + ], + "properties": { + "amount": { + "type": "integer" + }, + "bank_reference": { + "type": "string" + }, + "customer_id": { + "type": "integer" + }, + "sender_account": { + "type": "string" + } + } + }, "domain.ErrorResponse": { "type": "object", "properties": { @@ -7659,6 +8327,23 @@ } } }, + "domain.GeneratePaymentURLInput": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "phoneNumber": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + }, "domain.GetCompanyRes": { "type": "object", "properties": { @@ -7730,26 +8415,6 @@ } } }, - "domain.Item": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "image": { - "type": "string" - }, - "name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "quantity": { - "type": "integer" - } - } - }, "domain.League": { "type": "object", "properties": { @@ -8300,6 +8965,82 @@ } } }, + "domain.ShopBetRes": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "bet_id": { + "type": "integer", + "example": 1 + }, + "branch_id": { + "type": "integer", + "example": 2 + }, + "cashed_out": { + "type": "boolean", + "example": false + }, + "cashout_id": { + "type": "string", + "example": "21234" + }, + "company_id": { + "type": "integer", + "example": 2 + }, + "created_at": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + }, + "full_name": { + "type": "string", + "example": "John" + }, + "id": { + "type": "integer" + }, + "number_of_outcomes": { + "type": "integer", + "example": 1 + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetOutcome" + } + }, + "phone_number": { + "type": "string", + "example": "1234567890" + }, + "shop_transaction_id": { + "type": "integer" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.OutcomeStatus" + } + ], + "example": 1 + }, + "total_odds": { + "type": "number", + "example": 4.22 + }, + "transaction_verified": { + "type": "boolean", + "example": true + }, + "updated_at": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + } + } + }, "domain.ShopDepositReq": { "type": "object", "properties": { @@ -8485,6 +9226,67 @@ } } }, + "domain.TelebirrPaymentCallbackPayload": { + "type": "object", + "properties": { + "appid": { + "description": "App ID provided by Telebirr", + "type": "string" + }, + "callback_info": { + "description": "Optional merchant-defined callback data", + "type": "string" + }, + "merch_code": { + "description": "Merchant short code", + "type": "string" + }, + "merch_order_id": { + "description": "Order ID from merchant system", + "type": "string" + }, + "notify_time": { + "description": "Notification timestamp (UTC, in seconds)", + "type": "string" + }, + "notify_url": { + "description": "Optional callback URL", + "type": "string" + }, + "payment_order_id": { + "description": "Order ID from Telebirr system", + "type": "string" + }, + "sign": { + "description": "Signature of the payload", + "type": "string" + }, + "sign_type": { + "description": "Signature type, e.g., SHA256WithRSA", + "type": "string" + }, + "total_amount": { + "description": "Payment amount", + "type": "string" + }, + "trade_status": { + "description": "Payment status (e.g., Completed, Failure)", + "type": "string" + }, + "trans_currency": { + "description": "Currency type (e.g., ETB)", + "type": "string" + }, + "trans_end_time": { + "description": "Transaction end time (UTC seconds)", + "type": "string" + }, + "trans_id": { + "description": "Transaction ID", + "type": "string" + } + } + }, "domain.TicketOutcome": { "type": "object", "properties": { @@ -8588,10 +9390,6 @@ "description": "Away team ID (can be empty/null)", "type": "integer" }, - "flagged": { - "description": "Whether the event is flagged or not", - "type": "boolean" - }, "home_kit_image": { "description": "Kit or image for home team (optional)", "type": "string" @@ -8608,6 +9406,14 @@ "description": "Event ID", "type": "string" }, + "is_active": { + "description": "Whether the event is featured or not", + "type": "boolean" + }, + "is_featured": { + "description": "Whether the event is featured or not", + "type": "boolean" + }, "league_cc": { "description": "League country code", "type": "string" @@ -8676,6 +9482,108 @@ } } }, + "domain.VerifyDirectDepositRequest": { + "type": "object", + "required": [ + "deposit_id", + "is_verified" + ], + "properties": { + "deposit_id": { + "type": "integer" + }, + "is_verified": { + "type": "boolean" + }, + "notes": { + "type": "string" + } + } + }, + "domain.WebhookRequest": { + "type": "object", + "properties": { + "nonce": { + "type": "string" + }, + "notificationUrl": { + "type": "string" + }, + "paymentMethod": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "sessionId": { + "type": "string" + }, + "totalAmount": { + "type": "integer" + }, + "transaction": { + "type": "object", + "properties": { + "transactionId": { + "type": "string" + }, + "transactionStatus": { + "type": "string" + } + } + }, + "transactionStatus": { + "type": "string" + }, + "uuid": { + "type": "string" + } + } + }, + "handlers.AdminProfileRes": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_verified": { + "type": "boolean" + }, + "role": { + "$ref": "#/definitions/domain.Role" + }, + "suspended": { + "type": "boolean" + }, + "suspended_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handlers.AdminRes": { "type": "object", "properties": { @@ -8848,6 +9756,53 @@ } } }, + "handlers.CustomerProfileRes": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_verified": { + "type": "boolean" + }, + "referral_code": { + "type": "string" + }, + "role": { + "$ref": "#/definitions/domain.Role" + }, + "suspended": { + "type": "boolean" + }, + "suspended_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handlers.CustomerWalletRes": { "type": "object", "properties": { @@ -9325,6 +10280,9 @@ "phone_verified": { "type": "boolean" }, + "referral_code": { + "type": "string" + }, "role": { "$ref": "#/definitions/domain.Role" }, @@ -9412,6 +10370,40 @@ } } }, + "handlers.loginAdminReq": { + "type": "object", + "required": [ + "password" + ], + "properties": { + "email": { + "type": "string", + "example": "john.doe@example.com" + }, + "password": { + "type": "string", + "example": "password123" + }, + "phone_number": { + "type": "string", + "example": "1234567890" + } + } + }, + "handlers.loginAdminRes": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, "handlers.loginCustomerReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 2cd514d..ca80f28 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,4 +1,11 @@ definitions: + domain.ARIFPAYPaymentMethod: + properties: + id: + type: integer + name: + type: string + type: object domain.AleaPlayCallback: properties: amount: @@ -31,12 +38,20 @@ definitions: user_id: type: string type: object - domain.ArifPayB2CRequest: + domain.ArifpayB2CRequest: properties: Phonenumber: type: string - Sessionid: + amount: + type: number + customerEmail: type: string + customerPhone: + type: string + required: + - amount + - customerEmail + - customerPhone type: object domain.ArifpayVerifyByTransactionIDRequest: properties: @@ -81,15 +96,6 @@ definitions: updated_at: type: string type: object - domain.Beneficiary: - properties: - accountNumber: - type: string - amount: - type: number - bank: - type: string - type: object domain.BetOutcome: properties: away_team_name: @@ -211,10 +217,22 @@ definitions: name: example: 4-kilo Branch type: string + profit_percentage: + example: 0.1 + type: number wallet_id: example: 1 type: integer type: object + domain.BranchLocation: + properties: + key: + example: addis_ababa + type: string + name: + example: Addis Ababa + type: string + type: object domain.BranchOperationRes: properties: description: @@ -247,6 +265,9 @@ definitions: name: example: 4-kilo Branch type: string + profit_percentage: + example: 0.1 + type: number wallet_id: example: 1 type: integer @@ -326,6 +347,19 @@ definitions: reference: type: string type: object + domain.CheckoutSessionClientRequest: + properties: + amount: + type: number + customerEmail: + type: string + customerPhone: + type: string + required: + - amount + - customerEmail + - customerPhone + type: object domain.CompanyRes: properties: admin_id: @@ -373,7 +407,6 @@ definitions: type: array required: - amount - - branch_id - outcomes type: object domain.CreateBetWithFastCodeReq: @@ -419,50 +452,23 @@ definitions: items: type: integer type: array + profit_percentage: + example: 0.1 + type: number required: - branch_manager_id - location - name - operations type: object - domain.CreateCheckoutSessionRequest: - properties: - beneficiaries: - items: - $ref: '#/definitions/domain.Beneficiary' - type: array - cancelUrl: - type: string - email: - type: string - errorUrl: - type: string - expireDate: - type: string - items: - items: - $ref: '#/definitions/domain.Item' - type: array - lang: - type: string - nonce: - type: string - notifyUrl: - type: string - paymentMethods: - items: - type: string - type: array - phone: - type: string - successUrl: - type: string - type: object domain.CreateCompanyReq: properties: admin_id: example: 1 type: integer + deducted_percentage: + example: 0.1 + type: number name: example: CompanyName type: string @@ -602,6 +608,22 @@ definitions: providerId: type: string type: object + domain.DirectDepositRequest: + properties: + amount: + type: integer + bank_reference: + type: string + customer_id: + type: integer + sender_account: + type: string + required: + - amount + - bank_reference + - customer_id + - sender_account + type: object domain.ErrorResponse: properties: error: @@ -794,6 +816,17 @@ definitions: meta: $ref: '#/definitions/domain.PaginationMeta' type: object + domain.GeneratePaymentURLInput: + properties: + amount: + type: integer + id: + type: string + phoneNumber: + type: string + reason: + type: string + type: object domain.GetCompanyRes: properties: admin_first_name: @@ -843,19 +876,6 @@ definitions: status: type: string type: object - domain.Item: - properties: - description: - type: string - image: - type: string - name: - type: string - price: - type: number - quantity: - type: integer - type: object domain.League: properties: bet365_id: @@ -1233,6 +1253,59 @@ definitions: reference_number: type: string type: object + domain.ShopBetRes: + properties: + amount: + type: integer + bet_id: + example: 1 + type: integer + branch_id: + example: 2 + type: integer + cashed_out: + example: false + type: boolean + cashout_id: + example: "21234" + type: string + company_id: + example: 2 + type: integer + created_at: + example: "2025-04-08T12:00:00Z" + type: string + full_name: + example: John + type: string + id: + type: integer + number_of_outcomes: + example: 1 + type: integer + outcomes: + items: + $ref: '#/definitions/domain.BetOutcome' + type: array + phone_number: + example: "1234567890" + type: string + shop_transaction_id: + type: integer + status: + allOf: + - $ref: '#/definitions/domain.OutcomeStatus' + example: 1 + total_odds: + example: 4.22 + type: number + transaction_verified: + example: true + type: boolean + updated_at: + example: "2025-04-08T12:00:00Z" + type: string + type: object domain.ShopDepositReq: properties: account_name: @@ -1363,6 +1436,51 @@ definitions: example: SportsBook type: string type: object + domain.TelebirrPaymentCallbackPayload: + properties: + appid: + description: App ID provided by Telebirr + type: string + callback_info: + description: Optional merchant-defined callback data + type: string + merch_code: + description: Merchant short code + type: string + merch_order_id: + description: Order ID from merchant system + type: string + notify_time: + description: Notification timestamp (UTC, in seconds) + type: string + notify_url: + description: Optional callback URL + type: string + payment_order_id: + description: Order ID from Telebirr system + type: string + sign: + description: Signature of the payload + type: string + sign_type: + description: Signature type, e.g., SHA256WithRSA + type: string + total_amount: + description: Payment amount + type: string + trade_status: + description: Payment status (e.g., Completed, Failure) + type: string + trans_currency: + description: Currency type (e.g., ETB) + type: string + trans_end_time: + description: Transaction end time (UTC seconds) + type: string + trans_id: + description: Transaction ID + type: string + type: object domain.TicketOutcome: properties: away_team_name: @@ -1436,9 +1554,6 @@ definitions: away_team_id: description: Away team ID (can be empty/null) type: integer - flagged: - description: Whether the event is flagged or not - type: boolean home_kit_image: description: Kit or image for home team (optional) type: string @@ -1451,6 +1566,12 @@ definitions: id: description: Event ID type: string + is_active: + description: Whether the event is featured or not + type: boolean + is_featured: + description: Whether the event is featured or not + type: boolean league_cc: description: League country code type: string @@ -1498,6 +1619,73 @@ definitions: example: true type: boolean type: object + domain.VerifyDirectDepositRequest: + properties: + deposit_id: + type: integer + is_verified: + type: boolean + notes: + type: string + required: + - deposit_id + - is_verified + type: object + domain.WebhookRequest: + properties: + nonce: + type: string + notificationUrl: + type: string + paymentMethod: + type: string + phone: + type: string + sessionId: + type: string + totalAmount: + type: integer + transaction: + properties: + transactionId: + type: string + transactionStatus: + type: string + type: object + transactionStatus: + type: string + uuid: + type: string + type: object + handlers.AdminProfileRes: + properties: + created_at: + type: string + email: + type: string + email_verified: + type: boolean + first_name: + type: string + id: + type: integer + last_login: + type: string + last_name: + type: string + phone_number: + type: string + phone_verified: + type: boolean + role: + $ref: '#/definitions/domain.Role' + suspended: + type: boolean + suspended_at: + type: string + updated_at: + type: string + type: object handlers.AdminRes: properties: created_at: @@ -1618,6 +1806,37 @@ definitions: example: cash type: string type: object + handlers.CustomerProfileRes: + properties: + created_at: + type: string + email: + type: string + email_verified: + type: boolean + first_name: + type: string + id: + type: integer + last_login: + type: string + last_name: + type: string + phone_number: + type: string + phone_verified: + type: boolean + referral_code: + type: string + role: + $ref: '#/definitions/domain.Role' + suspended: + type: boolean + suspended_at: + type: string + updated_at: + type: string + type: object handlers.CustomerWalletRes: properties: created_at: @@ -1941,6 +2160,8 @@ definitions: type: string phone_verified: type: boolean + referral_code: + type: string role: $ref: '#/definitions/domain.Role' suspended: @@ -2002,6 +2223,29 @@ definitions: launch_url: type: string type: object + handlers.loginAdminReq: + properties: + email: + example: john.doe@example.com + type: string + password: + example: password123 + type: string + phone_number: + example: "1234567890" + type: string + required: + - password + type: object + handlers.loginAdminRes: + properties: + access_token: + type: string + refresh_token: + type: string + role: + type: string + type: object handlers.loginCustomerReq: properties: email: @@ -2343,23 +2587,19 @@ paths: summary: Launch an Alea Play virtual game tags: - Alea Virtual Games - /api/v1/arifpay/b2c/transfer: + /api/v1/arifpay/b2c-webhook: post: consumes: - application/json - description: Initiates a B2C transfer via Telebirr, CBE, or MPESA through Arifpay + description: Handles webhook notifications from Arifpay for B2C transfers and + updates transfer + wallet status. parameters: - - description: Transfer mode (Telebirr, CBE, MPESA) - in: query - name: transfer_mode - required: true - type: string - - description: Transfer request payload + - description: Arifpay webhook payload in: body name: request required: true schema: - $ref: '#/definitions/domain.ArifPayB2CRequest' + $ref: '#/definitions/domain.WebhookRequest' produces: - application/json responses: @@ -2371,11 +2611,84 @@ paths: description: Bad Request schema: $ref: '#/definitions/domain.ErrorResponse' - "502": - description: Bad Gateway + "500": + description: Internal Server Error schema: $ref: '#/definitions/domain.ErrorResponse' - summary: Initiate B2C Transfer + summary: Handle Arifpay B2C Webhook + tags: + - Arifpay + /api/v1/arifpay/b2c/transfer: + post: + consumes: + - application/json + description: Initiates a B2C transfer using Telebirr, CBE, or MPESA depending + on the "type" query parameter + parameters: + - description: Transfer type (telebirr, cbe, mpesa) + in: query + name: type + required: true + type: string + - description: Transfer request payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.ArifpayB2CRequest' + produces: + - application/json + responses: + "200": + description: 'message: transfer executed successfully' + schema: + additionalProperties: + type: string + type: object + "400": + description: 'error: invalid request or unsupported transfer type' + schema: + additionalProperties: + type: string + type: object + "500": + description: 'error: internal server error' + schema: + additionalProperties: + type: string + type: object + summary: Execute B2C Transfer + tags: + - Arifpay + /api/v1/arifpay/c2b-webhook: + post: + consumes: + - application/json + description: Handles webhook notifications from Arifpay for C2B transfers and + updates transfer + wallet status. + parameters: + - description: Arifpay webhook payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.WebhookRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Handle Arifpay C2B Webhook tags: - Arifpay /api/v1/arifpay/checkout: @@ -2390,7 +2703,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/domain.CreateCheckoutSessionRequest' + $ref: '#/definitions/domain.CheckoutSessionClientRequest' produces: - application/json responses: @@ -2409,6 +2722,50 @@ paths: summary: Create Arifpay Checkout Session tags: - Arifpay + /api/v1/arifpay/checkout/{sessionId}/cancel: + post: + consumes: + - application/json + description: Cancels a payment session using Arifpay before completion. + parameters: + - description: Checkout session ID + in: path + name: sessionId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Cancel Arifpay Checkout Session + tags: + - Arifpay + /api/v1/arifpay/payment-methods: + get: + description: Returns all payment method IDs and names for Arifpay + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.ARIFPAYPaymentMethod' + type: array + summary: List Arifpay Payment Methods + tags: + - Arifpay /api/v1/arifpay/session-id/verify-transaction/{session_id}: get: consumes: @@ -2468,7 +2825,41 @@ paths: summary: Verify Arifpay Transaction tags: - Arifpay - /api/v1/auth/login: + /api/v1/auth/admin-login: + post: + consumes: + - application/json + description: Login customer + parameters: + - description: Login admin + in: body + name: login + required: true + schema: + $ref: '#/definitions/handlers.loginAdminReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.loginAdminRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Login customer + tags: + - auth + /api/v1/auth/customer-login: post: consumes: - application/json @@ -3029,6 +3420,31 @@ paths: summary: Gets branch for cahier tags: - branch + /api/v1/branchLocation: + get: + consumes: + - application/json + description: Gets all branch locations + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.BranchLocation' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets all branch locations + tags: + - branch /api/v1/branchWallet: get: consumes: @@ -3743,6 +4159,87 @@ paths: summary: Get all customer wallets tags: - wallet + /api/v1/direct_deposit: + post: + consumes: + - application/json + description: Customer initiates a direct deposit from mobile banking + parameters: + - description: Deposit details + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.DirectDepositRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Initiate a direct deposit + tags: + - Direct Deposits + /api/v1/direct_deposit/pending: + get: + description: Get list of direct deposits needing verification + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get pending direct deposits + tags: + - Direct Deposits + /api/v1/direct_deposit/verify: + post: + consumes: + - application/json + description: Cashier verifies a direct deposit transaction + parameters: + - description: Verification details + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.VerifyDirectDepositRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Verify a direct deposit + tags: + - Direct Deposits /api/v1/events: get: consumes: @@ -3854,7 +4351,7 @@ paths: put: consumes: - application/json - description: Update the event flagged + description: Update the event featured parameters: - description: Event ID in: path @@ -3876,7 +4373,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/response.APIResponse' - summary: update the event flagged + summary: update the event featured tags: - event /api/v1/issues: @@ -4703,6 +5200,36 @@ paths: summary: Get results for an event tags: - result + /api/v1/santimpay/payment: + post: + consumes: + - application/json + description: Generates a payment URL using SantimPay and returns it to the client. + parameters: + - description: SantimPay payment request payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.GeneratePaymentURLInput' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Create SantimPay Payment Session + tags: + - SantimPay /api/v1/search/branch: get: consumes: @@ -4760,6 +5287,30 @@ paths: tags: - company /api/v1/shop/bet: + get: + consumes: + - application/json + description: Gets all the shop bets + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.ShopBetRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets all shop bets + tags: + - bet post: consumes: - application/json @@ -5231,6 +5782,35 @@ paths: summary: Create a bet with fast code tags: - bet + /api/v1/sport/bet/fastcode/{fast_code}: + get: + consumes: + - application/json + description: Gets a single bet by fast_code + parameters: + - description: Bet ID + in: path + name: fast_code + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.BetRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets bet by fast_code + tags: + - bet /api/v1/sport/random/bet: post: consumes: @@ -5315,6 +5895,66 @@ paths: summary: Create a supported operation tags: - branch + /api/v1/telebirr/callback: + post: + consumes: + - application/json + description: Processes the Telebirr payment result and updates wallet balance. + parameters: + - description: Callback payload from Telebirr + in: body + name: payload + required: true + schema: + $ref: '#/definitions/domain.TelebirrPaymentCallbackPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Handle Telebirr Payment Callback + tags: + - Telebirr + /api/v1/telebirr/payment: + post: + consumes: + - application/json + description: Generates a payment URL using Telebirr and returns it to the client. + parameters: + - description: Telebirr payment request payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.GeneratePaymentURLInput' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Create Telebirr Payment Session + tags: + - Telebirr /api/v1/ticket: get: consumes: @@ -5509,6 +6149,31 @@ paths: summary: Get transfer by wallet tags: - transfer + /api/v1/user/admin-profile: + get: + consumes: + - application/json + description: Get user profile + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.AdminProfileRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - Bearer: [] + summary: Get user profile + tags: + - user /api/v1/user/bets: get: consumes: @@ -5564,6 +6229,31 @@ paths: summary: Check if phone number or email exist tags: - user + /api/v1/user/customer-profile: + get: + consumes: + - application/json + description: Get user profile + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CustomerProfileRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - Bearer: [] + summary: Get user profile + tags: + - user /api/v1/user/delete/{id}: delete: consumes: @@ -5593,31 +6283,6 @@ paths: summary: Delete user by ID tags: - user - /api/v1/user/profile: - get: - consumes: - - application/json - description: Get user profile - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handlers.UserProfileRes' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - Bearer: [] - summary: Get user profile - tags: - - user /api/v1/user/register: post: consumes: diff --git a/internal/domain/shop_bet.go b/internal/domain/shop_bet.go index 30a6b4e..72f280b 100644 --- a/internal/domain/shop_bet.go +++ b/internal/domain/shop_bet.go @@ -85,7 +85,7 @@ type ShopBetRes struct { BetID int64 `json:"bet_id" example:"1"` NumberOfOutcomes int64 `json:"number_of_outcomes" example:"1"` Status OutcomeStatus `json:"status" example:"1"` - Amount Currency `json:"amount" example:"100.0"` + Amount Currency `json:"amount"` Outcomes []BetOutcome `json:"outcomes"` TransactionVerified bool `json:"transaction_verified" example:"true"` UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:00:00Z"` From f32331bcdb624e93306b1a9c50896442ade51148 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Mon, 18 Aug 2025 14:06:45 +0300 Subject: [PATCH 3/4] veli games report API fix --- cmd/main.go | 2 +- internal/domain/veli_games.go | 51 +- internal/services/virtualGame/veli/client.go | 34 +- internal/services/virtualGame/veli/port.go | 1 + internal/services/virtualGame/veli/service.go | 445 +++++++++++++----- internal/web_server/handlers/veli_games.go | 52 +- internal/web_server/routes.go | 9 +- 7 files changed, 453 insertions(+), 141 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index a9b4970..5f26003 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -153,7 +153,7 @@ func main() { virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) veliCLient := veli.NewClient(cfg, walletSvc) - veliVirtualGameService := veli.New(veliCLient, walletSvc) + veliVirtualGameService := veli.New(veliCLient, walletSvc,cfg) recommendationSvc := recommendation.NewService(recommendationRepo) chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY) diff --git a/internal/domain/veli_games.go b/internal/domain/veli_games.go index 6768c75..6f40841 100644 --- a/internal/domain/veli_games.go +++ b/internal/domain/veli_games.go @@ -38,7 +38,7 @@ type GameStartRequest struct { ProviderID string `json:"providerId"` GameID string `json:"gameId"` Language string `json:"language"` - PlayerID string `json:"playerId"` + PlayerID string `json:"playerId,omitempty"` Currency string `json:"currency"` DeviceType string `json:"deviceType"` Country string `json:"country"` @@ -69,9 +69,9 @@ type BalanceRequest struct { SessionID string `json:"sessionId"` ProviderID string `json:"providerId"` PlayerID string `json:"playerId"` - Currency string `json:"currency"` BrandID string `json:"brandId"` GameID string `json:"gameId,omitempty"` + Currency string `json:"currency"` } type BalanceResponse struct { @@ -170,6 +170,7 @@ type BalanceDetail struct { Currency string `json:"currency"` Amount float64 `json:"amount"` } + // Request type GamingActivityRequest struct { FromDate string `json:"fromDate"` // YYYY-MM-DD @@ -181,7 +182,6 @@ type GamingActivityRequest struct { Page int `json:"page,omitempty"` // Optional, default 1 Size int `json:"size,omitempty"` // Optional, default 100 PlayerIDs []string `json:"playerIds,omitempty"` // Optional - BrandID string `json:"brandId"` // Required } // Response @@ -212,9 +212,44 @@ type GamingActivityItem struct { } type PaginationMeta struct { - TotalItems int `json:"totalItems"` - ItemCount int `json:"itemCount"` - ItemsPerPage int `json:"itemsPerPage"` - TotalPages int `json:"totalPages"` - CurrentPage int `json:"currentPage"` + TotalItems int `json:"totalItems"` + ItemCount int `json:"itemCount"` + ItemsPerPage int `json:"itemsPerPage"` + TotalPages int `json:"totalPages"` + CurrentPage int `json:"currentPage"` +} + +type HugeWinsRequest struct { + FromDate string `json:"fromDate"` + ToDate string `json:"toDate"` + ProviderID string `json:"providerId,omitempty"` + Currencies []string `json:"currencies,omitempty"` + BrandID string `json:"brandId"` + GameIDs []string `json:"gameIds,omitempty"` + Page int `json:"page,omitempty"` + Size int `json:"size,omitempty"` +} + +type HugeWinsResponse struct { + Items []HugeWinItem `json:"items"` + Meta PaginationMeta `json:"meta"` +} + +type HugeWinItem struct { + BetTransactionID string `json:"betTransactionId"` + WinTransactionID string `json:"winTransactionId"` + Currency string `json:"currency"` + GameID string `json:"gameId"` + BetAmount float64 `json:"betAmount"` + WinAmount float64 `json:"winAmount"` + BetAmountUsd float64 `json:"betAmountUsd"` + WinAmountUsd float64 `json:"winAmountUsd"` + ProviderID string `json:"providerId"` + PlayerID string `json:"playerId"` + RoundID string `json:"roundId"` + CorrelationID string `json:"correlationId"` + BrandID string `json:"brandId"` + OperatorID string `json:"operatorId"` + CreatedAt string `json:"createdAt"` + Reason string `json:"reason"` } diff --git a/internal/services/virtualGame/veli/client.go b/internal/services/virtualGame/veli/client.go index 13b0773..cf899e0 100644 --- a/internal/services/virtualGame/veli/client.go +++ b/internal/services/virtualGame/veli/client.go @@ -39,7 +39,7 @@ func NewClient(cfg *config.Config, walletSvc *wallet.Service) *Client { } // Signature generator -func (c *Client) generateSignature(params map[string]string) (string, error) { +func (c *Client) generateSignature(params map[string]any) (string, error) { keys := make([]string, 0, len(params)) for k := range params { keys = append(keys, k) @@ -47,11 +47,31 @@ func (c *Client) generateSignature(params map[string]string) (string, error) { sort.Strings(keys) var b strings.Builder - for i, k := range keys { - if i > 0 { + first := true + + appendKV := func(_ string, value string) { + if !first { b.WriteString(";") } - b.WriteString(fmt.Sprintf("%s:%s", k, params[k])) + b.WriteString(value) + first = false + } + + for _, k := range keys { + v := params[k] + + switch val := v.(type) { + case []string: + for i, item := range val { + appendKV(k, fmt.Sprintf("%s:%d:%s", k, i, item)) + } + case []any: + for i, item := range val { + appendKV(k, fmt.Sprintf("%s:%d:%v", k, i, item)) + } + default: + appendKV(k, fmt.Sprintf("%s:%v", k, val)) + } } fmt.Println("String being signed:", b.String()) @@ -63,12 +83,12 @@ func (c *Client) generateSignature(params map[string]string) (string, error) { signature := base64.StdEncoding.EncodeToString(hash) fmt.Println("Generated signature:", signature) return fmt.Sprintf("%s:%s", c.OperatorID, signature), nil - - // return fmt.Sprintf("%s:%s", c.OperatorID, base64.StdEncoding.EncodeToString(h.Sum(nil))), nil } + + // POST helper -func (c *Client) post(ctx context.Context, path string, body any, sigParams map[string]string, result any) error { +func (c *Client) post(ctx context.Context, path string, body any, sigParams map[string]any, result any) error { data, _ := json.Marshal(body) sig, err := c.generateSignature(sigParams) if err != nil { diff --git a/internal/services/virtualGame/veli/port.go b/internal/services/virtualGame/veli/port.go index 67e6e38..22c78ba 100644 --- a/internal/services/virtualGame/veli/port.go +++ b/internal/services/virtualGame/veli/port.go @@ -17,4 +17,5 @@ type VeliVirtualGameService interface { ProcessWin(ctx context.Context, req domain.WinRequest) (*domain.WinResponse, error) ProcessCancel(ctx context.Context, req domain.CancelRequest) (*domain.CancelResponse, error) GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error) + GetHugeWins(ctx context.Context, req domain.HugeWinsRequest) (*domain.HugeWinsResponse, error) } diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 1a07114..4c675cc 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -5,8 +5,8 @@ import ( "errors" "fmt" "strconv" - "strings" + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" ) @@ -19,20 +19,22 @@ var ( ) type service struct { - client *Client + client *Client walletSvc *wallet.Service + cfg *config.Config } -func New(client *Client, walletSvc *wallet.Service) VeliVirtualGameService { +func New(client *Client, walletSvc *wallet.Service, cfg *config.Config) VeliVirtualGameService { return &service{ - client: client, + client: client, walletSvc: walletSvc, + cfg: cfg, } } func (s *service) GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) { // Always mirror request body fields into sigParams - sigParams := map[string]string{ + sigParams := map[string]any{ "brandId": req.BrandID, } @@ -54,9 +56,8 @@ func (s *service) GetProviders(ctx context.Context, req domain.ProviderRequest) return &res, err } - func (s *service) GetGames(ctx context.Context, req domain.GameListRequest) ([]domain.GameEntity, error) { - sigParams := map[string]string{ + sigParams := map[string]any{ "brandId": req.BrandID, "providerId": req.ProviderID, } var res struct { @@ -67,10 +68,10 @@ func (s *service) GetGames(ctx context.Context, req domain.GameListRequest) ([]d } func (s *service) StartGame(ctx context.Context, req domain.GameStartRequest) (*domain.GameStartResponse, error) { - sigParams := map[string]string{ + sigParams := map[string]any{ "sessionId": req.SessionID, "providerId": req.ProviderID, "gameId": req.GameID, "language": req.Language, "playerId": req.PlayerID, - "currency": req.Currency, "deviceType": req.DeviceType, "country": req.Country, + "currency": req.Currency, "deviceType": req.DeviceType, "country": "US", "ip": req.IP, "brandId": req.BrandID, } var res domain.GameStartResponse @@ -79,7 +80,7 @@ func (s *service) StartGame(ctx context.Context, req domain.GameStartRequest) (* } func (s *service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) { - sigParams := map[string]string{ + sigParams := map[string]any{ "providerId": req.ProviderID, "gameId": req.GameID, "language": req.Language, "deviceType": req.DeviceType, "ip": req.IP, "brandId": req.BrandID, @@ -99,6 +100,9 @@ func (s *service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*d if err != nil { return nil, fmt.Errorf("failed to get real balance: %w", err) } + if len(playerWallets) == 0 { + return nil, fmt.Errorf("PLAYER_NOT_FOUND: no wallet found for player %s", req.PlayerID) + } realBalance := playerWallets[0].Balance @@ -116,7 +120,7 @@ func (s *service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*d Currency string `json:"currency"` Amount float64 `json:"amount"` }{ - Currency: string(playerWallets[0].Currency), + Currency: req.Currency, Amount: float64(realBalance), }, } @@ -135,139 +139,299 @@ func (s *service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*d } func (s *service) ProcessBet(ctx context.Context, req domain.BetRequest) (*domain.BetResponse, error) { - sigParams := map[string]string{ - "sessionId": req.SessionID, - "providerId": req.ProviderID, - "playerId": req.PlayerID, - "currency": req.Amount.Currency, - "brandId": req.BrandID, - "gameId": req.GameID, - "roundId": req.RoundID, - "transactionId": req.TransactionID, - "correlationId": req.CorrelationID, - } - if req.GameType != "" { - sigParams["gameType"] = req.GameType - } - if req.IsAdjustment { - sigParams["isAdjustment"] = "true" - } - if req.JackpotID != "" { - sigParams["jackpotId"] = req.JackpotID - sigParams["jackpotContribution"] = fmt.Sprintf("%.2f", req.JackpotContribution) - } - - var res domain.BetResponse - err := s.client.post(ctx, "/bet", req, sigParams, &res) + // --- 1. Validate PlayerID --- playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) if err != nil { - return &domain.BetResponse{}, fmt.Errorf("invalid PlayerID: %w", err) + return nil, fmt.Errorf("BAD_REQUEST: invalid PlayerID %s", req.PlayerID) } - wallets, err := s.client.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + + // // --- 2. Validate session (optional, if you have sessionSvc) --- + // sessionValid, expired, err := s.sessionSvc.ValidateSession(ctx, req.SessionID, req.PlayerID) + // if err != nil { + // return nil, fmt.Errorf("session validation failed") + // } + // if !sessionValid { + // if expired { + // return nil, fmt.Errorf("SESSION_EXPIRED: session %s expired", req.SessionID) + // } + // return nil, fmt.Errorf("SESSION_NOT_FOUND: session %s not found", req.SessionID) + // } + + // --- 3. Get player wallets --- + playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) if err != nil { - return &domain.BetResponse{}, err + return nil, fmt.Errorf("failed to get real balance: %w", err) + } + if len(playerWallets) == 0 { + return nil, fmt.Errorf("no wallets found for player %s", req.PlayerID) } - s.client.walletSvc.DeductFromWallet(ctx, wallets[0].ID, domain.Currency(req.Amount.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, - fmt.Sprintf("Deducting %v from wallet for creating Veli Game Bet", req.Amount.Amount), - ) + realWallet := playerWallets[0] + realBalance := float64(realWallet.Balance) - return &res, err + var bonusBalance float64 + if len(playerWallets) > 1 { + bonusBalance = float64(playerWallets[1].Balance) + } + + // --- 4. Check sufficient balance --- + totalBalance := realBalance + bonusBalance + if totalBalance < req.Amount.Amount { + return nil, fmt.Errorf("INSUFFICIENT_BALANCE") + } + + // --- 5. Deduct funds (bonus first, then real) --- + remaining := req.Amount.Amount + var usedBonus, usedReal float64 + + if bonusBalance > 0 { + if bonusBalance >= remaining { + // fully cover from bonus + usedBonus = remaining + bonusBalance -= remaining + remaining = 0 + } else { + // partially cover from bonus + usedBonus = bonusBalance + remaining -= bonusBalance + bonusBalance = 0 + } + } + + if remaining > 0 { + if realBalance >= remaining { + usedReal = remaining + realBalance -= remaining + remaining = 0 + } else { + // should never happen because of totalBalance check + return nil, fmt.Errorf("INSUFFICIENT_BALANCE") + } + } + + // --- 6. Persist wallet deductions --- + if usedBonus > 0 && len(playerWallets) > 1 { + _, err = s.walletSvc.DeductFromWallet(ctx, playerWallets[1].ID, + domain.Currency(usedBonus), + domain.ValidInt64{}, + domain.TRANSFER_DIRECT, + fmt.Sprintf("Deduct bonus %.2f for bet %s", usedBonus, req.TransactionID), + ) + if err != nil { + return nil, fmt.Errorf("bonus deduction failed: %w", err) + } + } + + if usedReal > 0 { + _, err = s.walletSvc.DeductFromWallet(ctx, realWallet.ID, + domain.Currency(usedReal), + domain.ValidInt64{}, + domain.TRANSFER_DIRECT, + fmt.Sprintf("Deduct real %.2f for bet %s", usedReal, req.TransactionID), + ) + if err != nil { + return nil, fmt.Errorf("real deduction failed: %w", err) + } + } + + // --- 7. Build response --- + res := &domain.BetResponse{ + Real: domain.BalanceDetail{ + Currency: "ETB", + Amount: realBalance, + }, + WalletTransactionID: req.TransactionID, + UsedRealAmount: usedReal, + UsedBonusAmount: usedBonus, + } + + if bonusBalance > 0 { + res.Bonus = &domain.BalanceDetail{ + Currency: "ETB", + Amount: bonusBalance, + } + } + + return res, nil } func (s *service) ProcessWin(ctx context.Context, req domain.WinRequest) (*domain.WinResponse, error) { - sigParams := map[string]string{ - "sessionId": req.SessionID, - "providerId": req.ProviderID, - "playerId": req.PlayerID, - "currency": req.Amount.Currency, - "brandId": req.BrandID, - "gameId": req.GameID, - "roundId": req.RoundID, - "transactionId": req.TransactionID, - "correlationId": req.CorrelationID, - "winType": req.WinType, - } - if req.GameType != "" { - sigParams["gameType"] = req.GameType - } - if req.RewardID != "" { - sigParams["rewardId"] = req.RewardID - } - if req.IsCashOut { - sigParams["isCashOut"] = "true" - } - - var res domain.WinResponse - err := s.client.post(ctx, "/win", req, sigParams, &res) - + // --- 1. Validate PlayerID --- playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) if err != nil { - return &domain.WinResponse{}, fmt.Errorf("invalid PlayerID: %w", err) + return nil, fmt.Errorf("BAD_REQUEST: invalid PlayerID %s", req.PlayerID) } - wallets, err := s.client.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + // --- 2. Get player wallets --- + playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) if err != nil { - return &domain.WinResponse{}, err + return nil, fmt.Errorf("failed to get wallets: %w", err) + } + if len(playerWallets) == 0 { + return nil, fmt.Errorf("PLAYER_NOT_FOUND: no wallets for player %s", req.PlayerID) } - s.client.walletSvc.AddToWallet(ctx, wallets[0].ID, domain.Currency(req.Amount.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, - fmt.Sprintf("Adding %v to wallet due to winning Veli Games bet", req.Amount), - ) + realWallet := playerWallets[0] + realBalance := float64(realWallet.Balance) - return &res, err + var bonusBalance float64 + if len(playerWallets) > 1 { + bonusBalance = float64(playerWallets[1].Balance) + } + + // --- 3. Apply winnings (for now, everything goes to real wallet) --- + winAmount := req.Amount.Amount + usedReal := winAmount + usedBonus := 0.0 + + // TODO: If you want to split between bonus/real (e.g. free spins), + // you can extend logic here based on req.WinType / req.RewardID. + + _, err = s.walletSvc.AddToWallet( + ctx, + realWallet.ID, + domain.Currency(winAmount), + domain.ValidInt64{}, + domain.TRANSFER_DIRECT, + domain.PaymentDetails{}, + fmt.Sprintf("Win %.2f for transaction %s", winAmount, req.TransactionID), + ) + if err != nil { + return nil, fmt.Errorf("failed to credit real wallet: %w", err) + } + + // --- 4. Reload balances after credit --- + updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + if err != nil { + return nil, fmt.Errorf("failed to reload balances: %w", err) + } + + updatedReal := updatedWallets[0] + realBalance = float64(updatedReal.Balance) + + if len(updatedWallets) > 1 { + bonusBalance = float64(updatedWallets[1].Balance) + } + + // --- 5. Build response --- + res := &domain.WinResponse{ + Real: domain.BalanceDetail{ + Currency: req.Amount.Currency, + Amount: realBalance, + }, + WalletTransactionID: req.TransactionID, + UsedRealAmount: usedReal, + UsedBonusAmount: usedBonus, + } + + if bonusBalance > 0 { + res.Bonus = &domain.BalanceDetail{ + Currency: req.Amount.Currency, + Amount: bonusBalance, + } + } + + return res, nil } func (s *service) ProcessCancel(ctx context.Context, req domain.CancelRequest) (*domain.CancelResponse, error) { - sigParams := map[string]string{ - "sessionId": req.SessionID, - "providerId": req.ProviderID, - "playerId": req.PlayerID, - "brandId": req.BrandID, - "gameId": req.GameID, - "roundId": req.RoundID, - "transactionId": req.TransactionID, - "cancelType": req.CancelType, - } - if req.GameType != "" { - sigParams["gameType"] = req.GameType - } - if req.CorrelationID != "" { - sigParams["correlationId"] = req.CorrelationID - } - if req.RefTransactionID != "" { - sigParams["refTransactionId"] = req.RefTransactionID - } - if req.AdjustmentRefund.Amount > 0 { - sigParams["adjustmentRefundAmount"] = fmt.Sprintf("%.2f", req.AdjustmentRefund.Amount) - sigParams["adjustmentRefundCurrency"] = req.AdjustmentRefund.Currency - } - - var res domain.CancelResponse - err := s.client.post(ctx, "/cancel", req, sigParams, &res) - + // --- 1. Validate PlayerID --- playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) if err != nil { - return &domain.CancelResponse{}, fmt.Errorf("invalid PlayerID: %w", err) + return nil, fmt.Errorf("invalid PlayerID %s", req.PlayerID) } - wallets, err := s.client.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + // --- 2. Get player wallets --- + playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) if err != nil { - return &domain.CancelResponse{}, err + return nil, fmt.Errorf("failed to get wallets: %w", err) + } + if len(playerWallets) == 0 { + return nil, fmt.Errorf("no wallets for player %s", req.PlayerID) } - s.client.walletSvc.AddToWallet(ctx, wallets[0].ID, domain.Currency(req.AdjustmentRefund.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, - fmt.Sprintf("Adding %v to wallet due to cancelling virtual game bet", req.AdjustmentRefund.Amount), + realWallet := playerWallets[0] + realBalance := float64(realWallet.Balance) + + var bonusBalance float64 + if len(playerWallets) > 1 { + bonusBalance = float64(playerWallets[1].Balance) + } + + // --- 3. Refund handling --- + var refundAmount float64 + if req.AdjustmentRefund.Amount > 0 { + refundAmount = req.AdjustmentRefund.Amount + } else { + // If cancelType = CANCEL_BET and no explicit adjustmentRefund, + // we may need to look up the original bet transaction and refund that. + // TODO: implement transaction lookup if required by your domain. + return nil, fmt.Errorf("missing adjustmentRefund for CANCEL_BET") + } + + // For now, we assume refund goes back fully to real wallet + usedReal := refundAmount + usedBonus := 0.0 + + _, err = s.walletSvc.AddToWallet( + ctx, + realWallet.ID, + domain.Currency(refundAmount), + domain.ValidInt64{}, + domain.TRANSFER_DIRECT, + domain.PaymentDetails{ + ReferenceNumber: domain.ValidString{ + Value: req.TransactionID, + Valid: true, + }, + BankNumber: domain.ValidString{}, + }, + fmt.Sprintf("Cancel %s refunded %.2f for transaction %s", req.CancelType, refundAmount, req.RefTransactionID), ) - return &res, err + if err != nil { + return nil, fmt.Errorf("failed to refund wallet: %w", err) + } + + // --- 4. Reload balances after refund --- + updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + if err != nil { + return nil, fmt.Errorf("failed to reload balances: %w", err) + } + + updatedReal := updatedWallets[0] + realBalance = float64(updatedReal.Balance) + + if len(updatedWallets) > 1 { + bonusBalance = float64(updatedWallets[1].Balance) + } + + // --- 5. Build response --- + res := &domain.CancelResponse{ + WalletTransactionID: req.TransactionID, + Real: domain.BalanceDetail{ + Currency: req.AdjustmentRefund.Currency, + Amount: realBalance, + }, + UsedRealAmount: usedReal, + UsedBonusAmount: usedBonus, + } + + if bonusBalance > 0 { + res.Bonus = &domain.BalanceDetail{ + Currency: req.AdjustmentRefund.Currency, + Amount: bonusBalance, + } + } + + return res, nil } func (s *service) GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error) { - // Prepare signature parameters (sorted string map of non-nested fields) - sigParams := map[string]string{ + // --- Signature Params (flattened strings for signing) --- + sigParams := map[string]any{ "fromDate": req.FromDate, "toDate": req.ToDate, - "brandId": req.BrandID, + "brandId": s.cfg.VeliGames.BrandID, } // Optional filters @@ -275,28 +439,77 @@ func (s *service) GetGamingActivity(ctx context.Context, req domain.GamingActivi sigParams["providerId"] = req.ProviderID } if len(req.PlayerIDs) > 0 { - sigParams["playerIds"] = strings.Join(req.PlayerIDs, ",") + sigParams["playerIds"] = req.PlayerIDs // pass as []string, not joined } if len(req.GameIDs) > 0 { - sigParams["gameIds"] = strings.Join(req.GameIDs, ",") + sigParams["gameIds"] = req.GameIDs // pass as []string } if len(req.Currencies) > 0 { - sigParams["currencies"] = strings.Join(req.Currencies, ",") + sigParams["currencies"] = req.Currencies // pass as []string } if req.Page > 0 { - sigParams["page"] = fmt.Sprintf("%d", req.Page) + sigParams["page"] = req.Page + } else { + sigParams["page"] = 1 + req.Page = 1 } if req.Size > 0 { - sigParams["size"] = fmt.Sprintf("%d", req.Size) + sigParams["size"] = req.Size + } else { + sigParams["size"] = 100 + req.Size = 100 } if req.ExcludeFreeWin != nil { - sigParams["excludeFreeWin"] = fmt.Sprintf("%t", *req.ExcludeFreeWin) + sigParams["excludeFreeWin"] = *req.ExcludeFreeWin } + // --- Actual API Call --- var res domain.GamingActivityResponse err := s.client.post(ctx, "/report-api/public/gaming-activity", req, sigParams, &res) if err != nil { return nil, err } + + // --- Return parsed response --- + return &res, nil +} + +func (s *service) GetHugeWins(ctx context.Context, req domain.HugeWinsRequest) (*domain.HugeWinsResponse, error) { + // --- Signature Params (flattened strings for signing) --- + sigParams := map[string]any{ + "fromDate": req.FromDate, + "toDate": req.ToDate, + "brandId": req.BrandID, + } + + if req.ProviderID != "" { + sigParams["providerId"] = req.ProviderID + } + if len(req.GameIDs) > 0 { + sigParams["gameIds"] = req.GameIDs // pass slice directly + } + if len(req.Currencies) > 0 { + sigParams["currencies"] = req.Currencies // pass slice directly + } + if req.Page > 0 { + sigParams["page"] = req.Page + } else { + sigParams["page"] = 1 + req.Page = 1 + } + if req.Size > 0 { + sigParams["size"] = req.Size + } else { + sigParams["size"] = 100 + req.Size = 100 + } + + // --- Actual API Call --- + var res domain.HugeWinsResponse + err := s.client.post(ctx, "/report-api/public/gaming-activity/huge-wins", req, sigParams, &res) + if err != nil { + return nil, err + } + return &res, nil } diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go index 4378f9b..8a005e6 100644 --- a/internal/web_server/handlers/veli_games.go +++ b/internal/web_server/handlers/veli_games.go @@ -3,6 +3,7 @@ package handlers import ( "context" "errors" + "fmt" "log" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -102,6 +103,14 @@ func (h *Handler) GetGamesByProvider(c *fiber.Ctx) error { // @Failure 502 {object} domain.ErrorResponse // @Router /api/v1/veli/start-game [post] func (h *Handler) StartGame(c *fiber.Ctx) error { + userId, ok := c.Locals("user_id").(int64) + if !ok { + return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ + Error: "missing user id", + Message: "Unauthorized", + }) + } + var req domain.GameStartRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ @@ -110,6 +119,7 @@ func (h *Handler) StartGame(c *fiber.Ctx) error { }) } + req.PlayerID = fmt.Sprintf("%d", userId) if req.BrandID == "" { req.BrandID = h.Cfg.VeliGames.BrandID } @@ -293,11 +303,6 @@ func (h *Handler) GetGamingActivity(c *fiber.Ctx) error { }) } - // Inject BrandID if not provided - if req.BrandID == "" { - req.BrandID = h.Cfg.VeliGames.BrandID - } - resp, err := h.veliVirtualGameSvc.GetGamingActivity(c.Context(), req) if err != nil { log.Println("GetGamingActivity error:", err) @@ -314,3 +319,40 @@ func (h *Handler) GetGamingActivity(c *fiber.Ctx) error { Success: true, }) } + +// GetHugeWins godoc +// @Summary Get Veli Huge Wins +// @Description Retrieves huge win transactions based on brand configuration (e.g. win > 10000 USD or 100x bet) +// @Tags Virtual Games - VeliGames +// @Accept json +// @Produce json +// @Param request body domain.HugeWinsRequest true "Huge Wins Request" +// @Success 200 {object} domain.Response{data=domain.HugeWinsResponse} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/veli/huge-wins [post] +func (h *Handler) GetHugeWins(c *fiber.Ctx) error { + var req domain.HugeWinsRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request payload", + Error: err.Error(), + }) + } + + resp, err := h.veliVirtualGameSvc.GetHugeWins(c.Context(), req) + if err != nil { + log.Println("GetHugeWins error:", err) + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve huge wins", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Huge wins retrieved successfully", + Data: resp, + StatusCode: fiber.StatusOK, + Success: true, + }) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index d27718f..2d40bfa 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -283,12 +283,13 @@ func (a *App) initAppRoutes() { groupV1.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback) //Veli Virtual Game Routes - groupV1.Post("/veli/providers", h.GetProviders) - groupV1.Post("/veli/games-list", h.GetGamesByProvider) - groupV1.Post("/veli/start-game", h.StartGame) + groupV1.Post("/veli/providers", a.authMiddleware, h.GetProviders) + groupV1.Post("/veli/games-list", a.authMiddleware, h.GetGamesByProvider) + groupV1.Post("/veli/start-game", a.authMiddleware, h.StartGame) groupV1.Post("/veli/start-demo-game", h.StartDemoGame) a.fiber.Post("/balance", h.GetBalance) - groupV1.Post("/veli/gaming-activity", h.GetGamingActivity) + groupV1.Post("/veli/gaming-activity", a.authMiddleware, h.GetGamingActivity) + groupV1.Post("/veli/huge-wins", a.authMiddleware, h.GetHugeWins) //mongoDB logs groupV1.Get("/logs", a.authMiddleware, a.SuperAdminOnly, handlers.GetLogsHandler(context.Background())) From d40bdcf33c9b14fbace96263f647dfbb04b182c1 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Mon, 18 Aug 2025 16:35:33 +0300 Subject: [PATCH 4/4] santimpay direct payment --- cmd/main.go | 4 +- docs/docs.go | 464 +++++++++++++++++++++- docs/swagger.json | 464 +++++++++++++++++++++- docs/swagger.yaml | 309 +++++++++++++- internal/config/config.go | 9 +- internal/domain/santimpay.go | 78 +++- internal/services/santimpay/client.go | 23 +- internal/services/santimpay/service.go | 359 ++++++++++++++++- internal/web_server/handlers/santimpay.go | 197 ++++++++- internal/web_server/handlers/telebirr.go | 2 +- internal/web_server/routes.go | 8 +- 11 files changed, 1841 insertions(+), 76 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 5f26003..ec66aeb 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -153,7 +153,7 @@ func main() { virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) veliCLient := veli.NewClient(cfg, walletSvc) - veliVirtualGameService := veli.New(veliCLient, walletSvc,cfg) + veliVirtualGameService := veli.New(veliCLient, walletSvc, cfg) recommendationSvc := recommendation.NewService(recommendationRepo) chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY) @@ -239,7 +239,7 @@ func main() { santimpayClient := santimpay.NewSantimPayClient(cfg) - santimpaySvc := santimpay.NewSantimPayService(santimpayClient, cfg, transferStore) + santimpaySvc := santimpay.NewSantimPayService(santimpayClient, cfg, transferStore, walletSvc) telebirrSvc := telebirr.NewTelebirrService(cfg, transferStore, walletSvc) // Initialize and start HTTP server diff --git a/docs/docs.go b/docs/docs.go index db60542..8add105 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4311,6 +4311,173 @@ const docTemplate = `{ } } }, + "/api/v1/santimpay/b2c-withdrawal": { + "post": { + "description": "Initiates a B2C withdrawal request with SantimPay and returns the response.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SantimPay" + ], + "summary": "Process SantimPay B2C Withdrawal", + "parameters": [ + { + "description": "SantimPay B2C withdrawal request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GeneratePaymentURLRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/santimpay/b2c/partners": { + "get": { + "description": "Fetches a list of available B2C payout partners (e.g., Telebirr, Mpesa, Banks) from SantimPay.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SantimPay" + ], + "summary": "Get SantimPay B2C Partners", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/santimpay/callback": { + "post": { + "description": "Processes a callback from SantimPay, updates transfer status, and credits user wallet if payment was successful.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SantimPay" + ], + "summary": "Process SantimPay Payment Callback", + "parameters": [ + { + "description": "SantimPay callback payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.SantimPayCallbackPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/santimpay/direct-payment": { + "post": { + "description": "Initiates a direct payment request with SantimPay and returns the response.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SantimPay" + ], + "summary": "Process SantimPay Direct Payment", + "parameters": [ + { + "description": "SantimPay direct payment request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GeneratePaymentURLRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/santimpay/payment": { "post": { "description": "Generates a payment URL using SantimPay and returns it to the client.", @@ -4331,7 +4498,53 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.GeneratePaymentURLInput" + "$ref": "#/definitions/domain.GeneratePaymentURLRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/santimpay/transaction-status": { + "post": { + "description": "Retrieves the real-time status of a transaction from SantimPay.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SantimPay" + ], + "summary": "Check SantimPay Transaction Status", + "parameters": [ + { + "description": "Transaction status request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.TransactionStatusRequest" } } ], @@ -5434,7 +5647,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.GeneratePaymentURLInput" + "$ref": "#/definitions/domain.GeneratePaymentURLRequest" } } ], @@ -6443,6 +6656,64 @@ const docTemplate = `{ } } }, + "/api/v1/veli/huge-wins": { + "post": { + "description": "Retrieves huge win transactions based on brand configuration (e.g. win \u003e 10000 USD or 100x bet)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - VeliGames" + ], + "summary": "Get Veli Huge Wins", + "parameters": [ + { + "description": "Huge Wins Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.HugeWinsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.HugeWinsResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/veli/providers": { "post": { "description": "Retrieves the list of VeliGames providers", @@ -8270,10 +8541,6 @@ const docTemplate = `{ "domain.GamingActivityRequest": { "type": "object", "properties": { - "brandId": { - "description": "Required", - "type": "string" - }, "currencies": { "description": "Optional", "type": "array", @@ -8335,20 +8602,20 @@ const docTemplate = `{ } } }, - "domain.GeneratePaymentURLInput": { + "domain.GeneratePaymentURLRequest": { "type": "object", "properties": { "amount": { "type": "integer" }, - "id": { + "paymentMethod": { + "type": "string" + }, + "paymentReason": { "type": "string" }, "phoneNumber": { "type": "string" - }, - "reason": { - "type": "string" } } }, @@ -8401,6 +8668,108 @@ const docTemplate = `{ } } }, + "domain.HugeWinItem": { + "type": "object", + "properties": { + "betAmount": { + "type": "number" + }, + "betAmountUsd": { + "type": "number" + }, + "betTransactionId": { + "type": "string" + }, + "brandId": { + "type": "string" + }, + "correlationId": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "gameId": { + "type": "string" + }, + "operatorId": { + "type": "string" + }, + "playerId": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "roundId": { + "type": "string" + }, + "winAmount": { + "type": "number" + }, + "winAmountUsd": { + "type": "number" + }, + "winTransactionId": { + "type": "string" + } + } + }, + "domain.HugeWinsRequest": { + "type": "object", + "properties": { + "brandId": { + "type": "string" + }, + "currencies": { + "type": "array", + "items": { + "type": "string" + } + }, + "fromDate": { + "type": "string" + }, + "gameIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "page": { + "type": "integer" + }, + "providerId": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "toDate": { + "type": "string" + } + } + }, + "domain.HugeWinsResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.HugeWinItem" + } + }, + "meta": { + "$ref": "#/definitions/domain.PaginationMeta" + } + } + }, "domain.InstResponse": { "type": "object", "properties": { @@ -8919,6 +9288,68 @@ const docTemplate = `{ "RoleCashier" ] }, + "domain.SantimPayCallbackPayload": { + "type": "object", + "properties": { + "accountNumber": { + "type": "string" + }, + "address": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "failureRedirectUrl": { + "type": "string" + }, + "merId": { + "type": "string" + }, + "merName": { + "type": "string" + }, + "message": { + "type": "string" + }, + "msisdn": { + "type": "string" + }, + "paymentVia": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "receiverWalletID": { + "type": "string" + }, + "refId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "successRedirectUrl": { + "type": "string" + }, + "thirdPartyId": { + "type": "string" + }, + "txnId": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "domain.ShopBetReq": { "type": "object", "properties": { @@ -9383,6 +9814,17 @@ const docTemplate = `{ } } }, + "domain.TransactionStatusRequest": { + "type": "object", + "properties": { + "fullParams": { + "type": "boolean" + }, + "id": { + "type": "string" + } + } + }, "domain.UpcomingEvent": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index efe4771..4201f9e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4303,6 +4303,173 @@ } } }, + "/api/v1/santimpay/b2c-withdrawal": { + "post": { + "description": "Initiates a B2C withdrawal request with SantimPay and returns the response.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SantimPay" + ], + "summary": "Process SantimPay B2C Withdrawal", + "parameters": [ + { + "description": "SantimPay B2C withdrawal request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GeneratePaymentURLRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/santimpay/b2c/partners": { + "get": { + "description": "Fetches a list of available B2C payout partners (e.g., Telebirr, Mpesa, Banks) from SantimPay.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SantimPay" + ], + "summary": "Get SantimPay B2C Partners", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/santimpay/callback": { + "post": { + "description": "Processes a callback from SantimPay, updates transfer status, and credits user wallet if payment was successful.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SantimPay" + ], + "summary": "Process SantimPay Payment Callback", + "parameters": [ + { + "description": "SantimPay callback payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.SantimPayCallbackPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/santimpay/direct-payment": { + "post": { + "description": "Initiates a direct payment request with SantimPay and returns the response.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SantimPay" + ], + "summary": "Process SantimPay Direct Payment", + "parameters": [ + { + "description": "SantimPay direct payment request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GeneratePaymentURLRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/santimpay/payment": { "post": { "description": "Generates a payment URL using SantimPay and returns it to the client.", @@ -4323,7 +4490,53 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.GeneratePaymentURLInput" + "$ref": "#/definitions/domain.GeneratePaymentURLRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/santimpay/transaction-status": { + "post": { + "description": "Retrieves the real-time status of a transaction from SantimPay.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SantimPay" + ], + "summary": "Check SantimPay Transaction Status", + "parameters": [ + { + "description": "Transaction status request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.TransactionStatusRequest" } } ], @@ -5426,7 +5639,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.GeneratePaymentURLInput" + "$ref": "#/definitions/domain.GeneratePaymentURLRequest" } } ], @@ -6435,6 +6648,64 @@ } } }, + "/api/v1/veli/huge-wins": { + "post": { + "description": "Retrieves huge win transactions based on brand configuration (e.g. win \u003e 10000 USD or 100x bet)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - VeliGames" + ], + "summary": "Get Veli Huge Wins", + "parameters": [ + { + "description": "Huge Wins Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.HugeWinsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.HugeWinsResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/veli/providers": { "post": { "description": "Retrieves the list of VeliGames providers", @@ -8262,10 +8533,6 @@ "domain.GamingActivityRequest": { "type": "object", "properties": { - "brandId": { - "description": "Required", - "type": "string" - }, "currencies": { "description": "Optional", "type": "array", @@ -8327,20 +8594,20 @@ } } }, - "domain.GeneratePaymentURLInput": { + "domain.GeneratePaymentURLRequest": { "type": "object", "properties": { "amount": { "type": "integer" }, - "id": { + "paymentMethod": { + "type": "string" + }, + "paymentReason": { "type": "string" }, "phoneNumber": { "type": "string" - }, - "reason": { - "type": "string" } } }, @@ -8393,6 +8660,108 @@ } } }, + "domain.HugeWinItem": { + "type": "object", + "properties": { + "betAmount": { + "type": "number" + }, + "betAmountUsd": { + "type": "number" + }, + "betTransactionId": { + "type": "string" + }, + "brandId": { + "type": "string" + }, + "correlationId": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "gameId": { + "type": "string" + }, + "operatorId": { + "type": "string" + }, + "playerId": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "roundId": { + "type": "string" + }, + "winAmount": { + "type": "number" + }, + "winAmountUsd": { + "type": "number" + }, + "winTransactionId": { + "type": "string" + } + } + }, + "domain.HugeWinsRequest": { + "type": "object", + "properties": { + "brandId": { + "type": "string" + }, + "currencies": { + "type": "array", + "items": { + "type": "string" + } + }, + "fromDate": { + "type": "string" + }, + "gameIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "page": { + "type": "integer" + }, + "providerId": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "toDate": { + "type": "string" + } + } + }, + "domain.HugeWinsResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.HugeWinItem" + } + }, + "meta": { + "$ref": "#/definitions/domain.PaginationMeta" + } + } + }, "domain.InstResponse": { "type": "object", "properties": { @@ -8911,6 +9280,68 @@ "RoleCashier" ] }, + "domain.SantimPayCallbackPayload": { + "type": "object", + "properties": { + "accountNumber": { + "type": "string" + }, + "address": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "failureRedirectUrl": { + "type": "string" + }, + "merId": { + "type": "string" + }, + "merName": { + "type": "string" + }, + "message": { + "type": "string" + }, + "msisdn": { + "type": "string" + }, + "paymentVia": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "receiverWalletID": { + "type": "string" + }, + "refId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "successRedirectUrl": { + "type": "string" + }, + "thirdPartyId": { + "type": "string" + }, + "txnId": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "domain.ShopBetReq": { "type": "object", "properties": { @@ -9375,6 +9806,17 @@ } } }, + "domain.TransactionStatusRequest": { + "type": "object", + "properties": { + "fullParams": { + "type": "boolean" + }, + "id": { + "type": "string" + } + } + }, "domain.UpcomingEvent": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ca80f28..5637a01 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -770,9 +770,6 @@ definitions: type: object domain.GamingActivityRequest: properties: - brandId: - description: Required - type: string currencies: description: Optional items: @@ -816,16 +813,16 @@ definitions: meta: $ref: '#/definitions/domain.PaginationMeta' type: object - domain.GeneratePaymentURLInput: + domain.GeneratePaymentURLRequest: properties: amount: type: integer - id: + paymentMethod: + type: string + paymentReason: type: string phoneNumber: type: string - reason: - type: string type: object domain.GetCompanyRes: properties: @@ -863,6 +860,73 @@ definitions: example: 1 type: integer type: object + domain.HugeWinItem: + properties: + betAmount: + type: number + betAmountUsd: + type: number + betTransactionId: + type: string + brandId: + type: string + correlationId: + type: string + createdAt: + type: string + currency: + type: string + gameId: + type: string + operatorId: + type: string + playerId: + type: string + providerId: + type: string + reason: + type: string + roundId: + type: string + winAmount: + type: number + winAmountUsd: + type: number + winTransactionId: + type: string + type: object + domain.HugeWinsRequest: + properties: + brandId: + type: string + currencies: + items: + type: string + type: array + fromDate: + type: string + gameIds: + items: + type: string + type: array + page: + type: integer + providerId: + type: string + size: + type: integer + toDate: + type: string + type: object + domain.HugeWinsResponse: + properties: + items: + items: + $ref: '#/definitions/domain.HugeWinItem' + type: array + meta: + $ref: '#/definitions/domain.PaginationMeta' + type: object domain.InstResponse: properties: data: @@ -1217,6 +1281,47 @@ definitions: - RoleBranchManager - RoleCustomer - RoleCashier + domain.SantimPayCallbackPayload: + properties: + accountNumber: + type: string + address: + type: string + amount: + type: string + created_at: + type: string + currency: + type: string + failureRedirectUrl: + type: string + merId: + type: string + merName: + type: string + message: + type: string + msisdn: + type: string + paymentVia: + type: string + reason: + type: string + receiverWalletID: + type: string + refId: + type: string + status: + type: string + successRedirectUrl: + type: string + thirdPartyId: + type: string + txnId: + type: string + updated_at: + type: string + type: object domain.ShopBetReq: properties: account_name: @@ -1543,6 +1648,13 @@ definitions: example: 4.22 type: number type: object + domain.TransactionStatusRequest: + properties: + fullParams: + type: boolean + id: + type: string + type: object domain.UpcomingEvent: properties: away_kit_image: @@ -5200,6 +5312,119 @@ paths: summary: Get results for an event tags: - result + /api/v1/santimpay/b2c-withdrawal: + post: + consumes: + - application/json + description: Initiates a B2C withdrawal request with SantimPay and returns the + response. + parameters: + - description: SantimPay B2C withdrawal request payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.GeneratePaymentURLRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Process SantimPay B2C Withdrawal + tags: + - SantimPay + /api/v1/santimpay/b2c/partners: + get: + consumes: + - application/json + description: Fetches a list of available B2C payout partners (e.g., Telebirr, + Mpesa, Banks) from SantimPay. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get SantimPay B2C Partners + tags: + - SantimPay + /api/v1/santimpay/callback: + post: + consumes: + - application/json + description: Processes a callback from SantimPay, updates transfer status, and + credits user wallet if payment was successful. + parameters: + - description: SantimPay callback payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.SantimPayCallbackPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Process SantimPay Payment Callback + tags: + - SantimPay + /api/v1/santimpay/direct-payment: + post: + consumes: + - application/json + description: Initiates a direct payment request with SantimPay and returns the + response. + parameters: + - description: SantimPay direct payment request payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.GeneratePaymentURLRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Process SantimPay Direct Payment + tags: + - SantimPay /api/v1/santimpay/payment: post: consumes: @@ -5211,7 +5436,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/domain.GeneratePaymentURLInput' + $ref: '#/definitions/domain.GeneratePaymentURLRequest' produces: - application/json responses: @@ -5230,6 +5455,36 @@ paths: summary: Create SantimPay Payment Session tags: - SantimPay + /api/v1/santimpay/transaction-status: + post: + consumes: + - application/json + description: Retrieves the real-time status of a transaction from SantimPay. + parameters: + - description: Transaction status request payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.TransactionStatusRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Check SantimPay Transaction Status + tags: + - SantimPay /api/v1/search/branch: get: consumes: @@ -5936,7 +6191,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/domain.GeneratePaymentURLInput' + $ref: '#/definitions/domain.GeneratePaymentURLRequest' produces: - application/json responses: @@ -6593,6 +6848,42 @@ paths: summary: Get Veli Gaming Activity tags: - Virtual Games - VeliGames + /api/v1/veli/huge-wins: + post: + consumes: + - application/json + description: Retrieves huge win transactions based on brand configuration (e.g. + win > 10000 USD or 100x bet) + parameters: + - description: Huge Wins Request + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.HugeWinsRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + $ref: '#/definitions/domain.HugeWinsResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get Veli Huge Wins + tags: + - Virtual Games - VeliGames /api/v1/veli/providers: post: consumes: diff --git a/internal/config/config.go b/internal/config/config.go index afc4d78..926d4fe 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -83,8 +83,11 @@ type ARIFPAYConfig struct { type SANTIMPAYConfig struct { SecretKey string `mapstructure:"secret_key"` - MerchantID string `mapstructure:"merchant_id"` + MerchantID string `mapstructure:"merchantId"` BaseURL string `mapstructure:"base_url"` + NotifyURL string `mapstructure:"notifyUrl"` + CancelUrl string `mapstructure:"cancelUrl"` + SuccessUrl string `mapstructure:"successUrl"` } type TELEBIRRConfig struct { @@ -254,7 +257,9 @@ func (c *Config) loadEnv() error { c.SANTIMPAY.SecretKey = os.Getenv("SANTIMPAY_SECRET_KEY") c.SANTIMPAY.MerchantID = os.Getenv("SANTIMPAY_MERCHANT_ID") - c.SANTIMPAY.BaseURL = os.Getenv("SANTIMPAY_Base_URL") + c.SANTIMPAY.BaseURL = os.Getenv("SANTIMPAY_BASE_URL") + c.SANTIMPAY.NotifyURL = os.Getenv("SANTIMPAY_NOTIFY_URL") + c.SANTIMPAY.CancelUrl = os.Getenv("SANTIMPAY_CANCEL_URL") //Alea Play aleaEnabled := os.Getenv("ALEA_ENABLED") diff --git a/internal/domain/santimpay.go b/internal/domain/santimpay.go index c616724..89c2c51 100644 --- a/internal/domain/santimpay.go +++ b/internal/domain/santimpay.go @@ -1,17 +1,13 @@ package domain -type GeneratePaymentURLInput struct { - ID string - Amount int - Reason string - PhoneNumber string - // SuccessRedirectURL string - // FailureRedirectURL string - // CancelRedirectURL string - // NotifyURL string +type GeneratePaymentURLRequest struct { + Amount int `json:"amount"` + Reason string `json:"paymentReason"` + PhoneNumber string `json:"phoneNumber"` + PaymentMethod string `json:"paymentMethod,omitempty"` } -type InitiatePaymentPayload struct { +type InitiatePaymentRequest struct { ID string `json:"id"` Amount int `json:"amount"` Reason string `json:"paymentReason"` @@ -22,4 +18,64 @@ type InitiatePaymentPayload struct { NotifyURL string `json:"notifyUrl"` CancelRedirectURL string `json:"cancelRedirectUrl"` PhoneNumber string `json:"phoneNumber"` -} \ No newline at end of file + PaymentMethod string `json:"paymentMethod,omitempty"` +} + +type SantimPayCallbackPayload struct { + TxnId string `json:"txnId"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ThirdPartyId string `json:"thirdPartyId"` + MerId string `json:"merId"` + MerName string `json:"merName"` + Address string `json:"address"` + Amount string `json:"amount"` + Currency string `json:"currency"` + Reason string `json:"reason"` + Msisdn string `json:"msisdn"` + AccountNumber string `json:"accountNumber"` + PaymentVia string `json:"paymentVia"` + RefId string `json:"refId"` + SuccessRedirectUrl string `json:"successRedirectUrl"` + FailureRedirectUrl string `json:"failureRedirectUrl"` + Message string `json:"message"` + Status string `json:"status"` + ReceiverWalletID string `json:"receiverWalletID"` +} + +type SantimTokenPayload struct { + Amount int `json:"amount"` + Reason string `json:"paymentReason"` + PaymentMethod string `json:"paymentMethod"` + PhoneNumber string `json:"phoneNumber"` + ID string `json:"id,omitempty"` +} + +type Partner struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Input string `json:"input"` +} + +// B2CPartnersResponse is the top-level response +type B2CPartnersResponse struct { + Partners []Partner `json:"partners"` +} + +type SantimpayB2CWithdrawalRequest struct { + ID string `json:"id"` + ClientReference string `json:"clientReference"` + Amount float64 `json:"amount"` + Reason string `json:"reason"` + MerchantID string `json:"merchantId"` + SignedToken string `json:"signedToken"` + ReceiverAccountNumber string `json:"receiverAccountNumber"` + NotifyURL string `json:"notifyUrl"` + PaymentMethod string `json:"paymentMethod"` +} + +type TransactionStatusRequest struct { + TransactionID string `json:"id"` + FullParams *bool `json:"fullParams,omitempty"` +} diff --git a/internal/services/santimpay/client.go b/internal/services/santimpay/client.go index 8dbdfbc..ead4104 100644 --- a/internal/services/santimpay/client.go +++ b/internal/services/santimpay/client.go @@ -5,11 +5,12 @@ import ( "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/golang-jwt/jwt/v5" ) type SantimPayClient interface { - GenerateSignedToken(amount int, reason string) (string, error) + GenerateSignedToken(payload domain.SantimTokenPayload) (string, error) CheckTransactionStatus(id string) } @@ -23,17 +24,26 @@ func NewSantimPayClient(cfg *config.Config) SantimPayClient { } } -func (c *santimClient) GenerateSignedToken(amount int, reason string) (string, error) { +func (c *santimClient) GenerateSignedToken(payload domain.SantimTokenPayload) (string, error) { now := time.Now().Unix() claims := jwt.MapClaims{ - "amount": amount, - "paymentReason": reason, - "merchantId": c.cfg.SANTIMPAY.MerchantID, - "generated": now, + "amount": payload.Amount, + "paymentReason": payload.Reason, + "merchantId": c.cfg.SANTIMPAY.MerchantID, + "generated": now, + } + + // Optional fields + if payload.PaymentMethod != "" { + claims["paymentMethod"] = payload.PaymentMethod + } + if payload.PhoneNumber != "" { + claims["phoneNumber"] = payload.PhoneNumber } token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) + privateKey, err := jwt.ParseECPrivateKeyFromPEM([]byte(c.cfg.SANTIMPAY.SecretKey)) if err != nil { return "", fmt.Errorf("invalid private key: %w", err) @@ -47,6 +57,7 @@ func (c *santimClient) GenerateSignedToken(amount int, reason string) (string, e return signedToken, nil } + func (c *santimClient) CheckTransactionStatus(id string) { // optional async checker — can log or poll transaction status fmt.Println("Checking transaction status for:", id) diff --git a/internal/services/santimpay/service.go b/internal/services/santimpay/service.go index a557f49..27e5b3e 100644 --- a/internal/services/santimpay/service.go +++ b/internal/services/santimpay/service.go @@ -6,6 +6,8 @@ import ( "encoding/json" "fmt" "net/http" + "strconv" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -14,42 +16,51 @@ import ( ) // type SantimPayService interface { -// GeneratePaymentURL(input domain.GeneratePaymentURLInput) (map[string]string, error) +// GeneratePaymentURL(req domain.GeneratePaymentURLreq) (map[string]string, error) // } type SantimPayService struct { client SantimPayClient cfg *config.Config transferStore wallet.TransferStore + walletSvc *wallet.Service } -func NewSantimPayService(client SantimPayClient, cfg *config.Config, transferStore wallet.TransferStore) *SantimPayService { +func NewSantimPayService(client SantimPayClient, cfg *config.Config, transferStore wallet.TransferStore, walletSvc *wallet.Service) *SantimPayService { return &SantimPayService{ client: client, cfg: cfg, transferStore: transferStore, + walletSvc: walletSvc, } } -func (s *SantimPayService) GeneratePaymentURL(input domain.GeneratePaymentURLInput) (map[string]string, error) { +func (s *SantimPayService) InitiatePayment(req domain.GeneratePaymentURLRequest) (map[string]string, error) { paymentID := uuid.NewString() - token, err := s.client.GenerateSignedToken(input.Amount, input.Reason) + tokenPayload := domain.SantimTokenPayload{ + Amount: req.Amount, + Reason: req.Reason, + } + + // 1. Generate signed token (used as Bearer token in headers) + token, err := s.client.GenerateSignedToken(tokenPayload) if err != nil { return nil, fmt.Errorf("token generation failed: %w", err) } - payload := domain.InitiatePaymentPayload{ + // 2. Prepare payload (without token in body) + payload := domain.InitiatePaymentRequest{ ID: paymentID, - Amount: input.Amount, - Reason: input.Reason, + Amount: req.Amount, + Reason: req.Reason, MerchantID: s.cfg.SANTIMPAY.MerchantID, + SuccessRedirectURL: s.cfg.SANTIMPAY.SuccessUrl, + FailureRedirectURL: s.cfg.SANTIMPAY.CancelUrl, + NotifyURL: s.cfg.SANTIMPAY.NotifyURL, + CancelRedirectURL: s.cfg.SANTIMPAY.CancelUrl, + PhoneNumber: req.PhoneNumber, SignedToken: token, - SuccessRedirectURL: s.cfg.ARIFPAY.SuccessUrl, - FailureRedirectURL: s.cfg.ARIFPAY.ErrorUrl, - NotifyURL: s.cfg.ARIFPAY.B2CNotifyUrl, - CancelRedirectURL: s.cfg.ARIFPAY.CancelUrl, - PhoneNumber: input.PhoneNumber, } jsonData, err := json.Marshal(payload) @@ -57,7 +68,16 @@ func (s *SantimPayService) GeneratePaymentURL(input domain.GeneratePaymentURLInp return nil, fmt.Errorf("failed to marshal payload: %w", err) } - resp, err := http.Post(s.cfg.SANTIMPAY.BaseURL+"/initiate-payment", "application/json", bytes.NewBuffer(jsonData)) + // 3. Prepare request with Bearer token header + httpReq, err := http.NewRequest("POST", s.cfg.SANTIMPAY.BaseURL+"/gateway/initiate-payment", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+token) + + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(httpReq) if err != nil { return nil, fmt.Errorf("failed to send HTTP request: %w", err) } @@ -72,21 +92,324 @@ func (s *SantimPayService) GeneratePaymentURL(input domain.GeneratePaymentURLInp return nil, fmt.Errorf("failed to decode response: %w", err) } - // Save transfer + // 4. Save transfer transfer := domain.CreateTransfer{ - Amount: domain.Currency(input.Amount), + Amount: domain.Currency(req.Amount), Verified: false, Type: domain.DEPOSIT, ReferenceNumber: paymentID, Status: string(domain.PaymentStatusPending), } - if _, err := s.transferStore.CreateTransfer(context.Background(), transfer); err != nil { return nil, fmt.Errorf("failed to create transfer: %w", err) } - // Optionally check transaction status in a goroutine - go s.client.CheckTransactionStatus(paymentID) + // 5. Optionally check transaction status asynchronously + // go s.client.CheckTransactionStatus(paymentID) + + return responseBody, nil +} + +func (s *SantimPayService) ProcessCallback(ctx context.Context, payload domain.SantimPayCallbackPayload) error { + // 1. Parse amount + amount, err := strconv.ParseFloat(payload.Amount, 64) + if err != nil { + return fmt.Errorf("invalid amount in callback: %w", err) + } + + // 2. Retrieve the corresponding transfer by txnId or refId + transfer, err := s.transferStore.GetTransferByReference(ctx, payload.TxnId) + if err != nil { + return fmt.Errorf("failed to fetch transfer for txnId %s: %w", payload.TxnId, err) + } + + // 3. Update transfer status based on callback status + switch payload.Status { + case "COMPLETED": + transfer.Status = string(domain.PaymentStatusSuccessful) + transfer.Verified = true + + userID, err := strconv.ParseInt(payload.ThirdPartyId, 10, 64) + if err != nil { + return fmt.Errorf("invalid ThirdPartyId '%s': %w", payload.ThirdPartyId, err) + } + + wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID) + if err != nil { + return fmt.Errorf("failed to get wallets for user %d: %w", userID, err) + } + + // Optionally, credit user wallet + if transfer.Type == domain.DEPOSIT { + if _, err := s.walletSvc.AddToWallet( + ctx, + wallets[0].ID, + domain.Currency(amount), + domain.ValidInt64{}, + domain.TRANSFER_SANTIMPAY, + domain.PaymentDetails{ + ReferenceNumber: domain.ValidString{ + Value: payload.TxnId, + Valid: true, + }, + BankNumber: domain.ValidString{}, + }, + "", + ); err != nil { + return fmt.Errorf("failed to credit wallet: %w", err) + } + } + + case "FAILED", "CANCELLED": + transfer.Status = string(domain.PaymentStatusFailed) + transfer.Verified = false + default: + // Unknown status + return fmt.Errorf("unknown callback status: %s", payload.Status) + } + + // 4. Save the updated transfer + if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.PaymentStatusCompleted)); err != nil { + return fmt.Errorf("failed to update transfer status: %w", err) + } + + if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { + return fmt.Errorf("failed to update transfer verification: %w", err) + } + + return nil +} + +func (s *SantimPayService) ProcessDirectPayment(ctx context.Context, req domain.GeneratePaymentURLRequest) (map[string]any, error) { + paymentID := uuid.NewString() + + tokenPayload := domain.SantimTokenPayload{ + Amount: req.Amount, + Reason: req.Reason, + PaymentMethod: req.PaymentMethod, + PhoneNumber: req.PhoneNumber, + } + + // 1. Generate signed token for direct payment + token, err := s.client.GenerateSignedToken(tokenPayload) + if err != nil { + return nil, fmt.Errorf("failed to generate signed token: %w", err) + } + + // 2. Build payload + payload := domain.InitiatePaymentRequest{ + ID: paymentID, + Amount: req.Amount, + Reason: req.Reason, + MerchantID: s.cfg.SANTIMPAY.MerchantID, + SignedToken: token, + PhoneNumber: req.PhoneNumber, + NotifyURL: s.cfg.SANTIMPAY.NotifyURL, + PaymentMethod: req.PaymentMethod, + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to marshal payload: %w", err) + } + + // 3. Prepare HTTP request + httpReq, err := http.NewRequest("POST", s.cfg.SANTIMPAY.BaseURL+"/direct-payment", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+token) + + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("failed to send HTTP request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("non-200 status code received: %d", resp.StatusCode) + } + + // 4. Decode response + var responseBody map[string]any + if err := json.NewDecoder(resp.Body).Decode(&responseBody); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + // 5. Save transfer in DB + transfer := domain.CreateTransfer{ + Amount: domain.Currency(req.Amount), + Verified: false, + Type: domain.DEPOSIT, + ReferenceNumber: paymentID, + Status: string(domain.PaymentStatusPending), + } + if _, err := s.transferStore.CreateTransfer(context.Background(), transfer); err != nil { + return nil, fmt.Errorf("failed to create transfer: %w", err) + } + + // 6. Optionally check transaction status async + // go s.client.CheckTransactionStatus(paymentID) + + return responseBody, nil +} + +func (s *SantimPayService) GetB2CPartners(ctx context.Context) (*domain.B2CPartnersResponse, error) { + url := fmt.Sprintf("%s/api/v1/gateway/payout/partners", s.cfg.SANTIMPAY.BaseURL) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + HTTPClient := &http.Client{Timeout: 15 * time.Second} + + resp, err := HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to call SantimPay API: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var partnersResp domain.B2CPartnersResponse + if err := json.NewDecoder(resp.Body).Decode(&partnersResp); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &partnersResp, nil +} + +func (s *SantimPayService) ProcessB2CWithdrawal(ctx context.Context, req domain.GeneratePaymentURLRequest, userId int64) (map[string]any, error) { + + transactID := uuid.NewString() + + // 1. Generate signed token for B2C + tokenPayload := domain.SantimTokenPayload{ + Amount: req.Amount, + Reason: req.Reason, + PaymentMethod: req.PaymentMethod, + PhoneNumber: req.PhoneNumber, + } + + signedToken, err := s.client.GenerateSignedToken(tokenPayload) + if err != nil { + return nil, fmt.Errorf("failed to generate signed token for B2C: %w", err) + } + + // 2. Build payload + payload := domain.SantimpayB2CWithdrawalRequest{ + ID: transactID, + ClientReference: string(rune(userId)), + Amount: float64(req.Amount), + Reason: req.Reason, + MerchantID: s.cfg.SANTIMPAY.MerchantID, + SignedToken: signedToken, + ReceiverAccountNumber: req.PhoneNumber, + NotifyURL: s.cfg.SANTIMPAY.NotifyURL, + PaymentMethod: req.PaymentMethod, + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to marshal B2C payload: %w", err) + } + + // 3. Send HTTP request + url := s.cfg.SANTIMPAY.BaseURL + "/payout-transfer" + httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create B2C request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+signedToken) + + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("failed to send B2C request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("B2C request failed with status code: %d", resp.StatusCode) + } + + // 4. Decode response + var responseBody map[string]any + if err := json.NewDecoder(resp.Body).Decode(&responseBody); err != nil { + return nil, fmt.Errorf("failed to decode B2C response: %w", err) + } + + // 5. Persist withdrawal record in DB + withdrawal := domain.CreateTransfer{ + Amount: domain.Currency(req.Amount), + Verified: false, + Type: domain.WITHDRAW, + ReferenceNumber: transactID, + Status: string(domain.PaymentStatusPending), + } + if _, err := s.transferStore.CreateTransfer(context.Background(), withdrawal); err != nil { + return nil, fmt.Errorf("failed to create withdrawal transfer: %w", err) + } + + return responseBody, nil +} + +func (s *SantimPayService) CheckTransactionStatus(ctx context.Context, req domain.TransactionStatusRequest) (map[string]any, error) { + // 1. Generate signed token for status check + tokenPayload := domain.SantimTokenPayload{ + ID: req.TransactionID, + } + + signedToken, err := s.client.GenerateSignedToken(tokenPayload) + if err != nil { + return nil, fmt.Errorf("failed to generate signed token for transaction status: %w", err) + } + + // 2. Build request payload + payload := map[string]any{ + "id": req.TransactionID, + "merchantId": s.cfg.SANTIMPAY.MerchantID, + "signedToken": signedToken, + "fullParams": req.FullParams, + "generated": time.Now().Unix(), + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to marshal transaction status payload: %w", err) + } + + // 3. Send HTTP request + url := s.cfg.SANTIMPAY.BaseURL + "/fetch-transaction-status" + httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create transaction status request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+signedToken) + + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("failed to send transaction status request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("transaction status request failed with status code: %d", resp.StatusCode) + } + + // 4. Decode response + var responseBody map[string]any + if err := json.NewDecoder(resp.Body).Decode(&responseBody); err != nil { + return nil, fmt.Errorf("failed to decode transaction status response: %w", err) + } return responseBody, nil } diff --git a/internal/web_server/handlers/santimpay.go b/internal/web_server/handlers/santimpay.go index 6920200..557716a 100644 --- a/internal/web_server/handlers/santimpay.go +++ b/internal/web_server/handlers/santimpay.go @@ -12,13 +12,13 @@ import ( // @Tags SantimPay // @Accept json // @Produce json -// @Param request body domain.GeneratePaymentURLInput true "SantimPay payment request payload" +// @Param request body domain.GeneratePaymentURLRequest true "SantimPay payment request payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/santimpay/payment [post] -func (h *Handler) CreateSantimPayPaymentHandler(c *fiber.Ctx) error { - var req domain.GeneratePaymentURLInput +func (h *Handler) InititateSantimPayPaymentHandler(c *fiber.Ctx) error { + var req domain.GeneratePaymentURLRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Error: err.Error(), @@ -26,7 +26,7 @@ func (h *Handler) CreateSantimPayPaymentHandler(c *fiber.Ctx) error { }) } - paymentURL, err := h.santimpaySvc.GeneratePaymentURL(req) + paymentURL, err := h.santimpaySvc.InitiatePayment(req) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Error: err.Error(), @@ -41,3 +41,192 @@ func (h *Handler) CreateSantimPayPaymentHandler(c *fiber.Ctx) error { StatusCode: fiber.StatusOK, }) } + +// ProcessSantimPayCallbackHandler handles incoming SantimPay payment callbacks. +// +// @Summary Process SantimPay Payment Callback +// @Description Processes a callback from SantimPay, updates transfer status, and credits user wallet if payment was successful. +// @Tags SantimPay +// @Accept json +// @Produce json +// @Param request body domain.SantimPayCallbackPayload true "SantimPay callback payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/santimpay/callback [post] +func (h *Handler) ProcessSantimPayCallbackHandler(c *fiber.Ctx) error { + var payload domain.SantimPayCallbackPayload + if err := c.BodyParser(&payload); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Invalid callback payload", + }) + } + + if err := h.santimpaySvc.ProcessCallback(c.Context(), payload); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Failed to process SantimPay callback", + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "SantimPay callback processed successfully", + Data: nil, + Success: true, + StatusCode: fiber.StatusOK, + }) +} + +// ProcessSantimPayDirectPaymentHandler initializes a direct payment session with SantimPay. +// +// @Summary Process SantimPay Direct Payment +// @Description Initiates a direct payment request with SantimPay and returns the response. +// @Tags SantimPay +// @Accept json +// @Produce json +// @Param request body domain.GeneratePaymentURLRequest true "SantimPay direct payment request payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/santimpay/direct-payment [post] +func (h *Handler) ProcessSantimPayDirectPaymentHandler(c *fiber.Ctx) error { + var req domain.GeneratePaymentURLRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Invalid direct payment request payload", + }) + } + + response, err := h.santimpaySvc.ProcessDirectPayment(c.Context(), req) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Failed to process SantimPay direct payment", + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "SantimPay direct payment processed successfully", + Data: response, + Success: true, + StatusCode: fiber.StatusOK, + }) +} + +// GetSantimPayB2CPartnersHandler retrieves all available SantimPay B2C payout partners. +// +// @Summary Get SantimPay B2C Partners +// @Description Fetches a list of available B2C payout partners (e.g., Telebirr, Mpesa, Banks) from SantimPay. +// @Tags SantimPay +// @Accept json +// @Produce json +// @Success 200 {object} domain.Response +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/santimpay/b2c/partners [get] +func (h *Handler) GetSantimPayB2CPartnersHandler(c *fiber.Ctx) error { + partners, err := h.santimpaySvc.GetB2CPartners(c.Context()) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Failed to fetch SantimPay B2C partners", + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "SantimPay B2C partners retrieved successfully", + Data: partners, + Success: true, + StatusCode: fiber.StatusOK, + }) +} + +// ProcessSantimPayB2CWithdrawalHandler processes a B2C (Withdrawal) transaction with SantimPay. +// +// @Summary Process SantimPay B2C Withdrawal +// @Description Initiates a B2C withdrawal request with SantimPay and returns the response. +// @Tags SantimPay +// @Accept json +// @Produce json +// @Param request body domain.GeneratePaymentURLRequest true "SantimPay B2C withdrawal request payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/santimpay/b2c-withdrawal [post] +func (h *Handler) ProcessSantimPayB2CWithdrawalHandler(c *fiber.Ctx) error { + var req domain.GeneratePaymentURLRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Invalid B2C withdrawal request payload", + }) + } + + // Extract userId from context/session (adapt based on your auth flow) + userId, ok := c.Locals("userId").(int64) + if !ok { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Error: "missing userId in context", + Message: "Could not process withdrawal without user ID", + }) + } + + response, err := h.santimpaySvc.ProcessB2CWithdrawal(c.Context(), req, userId) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Failed to process SantimPay B2C withdrawal", + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "SantimPay B2C withdrawal processed successfully", + Data: response, + Success: true, + StatusCode: fiber.StatusOK, + }) +} + +// CheckSantimPayTransactionStatusHandler checks the status of a SantimPay transaction. +// +// @Summary Check SantimPay Transaction Status +// @Description Retrieves the real-time status of a transaction from SantimPay. +// @Tags SantimPay +// @Accept json +// @Produce json +// @Param request body domain.TransactionStatusRequest true "Transaction status request payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/santimpay/transaction-status [post] +func (h *Handler) CheckSantimPayTransactionStatusHandler(c *fiber.Ctx) error { + var req domain.TransactionStatusRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Invalid transaction status request payload", + }) + } + + // Optional: extract fullParams from request, default to true if not provided + fullParams := true + if req.FullParams == nil { + req.FullParams = &fullParams + } + + response, err := h.santimpaySvc.CheckTransactionStatus(c.Context(), req) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Failed to check SantimPay transaction status", + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "SantimPay transaction status retrieved successfully", + Data: response, + Success: true, + StatusCode: fiber.StatusOK, + }) +} diff --git a/internal/web_server/handlers/telebirr.go b/internal/web_server/handlers/telebirr.go index c715484..dbb808e 100644 --- a/internal/web_server/handlers/telebirr.go +++ b/internal/web_server/handlers/telebirr.go @@ -14,7 +14,7 @@ import ( // @Tags Telebirr // @Accept json // @Produce json -// @Param request body domain.GeneratePaymentURLInput true "Telebirr payment request payload" +// @Param request body domain.GeneratePaymentURLRequest true "Telebirr payment request payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 2d40bfa..aa589b7 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -127,7 +127,13 @@ func (a *App) initAppRoutes() { groupV1.Post("/telebirr/callback", h.HandleTelebirrCallback) //Santimpay - groupV1.Post("/santimpay/init-payment", h.CreateSantimPayPaymentHandler) + groupV1.Post("/santimpay/init-payment", a.authMiddleware, h.InititateSantimPayPaymentHandler) + groupV1.Post("/santimpay/callback", h.ProcessSantimPayCallbackHandler) + groupV1.Post("/santimpay/direct-payment", a.authMiddleware, h.ProcessSantimPayDirectPaymentHandler) + groupV1.Get("/santimpay/b2c/partners", h.GetSantimPayB2CPartnersHandler) + groupV1.Post("/santimpay/b2c/withdraw", a.authMiddleware, h.ProcessSantimPayB2CWithdrawalHandler) + groupV1.Post("/santimpay/transaction/verify", a.authMiddleware, h.CheckSantimPayTransactionStatusHandler) + // groupV1.Post("/arifpay/b2c/transfer", a.authMiddleware, h.B2CTransferHandler) // groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler) // groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler)