From d40bdcf33c9b14fbace96263f647dfbb04b182c1 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Mon, 18 Aug 2025 16:35:33 +0300 Subject: [PATCH] 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)