From d654d5f2ef8f6ca43cdd0f29ab2be023a0dd7b45 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Thu, 6 Nov 2025 16:37:41 +0300 Subject: [PATCH] veli games fixes --- .../000004_virtual_game_Session.up.sql | 5 +- db/query/virtual_games.sql | 44 +- docs/docs.go | 187 +++------ docs/swagger.json | 187 +++------ docs/swagger.yaml | 139 +++---- gen/db/models.go | 3 - gen/db/virtual_games.sql.go | 93 ++--- internal/config/config.go | 2 + internal/domain/arifpay.go | 12 +- internal/domain/chapa.go | 168 +++++--- internal/domain/veli_games.go | 12 +- internal/repository/virtual_game.go | 50 ++- internal/services/arifpay/service.go | 377 ++++++++++++------ internal/services/chapa/client.go | 24 +- internal/services/chapa/port.go | 6 +- internal/services/chapa/service.go | 241 +++++++---- internal/services/virtualGame/Alea/service.go | 14 +- internal/services/virtualGame/veli/service.go | 18 +- internal/web_server/handlers/arifpay.go | 49 ++- internal/web_server/handlers/chapa.go | 127 +++--- internal/web_server/handlers/veli_games.go | 3 + internal/web_server/routes.go | 22 +- 22 files changed, 967 insertions(+), 816 deletions(-) diff --git a/db/migrations/000004_virtual_game_Session.up.sql b/db/migrations/000004_virtual_game_Session.up.sql index 2dc5ed2..b9fbd25 100644 --- a/db/migrations/000004_virtual_game_Session.up.sql +++ b/db/migrations/000004_virtual_game_Session.up.sql @@ -3,11 +3,8 @@ CREATE TABLE virtual_game_sessions ( user_id BIGINT NOT NULL REFERENCES users(id), game_id VARCHAR(50) NOT NULL, session_token VARCHAR(255) NOT NULL UNIQUE, - currency VARCHAR(3) NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE, COMPLETED, FAILED created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMP WITH TIME ZONE NOT NULL + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE virtual_game_transactions ( diff --git a/db/query/virtual_games.sql b/db/query/virtual_games.sql index 8bee846..32b61d1 100644 --- a/db/query/virtual_games.sql +++ b/db/query/virtual_games.sql @@ -61,40 +61,40 @@ RETURNING id, updated_at; -- name: CreateVirtualGameSession :one INSERT INTO virtual_game_sessions ( - user_id, - game_id, - session_token, - currency, - status, - expires_at - ) -VALUES ($1, $2, $3, $4, $5, $6) -RETURNING id, + user_id, + game_id, + session_token +) +VALUES ($1, $2, $3) +RETURNING + id, user_id, game_id, session_token, - currency, - status, created_at, - updated_at, - expires_at; + updated_at; + +-- name: GetVirtualGameSessionByUserID :one +SELECT + id, + user_id, + game_id, + session_token, + created_at, + updated_at +FROM virtual_game_sessions +WHERE user_id = $1; + -- name: GetVirtualGameSessionByToken :one SELECT id, user_id, game_id, session_token, - currency, - status, created_at, - updated_at, - expires_at + updated_at FROM virtual_game_sessions WHERE session_token = $1; --- name: UpdateVirtualGameSessionStatus :exec -UPDATE virtual_game_sessions -SET status = $2, - updated_at = CURRENT_TIMESTAMP -WHERE id = $1; + -- name: CreateVirtualGameTransaction :one INSERT INTO virtual_game_transactions ( session_id, diff --git a/docs/docs.go b/docs/docs.go index 6251fd1..361e99b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -458,7 +458,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.ArifpayB2CRequest" + "$ref": "#/definitions/domain.CheckoutSessionClientRequest" } } ], @@ -585,7 +585,7 @@ const docTemplate = `{ } } }, - "/api/v1/arifpay/checkout/{sessionId}/cancel": { + "/api/v1/arifpay/checkout/cancel/{sessionId}": { "post": { "description": "Cancels a payment session using Arifpay before completion.", "consumes": [ @@ -2254,7 +2254,7 @@ const docTemplate = `{ }, "/api/v1/chapa/payments/webhook/verify": { "post": { - "description": "Handles payment notifications from Chapa", + "description": "Handles payment and transfer notifications from Chapa", "consumes": [ "application/json" ], @@ -2272,7 +2272,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.ChapaWebhookPayload" + "$ref": "#/definitions/domain.ChapaWebhookPayment" } } ], @@ -2280,8 +2280,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -2329,7 +2328,7 @@ const docTemplate = `{ } ], "responses": { - "201": { + "200": { "description": "Chapa withdrawal process initiated successfully", "schema": { "$ref": "#/definitions/domain.Response" @@ -2364,7 +2363,7 @@ const docTemplate = `{ }, "/api/v1/chapa/swap": { "post": { - "description": "Perform a USD to ETB currency swap using Chapa's API", + "description": "Convert an amount from one currency to another using Chapa's currency swap API", "consumes": [ "application/json" ], @@ -2374,11 +2373,11 @@ const docTemplate = `{ "tags": [ "Chapa" ], - "summary": "Initiate a currency swap", + "summary": "Swap currency using Chapa API", "parameters": [ { - "description": "Swap Request Payload", - "name": "payload", + "description": "Swap request payload", + "name": "request", "in": "body", "required": true, "schema": { @@ -2536,7 +2535,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.ChapaVerificationResponse" + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -10229,28 +10228,6 @@ const docTemplate = `{ } } }, - "domain.ArifpayB2CRequest": { - "type": "object", - "required": [ - "amount", - "customerEmail", - "customerPhone" - ], - "properties": { - "Phonenumber": { - "type": "string" - }, - "amount": { - "type": "number" - }, - "customerEmail": { - "type": "string" - }, - "customerPhone": { - "type": "string" - } - } - }, "domain.ArifpayVerifyByTransactionIDRequest": { "type": "object", "properties": { @@ -10980,92 +10957,75 @@ const docTemplate = `{ } } }, - "domain.ChapaVerificationResponse": { + "domain.ChapaWebhookCustomization": { "type": "object", "properties": { - "data": { - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "charge": { - "type": "number" - }, - "created_at": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "customization": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "logo": {}, - "title": { - "type": "string" - } - } - }, - "email": { - "type": "string" - }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "meta": {}, - "method": { - "type": "string" - }, - "mode": { - "type": "string" - }, - "reference": { - "type": "string" - }, - "status": { - "type": "string" - }, - "tx_ref": { - "type": "string" - }, - "type": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "message": { + "description": { "type": "string" }, - "status": { + "logo": { + "type": "string" + }, + "title": { "type": "string" } } }, - "domain.ChapaWebhookPayload": { + "domain.ChapaWebhookPayment": { "type": "object", "properties": { "amount": { - "type": "integer" + "type": "string" + }, + "charge": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "customization": { + "$ref": "#/definitions/domain.ChapaWebhookCustomization" + }, + "email": { + "type": "string" + }, + "event": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "meta": { + "description": "may vary in structure, so kept flexible" + }, + "mobile": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "payment_method": { + "type": "string" + }, + "reference": { + "type": "string" }, "status": { - "description": "Currency string ` + "`" + `json:\"currency\"` + "`" + `", - "allOf": [ - { - "$ref": "#/definitions/domain.PaymentStatus" - } - ] + "type": "string" }, - "trx_ref": { + "tx_ref": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { "type": "string" } } @@ -12884,21 +12844,6 @@ const docTemplate = `{ "BANK" ] }, - "domain.PaymentStatus": { - "type": "string", - "enum": [ - "success", - "pending", - "completed", - "failed" - ], - "x-enum-varnames": [ - "PaymentStatusSuccessful", - "PaymentStatusPending", - "PaymentStatusCompleted", - "PaymentStatusFailed" - ] - }, "domain.PopOKCallback": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 42e5c28..fa4aba2 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -450,7 +450,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.ArifpayB2CRequest" + "$ref": "#/definitions/domain.CheckoutSessionClientRequest" } } ], @@ -577,7 +577,7 @@ } } }, - "/api/v1/arifpay/checkout/{sessionId}/cancel": { + "/api/v1/arifpay/checkout/cancel/{sessionId}": { "post": { "description": "Cancels a payment session using Arifpay before completion.", "consumes": [ @@ -2246,7 +2246,7 @@ }, "/api/v1/chapa/payments/webhook/verify": { "post": { - "description": "Handles payment notifications from Chapa", + "description": "Handles payment and transfer notifications from Chapa", "consumes": [ "application/json" ], @@ -2264,7 +2264,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.ChapaWebhookPayload" + "$ref": "#/definitions/domain.ChapaWebhookPayment" } } ], @@ -2272,8 +2272,7 @@ "200": { "description": "OK", "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -2321,7 +2320,7 @@ } ], "responses": { - "201": { + "200": { "description": "Chapa withdrawal process initiated successfully", "schema": { "$ref": "#/definitions/domain.Response" @@ -2356,7 +2355,7 @@ }, "/api/v1/chapa/swap": { "post": { - "description": "Perform a USD to ETB currency swap using Chapa's API", + "description": "Convert an amount from one currency to another using Chapa's currency swap API", "consumes": [ "application/json" ], @@ -2366,11 +2365,11 @@ "tags": [ "Chapa" ], - "summary": "Initiate a currency swap", + "summary": "Swap currency using Chapa API", "parameters": [ { - "description": "Swap Request Payload", - "name": "payload", + "description": "Swap request payload", + "name": "request", "in": "body", "required": true, "schema": { @@ -2528,7 +2527,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.ChapaVerificationResponse" + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -10221,28 +10220,6 @@ } } }, - "domain.ArifpayB2CRequest": { - "type": "object", - "required": [ - "amount", - "customerEmail", - "customerPhone" - ], - "properties": { - "Phonenumber": { - "type": "string" - }, - "amount": { - "type": "number" - }, - "customerEmail": { - "type": "string" - }, - "customerPhone": { - "type": "string" - } - } - }, "domain.ArifpayVerifyByTransactionIDRequest": { "type": "object", "properties": { @@ -10972,92 +10949,75 @@ } } }, - "domain.ChapaVerificationResponse": { + "domain.ChapaWebhookCustomization": { "type": "object", "properties": { - "data": { - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "charge": { - "type": "number" - }, - "created_at": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "customization": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "logo": {}, - "title": { - "type": "string" - } - } - }, - "email": { - "type": "string" - }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "meta": {}, - "method": { - "type": "string" - }, - "mode": { - "type": "string" - }, - "reference": { - "type": "string" - }, - "status": { - "type": "string" - }, - "tx_ref": { - "type": "string" - }, - "type": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "message": { + "description": { "type": "string" }, - "status": { + "logo": { + "type": "string" + }, + "title": { "type": "string" } } }, - "domain.ChapaWebhookPayload": { + "domain.ChapaWebhookPayment": { "type": "object", "properties": { "amount": { - "type": "integer" + "type": "string" + }, + "charge": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "customization": { + "$ref": "#/definitions/domain.ChapaWebhookCustomization" + }, + "email": { + "type": "string" + }, + "event": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "meta": { + "description": "may vary in structure, so kept flexible" + }, + "mobile": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "payment_method": { + "type": "string" + }, + "reference": { + "type": "string" }, "status": { - "description": "Currency string `json:\"currency\"`", - "allOf": [ - { - "$ref": "#/definitions/domain.PaymentStatus" - } - ] + "type": "string" }, - "trx_ref": { + "tx_ref": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { "type": "string" } } @@ -12876,21 +12836,6 @@ "BANK" ] }, - "domain.PaymentStatus": { - "type": "string", - "enum": [ - "success", - "pending", - "completed", - "failed" - ], - "x-enum-varnames": [ - "PaymentStatusSuccessful", - "PaymentStatusPending", - "PaymentStatusCompleted", - "PaymentStatusFailed" - ] - }, "domain.PopOKCallback": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d439353..6dddb84 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -38,21 +38,6 @@ definitions: user_id: type: string type: object - domain.ArifpayB2CRequest: - properties: - Phonenumber: - type: string - amount: - type: number - customerEmail: - type: string - customerPhone: - type: string - required: - - amount - - customerEmail - - customerPhone - type: object domain.ArifpayVerifyByTransactionIDRequest: properties: paymentType: @@ -546,62 +531,52 @@ definitions: updated_at: type: string type: object - domain.ChapaVerificationResponse: + domain.ChapaWebhookCustomization: properties: - data: - properties: - amount: - type: number - charge: - type: number - created_at: - type: string - currency: - type: string - customization: - properties: - description: - type: string - logo: {} - title: - type: string - type: object - email: - type: string - first_name: - type: string - last_name: - type: string - meta: {} - method: - type: string - mode: - type: string - reference: - type: string - status: - type: string - tx_ref: - type: string - type: - type: string - updated_at: - type: string - type: object - message: + description: type: string - status: + logo: + type: string + title: type: string type: object - domain.ChapaWebhookPayload: + domain.ChapaWebhookPayment: properties: amount: - type: integer + type: string + charge: + type: string + created_at: + type: string + currency: + type: string + customization: + $ref: '#/definitions/domain.ChapaWebhookCustomization' + email: + type: string + event: + type: string + first_name: + type: string + last_name: + type: string + meta: + description: may vary in structure, so kept flexible + mobile: + type: string + mode: + type: string + payment_method: + type: string + reference: + type: string status: - allOf: - - $ref: '#/definitions/domain.PaymentStatus' - description: Currency string `json:"currency"` - trx_ref: + type: string + tx_ref: + type: string + type: + type: string + updated_at: type: string type: object domain.ChapaWithdrawalRequest: @@ -1844,18 +1819,6 @@ definitions: - TELEBIRR_TRANSACTION - ARIFPAY_TRANSACTION - BANK - domain.PaymentStatus: - enum: - - success - - pending - - completed - - failed - type: string - x-enum-varnames: - - PaymentStatusSuccessful - - PaymentStatusPending - - PaymentStatusCompleted - - PaymentStatusFailed domain.PopOKCallback: properties: amount: @@ -4608,7 +4571,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/domain.ArifpayB2CRequest' + $ref: '#/definitions/domain.CheckoutSessionClientRequest' produces: - application/json responses: @@ -4695,7 +4658,7 @@ paths: summary: Create Arifpay Checkout Session tags: - Arifpay - /api/v1/arifpay/checkout/{sessionId}/cancel: + /api/v1/arifpay/checkout/cancel/{sessionId}: post: consumes: - application/json @@ -5789,22 +5752,21 @@ paths: post: consumes: - application/json - description: Handles payment notifications from Chapa + description: Handles payment and transfer notifications from Chapa parameters: - description: Webhook payload in: body name: request required: true schema: - $ref: '#/definitions/domain.ChapaWebhookPayload' + $ref: '#/definitions/domain.ChapaWebhookPayment' produces: - application/json responses: "200": description: OK schema: - additionalProperties: true - type: object + $ref: '#/definitions/domain.Response' "400": description: Bad Request schema: @@ -5832,7 +5794,7 @@ paths: produces: - application/json responses: - "201": + "200": description: Chapa withdrawal process initiated successfully schema: $ref: '#/definitions/domain.Response' @@ -5861,11 +5823,12 @@ paths: post: consumes: - application/json - description: Perform a USD to ETB currency swap using Chapa's API + description: Convert an amount from one currency to another using Chapa's currency + swap API parameters: - - description: Swap Request Payload + - description: Swap request payload in: body - name: payload + name: request required: true schema: $ref: '#/definitions/domain.SwapRequest' @@ -5884,7 +5847,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.ErrorResponse' - summary: Initiate a currency swap + summary: Swap currency using Chapa API tags: - Chapa /api/v1/chapa/transaction/cancel/{tx_ref}: @@ -5970,7 +5933,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/domain.ChapaVerificationResponse' + $ref: '#/definitions/domain.Response' "400": description: Bad Request schema: diff --git a/gen/db/models.go b/gen/db/models.go index a3ff73c..474e91f 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1093,11 +1093,8 @@ type VirtualGameSession struct { UserID int64 `json:"user_id"` GameID string `json:"game_id"` SessionToken string `json:"session_token"` - Currency string `json:"currency"` - Status string `json:"status"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` - ExpiresAt pgtype.Timestamptz `json:"expires_at"` } type VirtualGameTransaction struct { diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index a9e8bec..1e39f92 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -409,54 +409,36 @@ func (q *Queries) CreateVirtualGameReport(ctx context.Context, arg CreateVirtual const CreateVirtualGameSession = `-- name: CreateVirtualGameSession :one INSERT INTO virtual_game_sessions ( - user_id, - game_id, - session_token, - currency, - status, - expires_at - ) -VALUES ($1, $2, $3, $4, $5, $6) -RETURNING id, + user_id, + game_id, + session_token +) +VALUES ($1, $2, $3) +RETURNING + id, user_id, game_id, session_token, - currency, - status, created_at, - updated_at, - expires_at + updated_at ` type CreateVirtualGameSessionParams struct { - UserID int64 `json:"user_id"` - GameID string `json:"game_id"` - SessionToken string `json:"session_token"` - Currency string `json:"currency"` - Status string `json:"status"` - ExpiresAt pgtype.Timestamptz `json:"expires_at"` + UserID int64 `json:"user_id"` + GameID string `json:"game_id"` + SessionToken string `json:"session_token"` } func (q *Queries) CreateVirtualGameSession(ctx context.Context, arg CreateVirtualGameSessionParams) (VirtualGameSession, error) { - row := q.db.QueryRow(ctx, CreateVirtualGameSession, - arg.UserID, - arg.GameID, - arg.SessionToken, - arg.Currency, - arg.Status, - arg.ExpiresAt, - ) + row := q.db.QueryRow(ctx, CreateVirtualGameSession, arg.UserID, arg.GameID, arg.SessionToken) var i VirtualGameSession err := row.Scan( &i.ID, &i.UserID, &i.GameID, &i.SessionToken, - &i.Currency, - &i.Status, &i.CreatedAt, &i.UpdatedAt, - &i.ExpiresAt, ) return i, err } @@ -751,11 +733,8 @@ SELECT id, user_id, game_id, session_token, - currency, - status, created_at, - updated_at, - expires_at + updated_at FROM virtual_game_sessions WHERE session_token = $1 ` @@ -768,11 +747,34 @@ func (q *Queries) GetVirtualGameSessionByToken(ctx context.Context, sessionToken &i.UserID, &i.GameID, &i.SessionToken, - &i.Currency, - &i.Status, &i.CreatedAt, &i.UpdatedAt, - &i.ExpiresAt, + ) + return i, err +} + +const GetVirtualGameSessionByUserID = `-- name: GetVirtualGameSessionByUserID :one +SELECT + id, + user_id, + game_id, + session_token, + created_at, + updated_at +FROM virtual_game_sessions +WHERE user_id = $1 +` + +func (q *Queries) GetVirtualGameSessionByUserID(ctx context.Context, userID int64) (VirtualGameSession, error) { + row := q.db.QueryRow(ctx, GetVirtualGameSessionByUserID, userID) + var i VirtualGameSession + err := row.Scan( + &i.ID, + &i.UserID, + &i.GameID, + &i.SessionToken, + &i.CreatedAt, + &i.UpdatedAt, ) return i, err } @@ -1117,23 +1119,6 @@ func (q *Queries) UpdateVirtualGameProviderReportByDate(ctx context.Context, arg return err } -const UpdateVirtualGameSessionStatus = `-- name: UpdateVirtualGameSessionStatus :exec -UPDATE virtual_game_sessions -SET status = $2, - updated_at = CURRENT_TIMESTAMP -WHERE id = $1 -` - -type UpdateVirtualGameSessionStatusParams struct { - ID int64 `json:"id"` - Status string `json:"status"` -} - -func (q *Queries) UpdateVirtualGameSessionStatus(ctx context.Context, arg UpdateVirtualGameSessionStatusParams) error { - _, err := q.db.Exec(ctx, UpdateVirtualGameSessionStatus, arg.ID, arg.Status) - return err -} - const UpdateVirtualGameTransactionStatus = `-- name: UpdateVirtualGameTransactionStatus :exec UPDATE virtual_game_transactions SET status = $2, diff --git a/internal/config/config.go b/internal/config/config.go index b802be0..717cc19 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -136,6 +136,7 @@ type Config struct { AFRO_SMS_SENDER_NAME string AFRO_SMS_RECEIVER_PHONE_NUMBER string ADRO_SMS_HOST_URL string + CHAPA_WEBHOOK_SECRET string CHAPA_TRANSFER_TYPE string CHAPA_PAYMENT_TYPE string CHAPA_SECRET_KEY string @@ -259,6 +260,7 @@ func (c *Config) loadEnv() error { c.TELEBIRR.TelebirrCallbackURL = os.Getenv("TELEBIRR_CALLBACK_URL") //Chapa + c.CHAPA_WEBHOOK_SECRET = os.Getenv("CHAPA_WEBHOOK_SECRET") c.CHAPA_SECRET_KEY = os.Getenv("CHAPA_SECRET_KEY") c.CHAPA_PUBLIC_KEY = os.Getenv("CHAPA_PUBLIC_KEY") c.CHAPA_ENCRYPTION_KEY = os.Getenv("CHAPA_ENCRYPTION_KEY") diff --git a/internal/domain/arifpay.go b/internal/domain/arifpay.go index 94ac010..2be1616 100644 --- a/internal/domain/arifpay.go +++ b/internal/domain/arifpay.go @@ -59,12 +59,12 @@ type WebhookRequest struct { SessionID string `json:"sessionId"` } -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 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"` diff --git a/internal/domain/chapa.go b/internal/domain/chapa.go index 1d814ee..ef749d9 100644 --- a/internal/domain/chapa.go +++ b/internal/domain/chapa.go @@ -76,7 +76,7 @@ type ChapaDepositVerification struct { Currency string } -type ChapaVerificationResponse struct { +type ChapaPaymentVerificationResponse struct { Message string `json:"message"` Status string `json:"status"` Data struct { @@ -103,6 +103,31 @@ type ChapaVerificationResponse struct { } `json:"data"` } +type ChapaTransferVerificationResponse struct { + Message string `json:"message"` + Status string `json:"status"` + Data struct { + AccountName string `json:"account_name"` + AccountNumber string `json:"account_number"` + Mobile interface{} `json:"mobile"` + Currency string `json:"currency"` + Amount float64 `json:"amount"` + Charge float64 `json:"charge"` + Mode string `json:"mode"` + TransferMethod string `json:"transfer_method"` + Narration interface{} `json:"narration"` + ChapaTransferID string `json:"chapa_transfer_id"` + BankCode int `json:"bank_code"` + BankName string `json:"bank_name"` + CrossPartyReference interface{} `json:"cross_party_reference"` + IPAddress string `json:"ip_address"` + Status string `json:"status"` + TxRef string `json:"tx_ref"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + } `json:"data"` +} + type ChapaAllTransactionsResponse struct { Message string `json:"message"` Status string `json:"status"` @@ -182,6 +207,57 @@ type ChapaCustomer struct { // BankLogo string `json:"bank_logo"` // URL or base64 // } +type SwapResponse struct { + Message string `json:"message"` + Status string `json:"status"` + Data struct { + Status string `json:"status"` + RefID string `json:"ref_id"` + FromCurrency string `json:"from_currency"` + ToCurrency string `json:"to_currency"` + Amount float64 `json:"amount"` + ExchangedAmount float64 `json:"exchanged_amount"` + Charge float64 `json:"charge"` + Rate float64 `json:"rate"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + } `json:"data"` +} + +type ChapaTransfersListResponse struct { + Message string `json:"message"` + Status string `json:"status"` + Meta struct { + CurrentPage int `json:"current_page"` + FirstPageURL string `json:"first_page_url"` + LastPage int `json:"last_page"` + LastPageURL string `json:"last_page_url"` + NextPageURL string `json:"next_page_url"` + Path string `json:"path"` + PerPage int `json:"per_page"` + PrevPageURL interface{} `json:"prev_page_url"` + To int `json:"to"` + Total int `json:"total"` + Error []interface{} `json:"error"` + } `json:"meta"` + Data []struct { + AccountName string `json:"account_name"` + AccountNumber string `json:"account_number"` + Currency string `json:"currency"` + Amount float64 `json:"amount"` + Charge float64 `json:"charge"` + TransferType string `json:"transfer_type"` + ChapaReference string `json:"chapa_reference"` + BankCode int `json:"bank_code"` + BankName string `json:"bank_name"` + BankReference interface{} `json:"bank_reference"` + Status string `json:"status"` + Reference interface{} `json:"reference"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + } `json:"data"` +} + type BankResponse struct { Message string `json:"message"` Status string `json:"status"` @@ -246,44 +322,49 @@ type ChapaTransactionType struct { Type string `json:"type"` } -type ChapaWebHookTransfer struct { - AccountName string `json:"account_name"` - AccountNumber string `json:"account_number"` - BankId string `json:"bank_id"` - BankName string `json:"bank_name"` - Currency string `json:"currency"` - Amount string `json:"amount"` - Type string `json:"type"` - Status string `json:"status"` - Reference string `json:"reference"` - TxRef string `json:"tx_ref"` - ChapaReference string `json:"chapa_reference"` - CreatedAt time.Time `json:"created_at"` +type ChapaWebhookTransfer struct { + Event string `json:"event"` + Type string `json:"type"` + AccountName string `json:"account_name"` + AccountNumber string `json:"account_number"` + BankID int `json:"bank_id"` + BankName string `json:"bank_name"` + Amount string `json:"amount"` + Charge string `json:"charge"` + Currency string `json:"currency"` + Status string `json:"status"` + Reference string `json:"reference"` + ChapaReference string `json:"chapa_reference"` + BankReference string `json:"bank_reference"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } -type ChapaWebHookPayment struct { - Event string `json:"event"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email string `json:"email"` - Mobile interface{} `json:"mobile"` - Currency string `json:"currency"` - Amount string `json:"amount"` - Charge string `json:"charge"` - Status string `json:"status"` - Mode string `json:"mode"` - Reference string `json:"reference"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Type string `json:"type"` - TxRef string `json:"tx_ref"` - PaymentMethod string `json:"payment_method"` - Customization struct { - Title interface{} `json:"title"` - Description interface{} `json:"description"` - Logo interface{} `json:"logo"` - } `json:"customization"` - Meta string `json:"meta"` +type ChapaWebhookPayment struct { + Event string `json:"event"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email *string `json:"email,omitempty"` + Mobile string `json:"mobile"` + Currency string `json:"currency"` + Amount string `json:"amount"` + Charge string `json:"charge"` + Status string `json:"status"` + Mode string `json:"mode"` + Reference string `json:"reference"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Type string `json:"type"` + TxRef string `json:"tx_ref"` + PaymentMethod string `json:"payment_method"` + Customization ChapaWebhookCustomization `json:"customization"` + Meta interface{} `json:"meta"` // may vary in structure, so kept flexible +} + +type ChapaWebhookCustomization struct { + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + Logo *string `json:"logo,omitempty"` } type Balance struct { @@ -298,19 +379,6 @@ type SwapRequest struct { Amount float64 `json:"amount"` } -type SwapResponse struct { - Status string `json:"status"` - RefID string `json:"ref_id"` - FromCurrency string `json:"from_currency"` - ToCurrency string `json:"to_currency"` - Amount float64 `json:"amount"` - ExchangedAmount float64 `json:"exchanged_amount"` - Charge float64 `json:"charge"` - Rate float64 `json:"rate"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` -} - type ChapaCancelResponse struct { Message string `json:"message"` Status string `json:"status"` diff --git a/internal/domain/veli_games.go b/internal/domain/veli_games.go index db6ad6e..0d02e4b 100644 --- a/internal/domain/veli_games.go +++ b/internal/domain/veli_games.go @@ -58,10 +58,10 @@ type GameStartRequest struct { Country string `json:"country"` IP string `json:"ip"` BrandID string `json:"brandId"` - UserAgent string `json:"userAgent,omitempty"` - LobbyURL string `json:"lobbyUrl,omitempty"` - CashierURL string `json:"cashierUrl,omitempty"` - PlayerName string `json:"playerName,omitempty"` + // UserAgent string `json:"userAgent,omitempty"` + // LobbyURL string `json:"lobbyUrl,omitempty"` + // CashierURL string `json:"cashierUrl,omitempty"` + // PlayerName string `json:"playerName,omitempty"` } type DemoGameRequest struct { @@ -71,8 +71,8 @@ type DemoGameRequest struct { DeviceType string `json:"deviceType"` IP string `json:"ip"` BrandID string `json:"brandId"` - PlayerID string `json:"playerId,omitempty"` - Country string `json:"country,omitempty"` + // PlayerID string `json:"playerId,omitempty"` + // Country string `json:"country,omitempty"` } type GameStartResponse struct { diff --git a/internal/repository/virtual_game.go b/internal/repository/virtual_game.go index f970a9d..e7befef 100644 --- a/internal/repository/virtual_game.go +++ b/internal/repository/virtual_game.go @@ -21,8 +21,9 @@ type VirtualGameRepository interface { ListVirtualGameProviders(ctx context.Context, limit, offset int32) ([]dbgen.VirtualGameProvider, error) UpdateVirtualGameProviderEnabled(ctx context.Context, providerID string, enabled bool) (dbgen.VirtualGameProvider, error) CreateVirtualGameSession(ctx context.Context, session *domain.VirtualGameSession) error + GetVirtualGameSessionByUserID(ctx context.Context, userID int64) (*domain.VirtualGameSession, error) GetVirtualGameSessionByToken(ctx context.Context, token string) (*domain.VirtualGameSession, error) - UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error + // UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error) UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error @@ -166,14 +167,33 @@ func (r *VirtualGameRepo) CreateVirtualGameSession(ctx context.Context, session UserID: session.UserID, GameID: session.GameID, SessionToken: session.SessionToken, - Currency: session.Currency, - Status: session.Status, - ExpiresAt: pgtype.Timestamptz{Time: session.ExpiresAt, Valid: true}, + // Currency: session.Currency, + // Status: session.Status, + // ExpiresAt: pgtype.Timestamptz{Time: session.ExpiresAt, Valid: true}, } _, err := r.store.queries.CreateVirtualGameSession(ctx, params) return err } +func (r *VirtualGameRepo) GetVirtualGameSessionByUserID(ctx context.Context, userID int64) (*domain.VirtualGameSession, error) { + dbSession, err := r.store.queries.GetVirtualGameSessionByUserID(ctx, userID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + + return &domain.VirtualGameSession{ + ID: dbSession.ID, + UserID: dbSession.UserID, + GameID: dbSession.GameID, + SessionToken: dbSession.SessionToken, + CreatedAt: dbSession.CreatedAt.Time, + UpdatedAt: dbSession.UpdatedAt.Time, + }, nil +} + func (r *VirtualGameRepo) GetVirtualGameSessionByToken(ctx context.Context, token string) (*domain.VirtualGameSession, error) { dbSession, err := r.store.queries.GetVirtualGameSessionByToken(ctx, token) if err != nil { @@ -187,20 +207,20 @@ func (r *VirtualGameRepo) GetVirtualGameSessionByToken(ctx context.Context, toke UserID: dbSession.UserID, GameID: dbSession.GameID, SessionToken: dbSession.SessionToken, - Currency: dbSession.Currency, - Status: dbSession.Status, - CreatedAt: dbSession.CreatedAt.Time, - UpdatedAt: dbSession.UpdatedAt.Time, - ExpiresAt: dbSession.ExpiresAt.Time, + // Currency: dbSession.Currency, + // Status: dbSession.Status, + CreatedAt: dbSession.CreatedAt.Time, + UpdatedAt: dbSession.UpdatedAt.Time, + // ExpiresAt: dbSession.ExpiresAt.Time, }, nil } -func (r *VirtualGameRepo) UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error { - return r.store.queries.UpdateVirtualGameSessionStatus(ctx, dbgen.UpdateVirtualGameSessionStatusParams{ - ID: id, - Status: status, - }) -} +// func (r *VirtualGameRepo) UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error { +// return r.store.queries.UpdateVirtualGameSessionStatus(ctx, dbgen.UpdateVirtualGameSessionStatusParams{ +// ID: id, +// Status: status, +// }) +// } func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error { params := dbgen.CreateVirtualGameTransactionParams{ diff --git a/internal/services/arifpay/service.go b/internal/services/arifpay/service.go index a074d98..37c3c08 100644 --- a/internal/services/arifpay/service.go +++ b/internal/services/arifpay/service.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -31,15 +32,15 @@ func NewArifpayService(cfg *config.Config, transferStore wallet.TransferStore, w } } -func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientRequest, isDeposit bool) (map[string]any, error) { +func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientRequest, isDeposit bool, userId int64) (map[string]any, error) { // Generate unique nonce nonce := uuid.NewString() var NotifyURL string - if isDeposit{ + if isDeposit { NotifyURL = s.cfg.ARIFPAY.C2BNotifyUrl - }else{ + } else { NotifyURL = s.cfg.ARIFPAY.B2CNotifyUrl } @@ -129,6 +130,10 @@ func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientR ReferenceNumber: nonce, SessionID: fmt.Sprintf("%v", data["sessionId"]), Status: string(domain.PaymentStatusPending), + CashierID: domain.ValidInt64{ + Value: userId, + Valid: true, + }, } if _, err := s.transferStore.CreateTransfer(context.Background(), transfer); err != nil { @@ -138,7 +143,7 @@ func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientR return data, nil } -func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID string) (*domain.CancelCheckoutSessionResponse, error) { +func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID string) (any, error) { // Build the cancel URL url := fmt.Sprintf("%s/api/sandbox/checkout/session/%s", s.cfg.ARIFPAY.BaseURL, sessionID) @@ -176,17 +181,19 @@ func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID st return nil, fmt.Errorf("failed to unmarshal cancel response: %w", err) } - return &cancelResp, nil + return cancelResp.Data, nil } -func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRequest, userId int64, isDepost bool) error { +func (s *ArifpayService) ProcessWebhook(ctx context.Context, req domain.WebhookRequest, isDeposit bool) error { // 1. Get transfer by SessionID - transfer, err := s.transferStore.GetTransferByReference(ctx, req.Transaction.TransactionID) + transfer, err := s.transferStore.GetTransferByReference(ctx, req.Nonce) if err != nil { return err } - wallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) + userId := transfer.DepositorID.Value + + wallet, err := s.walletSvc.GetCustomerWallet(ctx, userId) if err != nil { return err } @@ -196,7 +203,7 @@ func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRe } // 2. Update transfer status - newStatus := req.Transaction.TransactionStatus + newStatus := strings.ToLower(req.Transaction.TransactionStatus) // if req.Transaction.TransactionStatus != "" { // newStatus = req.Transaction.TransactionStatus // } @@ -212,10 +219,10 @@ func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRe } // 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{ + if (newStatus == "success" && isDeposit) || (newStatus == "failed" && !isDeposit) { + _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.TotalAmount), domain.ValidInt64{}, transfer.PaymentMethod, domain.PaymentDetails{ ReferenceNumber: domain.ValidString{ - Value: req.Transaction.TransactionID, + Value: req.Nonce, Valid: true, }, BankNumber: domain.ValidString{ @@ -231,35 +238,94 @@ func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRe return nil } -func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error { +func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error { // Step 1: Create Session + + userWallet, err := s.walletSvc.GetCustomerWallet(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, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + "", + ) + if err != nil { + return fmt.Errorf("failed to deduct from wallet: %w", err) + } + referenceNum := uuid.NewString() sessionReq := domain.CheckoutSessionClientRequest{ Amount: req.Amount, CustomerEmail: req.CustomerEmail, - CustomerPhone: req.CustomerPhone, + CustomerPhone: "251" + req.CustomerPhone[:9], } - sessionResp, err := s.CreateCheckoutSession(sessionReq, false) + sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId) if err != nil { + _, err = s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if err != nil { + return fmt.Errorf("failed to deduct from wallet: %w", err) + } return fmt.Errorf("failed to create session: %w", err) } + sessionRespData := sessionResp["data"].(map[string]any) + // 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, + "Sessionid": sessionRespData["sessionId"], + "Phonenumber": "251" + req.CustomerPhone[:9], } payload, err := json.Marshal(reqBody) if err != nil { + _, err = s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if err != nil { + return fmt.Errorf("failed to deduct from wallet: %w", err) + } return fmt.Errorf("failed to marshal transfer request: %w", err) } transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload)) if err != nil { + _, err = s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if err != nil { + return fmt.Errorf("failed to deduct from wallet: %w", err) + } return fmt.Errorf("failed to build transfer request: %w", err) } transferReq.Header.Set("Content-Type", "application/json") @@ -267,11 +333,35 @@ func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req dom transferResp, err := s.httpClient.Do(transferReq) if err != nil { + _, err = s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if err != nil { + return fmt.Errorf("failed to deduct from wallet: %w", err) + } return fmt.Errorf("failed to execute transfer request: %w", err) } defer transferResp.Body.Close() - if transferResp.StatusCode >= 300 { + if transferResp.StatusCode != http.StatusOK { + _, err = s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if err != nil { + return fmt.Errorf("failed to deduct from wallet: %w", err) + } body, _ := io.ReadAll(transferResp.Body) return fmt.Errorf("transfer failed with status %d: %s", transferResp.StatusCode, string(body)) } @@ -282,109 +372,33 @@ func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req dom Verified: false, Type: domain.WITHDRAW, // B2C = payout ReferenceNumber: referenceNum, - SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]), + SessionID: fmt.Sprintf("%v", sessionRespData["sessionId"]), Status: string(domain.PaymentStatusPending), PaymentMethod: domain.TRANSFER_ARIFPAY, + CashierID: domain.ValidInt64{ + Value: userId, + Valid: true, + }, } 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) +func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error { + // Step 1: Deduct from user wallet first + userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId) 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) + return fmt.Errorf("cbebirr: failed to get user wallet: %w", err) } _, err = s.walletSvc.DeductFromWallet( ctx, - userWallets[0].ID, + userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, @@ -394,55 +408,68 @@ func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.A 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() + // Step 2: Create Session sessionReq := domain.CheckoutSessionClientRequest{ Amount: req.Amount, CustomerEmail: req.CustomerEmail, - CustomerPhone: req.CustomerPhone, + CustomerPhone: "251" + req.CustomerPhone[:9], } - sessionResp, err := s.CreateCheckoutSession(sessionReq, false) + sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId) if err != nil { - return fmt.Errorf("Mpesa: failed to create session: %w", err) + // refund wallet if session creation fails + _, refundErr := s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if refundErr != nil { + return fmt.Errorf("cbebirr: refund failed after session creation error: %v", refundErr) + } + return fmt.Errorf("cbebirr: failed to create session: %w", err) } - // Step 2: Execute Transfer - transferURL := fmt.Sprintf("%s/api/Mpesa/b2c/transfer", s.cfg.ARIFPAY.BaseURL) + // Step 3: Execute Transfer + transferURL := fmt.Sprintf("%s/api/Cbebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL) reqBody := map[string]any{ "Sessionid": sessionResp["sessionId"], - "Phonenumber": req.PhoneNumber, + "Phonenumber": "251" + req.CustomerPhone[:9], } payload, err := json.Marshal(reqBody) if err != nil { - return fmt.Errorf("Mpesa: failed to marshal transfer request: %w", err) + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + 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("Mpesa: failed to build transfer request: %w", err) + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + 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("Mpesa: failed to execute transfer request: %w", err) + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + return fmt.Errorf("cbebirr: failed to execute transfer request: %w", err) } defer transferResp.Body.Close() - if transferResp.StatusCode >= 300 { + if transferResp.StatusCode != http.StatusOK { body, _ := io.ReadAll(transferResp.Body) - return fmt.Errorf("Mpesa: transfer failed with status %d: %s", transferResp.StatusCode, string(body)) + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + return fmt.Errorf("cbebirr: transfer failed with status %d: %s", transferResp.StatusCode, string(body)) } - // Step 3: Store transfer in DB + // Step 4: Store transfer in DB transfer := domain.CreateTransfer{ Amount: domain.Currency(req.Amount), Verified: false, @@ -451,30 +478,116 @@ func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain 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) + CashierID: domain.ValidInt64{ + Value: userId, + Valid: true, + }, } - // 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 _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { + return fmt.Errorf("cbebirr: failed to store transfer: %w", err) } - if len(userWallets) == 0 { - return fmt.Errorf("Mpesa: no wallet found for user %d", userId) + + return nil +} + +func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error { + // Step 1: Deduct from user wallet first + userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId) + if err != nil { + return fmt.Errorf("mpesa: failed to get user wallet: %w", err) } _, err = s.walletSvc.DeductFromWallet( ctx, - userWallets[0].ID, + userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, "", ) if err != nil { - return fmt.Errorf("Mpesa: failed to deduct from wallet: %w", err) + return fmt.Errorf("mpesa: failed to deduct from wallet: %w", err) + } + + referenceNum := uuid.NewString() + + // Step 2: Create Session + sessionReq := domain.CheckoutSessionClientRequest{ + Amount: req.Amount, + CustomerEmail: req.CustomerEmail, + CustomerPhone: "251" + req.CustomerPhone[:9], + } + + sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId) + if err != nil { + // Refund wallet if session creation fails + _, refundErr := s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if refundErr != nil { + return fmt.Errorf("mpesa: refund failed after session creation error: %v", refundErr) + } + return fmt.Errorf("mpesa: failed to create session: %w", err) + } + + // Step 3: Execute Transfer + transferURL := fmt.Sprintf("%s/api/Mpesa/b2c/transfer", s.cfg.ARIFPAY.BaseURL) + reqBody := map[string]any{ + "Sessionid": sessionResp["sessionId"], + "Phonenumber": "251" + req.CustomerPhone[:9], + } + + payload, err := json.Marshal(reqBody) + if err != nil { + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + 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 { + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + 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 { + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + return fmt.Errorf("mpesa: failed to execute transfer request: %w", err) + } + defer transferResp.Body.Close() + + if transferResp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(transferResp.Body) + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + return fmt.Errorf("mpesa: transfer failed with status %d: %s", transferResp.StatusCode, string(body)) + } + + // Step 4: 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, + CashierID: domain.ValidInt64{ + Value: userId, + Valid: true, + }, + } + + if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { + return fmt.Errorf("mpesa: failed to store transfer: %w", err) } return nil diff --git a/internal/services/chapa/client.go b/internal/services/chapa/client.go index 01c0f41..764c273 100644 --- a/internal/services/chapa/client.go +++ b/internal/services/chapa/client.go @@ -37,7 +37,7 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaInitDepo "first_name": req.FirstName, "last_name": req.LastName, "tx_ref": req.TxRef, - "callback_url": req.CallbackURL, + // "callback_url": req.CallbackURL, "return_url": req.ReturnURL, "phone_number": req.PhoneNumber, } @@ -131,7 +131,7 @@ func (c *Client) VerifyPayment(ctx context.Context, reference string) (domain.Ch }, nil } -func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { +func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaPaymentVerificationResponse, error) { url := fmt.Sprintf("%s/transaction/verify/%s", c.baseURL, txRef) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -153,7 +153,7 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body)) } - var verification domain.ChapaVerificationResponse + var verification domain.ChapaPaymentVerificationResponse if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } @@ -169,7 +169,7 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain return &verification, nil } -func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { +func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaTransferVerificationResponse, error) { url := fmt.Sprintf("%s/transfers/verify/%s", c.baseURL, txRef) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -207,8 +207,8 @@ func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domai status = domain.PaymentStatusFailed } - return &domain.ChapaVerificationResponse{ - Status: string(status), + return &domain.ChapaTransferVerificationResponse{ + Status: string(status), // Amount: response.Amount, // Currency: response.Currency, }, nil @@ -277,7 +277,7 @@ func (c *Client) GetTransactionEvents(ctx context.Context, refId string) ([]doma return response.Data, nil } -func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) { +func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.BankData, error) { req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) @@ -300,9 +300,9 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) return nil, fmt.Errorf("failed to decode response: %w", err) } - var banks []domain.Bank + var banks []domain.BankData for _, bankData := range bankResponse.Data { - bank := domain.Bank{ + bank := domain.BankData{ ID: bankData.ID, Slug: bankData.Slug, Swift: bankData.Swift, @@ -324,7 +324,7 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) return banks, nil } -func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) { +func (c *Client) InitializeTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) { endpoint := c.baseURL + "/transfers" fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint) @@ -361,7 +361,7 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa return response.Status == string(domain.WithdrawalStatusSuccessful), nil } -func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaVerificationResponse, error) { +func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaTransferVerificationResponse, error) { base, err := url.Parse(c.baseURL) if err != nil { return nil, fmt.Errorf("invalid base URL: %w", err) @@ -385,7 +385,7 @@ func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain. return nil, fmt.Errorf("chapa api returned status: %d", resp.StatusCode) } - var verification domain.ChapaVerificationResponse + var verification domain.ChapaTransferVerificationResponse if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } diff --git a/internal/services/chapa/port.go b/internal/services/chapa/port.go index e2a0667..1739482 100644 --- a/internal/services/chapa/port.go +++ b/internal/services/chapa/port.go @@ -16,11 +16,11 @@ import ( type ChapaStore interface { InitializePayment(request domain.ChapaInitDepositRequest) (domain.ChapaDepositResponse, error) - ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) + ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaTransferVerificationResponse, error) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) CreateWithdrawal(userID string, amount float64, accountNumber, bankCode string) (*domain.ChapaWithdrawal, error) - HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error - HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error + HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebhookTransfer) error + HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebhookPayment) error GetPaymentReceiptURL(ctx context.Context, chapaRef string) (string, error) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) GetAccountBalance(ctx context.Context, currencyCode string) ([]domain.Balance, error) diff --git a/internal/services/chapa/service.go b/internal/services/chapa/service.go index 5ec26fb..2c64ba0 100644 --- a/internal/services/chapa/service.go +++ b/internal/services/chapa/service.go @@ -3,6 +3,9 @@ package chapa import ( "bytes" "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -43,6 +46,31 @@ func NewService( } } +func (s *Service) VerifyWebhookSignature(ctx context.Context, payload []byte, chapaSignature, xChapaSignature string) (bool, error) { + secret := s.cfg.CHAPA_WEBHOOK_SECRET // or os.Getenv("CHAPA_SECRET_KEY") + if secret == "" { + return false, fmt.Errorf("missing Chapa secret key in configuration") + } + + // Compute expected signature using HMAC SHA256 + h := hmac.New(sha256.New, []byte(secret)) + h.Write(payload) + expected := hex.EncodeToString(h.Sum(nil)) + + // Check either header + if chapaSignature == expected || xChapaSignature == expected { + return true, nil + } + + // Optionally log for debugging + var pretty map[string]interface{} + _ = json.Unmarshal(payload, &pretty) + fmt.Printf("[Webhook Verification Failed]\nExpected: %s\nGot chapa-signature: %s\nGot x-chapa-signature: %s\nPayload: %+v\n", + expected, chapaSignature, xChapaSignature, pretty) + + return false, fmt.Errorf("invalid webhook signature") +} + // InitiateDeposit starts a new deposit process func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) { // Validate amount @@ -88,7 +116,7 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma ReferenceNumber: reference, // ReceiverWalletID: 1, SenderWalletID: domain.ValidInt64{ - Value: senderWallet.ID, + Value: senderWallet.RegularID, Valid: true, }, Verified: false, @@ -135,9 +163,9 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma return response.CheckoutURL, nil } -func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaPaymentWebhookRequest) error { +func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, req domain.ChapaWebhookPayment) error { // Find payment by reference - payment, err := s.transferStore.GetTransferByReference(ctx, transfer.TxRef) + payment, err := s.transferStore.GetTransferByReference(ctx, req.TxRef) if err != nil { return domain.ErrPaymentNotFound } @@ -159,7 +187,7 @@ func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, transfer doma // } // If payment is completed, credit user's wallet - if transfer.Status == domain.PaymentStatusSuccessful { + if req.Status == string(domain.PaymentStatusSuccessful) { if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil { return fmt.Errorf("failed to update is payment verified value: %w", err) @@ -171,7 +199,7 @@ func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, transfer doma if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{ ReferenceNumber: domain.ValidString{ - Value: transfer.TxRef, + Value: req.TxRef, }, }, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil { return fmt.Errorf("failed to credit user wallet: %w", err) @@ -288,7 +316,7 @@ func (s *Service) FetchTransactionEvents(ctx context.Context, refID string) ([]d func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) { // Parse and validate amount - amount, err := strconv.ParseInt(req.Amount, 10, 64) + amount, err := strconv.ParseFloat(req.Amount, 64) if err != nil || amount <= 0 { return nil, domain.ErrInvalidWithdrawalAmount } @@ -319,7 +347,7 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma reference := uuid.New().String() createTransfer := domain.CreateTransfer{ - Message: fmt.Sprintf("Withdrawing %d into wallet using chapa. Reference Number %s", amount, reference), + Message: fmt.Sprintf("Withdrawing %f into wallet using chapa. Reference Number %s", amount, reference), Amount: domain.Currency(amount), Type: domain.WITHDRAW, SenderWalletID: domain.ValidInt64{ @@ -341,40 +369,49 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma transferReq := domain.ChapaWithdrawalRequest{ AccountName: req.AccountName, AccountNumber: req.AccountNumber, - Amount: fmt.Sprintf("%d", amount), + Amount: fmt.Sprintf("%f", amount), Currency: req.Currency, Reference: reference, // BeneficiaryName: fmt.Sprintf("%s %s", user.FirstName, user.LastName), BankCode: req.BankCode, } - success, err := s.chapaClient.InitiateTransfer(ctx, transferReq) - if err != nil { - _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) - return nil, fmt.Errorf("failed to initiate transfer: %w", err) - } - - if !success { - _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) - return nil, errors.New("chapa rejected the transfer request") - } - - // Update withdrawal status to processing - if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil { - return nil, fmt.Errorf("failed to update withdrawal status: %w", err) - } - // Deduct from wallet (or wait for webhook confirmation depending on your flow) newBalance := float64(wallet.RegularBalance) - float64(amount) if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil { return nil, fmt.Errorf("failed to update wallet balance: %w", err) } + success, err := s.chapaClient.InitializeTransfer(ctx, transferReq) + if err != nil { + _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) + newBalance := float64(wallet.RegularBalance) + float64(amount) + if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil { + return nil, fmt.Errorf("failed to update wallet balance: %w", err) + } + return nil, fmt.Errorf("failed to initiate transfer: %w", err) + } + + if !success { + _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) + newBalance := float64(wallet.RegularBalance) + float64(amount) + if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil { + return nil, fmt.Errorf("failed to update wallet balance: %w", err) + } + return nil, errors.New("chapa rejected the transfer request") + } + + // Update withdrawal status to processing + // if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil { + // return nil, fmt.Errorf("failed to update withdrawal status: %w", err) + // } + // Deduct from wallet (or wait for webhook confirmation depending on your flow) + return &transfer, nil } -func (s *Service) ProcessVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error { +func (s *Service) ProcessVerifyWithdrawWebhook(ctx context.Context, req domain.ChapaWebhookTransfer) error { // Find payment by reference - transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference) + transfer, err := s.transferStore.GetTransferByReference(ctx, req.Reference) if err != nil { return domain.ErrPaymentNotFound } @@ -395,7 +432,7 @@ func (s *Service) ProcessVerifyWithdrawWebhook(ctx context.Context, payment doma // verified = true // } - if payment.Status == string(domain.PaymentStatusSuccessful) { + if req.Status == string(domain.PaymentStatusSuccessful) { if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { return fmt.Errorf("failed to update payment status: %w", err) } // If payment is completed, credit user's walle @@ -420,15 +457,7 @@ func (s *Service) GetPaymentReceiptURL(refId string) (string, error) { return receiptURL, nil } -func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) { - banks, err := s.chapaClient.FetchSupportedBanks(ctx) - if err != nil { - return nil, fmt.Errorf("failed to fetch banks: %w", err) - } - return banks, nil -} - -func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { +func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (any, error) { // Lookup transfer by reference transfer, err := s.transferStore.GetTransferByReference(ctx, txRef) if err != nil { @@ -437,7 +466,7 @@ func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.Cha // If already verified, just return a completed response if transfer.Verified { - return &domain.ChapaVerificationResponse{}, errors.New("transfer already verified") + return map[string]any{}, errors.New("transfer already verified") } // Validate sender wallet @@ -445,12 +474,12 @@ func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.Cha return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID) } - var verification *domain.ChapaVerificationResponse + var verification any switch strings.ToLower(string(transfer.Type)) { case string(domain.DEPOSIT): // Verify Chapa payment - verification, err = s.chapaClient.ManualVerifyPayment(ctx, txRef) + verification, err := s.chapaClient.ManualVerifyPayment(ctx, txRef) if err != nil { return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err) } @@ -481,7 +510,7 @@ func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.Cha case string(domain.WITHDRAW): // Verify Chapa transfer - verification, err = s.chapaClient.ManualVerifyTransfer(ctx, txRef) + verification, err := s.chapaClient.ManualVerifyTransfer(ctx, txRef) if err != nil { return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err) } @@ -516,8 +545,16 @@ func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.Cha return verification, nil } -func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.chapa.co/v1/transfers", nil) +func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.BankData, error) { + banks, err := s.chapaClient.FetchSupportedBanks(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch banks: %w", err) + } + return banks, nil +} + +func (s *Service) GetAllTransfers(ctx context.Context) (*domain.ChapaTransfersListResponse, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.cfg.CHAPA_BASE_URL+"/transfers", nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } @@ -535,26 +572,22 @@ func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes)) } - var result struct { - Status string `json:"status"` - Message string `json:"message"` - Data []domain.Transfer `json:"data"` - } - + var result domain.ChapaTransfersListResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } - return result.Data, nil + // Return the decoded result directly; no intermediate dynamic map needed + return &result, nil } func (s *Service) GetAccountBalance(ctx context.Context, currencyCode string) ([]domain.Balance, error) { - baseURL := "https://api.chapa.co/v1/balances" + URL := s.cfg.CHAPA_BASE_URL + "/balances" if currencyCode != "" { - baseURL = fmt.Sprintf("%s/%s", baseURL, strings.ToLower(currencyCode)) + URL = fmt.Sprintf("%s/%s", URL, strings.ToLower(currencyCode)) } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL, nil) if err != nil { return nil, fmt.Errorf("failed to create balance request: %w", err) } @@ -585,59 +618,103 @@ func (s *Service) GetAccountBalance(ctx context.Context, currencyCode string) ([ return result.Data, nil } -func (s *Service) InitiateSwap(ctx context.Context, amount float64, from, to string) (*domain.SwapResponse, error) { - if amount < 1 { - return nil, fmt.Errorf("amount must be at least 1 USD") - } - if strings.ToUpper(from) != "USD" || strings.ToUpper(to) != "ETB" { - return nil, fmt.Errorf("only USD to ETB swap is supported") - } +func (s *Service) SwapCurrency(ctx context.Context, reqBody domain.SwapRequest) (*domain.SwapResponse, error) { + URL := s.cfg.CHAPA_BASE_URL + "/swap" - payload := domain.SwapRequest{ - Amount: amount, - From: strings.ToUpper(from), - To: strings.ToUpper(to), - } + // Normalize currency codes + reqBody.From = strings.ToUpper(reqBody.From) + reqBody.To = strings.ToUpper(reqBody.To) - // payload := map[string]any{ - // "amount": amount, - // "from": strings.ToUpper(from), - // "to": strings.ToUpper(to), - // } - - body, err := json.Marshal(payload) + // Marshal request body + body, err := json.Marshal(reqBody) if err != nil { - return nil, fmt.Errorf("failed to encode swap payload: %w", err) + return nil, fmt.Errorf("failed to marshal swap payload: %w", err) } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.chapa.co/v1/swap", bytes.NewBuffer(body)) + // Create HTTP request + req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL, bytes.NewBuffer(body)) if err != nil { return nil, fmt.Errorf("failed to create swap request: %w", err) } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY)) req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY)) + // Execute request resp, err := s.chapaClient.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute swap request: %w", err) } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + // Handle unexpected status + if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes)) } - var result struct { - Message string `json:"message"` - Status string `json:"status"` - Data domain.SwapResponse `json:"data"` - } - + // Decode response + var result domain.SwapResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("failed to decode swap response: %w", err) } - return &result.Data, nil + return &result, nil } + +// func (s *Service) InitiateSwap(ctx context.Context, amount float64, from, to string) (*domain.SwapResponse, error) { +// if amount < 1 { +// return nil, fmt.Errorf("amount must be at least 1 USD") +// } +// if strings.ToUpper(from) != "USD" || strings.ToUpper(to) != "ETB" { +// return nil, fmt.Errorf("only USD to ETB swap is supported") +// } + +// payload := domain.SwapRequest{ +// Amount: amount, +// From: strings.ToUpper(from), +// To: strings.ToUpper(to), +// } + +// // payload := map[string]any{ +// // "amount": amount, +// // "from": strings.ToUpper(from), +// // "to": strings.ToUpper(to), +// // } + +// body, err := json.Marshal(payload) +// if err != nil { +// return nil, fmt.Errorf("failed to encode swap payload: %w", err) +// } + +// req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.chapa.co/v1/swap", bytes.NewBuffer(body)) +// if err != nil { +// return nil, fmt.Errorf("failed to create swap request: %w", err) +// } + +// req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY)) +// req.Header.Set("Content-Type", "application/json") + +// resp, err := s.chapaClient.httpClient.Do(req) +// if err != nil { +// return nil, fmt.Errorf("failed to execute swap request: %w", err) +// } +// defer resp.Body.Close() + +// if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { +// bodyBytes, _ := io.ReadAll(resp.Body) +// return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes)) +// } + +// var result struct { +// Message string `json:"message"` +// Status string `json:"status"` +// Data domain.SwapResponse `json:"data"` +// } + +// if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { +// return nil, fmt.Errorf("failed to decode swap response: %w", err) +// } + +// return &result.Data, nil +// } diff --git a/internal/services/virtualGame/Alea/service.go b/internal/services/virtualGame/Alea/service.go index e30a61e..5c7b367 100644 --- a/internal/services/virtualGame/Alea/service.go +++ b/internal/services/virtualGame/Alea/service.go @@ -102,13 +102,13 @@ func (s *AleaPlayService) HandleCallback(ctx context.Context, callback *domain.A } // Update session status using the proper repository method - if callback.Type == "SESSION_END" { - if err := s.repo.UpdateVirtualGameSessionStatus(ctx, session.ID, "COMPLETED"); err != nil { - s.logger.Error("failed to update session status", - "sessionID", session.ID, - "error", err) - } - } + // if callback.Type == "SESSION_END" { + // if err := s.repo.UpdateVirtualGameSessionStatus(ctx, session.ID, "COMPLETED"); err != nil { + // s.logger.Error("failed to update session status", + // "sessionID", session.ID, + // "error", err) + // } + // } return nil } diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 324b20d..fdf6fa3 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -15,6 +15,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" + "github.com/google/uuid" "go.uber.org/zap" ) @@ -167,7 +168,7 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (* "playerId": req.PlayerID, "currency": req.Currency, "deviceType": req.DeviceType, - "country": "US", + "country": req.Country, "ip": req.IP, "brandId": req.BrandID, } @@ -178,6 +179,21 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (* return nil, fmt.Errorf("failed to start game with provider %s: %w", req.ProviderID, err) } + playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid PlayerID: %w", err) + } + + session := &domain.VirtualGameSession{ + UserID: playerIDInt64, + GameID: req.GameID, + SessionToken: uuid.NewString(), + } + + if err := s.repo.CreateVirtualGameSession(ctx, session); err != nil { + return nil, fmt.Errorf("failed to create virtual game session: %w", err) + } + return &res, nil } diff --git a/internal/web_server/handlers/arifpay.go b/internal/web_server/handlers/arifpay.go index c61d24e..51e248f 100644 --- a/internal/web_server/handlers/arifpay.go +++ b/internal/web_server/handlers/arifpay.go @@ -18,6 +18,15 @@ import ( // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/arifpay/checkout [post] func (h *Handler) CreateCheckoutSessionHandler(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.CheckoutSessionClientRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ @@ -26,7 +35,7 @@ func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error { }) } - data, err := h.arifpaySvc.CreateCheckoutSession(req, true) + data, err := h.arifpaySvc.CreateCheckoutSession(req, true, userId) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Error: err.Error(), @@ -53,7 +62,7 @@ func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error { // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/arifpay/checkout/{sessionId}/cancel [post] +// @Router /api/v1/arifpay/checkout/cancel/{sessionId} [post] func (h *Handler) CancelCheckoutSessionHandler(c *fiber.Ctx) error { sessionID := c.Params("sessionId") if sessionID == "" { @@ -103,15 +112,15 @@ func (h *Handler) HandleArifpayC2BWebhook(c *fiber.Ctx) error { // 🚨 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", - }) - } + // 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) + err := h.arifpaySvc.ProcessWebhook(c.Context(), req, true) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Error: err.Error(), @@ -150,15 +159,15 @@ func (h *Handler) HandleArifpayB2CWebhook(c *fiber.Ctx) error { // 🚨 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", - }) - } + // 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) + err := h.arifpaySvc.ProcessWebhook(c.Context(), req, false) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Error: err.Error(), @@ -253,7 +262,7 @@ func (h *Handler) ArifpayVerifyBySessionIDHandler(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param type query string true "Transfer type (telebirr, cbe, mpesa)" -// @Param request body domain.ArifpayB2CRequest true "Transfer request payload" +// @Param request body domain.CheckoutSessionClientRequest 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" @@ -275,7 +284,7 @@ func (h *Handler) ExecuteArifpayB2CTransfer(c *fiber.Ctx) error { }) } - var req domain.ArifpayB2CRequest + var req domain.CheckoutSessionClientRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to process your withdrawal request", diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index c671dbb..4af65bc 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -1,7 +1,9 @@ package handlers import ( + "encoding/json" "fmt" + "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/gofiber/fiber/v2" @@ -61,69 +63,66 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { // WebhookCallback godoc // @Summary Chapa payment webhook callback (used by Chapa) -// @Description Handles payment notifications from Chapa +// @Description Handles payment and transfer notifications from Chapa // @Tags Chapa // @Accept json // @Produce json -// @Param request body domain.ChapaWebhookPayload true "Webhook payload" -// @Success 200 {object} map[string]interface{} +// @Param request body domain.ChapaWebhookPayment true "Webhook payload" +// @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/chapa/payments/webhook/verify [post] func (h *Handler) WebhookCallback(c *fiber.Ctx) error { + body := c.Body() - chapaTransactionType := new(domain.ChapaTransactionType) + // Retrieve signature headers + chapaSignature := c.Get("chapa-signature") + xChapaSignature := c.Get("x-chapa-signature") - if parseTypeErr := c.BodyParser(chapaTransactionType); parseTypeErr != nil { - return domain.UnProcessableEntityResponse(c) + // Verify webhook signature + valid, err := h.chapaSvc.VerifyWebhookSignature(c.Context(), body, chapaSignature, xChapaSignature) + if err != nil || !valid { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid Chapa webhook signature", + Error: err.Error(), + }) } - switch chapaTransactionType.Type { - case h.Cfg.CHAPA_PAYMENT_TYPE: - chapaTransferVerificationRequest := new(domain.ChapaPaymentWebhookRequest) + // Try parsing as transfer webhook first + var transfer domain.ChapaWebhookTransfer + if err := json.Unmarshal(body, &transfer); err == nil && + strings.EqualFold(transfer.Type, "payout") { - if err := c.BodyParser(chapaTransferVerificationRequest); err != nil { - return domain.UnProcessableEntityResponse(c) - } - - err := h.chapaSvc.ProcessVerifyDepositWebhook(c.Context(), *chapaTransferVerificationRequest) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Failed to verify Chapa deposit", - Error: err.Error(), - }) - } - - return c.Status(fiber.StatusOK).JSON(domain.Response{ - StatusCode: 200, - Message: "Chapa deposit transaction verified successfully", - Data: chapaTransferVerificationRequest, - Success: true, - }) - case h.Cfg.CHAPA_TRANSFER_TYPE: - chapaPaymentVerificationRequest := new(domain.ChapaWebHookPayment) - if err := c.BodyParser(chapaPaymentVerificationRequest); err != nil { - return domain.UnProcessableEntityResponse(c) - } - - err := h.chapaSvc.ProcessVerifyWithdrawWebhook(c.Context(), *chapaPaymentVerificationRequest) - if err != nil { + if err := h.chapaSvc.ProcessVerifyWithdrawWebhook(c.Context(), transfer); err != nil { return domain.UnExpectedErrorResponse(c) } return c.Status(fiber.StatusOK).JSON(domain.Response{ StatusCode: 200, - Message: "Chapa withdrawal transaction verified successfully", - Data: chapaPaymentVerificationRequest, + Message: "Chapa withdrawal webhook processed successfully", + // Data: transfer, Success: true, }) - } - // Return a 400 Bad Request if the type does not match any known case - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Message: "Invalid Chapa webhook type", - Error: "Unknown transaction type", + // Otherwise, try as payment webhook + var payment domain.ChapaWebhookPayment + if err := json.Unmarshal(body, &payment); err != nil { + return domain.UnProcessableEntityResponse(c) + } + + if err := h.chapaSvc.ProcessVerifyDepositWebhook(c.Context(), payment); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to verify Chapa deposit", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + StatusCode: 200, + Message: "Chapa deposit webhook processed successfully", + // Data: payment, + Success: true, }) } @@ -248,7 +247,7 @@ func (h *Handler) GetTransactionEvents(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param tx_ref path string true "Transaction Reference" -// @Success 200 {object} domain.ChapaVerificationResponse +// @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/chapa/transaction/manual/verify/{tx_ref} [get] @@ -311,7 +310,7 @@ func (h *Handler) GetSupportedBanks(c *fiber.Ctx) error { // @Produce json // @Security ApiKeyAuth // @Param request body domain.ChapaWithdrawalRequest true "Withdrawal request details" -// @Success 201 {object} domain.Response "Chapa withdrawal process initiated successfully" +// @Success 200 {object} domain.Response "Chapa withdrawal process initiated successfully" // @Failure 400 {object} domain.ErrorResponse "Invalid request body" // @Failure 401 {object} domain.ErrorResponse "Unauthorized" // @Failure 422 {object} domain.ErrorResponse "Unprocessable entity" @@ -336,9 +335,9 @@ func (h *Handler) InitiateWithdrawal(c *fiber.Ctx) error { }) } - return c.Status(fiber.StatusCreated).JSON(domain.Response{ + return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Chapa withdrawal process initiated successfully", - StatusCode: 201, + StatusCode: 200, Success: true, Data: withdrawal, }) @@ -430,44 +429,56 @@ func (h *Handler) GetAccountBalance(c *fiber.Ctx) error { } return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Chapa account balance retrieved successfully", + Message: "Chapa account balances retrieved successfully", Data: balances, StatusCode: fiber.StatusOK, Success: true, }) } -// InitiateSwap godoc -// @Summary Initiate a currency swap -// @Description Perform a USD to ETB currency swap using Chapa's API +// SwapCurrency godoc +// @Summary Swap currency using Chapa API +// @Description Convert an amount from one currency to another using Chapa's currency swap API // @Tags Chapa // @Accept json // @Produce json -// @Param payload body domain.SwapRequest true "Swap Request Payload" +// @Param request body domain.SwapRequest true "Swap request payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/chapa/swap [post] -func (h *Handler) InitiateSwap(c *fiber.Ctx) error { - var req domain.SwapRequest - if err := c.BodyParser(&req); err != nil { +func (h *Handler) SwapCurrency(c *fiber.Ctx) error { + var reqBody domain.SwapRequest + + // Parse request body + if err := c.BodyParser(&reqBody); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request payload", Error: err.Error(), }) } - swapResult, err := h.chapaSvc.InitiateSwap(c.Context(), req.Amount, req.From, req.To) + // Validate input + if reqBody.From == "" || reqBody.To == "" || reqBody.Amount <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Missing or invalid swap parameters", + Error: "from, to, and amount are required fields", + }) + } + + // Call service + resp, err := h.chapaSvc.SwapCurrency(c.Context(), reqBody) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Failed to initiate currency swap", + Message: "Failed to perform currency swap", Error: err.Error(), }) } + // Success response return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Currency swap initiated successfully", - Data: swapResult, + Message: "Currency swapped successfully", + Data: resp, StatusCode: fiber.StatusOK, Success: true, }) diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go index 5bf3c1b..607ac06 100644 --- a/internal/web_server/handlers/veli_games.go +++ b/internal/web_server/handlers/veli_games.go @@ -135,7 +135,10 @@ func (h *Handler) StartGame(c *fiber.Ctx) error { req.BrandID = h.Cfg.VeliGames.BrandID } + useId := c.Locals("user_id") + req.IP = c.IP() + req.PlayerID = useId.(string) // 1️⃣ Call StartGame service res, err := h.veliVirtualGameSvc.StartGame(context.Background(), req) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 01c70aa..21ee01f 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -150,9 +150,9 @@ func (a *App) initAppRoutes() { //Arifpay groupV1.Post("/arifpay/checkout", a.authMiddleware, h.CreateCheckoutSessionHandler) - 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/checkout/cancel/:sessionId", a.authMiddleware, h.CancelCheckoutSessionHandler) + groupV1.Post("/api/v1/arifpay/c2b-webhook", h.HandleArifpayC2BWebhook) + groupV1.Post("/api/v1/arifpay/b2c-webhook", 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) @@ -381,17 +381,17 @@ func (a *App) initAppRoutes() { //Chapa Routes groupV1.Post("/chapa/payments/webhook/verify", h.WebhookCallback) - groupV1.Get("/chapa/transaction/manual/verify/:tx_ref", h.ManualVerifyTransaction) + groupV1.Get("/chapa/transaction/manual/verify/:tx_ref", a.authMiddleware, h.ManualVerifyTransaction) groupV1.Put("/chapa/transaction/cancel/:tx_ref", a.authMiddleware, h.CancelDeposit) - groupV1.Get("/chapa/transactions", h.FetchAllTransactions) - groupV1.Get("/chapa/transaction/events/:ref_id", h.GetTransactionEvents) + groupV1.Get("/chapa/transactions", a.authMiddleware, h.FetchAllTransactions) + groupV1.Get("/chapa/transaction/events/:ref_id", a.authMiddleware, h.GetTransactionEvents) groupV1.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit) groupV1.Post("/chapa/payments/withdraw", a.authMiddleware, h.InitiateWithdrawal) groupV1.Get("/chapa/banks", h.GetSupportedBanks) - groupV1.Get("/chapa/payments/receipt/:chapa_ref", h.GetPaymentReceipt) - groupV1.Get("/chapa/transfers", h.GetAllTransfers) - groupV1.Get("/chapa/balance", h.GetAccountBalance) - groupV1.Post("/chapa/init-swap", h.InitiateSwap) + groupV1.Get("/chapa/payments/receipt/:chapa_ref", a.authMiddleware, h.GetPaymentReceipt) + groupV1.Get("/chapa/transfers", a.authMiddleware, h.GetAllTransfers) + groupV1.Get("/chapa/balance", a.authMiddleware, h.GetAccountBalance) + groupV1.Post("/chapa/swap", a.authMiddleware, h.SwapCurrency) // Currencies groupV1.Get("/currencies", h.GetSupportedCurrencies) @@ -409,7 +409,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", h.StartGame) + 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", a.authMiddleware, h.GetGamingActivity)