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:
n.nakhostin
2025-10-25 18:15:39 +03:30
parent 889a56a9f9
commit 875447ac58
11 changed files with 2151 additions and 10 deletions
+492
View File
@@ -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": {
"get": {
"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": {
"get": {
"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": {
"type": "object",
"properties": {
+492
View File
@@ -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": {
"get": {
"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": {
"get": {
"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": {
"type": "object",
"properties": {
+319
View File
@@ -302,6 +302,75 @@ definitions:
updated_at:
type: integer
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:
properties:
companies:
@@ -1999,6 +2068,221 @@ paths:
summary: Toggle category publish status
tags:
- 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:
get:
consumes:
@@ -4314,6 +4598,41 @@ paths:
summary: Get my company profile
tags:
- 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:
get:
consumes:
+10 -6
View File
@@ -85,6 +85,7 @@ import (
"tm/internal/assets"
"tm/internal/company"
"tm/internal/company_category"
"tm/internal/contact"
"tm/internal/customer"
"tm/internal/feedback"
"tm/internal/inquiry"
@@ -142,8 +143,9 @@ func main() {
feedbackRepo := feedback.NewRepository(mongoManager, logger)
tenderApprovalRepository := tender_approval.NewRepository(mongoManager, logger)
inquiryRepository := inquiry.NewRepository(mongoManager, logger)
contactRepository := contact.NewContactRepository(mongoManager, logger)
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
@@ -161,8 +163,9 @@ func main() {
inquiryService := inquiry.NewService(inquiryRepository, notificationSDK, logger)
flagService := assets.NewService(logger, conf.Assets.FlagsPath)
notificationService := notification.NewService(notificationSDK, userService, customerService, logger)
contactService := contact.NewService(contactRepository, logger)
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
@@ -175,17 +178,18 @@ func main() {
tenderApprovalHandler := tender_approval.NewHandler(tenderApprovalService, logger)
inquiryHandler := inquiry.NewHandler(inquiryService, 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{}{
"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
e := bootstrap.InitHTTPServer(conf, logger)
// Register routes
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler)
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, 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, contactHandler)
// Start server
serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port)
+75
View File
@@ -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
}
+18
View File
@@ -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")
)
+79
View File
@@ -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:"-"`
}
+196
View File
@@ -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")
}
+220
View File
@@ -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)
}
+249
View File
@@ -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
}
+1 -4
View File
@@ -8,21 +8,18 @@ import (
"tm/internal/customer"
"tm/internal/user"
"tm/pkg/logger"
"tm/pkg/response"
)
// NotificationHandler handles HTTP requests for notification operations
type NotificationHandler struct {
service Service
logger logger.Logger
}
// NewNotificationHandler creates a new notification handler
func NewHandler(service Service, logger logger.Logger) *NotificationHandler {
func NewHandler(service Service) *NotificationHandler {
return &NotificationHandler{
service: service,
logger: logger,
}
}