diff --git a/cmd/main.go b/cmd/main.go index 3cf89fa..8553aaa 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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, diff --git a/db/migrations/000008_issue_reporting.down.sql b/db/migrations/000008_issue_reporting.down.sql new file mode 100644 index 0000000..59d3f24 --- /dev/null +++ b/db/migrations/000008_issue_reporting.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS reported_issues; + diff --git a/db/migrations/000008_issue_reporting.up.sql b/db/migrations/000008_issue_reporting.up.sql new file mode 100644 index 0000000..53ad252 --- /dev/null +++ b/db/migrations/000008_issue_reporting.up.sql @@ -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 +); + diff --git a/db/query/issue_reporting.sql b/db/query/issue_reporting.sql new file mode 100644 index 0000000..31ea229 --- /dev/null +++ b/db/query/issue_reporting.sql @@ -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; diff --git a/docs/docs.go b/docs/docs.go index a4a551c..ec52242 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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": { diff --git a/docs/swagger.json b/docs/swagger.json index 6be7103..52af909 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 09d1e07..86b5932 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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: diff --git a/gen/db/issue_reporting.sql.go b/gen/db/issue_reporting.sql.go new file mode 100644 index 0000000..c737b9e --- /dev/null +++ b/gen/db/issue_reporting.sql.go @@ -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 +} diff --git a/gen/db/models.go b/gen/db/models.go index 75e1d83..2f7457c 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -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"` diff --git a/internal/domain/issue_reporting.go b/internal/domain/issue_reporting.go new file mode 100644 index 0000000..1f55aee --- /dev/null +++ b/internal/domain/issue_reporting.go @@ -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"` +} diff --git a/internal/repository/issue_reporting.go b/internal/repository/issue_reporting.go new file mode 100644 index 0000000..01687f3 --- /dev/null +++ b/internal/repository/issue_reporting.go @@ -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) +} diff --git a/internal/services/issue_reporting/service.go b/internal/services/issue_reporting/service.go new file mode 100644 index 0000000..88aba2f --- /dev/null +++ b/internal/services/issue_reporting/service.go @@ -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) +} diff --git a/internal/services/issues/port.go b/internal/services/issues/port.go deleted file mode 100644 index 689aaa1..0000000 --- a/internal/services/issues/port.go +++ /dev/null @@ -1 +0,0 @@ -package issues \ No newline at end of file diff --git a/internal/services/issues/service.go b/internal/services/issues/service.go deleted file mode 100644 index 689aaa1..0000000 --- a/internal/services/issues/service.go +++ /dev/null @@ -1 +0,0 @@ -package issues \ No newline at end of file diff --git a/internal/web_server/app.go b/internal/web_server/app.go index be16c0c..787770c 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -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, diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 25615bc..3086317 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -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, diff --git a/internal/web_server/handlers/issue_reporting.go b/internal/web_server/handlers/issue_reporting.go new file mode 100644 index 0000000..d49c6f5 --- /dev/null +++ b/internal/web_server/handlers/issue_reporting.go @@ -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 +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index c6a603c..7e786f0 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -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