Add Contact Management Functionality
- Introduced a new contact management feature, including the ability to create, retrieve, search, update, and delete contact messages. - Implemented the Contact entity, repository, service, and handler layers following Clean Architecture principles. - Added API endpoints for contact operations in Swagger documentation, ensuring comprehensive API specifications. - Enhanced error handling and validation for contact data, improving robustness and user experience. - Updated the main application to initialize the contact repository and service, integrating them into the existing system.
This commit is contained in:
@@ -1043,6 +1043,337 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/v1/contacts": {
|
||||||
|
"get": {
|
||||||
|
"description": "Search contacts with filters and pagination (admin only)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"contacts"
|
||||||
|
],
|
||||||
|
"summary": "Search contacts",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search query",
|
||||||
|
"name": "q",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"pending",
|
||||||
|
"in_progress",
|
||||||
|
"resolved",
|
||||||
|
"closed"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"description": "Contact status",
|
||||||
|
"name": "status",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Date from (Unix timestamp)",
|
||||||
|
"name": "date_from",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Date to (Unix timestamp)",
|
||||||
|
"name": "date_to",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"maximum": 100,
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "integer",
|
||||||
|
"default": 20,
|
||||||
|
"description": "Number of contacts per page (1-100)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0,
|
||||||
|
"description": "Number of contacts to skip for pagination",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"full_name",
|
||||||
|
"email",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"default": "created_at",
|
||||||
|
"description": "Field to sort by",
|
||||||
|
"name": "sort_by",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"asc",
|
||||||
|
"desc"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"default": "desc",
|
||||||
|
"description": "Sort order",
|
||||||
|
"name": "sort_order",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/contact.ContactListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/v1/contacts/stats": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get contact statistics by status (admin only)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"contacts"
|
||||||
|
],
|
||||||
|
"summary": "Get contact statistics",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/contact.ContactStats"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/v1/contacts/{id}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Retrieve a contact message by its ID (admin only)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"contacts"
|
||||||
|
],
|
||||||
|
"summary": "Get contact by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Contact ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/contact.ContactResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"description": "Delete a contact message by ID (admin only)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"contacts"
|
||||||
|
],
|
||||||
|
"summary": "Delete contact",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Contact ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/v1/contacts/{id}/status": {
|
||||||
|
"put": {
|
||||||
|
"description": "Update the status of a contact message (admin only)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"contacts"
|
||||||
|
],
|
||||||
|
"summary": "Update contact status",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Contact ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Status update information",
|
||||||
|
"name": "status",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/contact.UpdateContactStatusForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/v1/customers": {
|
"/admin/v1/customers": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -4699,6 +5030,64 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/contacts": {
|
||||||
|
"post": {
|
||||||
|
"description": "Create a new contact message from a user",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"contacts"
|
||||||
|
],
|
||||||
|
"summary": "Create a new contact message",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Contact information",
|
||||||
|
"name": "contact",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/contact.CreateContactForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/contact.ContactResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/feedback": {
|
"/api/v1/feedback": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -7369,6 +7758,109 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"contact.ContactListResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contacts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/contact.ContactResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact.ContactResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"admin_notes": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"full_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"resolved_at": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"resolved_by": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact.ContactStats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"closed": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"in_progress": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"pending": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"resolved": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact.CreateContactForm": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "john.doe@example.com"
|
||||||
|
},
|
||||||
|
"full_name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "John Doe"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "I have a question about your services"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "+1234567890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact.UpdateContactStatusForm": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"admin_notes": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Customer inquiry handled"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "in_progress"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"customer.AssignCompaniesForm": {
|
"customer.AssignCompaniesForm": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1035,6 +1035,337 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/v1/contacts": {
|
||||||
|
"get": {
|
||||||
|
"description": "Search contacts with filters and pagination (admin only)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"contacts"
|
||||||
|
],
|
||||||
|
"summary": "Search contacts",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search query",
|
||||||
|
"name": "q",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"pending",
|
||||||
|
"in_progress",
|
||||||
|
"resolved",
|
||||||
|
"closed"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"description": "Contact status",
|
||||||
|
"name": "status",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Date from (Unix timestamp)",
|
||||||
|
"name": "date_from",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Date to (Unix timestamp)",
|
||||||
|
"name": "date_to",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"maximum": 100,
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "integer",
|
||||||
|
"default": 20,
|
||||||
|
"description": "Number of contacts per page (1-100)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0,
|
||||||
|
"description": "Number of contacts to skip for pagination",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"full_name",
|
||||||
|
"email",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"default": "created_at",
|
||||||
|
"description": "Field to sort by",
|
||||||
|
"name": "sort_by",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"asc",
|
||||||
|
"desc"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"default": "desc",
|
||||||
|
"description": "Sort order",
|
||||||
|
"name": "sort_order",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/contact.ContactListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/v1/contacts/stats": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get contact statistics by status (admin only)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"contacts"
|
||||||
|
],
|
||||||
|
"summary": "Get contact statistics",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/contact.ContactStats"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/v1/contacts/{id}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Retrieve a contact message by its ID (admin only)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"contacts"
|
||||||
|
],
|
||||||
|
"summary": "Get contact by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Contact ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/contact.ContactResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"description": "Delete a contact message by ID (admin only)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"contacts"
|
||||||
|
],
|
||||||
|
"summary": "Delete contact",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Contact ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/v1/contacts/{id}/status": {
|
||||||
|
"put": {
|
||||||
|
"description": "Update the status of a contact message (admin only)",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"contacts"
|
||||||
|
],
|
||||||
|
"summary": "Update contact status",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Contact ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Status update information",
|
||||||
|
"name": "status",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/contact.UpdateContactStatusForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/v1/customers": {
|
"/admin/v1/customers": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -4691,6 +5022,64 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/contacts": {
|
||||||
|
"post": {
|
||||||
|
"description": "Create a new contact message from a user",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"contacts"
|
||||||
|
],
|
||||||
|
"summary": "Create a new contact message",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Contact information",
|
||||||
|
"name": "contact",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/contact.CreateContactForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/contact.ContactResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/feedback": {
|
"/api/v1/feedback": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -7361,6 +7750,109 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"contact.ContactListResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contacts": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/contact.ContactResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact.ContactResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"admin_notes": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"full_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"resolved_at": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"resolved_by": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact.ContactStats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"closed": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"in_progress": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"pending": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"resolved": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact.CreateContactForm": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "john.doe@example.com"
|
||||||
|
},
|
||||||
|
"full_name": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "John Doe"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "I have a question about your services"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "+1234567890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact.UpdateContactStatusForm": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"admin_notes": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Customer inquiry handled"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "in_progress"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"customer.AssignCompaniesForm": {
|
"customer.AssignCompaniesForm": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -302,6 +302,75 @@ definitions:
|
|||||||
updated_at:
|
updated_at:
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
|
contact.ContactListResponse:
|
||||||
|
properties:
|
||||||
|
contacts:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/contact.ContactResponse'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
contact.ContactResponse:
|
||||||
|
properties:
|
||||||
|
admin_notes:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: integer
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
full_name:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
phone:
|
||||||
|
type: string
|
||||||
|
resolved_at:
|
||||||
|
type: integer
|
||||||
|
resolved_by:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
updated_at:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
contact.ContactStats:
|
||||||
|
properties:
|
||||||
|
closed:
|
||||||
|
type: integer
|
||||||
|
in_progress:
|
||||||
|
type: integer
|
||||||
|
pending:
|
||||||
|
type: integer
|
||||||
|
resolved:
|
||||||
|
type: integer
|
||||||
|
total:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
contact.CreateContactForm:
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
example: john.doe@example.com
|
||||||
|
type: string
|
||||||
|
full_name:
|
||||||
|
example: John Doe
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
example: I have a question about your services
|
||||||
|
type: string
|
||||||
|
phone:
|
||||||
|
example: "+1234567890"
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
contact.UpdateContactStatusForm:
|
||||||
|
properties:
|
||||||
|
admin_notes:
|
||||||
|
example: Customer inquiry handled
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
example: in_progress
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
customer.AssignCompaniesForm:
|
customer.AssignCompaniesForm:
|
||||||
properties:
|
properties:
|
||||||
companies:
|
companies:
|
||||||
@@ -1999,6 +2068,221 @@ paths:
|
|||||||
summary: Toggle category publish status
|
summary: Toggle category publish status
|
||||||
tags:
|
tags:
|
||||||
- Admin-Company-Categories
|
- Admin-Company-Categories
|
||||||
|
/admin/v1/contacts:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Search contacts with filters and pagination (admin only)
|
||||||
|
parameters:
|
||||||
|
- description: Search query
|
||||||
|
in: query
|
||||||
|
name: q
|
||||||
|
type: string
|
||||||
|
- description: Contact status
|
||||||
|
enum:
|
||||||
|
- pending
|
||||||
|
- in_progress
|
||||||
|
- resolved
|
||||||
|
- closed
|
||||||
|
in: query
|
||||||
|
name: status
|
||||||
|
type: string
|
||||||
|
- description: Date from (Unix timestamp)
|
||||||
|
in: query
|
||||||
|
name: date_from
|
||||||
|
type: integer
|
||||||
|
- description: Date to (Unix timestamp)
|
||||||
|
in: query
|
||||||
|
name: date_to
|
||||||
|
type: integer
|
||||||
|
- default: 20
|
||||||
|
description: Number of contacts per page (1-100)
|
||||||
|
in: query
|
||||||
|
maximum: 100
|
||||||
|
minimum: 1
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
- default: 0
|
||||||
|
description: Number of contacts to skip for pagination
|
||||||
|
in: query
|
||||||
|
minimum: 0
|
||||||
|
name: offset
|
||||||
|
type: integer
|
||||||
|
- default: created_at
|
||||||
|
description: Field to sort by
|
||||||
|
enum:
|
||||||
|
- full_name
|
||||||
|
- email
|
||||||
|
- created_at
|
||||||
|
- updated_at
|
||||||
|
- status
|
||||||
|
in: query
|
||||||
|
name: sort_by
|
||||||
|
type: string
|
||||||
|
- default: desc
|
||||||
|
description: Sort order
|
||||||
|
enum:
|
||||||
|
- asc
|
||||||
|
- desc
|
||||||
|
in: query
|
||||||
|
name: sort_order
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/contact.ContactListResponse'
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
summary: Search contacts
|
||||||
|
tags:
|
||||||
|
- contacts
|
||||||
|
/admin/v1/contacts/{id}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Delete a contact message by ID (admin only)
|
||||||
|
parameters:
|
||||||
|
- description: Contact ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
summary: Delete contact
|
||||||
|
tags:
|
||||||
|
- contacts
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Retrieve a contact message by its ID (admin only)
|
||||||
|
parameters:
|
||||||
|
- description: Contact ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/contact.ContactResponse'
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
summary: Get contact by ID
|
||||||
|
tags:
|
||||||
|
- contacts
|
||||||
|
/admin/v1/contacts/{id}/status:
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Update the status of a contact message (admin only)
|
||||||
|
parameters:
|
||||||
|
- description: Contact ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Status update information
|
||||||
|
in: body
|
||||||
|
name: status
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/contact.UpdateContactStatusForm'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
summary: Update contact status
|
||||||
|
tags:
|
||||||
|
- contacts
|
||||||
|
/admin/v1/contacts/stats:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Get contact statistics by status (admin only)
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/contact.ContactStats'
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
summary: Get contact statistics
|
||||||
|
tags:
|
||||||
|
- contacts
|
||||||
/admin/v1/customers:
|
/admin/v1/customers:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -4314,6 +4598,41 @@ paths:
|
|||||||
summary: Get my company profile
|
summary: Get my company profile
|
||||||
tags:
|
tags:
|
||||||
- Company
|
- Company
|
||||||
|
/api/v1/contacts:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Create a new contact message from a user
|
||||||
|
parameters:
|
||||||
|
- description: Contact information
|
||||||
|
in: body
|
||||||
|
name: contact
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/contact.CreateContactForm'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Created
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/contact.ContactResponse'
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
summary: Create a new contact message
|
||||||
|
tags:
|
||||||
|
- contacts
|
||||||
/api/v1/feedback:
|
/api/v1/feedback:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
+10
-6
@@ -85,6 +85,7 @@ import (
|
|||||||
"tm/internal/assets"
|
"tm/internal/assets"
|
||||||
"tm/internal/company"
|
"tm/internal/company"
|
||||||
"tm/internal/company_category"
|
"tm/internal/company_category"
|
||||||
|
"tm/internal/contact"
|
||||||
"tm/internal/customer"
|
"tm/internal/customer"
|
||||||
"tm/internal/feedback"
|
"tm/internal/feedback"
|
||||||
"tm/internal/inquiry"
|
"tm/internal/inquiry"
|
||||||
@@ -142,8 +143,9 @@ func main() {
|
|||||||
feedbackRepo := feedback.NewRepository(mongoManager, logger)
|
feedbackRepo := feedback.NewRepository(mongoManager, logger)
|
||||||
tenderApprovalRepository := tender_approval.NewRepository(mongoManager, logger)
|
tenderApprovalRepository := tender_approval.NewRepository(mongoManager, logger)
|
||||||
inquiryRepository := inquiry.NewRepository(mongoManager, logger)
|
inquiryRepository := inquiry.NewRepository(mongoManager, logger)
|
||||||
|
contactRepository := contact.NewContactRepository(mongoManager, logger)
|
||||||
logger.Info("Repositories initialized successfully", map[string]interface{}{
|
logger.Info("Repositories initialized successfully", map[string]interface{}{
|
||||||
"repositories": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry"},
|
"repositories": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "contact"},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Initialize validation services
|
// Initialize validation services
|
||||||
@@ -161,8 +163,9 @@ func main() {
|
|||||||
inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger)
|
inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger)
|
||||||
flagService := assets.NewService(logger, conf.Assets.FlagsPath)
|
flagService := assets.NewService(logger, conf.Assets.FlagsPath)
|
||||||
notificationService := notification.NewService(notificationSDK, userService, customerService, logger)
|
notificationService := notification.NewService(notificationSDK, userService, customerService, logger)
|
||||||
|
contactService := contact.NewService(contactRepository, logger)
|
||||||
logger.Info("Services initialized successfully", map[string]interface{}{
|
logger.Info("Services initialized successfully", map[string]interface{}{
|
||||||
"services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification"},
|
"services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact"},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Initialize handlers with services
|
// Initialize handlers with services
|
||||||
@@ -175,17 +178,18 @@ func main() {
|
|||||||
tenderApprovalHandler := tender_approval.NewHandler(tenderApprovalService, logger)
|
tenderApprovalHandler := tender_approval.NewHandler(tenderApprovalService, logger)
|
||||||
inquiryHandler := inquiry.NewHandler(inquiryService, logger)
|
inquiryHandler := inquiry.NewHandler(inquiryService, logger)
|
||||||
flagHandler := assets.NewHandler(flagService, logger)
|
flagHandler := assets.NewHandler(flagService, logger)
|
||||||
notificationHandler := notification.NewHandler(notificationService, logger)
|
notificationHandler := notification.NewHandler(notificationService)
|
||||||
|
contactHandler := contact.NewHandler(contactService)
|
||||||
logger.Info("Handlers initialized successfully", map[string]interface{}{
|
logger.Info("Handlers initialized successfully", map[string]interface{}{
|
||||||
"handlers": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification"},
|
"handlers": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification", "contact"},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Initialize HTTP server
|
// Initialize HTTP server
|
||||||
e := bootstrap.InitHTTPServer(conf, logger)
|
e := bootstrap.InitHTTPServer(conf, logger)
|
||||||
|
|
||||||
// Register routes
|
// Register routes
|
||||||
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler)
|
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler)
|
||||||
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler)
|
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler)
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port)
|
serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port)
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package contact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tm/pkg/mongo"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContactStatus represents the status of a contact message
|
||||||
|
type ContactStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContactStatusPending ContactStatus = "pending"
|
||||||
|
ContactStatusInProgress ContactStatus = "in_progress"
|
||||||
|
ContactStatusResolved ContactStatus = "resolved"
|
||||||
|
ContactStatusClosed ContactStatus = "closed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Contact represents a contact us message in the system
|
||||||
|
type Contact struct {
|
||||||
|
mongo.Model `bson:",inline"`
|
||||||
|
FullName string `bson:"full_name" json:"full_name" validate:"required"`
|
||||||
|
Email string `bson:"email" json:"email" validate:"required,email"`
|
||||||
|
Phone string `bson:"phone" json:"phone" validate:"required"`
|
||||||
|
Message string `bson:"message" json:"message" validate:"required"`
|
||||||
|
Status ContactStatus `bson:"status" json:"status" validate:"required,oneof=pending in_progress resolved closed"`
|
||||||
|
AdminNotes *string `bson:"admin_notes,omitempty" json:"admin_notes,omitempty"`
|
||||||
|
ResolvedAt *int64 `bson:"resolved_at,omitempty" json:"resolved_at,omitempty"`
|
||||||
|
ResolvedBy *string `bson:"resolved_by,omitempty" json:"resolved_by,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets the contact ID (implements IDSetter interface)
|
||||||
|
func (c *Contact) SetID(id string) {
|
||||||
|
c.ID, _ = bson.ObjectIDFromHex(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the contact ID (implements IDGetter interface)
|
||||||
|
func (c *Contact) GetID() string {
|
||||||
|
return c.ID.Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCreatedAt sets the created timestamp (implements Timestamp interface)
|
||||||
|
func (c *Contact) SetCreatedAt(timestamp int64) {
|
||||||
|
c.CreatedAt = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUpdatedAt sets the updated timestamp (implements Timestamp interface)
|
||||||
|
func (c *Contact) SetUpdatedAt(timestamp int64) {
|
||||||
|
c.UpdatedAt = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCreatedAt returns the created timestamp (implements Timestamp interface)
|
||||||
|
func (c *Contact) GetCreatedAt() int64 {
|
||||||
|
return c.CreatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUpdatedAt returns the updated timestamp (implements Timestamp interface)
|
||||||
|
func (c *Contact) GetUpdatedAt() int64 {
|
||||||
|
return c.UpdatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPending returns true if the contact is pending
|
||||||
|
func (c *Contact) IsPending() bool {
|
||||||
|
return c.Status == ContactStatusPending
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsResolved returns true if the contact is resolved
|
||||||
|
func (c *Contact) IsResolved() bool {
|
||||||
|
return c.Status == ContactStatusResolved || c.Status == ContactStatusClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanBeUpdated returns true if the contact can be updated
|
||||||
|
func (c *Contact) CanBeUpdated() bool {
|
||||||
|
return c.Status != ContactStatusClosed
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package contact
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Custom errors for contact domain
|
||||||
|
var (
|
||||||
|
ErrContactNotFound = errors.New("contact not found")
|
||||||
|
ErrContactAlreadyResolved = errors.New("contact is already resolved")
|
||||||
|
ErrContactClosed = errors.New("contact is closed and cannot be updated")
|
||||||
|
ErrInvalidStatus = errors.New("invalid contact status")
|
||||||
|
ErrInvalidDateRange = errors.New("invalid date range: date_from must be before date_to")
|
||||||
|
ErrUnauthorizedAccess = errors.New("unauthorized access to contact")
|
||||||
|
ErrInvalidContactData = errors.New("invalid contact data")
|
||||||
|
ErrEmailRequired = errors.New("email is required")
|
||||||
|
ErrPhoneRequired = errors.New("phone is required")
|
||||||
|
ErrMessageRequired = errors.New("message is required")
|
||||||
|
ErrFullNameRequired = errors.New("full name is required")
|
||||||
|
)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package contact
|
||||||
|
|
||||||
|
import "tm/pkg/response"
|
||||||
|
|
||||||
|
// CreateContactForm represents the form for creating a new contact message
|
||||||
|
type CreateContactForm struct {
|
||||||
|
FullName string `json:"full_name" valid:"required,length(2|100)" example:"John Doe"`
|
||||||
|
Email string `json:"email" valid:"required,email" example:"john.doe@example.com"`
|
||||||
|
Phone string `json:"phone" valid:"required,length(10|20)" example:"+1234567890"`
|
||||||
|
Message string `json:"message" valid:"required,length(10|1000)" example:"I have a question about your services"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateContactStatusForm represents the form for updating contact status (admin only)
|
||||||
|
type UpdateContactStatusForm struct {
|
||||||
|
Status string `json:"status" valid:"required,in(pending|in_progress|resolved|closed)" example:"in_progress"`
|
||||||
|
AdminNotes *string `json:"admin_notes,omitempty" valid:"optional,length(0|500)" example:"Customer inquiry handled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchContactsForm represents the form for searching contacts with filters
|
||||||
|
type SearchContactsForm struct {
|
||||||
|
Search *string `query:"q" valid:"optional"`
|
||||||
|
Status *string `query:"status" valid:"optional,in(pending|in_progress|resolved|closed)"`
|
||||||
|
DateFrom *int64 `query:"date_from" valid:"optional"`
|
||||||
|
DateTo *int64 `query:"date_to" valid:"optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContactResponse represents the contact data sent in API responses
|
||||||
|
type ContactResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
AdminNotes *string `json:"admin_notes,omitempty"`
|
||||||
|
ResolvedAt *int64 `json:"resolved_at,omitempty"`
|
||||||
|
ResolvedBy *string `json:"resolved_by,omitempty"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicContactResponse represents the contact data sent in public API responses (limited fields)
|
||||||
|
type PublicContactResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToResponse converts Contact to ContactResponse
|
||||||
|
func (c *Contact) ToResponse() *ContactResponse {
|
||||||
|
return &ContactResponse{
|
||||||
|
ID: c.ID.Hex(),
|
||||||
|
FullName: c.FullName,
|
||||||
|
Email: c.Email,
|
||||||
|
Phone: c.Phone,
|
||||||
|
Message: c.Message,
|
||||||
|
Status: string(c.Status),
|
||||||
|
AdminNotes: c.AdminNotes,
|
||||||
|
ResolvedAt: c.ResolvedAt,
|
||||||
|
ResolvedBy: c.ResolvedBy,
|
||||||
|
CreatedAt: c.CreatedAt,
|
||||||
|
UpdatedAt: c.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToPublicResponse converts Contact to PublicContactResponse (limited fields for public API)
|
||||||
|
func (c *Contact) ToPublicResponse() *PublicContactResponse {
|
||||||
|
return &PublicContactResponse{
|
||||||
|
ID: c.ID.Hex(),
|
||||||
|
Status: string(c.Status),
|
||||||
|
CreatedAt: c.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContactListResponse represents the response for listing contacts
|
||||||
|
type ContactListResponse struct {
|
||||||
|
Contacts []*ContactResponse `json:"contacts"`
|
||||||
|
Meta *response.Meta `json:"-"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package contact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tm/pkg/response"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler handles HTTP requests for contact operations
|
||||||
|
type Handler struct {
|
||||||
|
service Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler creates a new contact handler
|
||||||
|
func NewHandler(service Service) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
service: service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create handles creating a new contact message
|
||||||
|
// @Summary Create a new contact message
|
||||||
|
// @Description Create a new contact message from a user
|
||||||
|
// @Tags contacts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param contact body CreateContactForm true "Contact information"
|
||||||
|
// @Success 201 {object} response.APIResponse{data=ContactResponse}
|
||||||
|
// @Failure 400 {object} response.APIResponse
|
||||||
|
// @Failure 500 {object} response.APIResponse
|
||||||
|
// @Router /api/v1/contacts [post]
|
||||||
|
func (h *Handler) Create(c echo.Context) error {
|
||||||
|
form, err := response.Parse[CreateContactForm](c)
|
||||||
|
if err != nil {
|
||||||
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create contact
|
||||||
|
contact, err := h.service.Create(c.Request().Context(), form)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to create contact")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return public response (limited fields)
|
||||||
|
return response.Created(c, &PublicContactResponse{
|
||||||
|
ID: contact.ID,
|
||||||
|
Status: contact.Status,
|
||||||
|
CreatedAt: contact.CreatedAt,
|
||||||
|
}, "Contact message created successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID handles retrieving a contact by ID (admin only)
|
||||||
|
// @Summary Get contact by ID
|
||||||
|
// @Description Retrieve a contact message by its ID (admin only)
|
||||||
|
// @Tags contacts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Contact ID"
|
||||||
|
// @Success 200 {object} response.APIResponse{data=ContactResponse}
|
||||||
|
// @Failure 400 {object} response.APIResponse
|
||||||
|
// @Failure 404 {object} response.APIResponse
|
||||||
|
// @Failure 500 {object} response.APIResponse
|
||||||
|
// @Router /admin/v1/contacts/{id} [get]
|
||||||
|
func (h *Handler) GetByID(c echo.Context) error {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
contact, err := h.service.GetByID(c.Request().Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == "contact not found" {
|
||||||
|
return response.NotFound(c, "Contact not found")
|
||||||
|
}
|
||||||
|
return response.InternalServerError(c, "Failed to get contact")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, contact, "Contact retrieved successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search handles searching contacts with filters (admin only)
|
||||||
|
// @Summary Search contacts
|
||||||
|
// @Description Search contacts with filters and pagination (admin only)
|
||||||
|
// @Tags contacts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param q query string false "Search query"
|
||||||
|
// @Param status query string false "Contact status" Enums(pending, in_progress, resolved, closed)
|
||||||
|
// @Param date_from query int false "Date from (Unix timestamp)"
|
||||||
|
// @Param date_to query int false "Date to (Unix timestamp)"
|
||||||
|
// @Param limit query integer false "Number of contacts per page (1-100)" minimum(1) maximum(100) default(20)
|
||||||
|
// @Param offset query integer false "Number of contacts to skip for pagination" minimum(0) default(0)
|
||||||
|
// @Param sort_by query string false "Field to sort by" Enums(full_name, email, created_at, updated_at, status) default(created_at)
|
||||||
|
// @Param sort_order query string false "Sort order" Enums(asc, desc) default(desc)
|
||||||
|
// @Success 200 {object} response.APIResponse{data=ContactListResponse}
|
||||||
|
// @Failure 400 {object} response.APIResponse
|
||||||
|
// @Failure 500 {object} response.APIResponse
|
||||||
|
// @Router /admin/v1/contacts [get]
|
||||||
|
func (h *Handler) Search(c echo.Context) error {
|
||||||
|
form, err := response.Parse[SearchContactsForm](c)
|
||||||
|
if err != nil {
|
||||||
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
pagination := response.NewPagination(c)
|
||||||
|
|
||||||
|
result, err := h.service.Search(c.Request().Context(), form, pagination)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to search contacts")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.SuccessWithMeta(c, result, result.Meta, "Contacts retrieved successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus handles updating contact status (admin only)
|
||||||
|
// @Summary Update contact status
|
||||||
|
// @Description Update the status of a contact message (admin only)
|
||||||
|
// @Tags contacts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Contact ID"
|
||||||
|
// @Param status body UpdateContactStatusForm true "Status update information"
|
||||||
|
// @Success 200 {object} response.APIResponse
|
||||||
|
// @Failure 400 {object} response.APIResponse
|
||||||
|
// @Failure 404 {object} response.APIResponse
|
||||||
|
// @Failure 500 {object} response.APIResponse
|
||||||
|
// @Router /admin/v1/contacts/{id}/status [put]
|
||||||
|
func (h *Handler) UpdateStatus(c echo.Context) error {
|
||||||
|
id := c.Param("id")
|
||||||
|
form, err := response.Parse[UpdateContactStatusForm](c)
|
||||||
|
if err != nil {
|
||||||
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get admin ID from context (assuming it's set by auth middleware)
|
||||||
|
adminID := c.Get("user_id")
|
||||||
|
if adminID == nil {
|
||||||
|
return response.Unauthorized(c, "Admin authentication required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update contact status
|
||||||
|
err = h.service.UpdateStatus(c.Request().Context(), id, form, adminID.(string))
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == "contact not found" {
|
||||||
|
return response.NotFound(c, "Contact not found")
|
||||||
|
}
|
||||||
|
return response.InternalServerError(c, "Failed to update contact status")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, map[string]interface{}{
|
||||||
|
"message": "Contact status updated successfully",
|
||||||
|
}, "Contact status updated successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats handles retrieving contact statistics (admin only)
|
||||||
|
// @Summary Get contact statistics
|
||||||
|
// @Description Get contact statistics by status (admin only)
|
||||||
|
// @Tags contacts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} response.APIResponse{data=ContactStats}
|
||||||
|
// @Failure 500 {object} response.APIResponse
|
||||||
|
// @Router /admin/v1/contacts/stats [get]
|
||||||
|
func (h *Handler) GetStats(c echo.Context) error {
|
||||||
|
stats, err := h.service.GetStats(c.Request().Context())
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to get contact statistics")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, stats, "Contact statistics retrieved successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete handles deleting a contact (admin only)
|
||||||
|
// @Summary Delete contact
|
||||||
|
// @Description Delete a contact message by ID (admin only)
|
||||||
|
// @Tags contacts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Contact ID"
|
||||||
|
// @Success 200 {object} response.APIResponse
|
||||||
|
// @Failure 400 {object} response.APIResponse
|
||||||
|
// @Failure 404 {object} response.APIResponse
|
||||||
|
// @Failure 500 {object} response.APIResponse
|
||||||
|
// @Router /admin/v1/contacts/{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() == "contact not found" {
|
||||||
|
return response.NotFound(c, "Contact not found")
|
||||||
|
}
|
||||||
|
return response.InternalServerError(c, "Failed to delete contact")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, map[string]interface{}{
|
||||||
|
"message": "Contact deleted successfully",
|
||||||
|
}, "Contact deleted successfully")
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package contact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"tm/pkg/logger"
|
||||||
|
orm "tm/pkg/mongo"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContactRepository defines the interface for contact data operations
|
||||||
|
type ContactRepository interface {
|
||||||
|
// Create creates a new contact message
|
||||||
|
Create(ctx context.Context, contact *Contact) error
|
||||||
|
|
||||||
|
// GetByID retrieves a contact by ID
|
||||||
|
GetByID(ctx context.Context, id string) (*Contact, error)
|
||||||
|
|
||||||
|
// Search searches contacts with filters and pagination
|
||||||
|
Search(ctx context.Context, filters SearchContactsForm, pagination orm.Pagination) (*orm.PaginatedResult[Contact], error)
|
||||||
|
|
||||||
|
// UpdateStatus updates the status of a contact
|
||||||
|
UpdateStatus(ctx context.Context, id string, status ContactStatus, adminNotes *string, resolvedBy *string) error
|
||||||
|
|
||||||
|
// GetStats returns contact statistics
|
||||||
|
GetStats(ctx context.Context) (*ContactStats, error)
|
||||||
|
|
||||||
|
// Delete deletes a contact by ID
|
||||||
|
Delete(ctx context.Context, id string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContactStats represents contact statistics
|
||||||
|
type ContactStats struct {
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Pending int64 `json:"pending"`
|
||||||
|
InProgress int64 `json:"in_progress"`
|
||||||
|
Resolved int64 `json:"resolved"`
|
||||||
|
Closed int64 `json:"closed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// contactRepository implements ContactRepository interface
|
||||||
|
type contactRepository struct {
|
||||||
|
repo orm.Repository[Contact]
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContactRepository creates a new contact repository
|
||||||
|
func NewContactRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) ContactRepository {
|
||||||
|
// Create indexes for contact collection
|
||||||
|
contactIndexes := []orm.Index{
|
||||||
|
*orm.NewIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
|
||||||
|
*orm.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
|
||||||
|
*orm.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||||
|
*orm.CreateTextIndex("contact_text_idx", "full_name", "email", "message"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
if err := mongoManager.CreateIndexes("contacts", contactIndexes); err != nil {
|
||||||
|
logger.Error("Failed to create contact indexes", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
collection := mongoManager.GetCollection("contacts")
|
||||||
|
return &contactRepository{
|
||||||
|
repo: orm.NewRepository[Contact](collection, logger),
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a new contact message
|
||||||
|
func (r *contactRepository) Create(ctx context.Context, contact *Contact) error {
|
||||||
|
// Set default status if not provided
|
||||||
|
if contact.Status == "" {
|
||||||
|
contact.Status = ContactStatusPending
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.repo.Create(ctx, contact)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID retrieves a contact by ID
|
||||||
|
func (r *contactRepository) GetByID(ctx context.Context, id string) (*Contact, error) {
|
||||||
|
return r.repo.FindByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search searches contacts with filters and pagination
|
||||||
|
func (r *contactRepository) Search(ctx context.Context, filters SearchContactsForm, pagination orm.Pagination) (*orm.PaginatedResult[Contact], error) {
|
||||||
|
filter := bson.M{}
|
||||||
|
|
||||||
|
// Apply text search if provided
|
||||||
|
if filters.Search != nil && *filters.Search != "" {
|
||||||
|
filter["$text"] = bson.M{"$search": *filters.Search}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply status filter
|
||||||
|
if filters.Status != nil && *filters.Status != "" {
|
||||||
|
filter["status"] = *filters.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply date range filters
|
||||||
|
if filters.DateFrom != nil || filters.DateTo != nil {
|
||||||
|
dateFilter := bson.M{}
|
||||||
|
if filters.DateFrom != nil {
|
||||||
|
dateFilter["$gte"] = *filters.DateFrom
|
||||||
|
}
|
||||||
|
if filters.DateTo != nil {
|
||||||
|
dateFilter["$lte"] = *filters.DateTo
|
||||||
|
}
|
||||||
|
filter["created_at"] = dateFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.repo.FindAll(ctx, filter, pagination)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus updates the status of a contact
|
||||||
|
func (r *contactRepository) UpdateStatus(ctx context.Context, id string, status ContactStatus, adminNotes *string, resolvedBy *string) error {
|
||||||
|
// Get the existing contact
|
||||||
|
contact, err := r.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the contact fields
|
||||||
|
contact.Status = status
|
||||||
|
contact.UpdatedAt = time.Now().Unix()
|
||||||
|
|
||||||
|
if adminNotes != nil {
|
||||||
|
contact.AdminNotes = adminNotes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set resolved fields if status is resolved or closed
|
||||||
|
if status == ContactStatusResolved || status == ContactStatusClosed {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
contact.ResolvedAt = &now
|
||||||
|
if resolvedBy != nil {
|
||||||
|
contact.ResolvedBy = resolvedBy
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unset resolved fields if status is changed back to pending or in_progress
|
||||||
|
contact.ResolvedAt = nil
|
||||||
|
contact.ResolvedBy = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.repo.Update(ctx, contact)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to update contact status", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"status": status,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("failed to update contact status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Contact status updated successfully", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"status": status,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats returns contact statistics
|
||||||
|
func (r *contactRepository) GetStats(ctx context.Context) (*ContactStats, error) {
|
||||||
|
pipeline := mongo.Pipeline{
|
||||||
|
{
|
||||||
|
{Key: "$group", Value: bson.D{
|
||||||
|
{Key: "_id", Value: "$status"},
|
||||||
|
{Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := r.repo.Aggregate(ctx, pipeline)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get contact stats", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to get contact stats: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := &ContactStats{}
|
||||||
|
for _, result := range results {
|
||||||
|
status, ok := result["_id"].(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
count, ok := result["count"].(int32)
|
||||||
|
if !ok {
|
||||||
|
if count64, ok := result["count"].(int64); ok {
|
||||||
|
count = int32(count64)
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ContactStatus(status) {
|
||||||
|
case ContactStatusPending:
|
||||||
|
stats.Pending = int64(count)
|
||||||
|
case ContactStatusInProgress:
|
||||||
|
stats.InProgress = int64(count)
|
||||||
|
case ContactStatusResolved:
|
||||||
|
stats.Resolved = int64(count)
|
||||||
|
case ContactStatusClosed:
|
||||||
|
stats.Closed = int64(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.Total += int64(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a contact by ID
|
||||||
|
func (r *contactRepository) Delete(ctx context.Context, id string) error {
|
||||||
|
return r.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
package contact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"tm/pkg/logger"
|
||||||
|
orm "tm/pkg/mongo"
|
||||||
|
"tm/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service defines the interface for contact business operations
|
||||||
|
type Service interface {
|
||||||
|
// Create creates a new contact message
|
||||||
|
Create(ctx context.Context, form *CreateContactForm) (*ContactResponse, error)
|
||||||
|
|
||||||
|
// GetByID retrieves a contact by ID
|
||||||
|
GetByID(ctx context.Context, id string) (*ContactResponse, error)
|
||||||
|
|
||||||
|
// Search searches contacts with filters and pagination
|
||||||
|
Search(ctx context.Context, form *SearchContactsForm, pagination *response.Pagination) (*ContactListResponse, error)
|
||||||
|
|
||||||
|
// UpdateStatus updates the status of a contact (admin only)
|
||||||
|
UpdateStatus(ctx context.Context, id string, form *UpdateContactStatusForm, adminID string) error
|
||||||
|
|
||||||
|
// GetStats returns contact statistics (admin only)
|
||||||
|
GetStats(ctx context.Context) (*ContactStats, error)
|
||||||
|
|
||||||
|
// Delete deletes a contact by ID (admin only)
|
||||||
|
Delete(ctx context.Context, id string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// contactService implements Service interface
|
||||||
|
type contactService struct {
|
||||||
|
repo ContactRepository
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new contact service
|
||||||
|
func NewService(repo ContactRepository, logger logger.Logger) Service {
|
||||||
|
return &contactService{
|
||||||
|
repo: repo,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a new contact message
|
||||||
|
func (s *contactService) Create(ctx context.Context, form *CreateContactForm) (*ContactResponse, error) {
|
||||||
|
s.logger.Info("Creating new contact message", map[string]interface{}{
|
||||||
|
"email": form.Email,
|
||||||
|
"full_name": form.FullName,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create contact entity
|
||||||
|
contact := &Contact{
|
||||||
|
FullName: form.FullName,
|
||||||
|
Email: form.Email,
|
||||||
|
Phone: form.Phone,
|
||||||
|
Message: form.Message,
|
||||||
|
Status: ContactStatusPending,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to database
|
||||||
|
if err := s.repo.Create(ctx, contact); err != nil {
|
||||||
|
s.logger.Error("Failed to create contact", map[string]interface{}{
|
||||||
|
"email": form.Email,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to create contact: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Contact message created successfully", map[string]interface{}{
|
||||||
|
"id": contact.GetID(),
|
||||||
|
"email": form.Email,
|
||||||
|
})
|
||||||
|
|
||||||
|
return contact.ToResponse(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID retrieves a contact by ID
|
||||||
|
func (s *contactService) GetByID(ctx context.Context, id string) (*ContactResponse, error) {
|
||||||
|
s.logger.Debug("Retrieving contact by ID", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
})
|
||||||
|
|
||||||
|
contact, err := s.repo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to get contact by ID", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to get contact: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return contact.ToResponse(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search searches contacts with filters and pagination
|
||||||
|
func (s *contactService) Search(ctx context.Context, form *SearchContactsForm, pagination *response.Pagination) (*ContactListResponse, error) {
|
||||||
|
s.logger.Debug("Searching contacts", map[string]interface{}{
|
||||||
|
"filters": form,
|
||||||
|
"pagination": pagination,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validate date range if provided
|
||||||
|
if form.DateFrom != nil && form.DateTo != nil && *form.DateFrom > *form.DateTo {
|
||||||
|
return nil, fmt.Errorf("invalid date range: date_from must be before date_to")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert response.Pagination to orm.Pagination
|
||||||
|
ormPagination := orm.Pagination{
|
||||||
|
Limit: pagination.Limit,
|
||||||
|
Skip: pagination.Offset,
|
||||||
|
SortField: pagination.SortBy,
|
||||||
|
SortOrder: 1, // Default ascending
|
||||||
|
}
|
||||||
|
if pagination.SortOrder == "desc" {
|
||||||
|
ormPagination.SortOrder = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.repo.Search(ctx, *form, ormPagination)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to search contacts", map[string]interface{}{
|
||||||
|
"filters": form,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to search contacts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to response format
|
||||||
|
contactResponses := make([]*ContactResponse, len(result.Items))
|
||||||
|
for i, contact := range result.Items {
|
||||||
|
contactResponses[i] = contact.ToResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Contact search completed", map[string]interface{}{
|
||||||
|
"total_count": result.TotalCount,
|
||||||
|
"items_count": len(result.Items),
|
||||||
|
})
|
||||||
|
|
||||||
|
return &ContactListResponse{
|
||||||
|
Contacts: contactResponses,
|
||||||
|
Meta: pagination.Response(result.TotalCount),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatus updates the status of a contact (admin only)
|
||||||
|
func (s *contactService) UpdateStatus(ctx context.Context, id string, form *UpdateContactStatusForm, adminID string) error {
|
||||||
|
s.logger.Info("Updating contact status", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"status": form.Status,
|
||||||
|
"admin_id": adminID,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validate status transition
|
||||||
|
contact, err := s.repo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to get contact for status update", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("failed to get contact: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if contact can be updated
|
||||||
|
if !contact.CanBeUpdated() {
|
||||||
|
s.logger.Warn("Attempted to update closed contact", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"current_status": contact.Status,
|
||||||
|
"new_status": form.Status,
|
||||||
|
})
|
||||||
|
return fmt.Errorf("contact is closed and cannot be updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
status := ContactStatus(form.Status)
|
||||||
|
err = s.repo.UpdateStatus(ctx, id, status, form.AdminNotes, &adminID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to update contact status", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"status": form.Status,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("failed to update contact status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Contact status updated successfully", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"status": form.Status,
|
||||||
|
"admin_id": adminID,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats returns contact statistics (admin only)
|
||||||
|
func (s *contactService) GetStats(ctx context.Context) (*ContactStats, error) {
|
||||||
|
s.logger.Debug("Getting contact statistics", nil)
|
||||||
|
|
||||||
|
stats, err := s.repo.GetStats(ctx)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to get contact stats", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to get contact stats: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Contact statistics retrieved", map[string]interface{}{
|
||||||
|
"total": stats.Total,
|
||||||
|
"pending": stats.Pending,
|
||||||
|
"in_progress": stats.InProgress,
|
||||||
|
"resolved": stats.Resolved,
|
||||||
|
"closed": stats.Closed,
|
||||||
|
})
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a contact by ID (admin only)
|
||||||
|
func (s *contactService) Delete(ctx context.Context, id string) error {
|
||||||
|
s.logger.Info("Deleting contact", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if contact exists
|
||||||
|
contact, err := s.repo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to get contact for deletion", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("failed to get contact: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the contact
|
||||||
|
if err := s.repo.Delete(ctx, id); err != nil {
|
||||||
|
s.logger.Error("Failed to delete contact", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("failed to delete contact: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Contact deleted successfully", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"email": contact.Email,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -8,21 +8,18 @@ import (
|
|||||||
|
|
||||||
"tm/internal/customer"
|
"tm/internal/customer"
|
||||||
"tm/internal/user"
|
"tm/internal/user"
|
||||||
"tm/pkg/logger"
|
|
||||||
"tm/pkg/response"
|
"tm/pkg/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NotificationHandler handles HTTP requests for notification operations
|
// NotificationHandler handles HTTP requests for notification operations
|
||||||
type NotificationHandler struct {
|
type NotificationHandler struct {
|
||||||
service Service
|
service Service
|
||||||
logger logger.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewNotificationHandler creates a new notification handler
|
// NewNotificationHandler creates a new notification handler
|
||||||
func NewHandler(service Service, logger logger.Logger) *NotificationHandler {
|
func NewHandler(service Service) *NotificationHandler {
|
||||||
return &NotificationHandler{
|
return &NotificationHandler{
|
||||||
service: service,
|
service: service,
|
||||||
logger: logger,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user