issue reporting service

This commit is contained in:
Yared Yemane 2025-06-24 18:45:34 +03:00
parent 25230e3fcf
commit b1c3b73d9c
18 changed files with 1330 additions and 82 deletions

View File

@ -36,6 +36,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
@ -218,10 +219,15 @@ func main() {
log.Println("Live metrics broadcasted successfully")
}
issueReportingRepo := repository.NewReportedIssueRepository(store)
issueReportingSvc := issuereporting.New(issueReportingRepo)
// go httpserver.SetupReportCronJob(reportWorker)
// Initialize and start HTTP server
app := httpserver.NewApp(
issueReportingSvc,
instSvc,
currSvc,
cfg.Port,

View File

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS reported_issues;

View File

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS reported_issues (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL,
subject TEXT NOT NULL,
description TEXT NOT NULL,
issue_type TEXT NOT NULL, -- e.g., "deposit", "withdrawal", "bet", "technical"
status TEXT NOT NULL DEFAULT 'pending', -- pending, in_progress, resolved, rejected
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,32 @@
-- name: CreateReportedIssue :one
INSERT INTO reported_issues (
customer_id, subject, description, issue_type, metadata
) VALUES (
$1, $2, $3, $4, $5
)
RETURNING *;
-- name: ListReportedIssues :many
SELECT * FROM reported_issues
ORDER BY created_at DESC
LIMIT $1 OFFSET $2;
-- name: ListReportedIssuesByCustomer :many
SELECT * FROM reported_issues
WHERE customer_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3;
-- name: CountReportedIssues :one
SELECT COUNT(*) FROM reported_issues;
-- name: CountReportedIssuesByCustomer :one
SELECT COUNT(*) FROM reported_issues WHERE customer_id = $1;
-- name: UpdateReportedIssueStatus :exec
UPDATE reported_issues
SET status = $2, updated_at = NOW()
WHERE id = $1;
-- name: DeleteReportedIssue :exec
DELETE FROM reported_issues WHERE id = $1;

View File

@ -845,6 +845,230 @@ const docTemplate = `{
}
}
},
"/api/v1/issues": {
"get": {
"description": "Admin endpoint to list all reported issues with pagination",
"produces": [
"application/json"
],
"tags": [
"Issues"
],
"summary": "Get all reported issues",
"parameters": [
{
"type": "integer",
"description": "Limit",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Offset",
"name": "offset",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.ReportedIssue"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
},
"post": {
"description": "Allows a customer to report a new issue related to the betting platform",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Issues"
],
"summary": "Report an issue",
"parameters": [
{
"description": "Issue to report",
"name": "issue",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ReportedIssue"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/domain.ReportedIssue"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/issues/customer/{customer_id}": {
"get": {
"description": "Returns all issues reported by a specific customer",
"produces": [
"application/json"
],
"tags": [
"Issues"
],
"summary": "Get reported issues by a customer",
"parameters": [
{
"type": "integer",
"description": "Customer ID",
"name": "customer_id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Limit",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Offset",
"name": "offset",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.ReportedIssue"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/issues/{issue_id}": {
"delete": {
"description": "Admin endpoint to delete a reported issue",
"tags": [
"Issues"
],
"summary": "Delete a reported issue",
"parameters": [
{
"type": "integer",
"description": "Issue ID",
"name": "issue_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/issues/{issue_id}/status": {
"patch": {
"description": "Admin endpoint to update the status of a reported issue",
"consumes": [
"application/json"
],
"tags": [
"Issues"
],
"summary": "Update issue status",
"parameters": [
{
"type": "integer",
"description": "Issue ID",
"name": "issue_id",
"in": "path",
"required": true
},
{
"description": "New issue status (pending, in_progress, resolved, rejected)",
"name": "status",
"in": "body",
"required": true,
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
}
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/logs": {
"get": {
"description": "Fetches the 100 most recent application logs from MongoDB",
@ -1053,6 +1277,33 @@ const docTemplate = `{
}
},
"/api/v1/virtual-game/favorites": {
"get": {
"description": "Lists the games that the user marked as favorite",
"produces": [
"application/json"
],
"tags": [
"VirtualGames - Favourites"
],
"summary": "Get user's favorite games",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.GameRecommendation"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
},
"post": {
"description": "Adds a game to the user's favorite games list",
"consumes": [
@ -1098,36 +1349,7 @@ const docTemplate = `{
}
}
},
"/api/v1/virtual-games/favorites": {
"get": {
"description": "Lists the games that the user marked as favorite",
"produces": [
"application/json"
],
"tags": [
"VirtualGames - Favourites"
],
"summary": "Get user's favorite games",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.GameRecommendation"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/virtual-games/favorites/{gameID}": {
"/api/v1/virtual-game/favorites/{gameID}": {
"delete": {
"description": "Removes a game from the user's favorites",
"produces": [
@ -5947,6 +6169,39 @@ const docTemplate = `{
}
}
},
"domain.ReportedIssue": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"customer_id": {
"type": "integer"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"issue_type": {
"type": "string"
},
"metadata": {
"type": "object",
"additionalProperties": true
},
"status": {
"type": "string"
},
"subject": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"domain.Response": {
"type": "object",
"properties": {

View File

@ -837,6 +837,230 @@
}
}
},
"/api/v1/issues": {
"get": {
"description": "Admin endpoint to list all reported issues with pagination",
"produces": [
"application/json"
],
"tags": [
"Issues"
],
"summary": "Get all reported issues",
"parameters": [
{
"type": "integer",
"description": "Limit",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Offset",
"name": "offset",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.ReportedIssue"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
},
"post": {
"description": "Allows a customer to report a new issue related to the betting platform",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Issues"
],
"summary": "Report an issue",
"parameters": [
{
"description": "Issue to report",
"name": "issue",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ReportedIssue"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/domain.ReportedIssue"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/issues/customer/{customer_id}": {
"get": {
"description": "Returns all issues reported by a specific customer",
"produces": [
"application/json"
],
"tags": [
"Issues"
],
"summary": "Get reported issues by a customer",
"parameters": [
{
"type": "integer",
"description": "Customer ID",
"name": "customer_id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Limit",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Offset",
"name": "offset",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.ReportedIssue"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/issues/{issue_id}": {
"delete": {
"description": "Admin endpoint to delete a reported issue",
"tags": [
"Issues"
],
"summary": "Delete a reported issue",
"parameters": [
{
"type": "integer",
"description": "Issue ID",
"name": "issue_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/issues/{issue_id}/status": {
"patch": {
"description": "Admin endpoint to update the status of a reported issue",
"consumes": [
"application/json"
],
"tags": [
"Issues"
],
"summary": "Update issue status",
"parameters": [
{
"type": "integer",
"description": "Issue ID",
"name": "issue_id",
"in": "path",
"required": true
},
{
"description": "New issue status (pending, in_progress, resolved, rejected)",
"name": "status",
"in": "body",
"required": true,
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
}
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/logs": {
"get": {
"description": "Fetches the 100 most recent application logs from MongoDB",
@ -1045,6 +1269,33 @@
}
},
"/api/v1/virtual-game/favorites": {
"get": {
"description": "Lists the games that the user marked as favorite",
"produces": [
"application/json"
],
"tags": [
"VirtualGames - Favourites"
],
"summary": "Get user's favorite games",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.GameRecommendation"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
},
"post": {
"description": "Adds a game to the user's favorite games list",
"consumes": [
@ -1090,36 +1341,7 @@
}
}
},
"/api/v1/virtual-games/favorites": {
"get": {
"description": "Lists the games that the user marked as favorite",
"produces": [
"application/json"
],
"tags": [
"VirtualGames - Favourites"
],
"summary": "Get user's favorite games",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.GameRecommendation"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/virtual-games/favorites/{gameID}": {
"/api/v1/virtual-game/favorites/{gameID}": {
"delete": {
"description": "Removes a game from the user's favorites",
"produces": [
@ -5939,6 +6161,39 @@
}
}
},
"domain.ReportedIssue": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"customer_id": {
"type": "integer"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"issue_type": {
"type": "string"
},
"metadata": {
"type": "object",
"additionalProperties": true
},
"status": {
"type": "string"
},
"subject": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"domain.Response": {
"type": "object",
"properties": {

View File

@ -636,6 +636,28 @@ definitions:
totalRewardEarned:
type: number
type: object
domain.ReportedIssue:
properties:
created_at:
type: string
customer_id:
type: integer
description:
type: string
id:
type: integer
issue_type:
type: string
metadata:
additionalProperties: true
type: object
status:
type: string
subject:
type: string
updated_at:
type: string
type: object
domain.Response:
properties:
data: {}
@ -2140,6 +2162,154 @@ paths:
summary: Convert currency
tags:
- Multi-Currency
/api/v1/issues:
get:
description: Admin endpoint to list all reported issues with pagination
parameters:
- description: Limit
in: query
name: limit
type: integer
- description: Offset
in: query
name: offset
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.ReportedIssue'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get all reported issues
tags:
- Issues
post:
consumes:
- application/json
description: Allows a customer to report a new issue related to the betting
platform
parameters:
- description: Issue to report
in: body
name: issue
required: true
schema:
$ref: '#/definitions/domain.ReportedIssue'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/domain.ReportedIssue'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Report an issue
tags:
- Issues
/api/v1/issues/{issue_id}:
delete:
description: Admin endpoint to delete a reported issue
parameters:
- description: Issue ID
in: path
name: issue_id
required: true
type: integer
responses:
"204":
description: No Content
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Delete a reported issue
tags:
- Issues
/api/v1/issues/{issue_id}/status:
patch:
consumes:
- application/json
description: Admin endpoint to update the status of a reported issue
parameters:
- description: Issue ID
in: path
name: issue_id
required: true
type: integer
- description: New issue status (pending, in_progress, resolved, rejected)
in: body
name: status
required: true
schema:
properties:
status:
type: string
type: object
responses:
"204":
description: No Content
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Update issue status
tags:
- Issues
/api/v1/issues/customer/{customer_id}:
get:
description: Returns all issues reported by a specific customer
parameters:
- description: Customer ID
in: path
name: customer_id
required: true
type: integer
- description: Limit
in: query
name: limit
type: integer
- description: Offset
in: query
name: offset
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.ReportedIssue'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get reported issues by a customer
tags:
- Issues
/api/v1/logs:
get:
description: Fetches the 100 most recent application logs from MongoDB
@ -2274,6 +2444,24 @@ paths:
tags:
- Reports
/api/v1/virtual-game/favorites:
get:
description: Lists the games that the user marked as favorite
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.GameRecommendation'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get user's favorite games
tags:
- VirtualGames - Favourites
post:
consumes:
- application/json
@ -2303,26 +2491,7 @@ paths:
summary: Add game to favorites
tags:
- VirtualGames - Favourites
/api/v1/virtual-games/favorites:
get:
description: Lists the games that the user marked as favorite
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.GameRecommendation'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get user's favorite games
tags:
- VirtualGames - Favourites
/api/v1/virtual-games/favorites/{gameID}:
/api/v1/virtual-game/favorites/{gameID}:
delete:
description: Removes a game from the user's favorites
parameters:

View File

@ -0,0 +1,181 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: issue_reporting.sql
package dbgen
import (
"context"
)
const CountReportedIssues = `-- name: CountReportedIssues :one
SELECT COUNT(*) FROM reported_issues
`
func (q *Queries) CountReportedIssues(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, CountReportedIssues)
var count int64
err := row.Scan(&count)
return count, err
}
const CountReportedIssuesByCustomer = `-- name: CountReportedIssuesByCustomer :one
SELECT COUNT(*) FROM reported_issues WHERE customer_id = $1
`
func (q *Queries) CountReportedIssuesByCustomer(ctx context.Context, customerID int64) (int64, error) {
row := q.db.QueryRow(ctx, CountReportedIssuesByCustomer, customerID)
var count int64
err := row.Scan(&count)
return count, err
}
const CreateReportedIssue = `-- name: CreateReportedIssue :one
INSERT INTO reported_issues (
customer_id, subject, description, issue_type, metadata
) VALUES (
$1, $2, $3, $4, $5
)
RETURNING id, customer_id, subject, description, issue_type, status, metadata, created_at, updated_at
`
type CreateReportedIssueParams struct {
CustomerID int64 `json:"customer_id"`
Subject string `json:"subject"`
Description string `json:"description"`
IssueType string `json:"issue_type"`
Metadata []byte `json:"metadata"`
}
func (q *Queries) CreateReportedIssue(ctx context.Context, arg CreateReportedIssueParams) (ReportedIssue, error) {
row := q.db.QueryRow(ctx, CreateReportedIssue,
arg.CustomerID,
arg.Subject,
arg.Description,
arg.IssueType,
arg.Metadata,
)
var i ReportedIssue
err := row.Scan(
&i.ID,
&i.CustomerID,
&i.Subject,
&i.Description,
&i.IssueType,
&i.Status,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const DeleteReportedIssue = `-- name: DeleteReportedIssue :exec
DELETE FROM reported_issues WHERE id = $1
`
func (q *Queries) DeleteReportedIssue(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteReportedIssue, id)
return err
}
const ListReportedIssues = `-- name: ListReportedIssues :many
SELECT id, customer_id, subject, description, issue_type, status, metadata, created_at, updated_at FROM reported_issues
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`
type ListReportedIssuesParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) ListReportedIssues(ctx context.Context, arg ListReportedIssuesParams) ([]ReportedIssue, error) {
rows, err := q.db.Query(ctx, ListReportedIssues, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ReportedIssue
for rows.Next() {
var i ReportedIssue
if err := rows.Scan(
&i.ID,
&i.CustomerID,
&i.Subject,
&i.Description,
&i.IssueType,
&i.Status,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ListReportedIssuesByCustomer = `-- name: ListReportedIssuesByCustomer :many
SELECT id, customer_id, subject, description, issue_type, status, metadata, created_at, updated_at FROM reported_issues
WHERE customer_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
`
type ListReportedIssuesByCustomerParams struct {
CustomerID int64 `json:"customer_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) ListReportedIssuesByCustomer(ctx context.Context, arg ListReportedIssuesByCustomerParams) ([]ReportedIssue, error) {
rows, err := q.db.Query(ctx, ListReportedIssuesByCustomer, arg.CustomerID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ReportedIssue
for rows.Next() {
var i ReportedIssue
if err := rows.Scan(
&i.ID,
&i.CustomerID,
&i.Subject,
&i.Description,
&i.IssueType,
&i.Status,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateReportedIssueStatus = `-- name: UpdateReportedIssueStatus :exec
UPDATE reported_issues
SET status = $2, updated_at = NOW()
WHERE id = $1
`
type UpdateReportedIssueStatusParams struct {
ID int64 `json:"id"`
Status string `json:"status"`
}
func (q *Queries) UpdateReportedIssueStatus(ctx context.Context, arg UpdateReportedIssueStatusParams) error {
_, err := q.db.Exec(ctx, UpdateReportedIssueStatus, arg.ID, arg.Status)
return err
}

View File

@ -334,6 +334,18 @@ type RefreshToken struct {
Revoked bool `json:"revoked"`
}
type ReportedIssue struct {
ID int64 `json:"id"`
CustomerID int64 `json:"customer_id"`
Subject string `json:"subject"`
Description string `json:"description"`
IssueType string `json:"issue_type"`
Status string `json:"status"`
Metadata []byte `json:"metadata"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type Result struct {
ID int64 `json:"id"`
BetOutcomeID int64 `json:"bet_outcome_id"`

View File

@ -0,0 +1,15 @@
package domain
import "time"
type ReportedIssue struct {
ID int64 `json:"id"`
CustomerID int64 `json:"customer_id"`
Subject string `json:"subject"`
Description string `json:"description"`
IssueType string `json:"issue_type"`
Status string `json:"status"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -0,0 +1,65 @@
package repository
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
)
type ReportedIssueRepository interface {
CreateReportedIssue(ctx context.Context, arg dbgen.CreateReportedIssueParams) (dbgen.ReportedIssue, error)
ListReportedIssues(ctx context.Context, limit, offset int32) ([]dbgen.ReportedIssue, error)
ListReportedIssuesByCustomer(ctx context.Context, customerID int64, limit, offset int32) ([]dbgen.ReportedIssue, error)
CountReportedIssues(ctx context.Context) (int64, error)
CountReportedIssuesByCustomer(ctx context.Context, customerID int64) (int64, error)
UpdateReportedIssueStatus(ctx context.Context, id int64, status string) error
DeleteReportedIssue(ctx context.Context, id int64) error
}
type ReportedIssueRepo struct {
store *Store
}
func NewReportedIssueRepository(store *Store) ReportedIssueRepository {
return &ReportedIssueRepo{store: store}
}
func (s *ReportedIssueRepo) CreateReportedIssue(ctx context.Context, arg dbgen.CreateReportedIssueParams) (dbgen.ReportedIssue, error) {
return s.store.queries.CreateReportedIssue(ctx, arg)
}
func (s *ReportedIssueRepo) ListReportedIssues(ctx context.Context, limit, offset int32) ([]dbgen.ReportedIssue, error) {
params := dbgen.ListReportedIssuesParams{
Limit: limit,
Offset: offset,
}
return s.store.queries.ListReportedIssues(ctx, params)
}
func (s *ReportedIssueRepo) ListReportedIssuesByCustomer(ctx context.Context, customerID int64, limit, offset int32) ([]dbgen.ReportedIssue, error) {
params := dbgen.ListReportedIssuesByCustomerParams{
CustomerID: customerID,
Limit: limit,
Offset: offset,
}
return s.store.queries.ListReportedIssuesByCustomer(ctx, params)
}
func (s *ReportedIssueRepo) CountReportedIssues(ctx context.Context) (int64, error) {
return s.store.queries.CountReportedIssues(ctx)
}
func (s *ReportedIssueRepo) CountReportedIssuesByCustomer(ctx context.Context, customerID int64) (int64, error) {
return s.store.queries.CountReportedIssuesByCustomer(ctx, customerID)
}
func (s *ReportedIssueRepo) UpdateReportedIssueStatus(ctx context.Context, id int64, status string) error {
return s.store.queries.UpdateReportedIssueStatus(ctx, dbgen.UpdateReportedIssueStatusParams{
ID: id,
Status: status,
})
}
func (s *ReportedIssueRepo) DeleteReportedIssue(ctx context.Context, id int64) error {
return s.store.queries.DeleteReportedIssue(ctx, id)
}

View File

@ -0,0 +1,83 @@
package issuereporting
import (
"context"
"errors"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
)
type Service struct {
repo repository.ReportedIssueRepository
}
func New(repo repository.ReportedIssueRepository) *Service {
return &Service{repo: repo}
}
func (s *Service) CreateReportedIssue(ctx context.Context, issue domain.ReportedIssue) (domain.ReportedIssue, error) {
params := dbgen.CreateReportedIssueParams{
// Map fields from domain.ReportedIssue to dbgen.CreateReportedIssueParams here.
// Example:
// Title: issue.Title,
// Description: issue.Description,
// CustomerID: issue.CustomerID,
// Status: issue.Status,
// Add other fields as necessary.
}
dbIssue, err := s.repo.CreateReportedIssue(ctx, params)
if err != nil {
return domain.ReportedIssue{}, err
}
// Map dbgen.ReportedIssue to domain.ReportedIssue
reportedIssue := domain.ReportedIssue{
ID: dbIssue.ID,
Subject: dbIssue.Subject,
Description: dbIssue.Description,
CustomerID: dbIssue.CustomerID,
Status: dbIssue.Status,
CreatedAt: dbIssue.CreatedAt.Time,
UpdatedAt: dbIssue.UpdatedAt.Time,
// Add other fields as necessary
}
return reportedIssue, nil
}
func (s *Service) GetIssuesForCustomer(ctx context.Context, customerID int64, limit, offset int) ([]domain.ReportedIssue, error) {
dbIssues, err := s.repo.ListReportedIssuesByCustomer(ctx, customerID, int32(limit), int32(offset))
if err != nil {
return nil, err
}
reportedIssues := make([]domain.ReportedIssue, len(dbIssues))
for i, dbIssue := range dbIssues {
reportedIssues[i] = domain.ReportedIssue{
ID: dbIssue.ID,
Subject: dbIssue.Subject,
Description: dbIssue.Description,
CustomerID: dbIssue.CustomerID,
Status: dbIssue.Status,
CreatedAt: dbIssue.CreatedAt.Time,
UpdatedAt: dbIssue.UpdatedAt.Time,
// Add other fields as necessary
}
}
return reportedIssues, nil
}
func (s *Service) GetAllIssues(ctx context.Context, limit, offset int) ([]dbgen.ReportedIssue, error) {
return s.repo.ListReportedIssues(ctx, int32(limit), int32(offset))
}
func (s *Service) UpdateIssueStatus(ctx context.Context, issueID int64, status string) error {
validStatuses := map[string]bool{"pending": true, "in_progress": true, "resolved": true, "rejected": true}
if !validStatuses[status] {
return errors.New("invalid status")
}
return s.repo.UpdateReportedIssueStatus(ctx, issueID, status)
}
func (s *Service) DeleteIssue(ctx context.Context, issueID int64) error {
return s.repo.DeleteReportedIssue(ctx, issueID)
}

View File

@ -1 +0,0 @@
package issues

View File

@ -1 +0,0 @@
package issues

View File

@ -13,6 +13,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
@ -37,6 +38,7 @@ import (
)
type App struct {
issueReportingSvc *issuereporting.Service
instSvc *institutions.Service
currSvc *currency.Service
fiber *fiber.App
@ -70,6 +72,7 @@ type App struct {
}
func NewApp(
issueReportingSvc *issuereporting.Service,
instSvc *institutions.Service,
currSvc *currency.Service,
port int, validator *customvalidator.CustomValidator,
@ -113,6 +116,7 @@ func NewApp(
}))
s := &App{
issueReportingSvc: issueReportingSvc,
instSvc: instSvc,
currSvc: currSvc,
fiber: app,

View File

@ -12,6 +12,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
@ -31,6 +32,7 @@ import (
)
type Handler struct {
issueReportingSvc *issuereporting.Service
instSvc *institutions.Service
currSvc *currency.Service
logger *slog.Logger
@ -61,6 +63,7 @@ type Handler struct {
}
func New(
issueReportingSvc *issuereporting.Service,
instSvc *institutions.Service,
currSvc *currency.Service,
logger *slog.Logger,
@ -90,6 +93,7 @@ func New(
mongoLoggerSvc *zap.Logger,
) *Handler {
return &Handler{
issueReportingSvc: issueReportingSvc,
instSvc: instSvc,
currSvc: currSvc,
logger: logger,

View File

@ -0,0 +1,147 @@
package handlers
import (
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
)
// CreateIssue godoc
// @Summary Report an issue
// @Description Allows a customer to report a new issue related to the betting platform
// @Tags Issues
// @Accept json
// @Produce json
// @Param issue body domain.ReportedIssue true "Issue to report"
// @Success 201 {object} domain.ReportedIssue
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues [post]
func (h *Handler) CreateIssue(c *fiber.Ctx) error {
var req domain.ReportedIssue
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
created, err := h.issueReportingSvc.CreateReportedIssue(c.Context(), req)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.Status(fiber.StatusCreated).JSON(created)
}
// GetCustomerIssues godoc
// @Summary Get reported issues by a customer
// @Description Returns all issues reported by a specific customer
// @Tags Issues
// @Produce json
// @Param customer_id path int true "Customer ID"
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {array} domain.ReportedIssue
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues/customer/{customer_id} [get]
func (h *Handler) GetCustomerIssues(c *fiber.Ctx) error {
customerID, err := strconv.ParseInt(c.Params("customer_id"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid customer ID")
}
limit, offset := getPaginationParams(c)
issues, err := h.issueReportingSvc.GetIssuesForCustomer(c.Context(), customerID, limit, offset)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(issues)
}
// GetAllIssues godoc
// @Summary Get all reported issues
// @Description Admin endpoint to list all reported issues with pagination
// @Tags Issues
// @Produce json
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {array} domain.ReportedIssue
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues [get]
func (h *Handler) GetAllIssues(c *fiber.Ctx) error {
limit, offset := getPaginationParams(c)
issues, err := h.issueReportingSvc.GetAllIssues(c.Context(), limit, offset)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(issues)
}
// UpdateIssueStatus godoc
// @Summary Update issue status
// @Description Admin endpoint to update the status of a reported issue
// @Tags Issues
// @Accept json
// @Param issue_id path int true "Issue ID"
// @Param status body object{status=string} true "New issue status (pending, in_progress, resolved, rejected)"
// @Success 204
// @Failure 400 {object} domain.ErrorResponse
// @Router /api/v1/issues/{issue_id}/status [patch]
func (h *Handler) UpdateIssueStatus(c *fiber.Ctx) error {
issueID, err := strconv.ParseInt(c.Params("issue_id"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid issue ID")
}
var body struct {
Status string `json:"status"`
}
if err := c.BodyParser(&body); err != nil || body.Status == "" {
return fiber.NewError(fiber.StatusBadRequest, "Invalid status payload")
}
if err := h.issueReportingSvc.UpdateIssueStatus(c.Context(), issueID, body.Status); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusNoContent)
}
// DeleteIssue godoc
// @Summary Delete a reported issue
// @Description Admin endpoint to delete a reported issue
// @Tags Issues
// @Param issue_id path int true "Issue ID"
// @Success 204
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues/{issue_id} [delete]
func (h *Handler) DeleteIssue(c *fiber.Ctx) error {
issueID, err := strconv.ParseInt(c.Params("issue_id"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid issue ID")
}
if err := h.issueReportingSvc.DeleteIssue(c.Context(), issueID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusNoContent)
}
func getPaginationParams(c *fiber.Ctx) (limit, offset int) {
limit = 20
offset = 0
if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 {
limit = l
}
if o, err := strconv.Atoi(c.Query("offset")); err == nil && o >= 0 {
offset = o
}
return
}

View File

@ -20,6 +20,7 @@ import (
func (a *App) initAppRoutes() {
h := handlers.New(
a.issueReportingSvc,
a.instSvc,
a.currSvc,
a.logger,
@ -282,6 +283,13 @@ func (a *App) initAppRoutes() {
group.Post("/virtual-game/favorites", a.authMiddleware, h.AddFavorite)
group.Delete("/virtual-game/favorites/:gameID", a.authMiddleware, h.RemoveFavorite)
group.Get("/virtual-game/favorites", a.authMiddleware, h.ListFavorites)
//Issue Reporting Routes
group.Post("/issues", a.authMiddleware, a.OnlyAdminAndAbove, h.CreateIssue)
group.Get("/issues/customer/:customer_id", a.authMiddleware, a.OnlyAdminAndAbove, h.GetCustomerIssues)
group.Get("/issues", a.authMiddleware, a.OnlyAdminAndAbove, h.GetAllIssues)
group.Patch("/issues/:issue_id/status", a.authMiddleware, a.OnlyAdminAndAbove, h.UpdateIssueStatus)
group.Delete("/issues/:issue_id", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteIssue)
}
///user/profile get