From 875447ac58b0a96d4f7edba731b3981ba0f6cc09 Mon Sep 17 00:00:00 2001 From: "n.nakhostin" Date: Sat, 25 Oct 2025 18:15:39 +0330 Subject: [PATCH] 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. --- cmd/web/docs/docs.go | 492 +++++++++++++++++++++++++++++++ cmd/web/docs/swagger.json | 492 +++++++++++++++++++++++++++++++ cmd/web/docs/swagger.yaml | 319 ++++++++++++++++++++ cmd/web/main.go | 16 +- internal/contact/entity.go | 75 +++++ internal/contact/errors.go | 18 ++ internal/contact/form.go | 79 +++++ internal/contact/handler.go | 196 ++++++++++++ internal/contact/repository.go | 220 ++++++++++++++ internal/contact/service.go | 249 ++++++++++++++++ internal/notification/handler.go | 5 +- 11 files changed, 2151 insertions(+), 10 deletions(-) create mode 100644 internal/contact/entity.go create mode 100644 internal/contact/errors.go create mode 100644 internal/contact/form.go create mode 100644 internal/contact/handler.go create mode 100644 internal/contact/repository.go create mode 100644 internal/contact/service.go diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 9783941..1439d7c 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -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": { diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index e672339..d4c494d 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -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": { diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index c9765ba..90cd48b 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -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: diff --git a/cmd/web/main.go b/cmd/web/main.go index c03f5ab..d116742 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -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) diff --git a/internal/contact/entity.go b/internal/contact/entity.go new file mode 100644 index 0000000..e10bd68 --- /dev/null +++ b/internal/contact/entity.go @@ -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 +} diff --git a/internal/contact/errors.go b/internal/contact/errors.go new file mode 100644 index 0000000..a36898a --- /dev/null +++ b/internal/contact/errors.go @@ -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") +) diff --git a/internal/contact/form.go b/internal/contact/form.go new file mode 100644 index 0000000..cb8b0d4 --- /dev/null +++ b/internal/contact/form.go @@ -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:"-"` +} diff --git a/internal/contact/handler.go b/internal/contact/handler.go new file mode 100644 index 0000000..ecf0aca --- /dev/null +++ b/internal/contact/handler.go @@ -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") +} diff --git a/internal/contact/repository.go b/internal/contact/repository.go new file mode 100644 index 0000000..bfe6a6b --- /dev/null +++ b/internal/contact/repository.go @@ -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) +} diff --git a/internal/contact/service.go b/internal/contact/service.go new file mode 100644 index 0000000..9c6dbc7 --- /dev/null +++ b/internal/contact/service.go @@ -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 +} diff --git a/internal/notification/handler.go b/internal/notification/handler.go index ede2ad4..678f2ee 100644 --- a/internal/notification/handler.go +++ b/internal/notification/handler.go @@ -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, } }