Add Company Category Management Functionality and Update API Documentation

- Introduced a new company category management feature, including CRUD operations for categories in both admin and public API endpoints.
- Implemented category entity, service, repository, and handler layers following clean architecture principles.
- Added new API routes for searching, retrieving, updating, deleting, and toggling publish status of categories, enhancing the overall functionality of the system.
- Updated Swagger and YAML documentation to include detailed descriptions and examples for the new category endpoints, ensuring clarity for API consumers.
- Enhanced error handling for duplicate categories and validation, improving the robustness of the category management process.
- Updated existing routes to integrate category handling, ensuring a seamless user experience across the application.
This commit is contained in:
n.nakhostin
2025-09-08 12:31:49 +03:30
parent 617547def8
commit 991771ee19
10 changed files with 2156 additions and 7 deletions
+509
View File
@@ -904,6 +904,454 @@ const docTemplate = `{
}
}
},
"/admin/v1/company-categories": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Search categories with filtering capabilities",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin-Company-Categories"
],
"summary": "Search categories",
"parameters": [
{
"type": "string",
"description": "Search query",
"name": "q",
"in": "query"
},
{
"type": "boolean",
"description": "Published status filter",
"name": "published",
"in": "query"
},
{
"type": "integer",
"default": 20,
"description": "Limit",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Offset",
"name": "offset",
"in": "query"
},
{
"enum": [
"name",
"created_at",
"updated_at",
"published_at"
],
"type": "string",
"description": "Sort by field",
"name": "sort_by",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Sort order",
"name": "sort_order",
"in": "query"
}
],
"responses": {
"200": {
"description": "Categories search completed",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/company_category.CategoryListResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid query parameters",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid query parameters",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Create a new category with name, description, and thumbnail",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin-Company-Categories"
],
"summary": "Create a new category",
"parameters": [
{
"description": "Category information",
"name": "category",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/company_category.CategoryForm"
}
}
],
"responses": {
"201": {
"description": "Category created successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/company_category.CategoryResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid input data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"409": {
"description": "Conflict - Category with this name already exists",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid request data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/admin/v1/company-categories/{id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Retrieve detailed category information by category ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin-Company-Categories"
],
"summary": "Get category by ID",
"parameters": [
{
"type": "string",
"description": "Category ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Category retrieved successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/company_category.CategoryResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid category ID",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"404": {
"description": "Not found - Category not found",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Update category information including name, description, and thumbnail",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin-Company-Categories"
],
"summary": "Update category information",
"parameters": [
{
"type": "string",
"description": "Category ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Category update information",
"name": "category",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/company_category.CategoryForm"
}
}
],
"responses": {
"200": {
"description": "Category updated successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/company_category.CategoryResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid input data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"404": {
"description": "Not found - Category not found",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"409": {
"description": "Conflict - Category with this name already exists",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid request data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Delete a category permanently",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin-Company-Categories"
],
"summary": "Delete category",
"parameters": [
{
"type": "string",
"description": "Category ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Category deleted successfully",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad request - Invalid category ID",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"404": {
"description": "Not found - Category not found",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/admin/v1/company-categories/{id}/publish": {
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "Toggle category publish/unpublish status",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin-Company-Categories"
],
"summary": "Toggle category publish status",
"parameters": [
{
"type": "string",
"description": "Category ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Category publish status updated successfully",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad request - Invalid input data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"404": {
"description": "Not found - Category not found",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid request data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/admin/v1/customers": {
"get": {
"security": [
@@ -5487,6 +5935,63 @@ const docTemplate = `{
}
}
},
"company_category.CategoryForm": {
"type": "object",
"properties": {
"description": {
"type": "string",
"example": "Technology related companies"
},
"name": {
"type": "string",
"example": "Technology"
},
"thumbnail": {
"type": "string",
"example": "https://example.com/thumbnail.jpg"
}
}
},
"company_category.CategoryListResponse": {
"type": "object",
"properties": {
"categories": {
"type": "array",
"items": {
"$ref": "#/definitions/company_category.CategoryResponse"
}
}
}
},
"company_category.CategoryResponse": {
"type": "object",
"properties": {
"created_at": {
"type": "integer"
},
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"published": {
"type": "boolean"
},
"published_at": {
"type": "integer"
},
"thumbnail": {
"type": "string"
},
"updated_at": {
"type": "integer"
}
}
},
"customer.AuthResponse": {
"type": "object",
"properties": {
@@ -6552,6 +7057,10 @@ const docTemplate = `{
"description": "Administrative company management operations for web panel including CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting",
"name": "Admin-Companies"
},
{
"description": "Administrative company category management operations for web panel including CRUD operations, publish/unpublish status, and comprehensive filtering with pagination",
"name": "Admin-Company-Categories"
},
{
"description": "Administrative tender management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination",
"name": "Admin-Tenders"
+509
View File
@@ -898,6 +898,454 @@
}
}
},
"/admin/v1/company-categories": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Search categories with filtering capabilities",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin-Company-Categories"
],
"summary": "Search categories",
"parameters": [
{
"type": "string",
"description": "Search query",
"name": "q",
"in": "query"
},
{
"type": "boolean",
"description": "Published status filter",
"name": "published",
"in": "query"
},
{
"type": "integer",
"default": 20,
"description": "Limit",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"default": 0,
"description": "Offset",
"name": "offset",
"in": "query"
},
{
"enum": [
"name",
"created_at",
"updated_at",
"published_at"
],
"type": "string",
"description": "Sort by field",
"name": "sort_by",
"in": "query"
},
{
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "Sort order",
"name": "sort_order",
"in": "query"
}
],
"responses": {
"200": {
"description": "Categories search completed",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/company_category.CategoryListResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid query parameters",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid query parameters",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Create a new category with name, description, and thumbnail",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin-Company-Categories"
],
"summary": "Create a new category",
"parameters": [
{
"description": "Category information",
"name": "category",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/company_category.CategoryForm"
}
}
],
"responses": {
"201": {
"description": "Category created successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/company_category.CategoryResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid input data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"409": {
"description": "Conflict - Category with this name already exists",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid request data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/admin/v1/company-categories/{id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Retrieve detailed category information by category ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin-Company-Categories"
],
"summary": "Get category by ID",
"parameters": [
{
"type": "string",
"description": "Category ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Category retrieved successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/company_category.CategoryResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid category ID",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"404": {
"description": "Not found - Category not found",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Update category information including name, description, and thumbnail",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin-Company-Categories"
],
"summary": "Update category information",
"parameters": [
{
"type": "string",
"description": "Category ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Category update information",
"name": "category",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/company_category.CategoryForm"
}
}
],
"responses": {
"200": {
"description": "Category updated successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/company_category.CategoryResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid input data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"404": {
"description": "Not found - Category not found",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"409": {
"description": "Conflict - Category with this name already exists",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid request data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Delete a category permanently",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin-Company-Categories"
],
"summary": "Delete category",
"parameters": [
{
"type": "string",
"description": "Category ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Category deleted successfully",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad request - Invalid category ID",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"404": {
"description": "Not found - Category not found",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/admin/v1/company-categories/{id}/publish": {
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "Toggle category publish/unpublish status",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin-Company-Categories"
],
"summary": "Toggle category publish status",
"parameters": [
{
"type": "string",
"description": "Category ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Category publish status updated successfully",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad request - Invalid input data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"404": {
"description": "Not found - Category not found",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid request data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/admin/v1/customers": {
"get": {
"security": [
@@ -5481,6 +5929,63 @@
}
}
},
"company_category.CategoryForm": {
"type": "object",
"properties": {
"description": {
"type": "string",
"example": "Technology related companies"
},
"name": {
"type": "string",
"example": "Technology"
},
"thumbnail": {
"type": "string",
"example": "https://example.com/thumbnail.jpg"
}
}
},
"company_category.CategoryListResponse": {
"type": "object",
"properties": {
"categories": {
"type": "array",
"items": {
"$ref": "#/definitions/company_category.CategoryResponse"
}
}
}
},
"company_category.CategoryResponse": {
"type": "object",
"properties": {
"created_at": {
"type": "integer"
},
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"published": {
"type": "boolean"
},
"published_at": {
"type": "integer"
},
"thumbnail": {
"type": "string"
},
"updated_at": {
"type": "integer"
}
}
},
"customer.AuthResponse": {
"type": "object",
"properties": {
@@ -6546,6 +7051,10 @@
"description": "Administrative company management operations for web panel including CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting",
"name": "Admin-Companies"
},
{
"description": "Administrative company category management operations for web panel including CRUD operations, publish/unpublish status, and comprehensive filtering with pagination",
"name": "Admin-Company-Categories"
},
{
"description": "Administrative tender management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination",
"name": "Admin-Tenders"
+321
View File
@@ -263,6 +263,44 @@ definitions:
is_verified:
type: boolean
type: object
company_category.CategoryForm:
properties:
description:
example: Technology related companies
type: string
name:
example: Technology
type: string
thumbnail:
example: https://example.com/thumbnail.jpg
type: string
type: object
company_category.CategoryListResponse:
properties:
categories:
items:
$ref: '#/definitions/company_category.CategoryResponse'
type: array
type: object
company_category.CategoryResponse:
properties:
created_at:
type: integer
description:
type: string
id:
type: string
name:
type: string
published:
type: boolean
published_at:
type: integer
thumbnail:
type: string
updated_at:
type: integer
type: object
customer.AuthResponse:
properties:
access_token:
@@ -1545,6 +1583,285 @@ paths:
summary: Update company verification status
tags:
- Admin-Companies
/admin/v1/company-categories:
get:
consumes:
- application/json
description: Search categories with filtering capabilities
parameters:
- description: Search query
in: query
name: q
type: string
- description: Published status filter
in: query
name: published
type: boolean
- default: 20
description: Limit
in: query
name: limit
type: integer
- default: 0
description: Offset
in: query
name: offset
type: integer
- description: Sort by field
enum:
- name
- created_at
- updated_at
- published_at
in: query
name: sort_by
type: string
- description: Sort order
enum:
- asc
- desc
in: query
name: sort_order
type: string
produces:
- application/json
responses:
"200":
description: Categories search completed
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/company_category.CategoryListResponse'
type: object
"400":
description: Bad request - Invalid query parameters
schema:
$ref: '#/definitions/response.APIResponse'
"422":
description: Validation error - Invalid query parameters
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Search categories
tags:
- Admin-Company-Categories
post:
consumes:
- application/json
description: Create a new category with name, description, and thumbnail
parameters:
- description: Category information
in: body
name: category
required: true
schema:
$ref: '#/definitions/company_category.CategoryForm'
produces:
- application/json
responses:
"201":
description: Category created successfully
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/company_category.CategoryResponse'
type: object
"400":
description: Bad request - Invalid input data
schema:
$ref: '#/definitions/response.APIResponse'
"409":
description: Conflict - Category with this name already exists
schema:
$ref: '#/definitions/response.APIResponse'
"422":
description: Validation error - Invalid request data
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Create a new category
tags:
- Admin-Company-Categories
/admin/v1/company-categories/{id}:
delete:
consumes:
- application/json
description: Delete a category permanently
parameters:
- description: Category ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Category deleted successfully
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad request - Invalid category ID
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not found - Category not found
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Delete category
tags:
- Admin-Company-Categories
get:
consumes:
- application/json
description: Retrieve detailed category information by category ID
parameters:
- description: Category ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Category retrieved successfully
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/company_category.CategoryResponse'
type: object
"400":
description: Bad request - Invalid category ID
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not found - Category not found
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Get category by ID
tags:
- Admin-Company-Categories
put:
consumes:
- application/json
description: Update category information including name, description, and thumbnail
parameters:
- description: Category ID
in: path
name: id
required: true
type: string
- description: Category update information
in: body
name: category
required: true
schema:
$ref: '#/definitions/company_category.CategoryForm'
produces:
- application/json
responses:
"200":
description: Category updated successfully
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/company_category.CategoryResponse'
type: object
"400":
description: Bad request - Invalid input data
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not found - Category not found
schema:
$ref: '#/definitions/response.APIResponse'
"409":
description: Conflict - Category with this name already exists
schema:
$ref: '#/definitions/response.APIResponse'
"422":
description: Validation error - Invalid request data
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Update category information
tags:
- Admin-Company-Categories
/admin/v1/company-categories/{id}/publish:
patch:
consumes:
- application/json
description: Toggle category publish/unpublish status
parameters:
- description: Category ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Category publish status updated successfully
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad request - Invalid input data
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not found - Category not found
schema:
$ref: '#/definitions/response.APIResponse'
"422":
description: Validation error - Invalid request data
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Toggle category publish status
tags:
- Admin-Company-Categories
/admin/v1/customers:
get:
consumes:
@@ -4185,6 +4502,10 @@ tags:
CRUD operations, tag management, verification, status updates, comprehensive search
and filtering, and statistical reporting
name: Admin-Companies
- description: Administrative company category management operations for web panel
including CRUD operations, publish/unpublish status, and comprehensive filtering
with pagination
name: Admin-Company-Categories
- description: Administrative tender management operations for web panel including
CRUD operations, search, statistics, and comprehensive filtering with pagination
name: Admin-Tenders
+12 -5
View File
@@ -35,6 +35,9 @@ package main
// @tag.name Admin-Companies
// @tag.description Administrative company management operations for web panel including CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting
// @tag.name Admin-Company-Categories
// @tag.description Administrative company category management operations for web panel including CRUD operations, publish/unpublish status, and comprehensive filtering with pagination
// @tag.name Admin-Tenders
// @tag.description Administrative tender management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination
@@ -71,6 +74,7 @@ import (
"net/http"
"time"
"tm/internal/company"
"tm/internal/company_category"
"tm/internal/customer"
"tm/internal/feedback"
"tm/internal/inquiry"
@@ -119,12 +123,13 @@ func main() {
userRepository := user.NewUserRepository(mongoManager, logger)
customerRepository := customer.NewCustomerRepository(mongoManager, logger)
companyRepository := company.NewCompanyRepository(mongoManager, logger)
categoryRepository := company_category.NewCategoryRepository(mongoManager, logger)
tenderRepository := tender.NewTenderRepository(mongoManager, logger)
feedbackRepo := feedback.NewFeedbackRepository(mongoManager, logger)
tenderApprovalRepository := tender_approval.NewTenderApprovalRepository(mongoManager, logger)
inquiryRepository := inquiry.NewInquiryRepository(mongoManager, logger)
logger.Info("Repositories initialized successfully", map[string]interface{}{
"repositories": []string{"customer", "user", "company", "tender", "feedback", "tender_approval"},
"repositories": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval"},
})
// Initialize validation service
@@ -133,33 +138,35 @@ func main() {
// Initialize services with repositories
userService := user.NewUserService(userRepository, logger, userAuthService, userValidator)
companyService := company.NewCompanyService(companyRepository, logger)
categoryService := company_category.NewCategoryService(categoryRepository, logger)
customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService, companyService)
tenderService := tender.NewTenderService(tenderRepository, companyService, logger)
feedbackService := feedback.NewFeedbackService(feedbackRepo, tenderService, logger)
tenderApprovalService := tender_approval.NewTenderApprovalService(tenderApprovalRepository, tenderService, logger)
inquiryService := inquiry.NewInquiryService(inquiryRepository, logger)
logger.Info("Services initialized successfully", map[string]interface{}{
"services": []string{"customer", "user", "company", "tender", "feedback", "tender_approval"},
"services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval"},
})
// Initialize handlers with services
userHandler := user.NewUserHandler(userService, logger, userValidator, userAuthService)
customerHandler := customer.NewHandler(customerService, userHandler, customerAuthService, logger)
companyHandler := company.NewHandler(companyService, userHandler, logger)
categoryHandler := company_category.NewHandler(categoryService, logger)
tenderHandler := tender.NewTenderHandler(tenderService, logger)
feedbackHandler := feedback.NewFeedbackHandler(feedbackService, userHandler, customerHandler, logger)
tenderApprovalHandler := tender_approval.NewHandler(tenderApprovalService, logger)
inquiryHandler := inquiry.NewHandler(inquiryService, logger)
logger.Info("Handlers initialized successfully", map[string]interface{}{
"handlers": []string{"customer", "user", "company", "tender", "feedback", "tender_approval"},
"handlers": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval"},
})
// Initialize HTTP server
e := bootstrap.InitHTTPServer(conf, logger)
// Register routes
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler)
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler)
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler)
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler)
// Start server
serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port)
+15 -2
View File
@@ -2,6 +2,7 @@ package router
import (
"tm/internal/company"
"tm/internal/company_category"
"tm/internal/customer"
"tm/internal/feedback"
"tm/internal/inquiry"
@@ -12,7 +13,7 @@ import (
"github.com/labstack/echo/v4"
)
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler) {
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler) {
adminV1 := e.Group("/admin/v1")
// Admin Users Routes
@@ -56,6 +57,18 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler
companiesGP.PATCH("/:id/verification", companyHandler.UpdateVerification)
}
// Admin Categories Routes
categoriesGP := adminV1.Group("/company-categories")
{
categoriesGP.Use(userHandler.AuthMiddleware())
categoriesGP.POST("", categoryHandler.Create)
categoriesGP.GET("", categoryHandler.Search)
categoriesGP.GET("/:id", categoryHandler.Get)
categoriesGP.PUT("/:id", categoryHandler.Update)
categoriesGP.DELETE("/:id", categoryHandler.Delete)
categoriesGP.PATCH("/:id/publish", categoryHandler.TogglePublish)
}
// Admin Customers Routes
customersGP := adminV1.Group("/customers")
{
@@ -109,7 +122,7 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler
}
}
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler) {
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler) {
v1 := e.Group("/api/v1")
customerGP := v1.Group("/profile")
+46
View File
@@ -0,0 +1,46 @@
package company_category
import (
"tm/pkg/mongo"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Category struct {
mongo.Model `bson:",inline"`
Name string `bson:"name" json:"name"`
Description *string `bson:"description" json:"description"`
Thumbnail *string `bson:"thumbnail" json:"thumbnail"`
Published bool `bson:"published" json:"published"`
PublishedAt *int64 `bson:"published_at" json:"published_at"`
}
// SetID sets the category ID (implements IDSetter interface)
func (c *Category) SetID(id string) {
c.ID, _ = primitive.ObjectIDFromHex(id)
}
// GetID returns the category ID (implements IDGetter interface)
func (c *Category) GetID() string {
return c.ID.Hex()
}
// SetCreatedAt sets the created timestamp (implements Timestamp interface)
func (c *Category) SetCreatedAt(timestamp int64) {
c.CreatedAt = timestamp
}
// SetUpdatedAt sets the updated timestamp (implements Timestamp interface)
func (c *Category) SetUpdatedAt(timestamp int64) {
c.UpdatedAt = timestamp
}
// GetCreatedAt returns the created timestamp (implements Timestamp interface)
func (c *Category) GetCreatedAt() int64 {
return c.CreatedAt
}
// GetUpdatedAt returns the updated timestamp (implements Timestamp interface)
func (c *Category) GetUpdatedAt() int64 {
return c.UpdatedAt
}
+52
View File
@@ -0,0 +1,52 @@
package company_category
import "tm/pkg/response"
// CategoryForm represents the form for creating/updating a category
type CategoryForm struct {
Name string `json:"name" valid:"required,length(2|100)" example:"Technology"`
Description *string `json:"description,omitempty" valid:"optional,length(10|500)" example:"Technology related companies"`
Thumbnail *string `json:"thumbnail,omitempty" valid:"optional,url" example:"https://example.com/thumbnail.jpg"`
}
// SearchForm represents the form for searching categories with filters
type SearchForm struct {
Search *string `query:"q" valid:"optional"`
Published *bool `query:"published" valid:"optional"`
Limit *int `query:"limit" valid:"optional,range(1|100)"`
Offset *int `query:"offset" valid:"optional,min(0)"`
SortBy *string `query:"sort_by" valid:"optional,in(name|created_at|updated_at|published_at)"`
SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"`
}
// CategoryListResponse represents the response for listing categories
type CategoryListResponse struct {
Categories []*CategoryResponse `json:"categories"`
Meta *response.Meta `json:"-"`
}
// CategoryResponse represents the category data sent in API responses
type CategoryResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
Published bool `json:"published"`
PublishedAt *int64 `json:"published_at,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
// ToResponse converts Category to CategoryResponse
func (c *Category) ToResponse() *CategoryResponse {
return &CategoryResponse{
ID: c.ID.Hex(),
Name: c.Name,
Description: c.Description,
Thumbnail: c.Thumbnail,
Published: c.Published,
PublishedAt: c.PublishedAt,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}
+213
View File
@@ -0,0 +1,213 @@
package company_category
import (
"tm/pkg/logger"
"tm/pkg/response"
"github.com/labstack/echo/v4"
)
// Handler handles HTTP requests for category operations
type Handler struct {
service Service
logger logger.Logger
}
// NewHandler creates a new category handler
func NewHandler(service Service, logger logger.Logger) *Handler {
return &Handler{
service: service,
logger: logger,
}
}
// Create creates a new category (Web Panel)
// @Summary Create a new category
// @Description Create a new category with name, description, and thumbnail
// @Tags Admin-Company-Categories
// @Accept json
// @Produce json
// @Param category body CategoryForm true "Category information"
// @Success 201 {object} response.APIResponse{data=CategoryResponse} "Category created successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 409 {object} response.APIResponse "Conflict - Category with this name already exists"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/company-categories [post]
func (h *Handler) Create(c echo.Context) error {
form, err := response.Parse[CategoryForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
category, err := h.service.Create(c.Request().Context(), form)
if err != nil {
if err.Error() == "category with this name already exists" {
return response.Conflict(c, err.Error())
}
return response.InternalServerError(c, "Failed to create category")
}
return response.Created(c, category, "Category created successfully")
}
// GetCategory retrieves a category by ID (Web Panel)
// @Summary Get category by ID
// @Description Retrieve detailed category information by category ID
// @Tags Admin-Company-Categories
// @Accept json
// @Produce json
// @Param id path string true "Category ID"
// @Success 200 {object} response.APIResponse{data=CategoryResponse} "Category retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid category ID"
// @Failure 404 {object} response.APIResponse "Not found - Category not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/company-categories/{id} [get]
func (h *Handler) Get(c echo.Context) error {
id := c.Param("id")
category, err := h.service.GetByID(c.Request().Context(), id)
if err != nil {
if err.Error() == "category not found" {
return response.NotFound(c, "Category not found")
}
return response.InternalServerError(c, "Failed to get category")
}
return response.Success(c, category, "Category retrieved successfully")
}
// Update updates a category (Web Panel)
// @Summary Update category information
// @Description Update category information including name, description, and thumbnail
// @Tags Admin-Company-Categories
// @Accept json
// @Produce json
// @Param id path string true "Category ID"
// @Param category body CategoryForm true "Category update information"
// @Success 200 {object} response.APIResponse{data=CategoryResponse} "Category updated successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 404 {object} response.APIResponse "Not found - Category not found"
// @Failure 409 {object} response.APIResponse "Conflict - Category with this name already exists"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/company-categories/{id} [put]
func (h *Handler) Update(c echo.Context) error {
id := c.Param("id")
form, err := response.Parse[CategoryForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
category, err := h.service.Update(c.Request().Context(), id, form)
if err != nil {
if err.Error() == "category not found" {
return response.NotFound(c, "Category not found")
}
if err.Error() == "category with this name already exists" {
return response.Conflict(c, err.Error())
}
return response.InternalServerError(c, "Failed to update category")
}
return response.Success(c, category, "Category updated successfully")
}
// Delete deletes a category (Web Panel)
// @Summary Delete category
// @Description Delete a category permanently
// @Tags Admin-Company-Categories
// @Accept json
// @Produce json
// @Param id path string true "Category ID"
// @Success 200 {object} response.APIResponse "Category deleted successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid category ID"
// @Failure 404 {object} response.APIResponse "Not found - Category not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/company-categories/{id} [delete]
func (h *Handler) Delete(c echo.Context) error {
id := c.Param("id")
err := h.service.Delete(c.Request().Context(), id)
if err != nil {
if err.Error() == "category not found" {
return response.NotFound(c, "Category not found")
}
return response.InternalServerError(c, "Failed to delete category")
}
return response.Success(c, map[string]interface{}{
"message": "Category deleted successfully",
}, "Category deleted successfully")
}
// Search searches categories with filters (Web Panel)
// @Summary Search categories
// @Description Search categories with filtering capabilities
// @Tags Admin-Company-Categories
// @Accept json
// @Produce json
// @Param q query string false "Search query"
// @Param published query boolean false "Published status filter"
// @Param limit query integer false "Limit" default(20)
// @Param offset query integer false "Offset" default(0)
// @Param sort_by query string false "Sort by field" Enums(name,created_at,updated_at,published_at)
// @Param sort_order query string false "Sort order" Enums(asc,desc)
// @Success 200 {object} response.APIResponse{data=CategoryListResponse} "Categories search completed"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid query parameters"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/company-categories [get]
func (h *Handler) Search(c echo.Context) error {
form, err := response.Parse[SearchForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.SearchCategories(c.Request().Context(), form, response.NewPagination(c))
if err != nil {
return response.InternalServerError(c, "Failed to search categories")
}
return response.SuccessWithMeta(c, result, result.Meta, "Categories retrieved successfully")
}
// TogglePublish toggles category publish status (Web Panel)
// @Summary Toggle category publish status
// @Description Toggle category publish/unpublish status
// @Tags Admin-Company-Categories
// @Accept json
// @Produce json
// @Param id path string true "Category ID"
// @Success 200 {object} response.APIResponse "Category publish status updated successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 404 {object} response.APIResponse "Not found - Category not found"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/company-categories/{id}/publish [patch]
func (h *Handler) TogglePublish(c echo.Context) error {
id := c.Param("id")
published, err := h.service.TogglePublish(c.Request().Context(), id)
if err != nil {
if err.Error() == "category not found" {
return response.NotFound(c, "Category not found")
}
return response.InternalServerError(c, "Failed to toggle category publish status")
}
return response.Success(
c,
map[string]interface{}{
"message": "Category publish status updated successfully",
"published": published,
},
"Category publish status updated successfully",
)
}
+255
View File
@@ -0,0 +1,255 @@
package company_category
import (
"context"
"errors"
"time"
"tm/pkg/logger"
orm "tm/pkg/mongo"
"tm/pkg/response"
"go.mongodb.org/mongo-driver/bson"
)
// Repository defines the interface for category data operations
type Repository interface {
Create(ctx context.Context, category *Category) error
Update(ctx context.Context, category *Category) error
GetByID(ctx context.Context, id string) (*Category, error)
GetByIDs(ctx context.Context, ids []string) ([]Category, error)
GetByName(ctx context.Context, name string) (*Category, error)
Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Category, int64, error)
Delete(ctx context.Context, id string) error
TogglePublish(ctx context.Context, id string) (bool, error)
}
// categoryRepository implements the Repository interface using the MongoDB ORM
type categoryRepository struct {
ormRepo orm.Repository[Category]
logger logger.Logger
}
// NewCategoryRepository creates a new category repository
func NewCategoryRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
// Create indexes using the ORM's index management
indexes := []orm.Index{
*orm.NewIndex("name_idx", bson.D{{Key: "name", Value: 1}}),
*orm.NewIndex("published_idx", bson.D{{Key: "published", Value: 1}}),
}
// Create indexes
err := mongoManager.CreateIndexes("company_categories", indexes)
if err != nil {
logger.Warn("Failed to create category indexes", map[string]interface{}{
"error": err.Error(),
})
}
// Create ORM repository
ormRepo := orm.NewRepository[Category](mongoManager.GetCollection("company_categories"), logger)
return &categoryRepository{
ormRepo: ormRepo,
logger: logger,
}
}
// Create creates a new category
func (r *categoryRepository) Create(ctx context.Context, category *Category) error {
// Set created/updated timestamps using Unix timestamps
now := time.Now().Unix()
category.SetCreatedAt(now)
// Use ORM to create category
err := r.ormRepo.Create(ctx, category)
if err != nil {
r.logger.Error("Failed to create category", map[string]interface{}{
"error": err.Error(),
"name": category.Name,
})
return err
}
r.logger.Info("Category created successfully", map[string]interface{}{
"category_id": category.ID,
"name": category.Name,
"published": category.Published,
})
return nil
}
// GetByID retrieves a category by ID
func (r *categoryRepository) GetByID(ctx context.Context, id string) (*Category, error) {
category, err := r.ormRepo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, errors.New("category not found")
}
r.logger.Error("Failed to get category by ID", map[string]interface{}{
"error": err.Error(),
"category_id": id,
})
return nil, err
}
return category, nil
}
// GetByIDs retrieves categories by IDs
func (r *categoryRepository) GetByIDs(ctx context.Context, ids []string) ([]Category, error) {
filter := bson.M{"_id": bson.M{"$in": ids}}
paginationBuilder := orm.NewPaginationBuilder().
Build()
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder)
if err != nil {
return nil, err
}
return result.Items, nil
}
// GetByName retrieves a category by name
func (r *categoryRepository) GetByName(ctx context.Context, name string) (*Category, error) {
category, err := r.ormRepo.FindOne(ctx, bson.M{"name": name})
if err != nil {
return nil, err
}
return category, nil
}
// Update updates a category
func (r *categoryRepository) Update(ctx context.Context, category *Category) error {
// Set updated timestamp using Unix timestamp
category.SetUpdatedAt(time.Now().Unix())
// Use ORM to update category
err := r.ormRepo.Update(ctx, category)
if err != nil {
r.logger.Error("Failed to update category", map[string]interface{}{
"error": err.Error(),
"category_id": category.ID,
})
return err
}
r.logger.Info("Category updated successfully", map[string]interface{}{
"category_id": category.ID,
"name": category.Name,
})
return nil
}
// Delete deletes a category
func (r *categoryRepository) Delete(ctx context.Context, id string) error {
// Get category first
_, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Delete from database
err = r.ormRepo.Delete(ctx, id)
if err != nil {
r.logger.Error("Failed to delete category", map[string]interface{}{
"error": err.Error(),
"category_id": id,
})
return err
}
r.logger.Info("Category deleted successfully", map[string]interface{}{
"category_id": id,
})
return nil
}
// Search retrieves categories with search and filters
func (r *categoryRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Category, int64, error) {
// Build filter
filter := bson.M{}
if form.Search != nil && *form.Search != "" {
// Use regex search for case-insensitive partial matching
searchRegex := bson.M{
"$regex": *form.Search,
"$options": "i", // case-insensitive
}
filter["$or"] = []bson.M{
{"name": searchRegex},
{"description": searchRegex},
}
}
if form.Published != nil {
filter["published"] = *form.Published
}
// Build sort
sort := "created_at"
if pagination.SortBy != "" {
sort = pagination.SortBy
}
sortOrder := "desc"
if pagination.SortOrder != "" {
sortOrder = pagination.SortOrder
}
// Build pagination
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
Skip(pagination.Offset).
SortBy(sort, sortOrder).
Build()
// Use ORM to find categories
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder)
if err != nil {
r.logger.Error("Failed to search categories", map[string]interface{}{
"error": err.Error(),
})
return nil, 0, err
}
return result.Items, result.TotalCount, nil
}
// TogglePublish toggles category publish status
func (r *categoryRepository) TogglePublish(ctx context.Context, id string) (bool, error) {
// Get category first
category, err := r.GetByID(ctx, id)
if err != nil {
return false, err
}
// Update publish status
category.Published = !category.Published
now := time.Now().Unix()
category.SetUpdatedAt(now)
// Set published_at timestamp if publishing
category.PublishedAt = &now
// Update in database
err = r.ormRepo.Update(ctx, category)
if err != nil {
r.logger.Error("Failed to toggle category publish status", map[string]interface{}{
"error": err.Error(),
"category_id": id,
"published": category.Published,
})
return false, err
}
r.logger.Info("Category publish status updated successfully", map[string]interface{}{
"category_id": id,
"published": category.Published,
})
return category.Published, nil
}
+224
View File
@@ -0,0 +1,224 @@
package company_category
import (
"context"
"errors"
"tm/pkg/logger"
"tm/pkg/response"
)
// Service defines business logic for category operations
type Service interface {
// Core category operations
Create(ctx context.Context, form *CategoryForm) (*CategoryResponse, error)
Update(ctx context.Context, id string, form *CategoryForm) (*CategoryResponse, error)
GetByID(ctx context.Context, id string) (*CategoryResponse, error)
GetByIDs(ctx context.Context, ids []string) ([]*CategoryResponse, error)
Delete(ctx context.Context, id string) error
// Category listing and search
SearchCategories(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*CategoryListResponse, error)
// Category management
TogglePublish(ctx context.Context, id string) (bool, error)
}
// categoryService implements the Service interface
type categoryService struct {
repository Repository
logger logger.Logger
}
// NewCategoryService creates a new category service
func NewCategoryService(repository Repository, logger logger.Logger) Service {
return &categoryService{
repository: repository,
logger: logger,
}
}
// Create creates a new category
func (s *categoryService) Create(ctx context.Context, form *CategoryForm) (*CategoryResponse, error) {
// Check if category name already exists
existingCategory, _ := s.repository.GetByName(ctx, form.Name)
if existingCategory != nil {
return nil, errors.New("category with this name already exists")
}
// Create category
category := &Category{
Name: form.Name,
Description: form.Description,
Thumbnail: form.Thumbnail,
Published: false, // Default to unpublished
}
// Save to database
err := s.repository.Create(ctx, category)
if err != nil {
s.logger.Error("Failed to create category", map[string]interface{}{
"error": err.Error(),
"name": form.Name,
})
return nil, err
}
s.logger.Info("Category created successfully", map[string]interface{}{
"category_id": category.ID,
"name": category.Name,
"published": category.Published,
})
return category.ToResponse(), nil
}
// GetByIDs retrieves categories by IDs
func (s *categoryService) GetByIDs(ctx context.Context, ids []string) ([]*CategoryResponse, error) {
categories, err := s.repository.GetByIDs(ctx, ids)
if err != nil {
return nil, err
}
var categoryResponses []*CategoryResponse
for _, category := range categories {
categoryResponses = append(categoryResponses, category.ToResponse())
}
return categoryResponses, nil
}
// GetByID retrieves a category by ID
func (s *categoryService) GetByID(ctx context.Context, id string) (*CategoryResponse, error) {
category, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get category by ID", map[string]interface{}{
"error": err.Error(),
"category_id": id,
})
return nil, err
}
s.logger.Info("Category retrieved successfully", map[string]interface{}{
"category_id": category.ID,
"name": category.Name,
})
return category.ToResponse(), nil
}
// Update updates a category
func (s *categoryService) Update(ctx context.Context, id string, form *CategoryForm) (*CategoryResponse, error) {
// Get existing category
category, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get category for update", map[string]interface{}{
"error": err.Error(),
"category_id": id,
})
return nil, errors.New("category not found")
}
// Check if name already exists (if changing name)
if form.Name != "" && form.Name != category.Name {
existingCategory, _ := s.repository.GetByName(ctx, form.Name)
if existingCategory != nil {
return nil, errors.New("category with this name already exists")
}
category.Name = form.Name
}
// Update fields if provided
if form.Description != nil {
category.Description = form.Description
}
if form.Thumbnail != nil {
category.Thumbnail = form.Thumbnail
}
// Update in database
err = s.repository.Update(ctx, category)
if err != nil {
s.logger.Error("Failed to update category", map[string]interface{}{
"error": err.Error(),
"category_id": id,
})
return nil, errors.New("failed to update category")
}
s.logger.Info("Category updated successfully", map[string]interface{}{
"category_id": category.ID,
"name": category.Name,
})
return category.ToResponse(), nil
}
// Delete deletes a category
func (s *categoryService) Delete(ctx context.Context, id string) error {
// Delete category
err := s.repository.Delete(ctx, id)
if err != nil {
s.logger.Error("Failed to delete category", map[string]interface{}{
"error": err.Error(),
"category_id": id,
})
return errors.New("failed to delete category")
}
s.logger.Info("Category deleted successfully", map[string]interface{}{
"category_id": id,
})
return nil
}
// SearchCategories searches categories with filters
func (s *categoryService) SearchCategories(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*CategoryListResponse, error) {
// Get categories using search
categories, total, err := s.repository.Search(ctx, form, pagination)
if err != nil {
s.logger.Error("Failed to search categories", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to search categories")
}
// Convert to responses
var categoryResponses []*CategoryResponse
for _, category := range categories {
categoryResponses = append(categoryResponses, category.ToResponse())
}
return &CategoryListResponse{
Categories: categoryResponses,
Meta: pagination.Response(total),
}, nil
}
// TogglePublish toggles category publish status
func (s *categoryService) TogglePublish(ctx context.Context, id string) (bool, error) {
// Get category to check if exists
_, err := s.repository.GetByID(ctx, id)
if err != nil {
return false, errors.New("category not found")
}
// Toggle publish status
published, err := s.repository.TogglePublish(ctx, id)
if err != nil {
s.logger.Error("Failed to toggle category publish status", map[string]interface{}{
"error": err.Error(),
"category_id": id,
})
return false, errors.New("failed to toggle category publish status")
}
s.logger.Info("Category publish status updated successfully", map[string]interface{}{
"category_id": id,
"published": published,
})
return published, nil
}