diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 50a2b25..4182825 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -1460,6 +1460,88 @@ const docTemplate = `{ } } }, + "/admin/v1/customers/{id}/role": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Assign a role (admin or analyst) to a customer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Customers" + ], + "summary": "Assign role to customer", + "parameters": [ + { + "type": "string", + "description": "Customer ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Role assignment information", + "name": "role", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.AssignRoleForm" + } + } + ], + "responses": { + "200": { + "description": "Role assigned successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.AssignRoleResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/customers/{id}/status": { "patch": { "security": [ @@ -2304,6 +2386,325 @@ const docTemplate = `{ } } }, + "/admin/v1/notifications": { + "get": { + "description": "Retrieve notifications for a specific user with filtering and pagination", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Get user notifications", + "parameters": [ + { + "type": "string", + "description": "Recipient", + "name": "recipient", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Notification type (info, warning, alert, reject)", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "Notification priority (low, medium, important)", + "name": "priority", + "in": "query" + }, + { + "type": "string", + "description": "Delivery status (pending, sent, failed)", + "name": "status", + "in": "query" + }, + { + "type": "boolean", + "description": "Read status", + "name": "is_read", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Items per page", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "default": "created_at", + "description": "Sort field", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "default": "desc", + "description": "Sort order (asc, desc)", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/notification.NotificationsResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Create a new notification with the provided information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Create a new notification", + "parameters": [ + { + "description": "Notification information", + "name": "notification", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_notification.NotificationRequest" + } + } + ], + "responses": { + "201": { + "description": "Notification created successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/notifications/{id}": { + "get": { + "description": "Retrieve a specific notification by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Get notification by ID", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/internal_notification.NotificationResponse" + } + } + } + ] + } + }, + "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" + } + } + } + }, + "put": { + "description": "Update an existing notification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Update notification", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated notification information", + "name": "notification", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_notification.NotificationRequest" + } + } + ], + "responses": { + "200": { + "description": "Notification updated successfully", + "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" + } + } + } + }, + "delete": { + "description": "Delete a specific notification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Delete notification", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Notification deleted successfully", + "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/profile": { "get": { "security": [ @@ -6497,6 +6898,28 @@ const docTemplate = `{ } } }, + "customer.AssignRoleForm": { + "type": "object", + "properties": { + "role": { + "type": "string", + "example": "analyst" + } + } + }, + "customer.AssignRoleResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Role assigned successfully" + }, + "success": { + "type": "boolean", + "example": true + } + } + }, "customer.AuthResponse": { "type": "object", "properties": { @@ -6550,6 +6973,10 @@ const docTemplate = `{ "type": "string", "example": "+1234567890" }, + "role": { + "type": "string", + "example": "analyst" + }, "type": { "type": "string", "example": "individual" @@ -6601,10 +7028,10 @@ const docTemplate = `{ "last_login_at": { "type": "integer" }, - "password": { + "phone": { "type": "string" }, - "phone": { + "role": { "type": "string" }, "status": { @@ -6715,6 +7142,10 @@ const docTemplate = `{ "type": "string", "example": "+1234567890" }, + "role": { + "type": "string", + "example": "analyst" + }, "type": { "type": "string", "example": "individual" @@ -6974,6 +7405,145 @@ const docTemplate = `{ } } }, + "internal_notification.NotificationRequest": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "$ref": "#/definitions/notification.DeliveryChannel" + } + }, + "description": { + "type": "string" + }, + "link": { + "type": "string" + }, + "priority": { + "$ref": "#/definitions/notification.NotificationPriority" + }, + "recipient": { + "type": "array", + "items": { + "type": "string" + } + }, + "schedule": { + "$ref": "#/definitions/notification.ScheduleRequest" + }, + "tender": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/notification.NotificationType" + } + } + }, + "internal_notification.NotificationResponse": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "$ref": "#/definitions/notification.DeliveryChannel" + } + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "link": { + "type": "string" + }, + "priority": { + "$ref": "#/definitions/notification.NotificationPriority" + }, + "schedule": { + "$ref": "#/definitions/notification.ScheduleResponse" + }, + "tender": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/notification.NotificationType" + } + } + }, + "notification.DeliveryChannel": { + "type": "string", + "enum": [ + "push", + "email" + ], + "x-enum-varnames": [ + "DeliveryChannelPush", + "DeliveryChannelEmail" + ] + }, + "notification.NotificationPriority": { + "type": "string", + "enum": [ + "low", + "medium", + "important" + ], + "x-enum-varnames": [ + "NotificationPriorityLow", + "NotificationPriorityMedium", + "NotificationPriorityImportant" + ] + }, + "notification.NotificationType": { + "type": "string", + "enum": [ + "info", + "warning", + "alert", + "reject" + ], + "x-enum-varnames": [ + "NotificationTypeInfo", + "NotificationTypeWarning", + "NotificationTypeAlert", + "NotificationTypeReject" + ] + }, + "notification.NotificationsResponse": { + "type": "object", + "properties": { + "notifications": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_notification.NotificationResponse" + } + } + } + }, + "notification.ScheduleRequest": { + "type": "object", + "properties": { + "time": { + "type": "integer" + } + } + }, + "notification.ScheduleResponse": { + "type": "object", + "properties": { + "time": { + "type": "integer" + } + } + }, "response.APIError": { "type": "object", "properties": { @@ -7675,6 +8245,10 @@ const docTemplate = `{ "description": "Administrative flag management operations for web panel including flag listing and retrieval", "name": "Admin-Flags" }, + { + "description": "Administrative notification management operations for web panel including CRUD operations, bulk notifications, queue management, and comprehensive filtering with pagination", + "name": "Admin-Notification" + }, { "description": "Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access", "name": "Authorization" diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index 48432db..b24b641 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -1454,6 +1454,88 @@ } } }, + "/admin/v1/customers/{id}/role": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Assign a role (admin or analyst) to a customer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Customers" + ], + "summary": "Assign role to customer", + "parameters": [ + { + "type": "string", + "description": "Customer ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Role assignment information", + "name": "role", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.AssignRoleForm" + } + } + ], + "responses": { + "200": { + "description": "Role assigned successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.AssignRoleResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/customers/{id}/status": { "patch": { "security": [ @@ -2298,6 +2380,325 @@ } } }, + "/admin/v1/notifications": { + "get": { + "description": "Retrieve notifications for a specific user with filtering and pagination", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Get user notifications", + "parameters": [ + { + "type": "string", + "description": "Recipient", + "name": "recipient", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Notification type (info, warning, alert, reject)", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "Notification priority (low, medium, important)", + "name": "priority", + "in": "query" + }, + { + "type": "string", + "description": "Delivery status (pending, sent, failed)", + "name": "status", + "in": "query" + }, + { + "type": "boolean", + "description": "Read status", + "name": "is_read", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Items per page", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "default": "created_at", + "description": "Sort field", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "default": "desc", + "description": "Sort order (asc, desc)", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/notification.NotificationsResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Create a new notification with the provided information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Create a new notification", + "parameters": [ + { + "description": "Notification information", + "name": "notification", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_notification.NotificationRequest" + } + } + ], + "responses": { + "201": { + "description": "Notification created successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/notifications/{id}": { + "get": { + "description": "Retrieve a specific notification by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Get notification by ID", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/internal_notification.NotificationResponse" + } + } + } + ] + } + }, + "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" + } + } + } + }, + "put": { + "description": "Update an existing notification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Update notification", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated notification information", + "name": "notification", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_notification.NotificationRequest" + } + } + ], + "responses": { + "200": { + "description": "Notification updated successfully", + "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" + } + } + } + }, + "delete": { + "description": "Delete a specific notification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Delete notification", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Notification deleted successfully", + "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/profile": { "get": { "security": [ @@ -6491,6 +6892,28 @@ } } }, + "customer.AssignRoleForm": { + "type": "object", + "properties": { + "role": { + "type": "string", + "example": "analyst" + } + } + }, + "customer.AssignRoleResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Role assigned successfully" + }, + "success": { + "type": "boolean", + "example": true + } + } + }, "customer.AuthResponse": { "type": "object", "properties": { @@ -6544,6 +6967,10 @@ "type": "string", "example": "+1234567890" }, + "role": { + "type": "string", + "example": "analyst" + }, "type": { "type": "string", "example": "individual" @@ -6595,10 +7022,10 @@ "last_login_at": { "type": "integer" }, - "password": { + "phone": { "type": "string" }, - "phone": { + "role": { "type": "string" }, "status": { @@ -6709,6 +7136,10 @@ "type": "string", "example": "+1234567890" }, + "role": { + "type": "string", + "example": "analyst" + }, "type": { "type": "string", "example": "individual" @@ -6968,6 +7399,145 @@ } } }, + "internal_notification.NotificationRequest": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "$ref": "#/definitions/notification.DeliveryChannel" + } + }, + "description": { + "type": "string" + }, + "link": { + "type": "string" + }, + "priority": { + "$ref": "#/definitions/notification.NotificationPriority" + }, + "recipient": { + "type": "array", + "items": { + "type": "string" + } + }, + "schedule": { + "$ref": "#/definitions/notification.ScheduleRequest" + }, + "tender": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/notification.NotificationType" + } + } + }, + "internal_notification.NotificationResponse": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "$ref": "#/definitions/notification.DeliveryChannel" + } + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "link": { + "type": "string" + }, + "priority": { + "$ref": "#/definitions/notification.NotificationPriority" + }, + "schedule": { + "$ref": "#/definitions/notification.ScheduleResponse" + }, + "tender": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/notification.NotificationType" + } + } + }, + "notification.DeliveryChannel": { + "type": "string", + "enum": [ + "push", + "email" + ], + "x-enum-varnames": [ + "DeliveryChannelPush", + "DeliveryChannelEmail" + ] + }, + "notification.NotificationPriority": { + "type": "string", + "enum": [ + "low", + "medium", + "important" + ], + "x-enum-varnames": [ + "NotificationPriorityLow", + "NotificationPriorityMedium", + "NotificationPriorityImportant" + ] + }, + "notification.NotificationType": { + "type": "string", + "enum": [ + "info", + "warning", + "alert", + "reject" + ], + "x-enum-varnames": [ + "NotificationTypeInfo", + "NotificationTypeWarning", + "NotificationTypeAlert", + "NotificationTypeReject" + ] + }, + "notification.NotificationsResponse": { + "type": "object", + "properties": { + "notifications": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_notification.NotificationResponse" + } + } + } + }, + "notification.ScheduleRequest": { + "type": "object", + "properties": { + "time": { + "type": "integer" + } + } + }, + "notification.ScheduleResponse": { + "type": "object", + "properties": { + "time": { + "type": "integer" + } + } + }, "response.APIError": { "type": "object", "properties": { @@ -7669,6 +8239,10 @@ "description": "Administrative flag management operations for web panel including flag listing and retrieval", "name": "Admin-Flags" }, + { + "description": "Administrative notification management operations for web panel including CRUD operations, bulk notifications, queue management, and comprehensive filtering with pagination", + "name": "Admin-Notification" + }, { "description": "Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access", "name": "Authorization" diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index 14aa0a2..1cd2cbb 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -303,6 +303,21 @@ definitions: updated_at: type: integer type: object + customer.AssignRoleForm: + properties: + role: + example: analyst + type: string + type: object + customer.AssignRoleResponse: + properties: + message: + example: Role assigned successfully + type: string + success: + example: true + type: boolean + type: object customer.AuthResponse: properties: access_token: @@ -339,6 +354,9 @@ definitions: phone: example: "+1234567890" type: string + role: + example: analyst + type: string type: example: individual type: string @@ -373,10 +391,10 @@ definitions: type: string last_login_at: type: integer - password: - type: string phone: type: string + role: + type: string status: type: string type: @@ -451,6 +469,9 @@ definitions: phone: example: "+1234567890" type: string + role: + example: analyst + type: string type: example: individual type: string @@ -627,6 +648,101 @@ definitions: example: reviewed type: string type: object + internal_notification.NotificationRequest: + properties: + channels: + items: + $ref: '#/definitions/notification.DeliveryChannel' + type: array + description: + type: string + link: + type: string + priority: + $ref: '#/definitions/notification.NotificationPriority' + recipient: + items: + type: string + type: array + schedule: + $ref: '#/definitions/notification.ScheduleRequest' + tender: + type: string + title: + type: string + type: + $ref: '#/definitions/notification.NotificationType' + type: object + internal_notification.NotificationResponse: + properties: + channels: + items: + $ref: '#/definitions/notification.DeliveryChannel' + type: array + description: + type: string + id: + type: string + link: + type: string + priority: + $ref: '#/definitions/notification.NotificationPriority' + schedule: + $ref: '#/definitions/notification.ScheduleResponse' + tender: + type: string + title: + type: string + type: + $ref: '#/definitions/notification.NotificationType' + type: object + notification.DeliveryChannel: + enum: + - push + - email + type: string + x-enum-varnames: + - DeliveryChannelPush + - DeliveryChannelEmail + notification.NotificationPriority: + enum: + - low + - medium + - important + type: string + x-enum-varnames: + - NotificationPriorityLow + - NotificationPriorityMedium + - NotificationPriorityImportant + notification.NotificationType: + enum: + - info + - warning + - alert + - reject + type: string + x-enum-varnames: + - NotificationTypeInfo + - NotificationTypeWarning + - NotificationTypeAlert + - NotificationTypeReject + notification.NotificationsResponse: + properties: + notifications: + items: + $ref: '#/definitions/internal_notification.NotificationResponse' + type: array + type: object + notification.ScheduleRequest: + properties: + time: + type: integer + type: object + notification.ScheduleResponse: + properties: + time: + type: integer + type: object response.APIError: properties: code: @@ -2002,6 +2118,56 @@ paths: summary: Update customer information tags: - Admin-Customers + /admin/v1/customers/{id}/role: + patch: + consumes: + - application/json + description: Assign a role (admin or analyst) to a customer + parameters: + - description: Customer ID + in: path + name: id + required: true + type: string + - description: Role assignment information + in: body + name: role + required: true + schema: + $ref: '#/definitions/customer.AssignRoleForm' + produces: + - application/json + responses: + "200": + description: Role assigned successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/customer.AssignRoleResponse' + type: object + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer not found + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid request data + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Assign role to customer + tags: + - Admin-Customers /admin/v1/customers/{id}/status: patch: consumes: @@ -2526,6 +2692,213 @@ paths: summary: Update inquiry status tags: - Admin-Inquiries + /admin/v1/notifications: + get: + consumes: + - application/json + description: Retrieve notifications for a specific user with filtering and pagination + parameters: + - description: Recipient + in: query + name: recipient + required: true + type: string + - description: Notification type (info, warning, alert, reject) + in: query + name: type + type: string + - description: Notification priority (low, medium, important) + in: query + name: priority + type: string + - description: Delivery status (pending, sent, failed) + in: query + name: status + type: string + - description: Read status + in: query + name: is_read + type: boolean + - default: 1 + description: Page number + in: query + name: page + type: integer + - default: 20 + description: Items per page + in: query + name: limit + type: integer + - default: created_at + description: Sort field + in: query + name: sort_by + type: string + - default: desc + description: Sort order (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/notification.NotificationsResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get user notifications + tags: + - Admin-Notification + post: + consumes: + - application/json + description: Create a new notification with the provided information + parameters: + - description: Notification information + in: body + name: notification + required: true + schema: + $ref: '#/definitions/internal_notification.NotificationRequest' + produces: + - application/json + responses: + "201": + description: Notification created successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Create a new notification + tags: + - Admin-Notification + /admin/v1/notifications/{id}: + delete: + consumes: + - application/json + description: Delete a specific notification + parameters: + - description: Notification ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Notification deleted successfully + 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 notification + tags: + - Admin-Notification + get: + consumes: + - application/json + description: Retrieve a specific notification by its ID + parameters: + - description: Notification 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/internal_notification.NotificationResponse' + 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 notification by ID + tags: + - Admin-Notification + put: + consumes: + - application/json + description: Update an existing notification + parameters: + - description: Notification ID + in: path + name: id + required: true + type: string + - description: Updated notification information + in: body + name: notification + required: true + schema: + $ref: '#/definitions/internal_notification.NotificationRequest' + produces: + - application/json + responses: + "200": + description: Notification updated successfully + 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 notification + tags: + - Admin-Notification /admin/v1/profile: get: consumes: @@ -4909,6 +5282,10 @@ tags: - description: Administrative flag management operations for web panel including flag listing and retrieval name: Admin-Flags +- description: Administrative notification management operations for web panel including + CRUD operations, bulk notifications, queue management, and comprehensive filtering + with pagination + name: Admin-Notification - description: Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access name: Authorization diff --git a/cmd/web/main.go b/cmd/web/main.go index aa04103..1a8bdef 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -53,6 +53,9 @@ package main // @tag.name Admin-Flags // @tag.description Administrative flag management operations for web panel including flag listing and retrieval +// @tag.name Admin-Notification +// @tag.description Administrative notification management operations for web panel including CRUD operations, bulk notifications, queue management, and comprehensive filtering with pagination + // @tag.name Authorization // @tag.description Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access @@ -85,6 +88,7 @@ import ( "tm/internal/customer" "tm/internal/feedback" "tm/internal/inquiry" + "tm/internal/notification" "tm/internal/tender" "tm/internal/tender_approval" "tm/internal/user" @@ -138,8 +142,9 @@ func main() { feedbackRepo := feedback.NewFeedbackRepository(mongoManager, logger) tenderApprovalRepository := tender_approval.NewTenderApprovalRepository(mongoManager, logger) inquiryRepository := inquiry.NewInquiryRepository(mongoManager, logger) + notificationRepository := notification.NewNotificationRepository(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", "notification"}, }) // Initialize validation service @@ -155,8 +160,9 @@ func main() { tenderApprovalService := tender_approval.NewTenderApprovalService(tenderApprovalRepository, tenderService, logger) inquiryService := inquiry.NewInquiryService(inquiryRepository, logger) flagService := assets.NewFlagService(logger, conf.Assets.FlagsPath) + notificationService := notification.NewNotificationService(notificationRepository, logger) logger.Info("Services initialized successfully", map[string]interface{}{ - "services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag"}, + "services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification"}, }) // Initialize handlers with services @@ -169,16 +175,17 @@ func main() { tenderApprovalHandler := tender_approval.NewHandler(tenderApprovalService, logger) inquiryHandler := inquiry.NewHandler(inquiryService, logger) flagHandler := assets.NewHandler(flagService, logger) + notificationHandler := notification.NewHandler(notificationService, logger) logger.Info("Handlers initialized successfully", map[string]interface{}{ - "handlers": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag"}, + "handlers": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification"}, }) // Initialize HTTP server e := bootstrap.InitHTTPServer(conf, logger) // Register routes - router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler) - router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler) + 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) // Start server serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port) diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index f159cef..a274432 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -7,6 +7,7 @@ import ( "tm/internal/customer" "tm/internal/feedback" "tm/internal/inquiry" + "tm/internal/notification" "tm/internal/tender" "tm/internal/tender_approval" "tm/internal/user" @@ -14,7 +15,7 @@ import ( "github.com/labstack/echo/v4" ) -func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler) { +func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler) { adminV1 := e.Group("/admin/v1") // Admin Users Routes @@ -128,9 +129,24 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler { flagsGP.GET("/:country_code", flagHandler.AdminGetFlagSVG) } + + // Admin Notifications Routes + // notificationsGP := adminV1.Group("/notifications") + // { + // notificationsGP.Use(userHandler.AuthMiddleware()) + // notificationsGP.POST("", notificationHandler.CreateNotification) + // notificationsGP.POST("/bulk", notificationHandler.CreateBulkNotification) + // notificationsGP.GET("", notificationHandler.GetNotifications) + // notificationsGP.GET("/stats", notificationHandler.GetNotificationStats) + // notificationsGP.POST("/mark-read", notificationHandler.MarkAsRead) + // notificationsGP.POST("/process-pending", notificationHandler.ProcessPendingNotifications) + // notificationsGP.GET("/:id", notificationHandler.GetNotificationByID) + // notificationsGP.PUT("/:id", notificationHandler.UpdateNotification) + // notificationsGP.DELETE("/:id", notificationHandler.DeleteNotification) + // } } -func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler) { +func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler) { v1 := e.Group("/api/v1") customerGP := v1.Group("/profile") @@ -198,4 +214,14 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende { flagsGP.GET("/:country_code", flagHandler.GetFlagSVG) } + + // Public Notifications Routes + // notificationsGP := v1.Group("/notifications") + // { + // notificationsGP.Use(customerHandler.AuthMiddleware()) + // notificationsGP.GET("", notificationHandler.GetNotifications) + // notificationsGP.GET("/stats", notificationHandler.GetNotificationStats) + // notificationsGP.POST("/mark-read", notificationHandler.MarkAsRead) + // notificationsGP.GET("/:id", notificationHandler.GetNotificationByID) + // } } diff --git a/go.mod b/go.mod index dc8063e..438eab5 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.24.4 require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/labstack/echo/v4 v4.13.4 - github.com/redis/go-redis/v9 v9.12.0 + github.com/redis/go-redis/v9 v9.14.0 github.com/stretchr/testify v1.10.0 github.com/swaggo/echo-swagger v1.4.1 github.com/swaggo/swag v1.16.6 diff --git a/go.sum b/go.sum index 7119583..476cb85 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.12.0 h1:XlVPGlflh4nxfhsNXPA8Qp6EmEfTo0rp8oaBzPipXnU= -github.com/redis/go-redis/v9 v9.12.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE= +github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= diff --git a/internal/notification/entity.go b/internal/notification/entity.go new file mode 100644 index 0000000..4ec3e1a --- /dev/null +++ b/internal/notification/entity.go @@ -0,0 +1,110 @@ +package notification + +import ( + "tm/pkg/mongo" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +// NotificationType represents the type of notification +type NotificationType string + +const ( + NotificationTypeInfo NotificationType = "info" + NotificationTypeWarning NotificationType = "warning" + NotificationTypeAlert NotificationType = "alert" + NotificationTypeReject NotificationType = "reject" +) + +// NotificationPriority represents the priority level of notification +type NotificationPriority string + +const ( + NotificationPriorityLow NotificationPriority = "low" + NotificationPriorityMedium NotificationPriority = "medium" + NotificationPriorityImportant NotificationPriority = "important" +) + +// DeliveryChannel represents the delivery channel for notification +type DeliveryChannel string + +const ( + DeliveryChannelPush DeliveryChannel = "push" + DeliveryChannelEmail DeliveryChannel = "email" +) + +// DeliveryStatus represents the delivery status of notification +type NotificationStatus string + +const ( + DeliveryStatusPending NotificationStatus = "pending" + DeliveryStatusSent NotificationStatus = "sent" + DeliveryStatusFailed NotificationStatus = "failed" +) + +// TargetAudienceType represents the type of target audience +// Supported audience types: +// - all_users: All users in the system +// - specific_users: Specific users by their IDs +// - all_role: All users with a specific role (e.g., all admins) +// - specific_role: Users with a specific role, optionally filtered by tender +// - specific_company: All users belonging to a specific company +// - all_companies: All users from all companies +// - specific_customer: All users belonging to a specific customer +// - all_customers: All users from all customers +type TargetAudienceType string + +const ( + TargetAudienceAllUsers TargetAudienceType = "all_users" + TargetAudienceSpecificUsers TargetAudienceType = "specific_users" + TargetAudienceAllRole TargetAudienceType = "all_role" + TargetAudienceSpecificRole TargetAudienceType = "specific_role" + TargetAudienceAllCompanies TargetAudienceType = "all_companies" + TargetAudienceSpecificCompany TargetAudienceType = "specific_company" + TargetAudienceAllCustomers TargetAudienceType = "all_customers" + TargetAudienceSpecificCustomer TargetAudienceType = "specific_customer" +) + +// Notification represents a notification entity +type Notification struct { + mongo.Model `bson:",inline"` + Recipient []string `bson:"recipients"` + Tender bson.ObjectID `bson:"tender"` + Channels []DeliveryChannel `bson:"channels"` + Type NotificationType `bson:"type"` + Priority NotificationPriority `bson:"priority"` + Status NotificationStatus `bson:"status"` + Target TargetAudienceType `bson:"target"` + Title string `bson:"title"` + Description string `bson:"description"` + Link *string `bson:"link"` + Schedule Schedule `bson:"schedule"` +} + +type Schedule struct { + Time int64 `bson:"time"` +} + +func (n *Notification) SetID(id string) { + n.ID, _ = bson.ObjectIDFromHex(id) +} + +func (n *Notification) GetID() string { + return n.ID.Hex() +} + +func (n *Notification) SetCreatedAt(timestamp int64) { + n.CreatedAt = timestamp +} + +func (n *Notification) SetUpdatedAt(timestamp int64) { + n.UpdatedAt = timestamp +} + +func (n *Notification) GetCreatedAt() int64 { + return n.CreatedAt +} + +func (n *Notification) GetUpdatedAt() int64 { + return n.UpdatedAt +} diff --git a/internal/notification/form.go b/internal/notification/form.go new file mode 100644 index 0000000..588c0e3 --- /dev/null +++ b/internal/notification/form.go @@ -0,0 +1,70 @@ +package notification + +import ( + "tm/pkg/response" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +// CreateRequest represents the request to create a notification +type NotificationRequest struct { + Recipient []string `json:"recipient" valid:"required,stringlength(1|100)"` + Tender bson.ObjectID `json:"tender" valid:"optional,stringlength(1|100)"` + Channels []DeliveryChannel `json:"channels" valid:"required,minlength(1)"` + Type NotificationType `json:"type" valid:"required,notification_type"` + Priority NotificationPriority `json:"priority" valid:"required,notification_priority"` + Target TargetAudienceType `json:"target" valid:"required,target_audience"` + Title string `json:"title" valid:"required,stringlength(1|200)"` + Description string `json:"description" valid:"required,stringlength(1|1000)"` + Link *string `json:"link" valid:"optional,url"` + Schedule ScheduleRequest `json:"schedule" valid:"optional"` +} + +type ScheduleRequest struct { + Time int64 `json:"time" valid:"required,int64"` +} + +// NotificationResponse represents the response after creating a notification +type NotificationResponse struct { + ID string `json:"id"` + Tender string `json:"tender"` + Channels []DeliveryChannel `json:"channels"` + Type NotificationType `json:"type"` + Priority NotificationPriority `json:"priority"` + Target TargetAudienceType `json:"target"` + Title string `json:"title"` + Description string `json:"description"` + Link string `json:"link"` + Schedule ScheduleResponse `json:"schedule"` +} + +type ScheduleResponse struct { + Time int64 `json:"time"` +} + +type NotificationsResponse struct { + Notifications []*NotificationResponse `json:"notifications"` + Meta *response.Meta `json:"-"` +} + +func (n *Notification) ToResponse() *NotificationResponse { + return &NotificationResponse{ + ID: n.ID.Hex(), + Tender: n.Tender.Hex(), + Channels: n.Channels, + Type: n.Type, + Priority: n.Priority, + Target: n.Target, + Title: n.Title, + Description: n.Description, + Link: *n.Link, + Schedule: ScheduleResponse{ + Time: n.Schedule.Time, + }, + } +} + +// SearchForm represents the form for searching notifications +type SearchForm struct { + Search *string `query:"q" valid:"optional"` +} diff --git a/internal/notification/handler.go b/internal/notification/handler.go new file mode 100644 index 0000000..d1564b5 --- /dev/null +++ b/internal/notification/handler.go @@ -0,0 +1,188 @@ +package notification + +import ( + "github.com/asaskevich/govalidator" + "github.com/labstack/echo/v4" + + "tm/pkg/logger" + "tm/pkg/response" +) + +// NotificationHandler handles HTTP requests for notification operations +type NotificationHandler struct { + service NotificationService + logger logger.Logger +} + +// NewNotificationHandler creates a new notification handler +func NewHandler(service NotificationService, logger logger.Logger) *NotificationHandler { + return &NotificationHandler{ + service: service, + logger: logger, + } +} + +// CreateNotification creates a new notification +// @Summary Create a new notification +// @Description Create a new notification with the provided information +// @Tags Admin-Notification +// @Accept json +// @Produce json +// @Param notification body NotificationRequest true "Notification information" +// @Success 201 {object} response.APIResponse "Notification created successfully" +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /admin/v1/notifications [post] +func (h *NotificationHandler) Create(c echo.Context) error { + var req NotificationRequest + if err := c.Bind(&req); err != nil { + return response.BadRequest(c, "Invalid request format", err.Error()) + } + + // Validate request + if valid, err := govalidator.ValidateStruct(req); !valid { + return response.ValidationError(c, "Validation failed", err.Error()) + } + + // Create notification + resp, err := h.service.Create(c.Request().Context(), &req) + if err != nil { + return response.InternalServerError(c, "Failed to create notification") + } + + return response.Created(c, resp, "Notification created successfully") +} + +// UpdateNotification updates a notification +// @Summary Update notification +// @Description Update an existing notification +// @Tags Admin-Notification +// @Accept json +// @Produce json +// @Param id path string true "Notification ID" +// @Param notification body NotificationRequest true "Updated notification information" +// @Success 200 {object} response.APIResponse "Notification updated successfully" +// @Failure 400 {object} response.APIResponse +// @Failure 404 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /admin/v1/notifications/{id} [put] +func (h *NotificationHandler) UpdateNotification(c echo.Context) error { + id := c.Param("id") + if id == "" { + return response.BadRequest(c, "Notification ID is required", "") + } + + var req NotificationRequest + if err := c.Bind(&req); err != nil { + return response.BadRequest(c, "Invalid request format", err.Error()) + } + + // Update notification + _, err := h.service.Update(c.Request().Context(), id, &req) + if err != nil { + if err.Error() == "notification not found" { + return response.NotFound(c, "Notification not found") + } + return response.BadRequest(c, "Failed to update notification", err.Error()) + } + + return response.Success(c, nil, "Notification updated successfully") +} + +// GetNotifications retrieves notifications for a user +// @Summary Get user notifications +// @Description Retrieve notifications for a specific user with filtering and pagination +// @Tags Admin-Notification +// @Accept json +// @Produce json +// @Param recipient query string true "Recipient" +// @Param type query string false "Notification type (info, warning, alert, reject)" +// @Param priority query string false "Notification priority (low, medium, important)" +// @Param status query string false "Delivery status (pending, sent, failed)" +// @Param is_read query bool false "Read status" +// @Param page query int false "Page number" default(1) +// @Param limit query int false "Items per page" default(20) +// @Param sort_by query string false "Sort field" default(created_at) +// @Param sort_order query string false "Sort order (asc, desc)" default(desc) +// @Success 200 {object} response.APIResponse{data=NotificationsResponse} +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /admin/v1/notifications [get] +func (h *NotificationHandler) Search(c echo.Context) error { + req, err := response.Parse[SearchForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + pagination := response.NewPagination(c) + + // Validate request + if valid, err := govalidator.ValidateStruct(req); !valid { + return response.ValidationError(c, "Validation failed", err.Error()) + } + + // Get notifications + resp, err := h.service.Search(c.Request().Context(), req, pagination) + if err != nil { + return response.InternalServerError(c, "Failed to get notifications") + } + + return response.Success(c, resp, "Notifications retrieved successfully") +} + +// Get retrieves a notification by ID +// @Summary Get notification by ID +// @Description Retrieve a specific notification by its ID +// @Tags Admin-Notification +// @Accept json +// @Produce json +// @Param id path string true "Notification ID" +// @Success 200 {object} response.APIResponse{data=NotificationResponse} +// @Failure 400 {object} response.APIResponse +// @Failure 404 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /admin/v1/notifications/{id} [get] +func (h *NotificationHandler) Get(c echo.Context) error { + id := c.Param("id") + if id == "" { + return response.BadRequest(c, "Notification ID is required", "") + } + + // Get notification + resp, err := h.service.Get(c.Request().Context(), id) + if err != nil { + if err.Error() == "notification not found" { + return response.NotFound(c, "Notification not found") + } + return response.InternalServerError(c, "Failed to get notification") + } + + return response.Success(c, resp, "Notification retrieved successfully") +} + +// DeleteNotification deletes a notification +// @Summary Delete notification +// @Description Delete a specific notification +// @Tags Admin-Notification +// @Accept json +// @Produce json +// @Param id path string true "Notification ID" +// @Success 200 {object} response.APIResponse "Notification deleted successfully" +// @Failure 400 {object} response.APIResponse +// @Failure 404 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /admin/v1/notifications/{id} [delete] +func (h *NotificationHandler) DeleteNotification(c echo.Context) error { + id := c.Param("id") + if id == "" { + return response.BadRequest(c, "Notification ID is required", "") + } + + // Delete notification + err := h.service.Delete(c.Request().Context(), id) + if err != nil { + return response.InternalServerError(c, "Failed to delete notification") + } + + return response.Success(c, nil, "Notification deleted successfully") +} diff --git a/internal/notification/repository.go b/internal/notification/repository.go new file mode 100644 index 0000000..4b70765 --- /dev/null +++ b/internal/notification/repository.go @@ -0,0 +1,103 @@ +package notification + +import ( + "context" + "time" + + "tm/pkg/logger" + orm "tm/pkg/mongo" + "tm/pkg/response" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +// NotificationRepository defines the interface for notification data operations +type NotificationRepository interface { + Create(ctx context.Context, notification *Notification) error + Update(ctx context.Context, notification *Notification) error + Get(ctx context.Context, id string) (*Notification, error) + Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Notification, int64, error) + Delete(ctx context.Context, id string) error +} + +// notificationRepository implements NotificationRepository interface +type notificationRepository struct { + ormRepo orm.Repository[Notification] + logger logger.Logger +} + +func collection() string { + return "notifications" +} + +// NewNotificationRepository creates a new notification repository +func NewNotificationRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) NotificationRepository { + // Create indexes using the ORM's index management + indexes := []orm.Index{ + *orm.CreateTextIndex("search_idx", "title", "description"), + } + + // Create indexes + err := mongoManager.CreateIndexes(collection(), indexes) + if err != nil { + logger.Warn("Failed to create customer indexes", map[string]interface{}{ + "error": err.Error(), + }) + } + + ormRepo := orm.NewRepository[Notification](mongoManager.GetCollection(collection()), logger) + + return ¬ificationRepository{ + ormRepo: ormRepo, + logger: logger, + } +} + +// Create creates a new notification +func (r *notificationRepository) Create(ctx context.Context, notification *Notification) error { + notification.SetCreatedAt(time.Now().Unix()) + return r.ormRepo.Create(ctx, notification) +} + +// Create creates a new notification +func (r *notificationRepository) Update(ctx context.Context, notification *Notification) error { + notification.SetUpdatedAt(time.Now().Unix()) + return r.ormRepo.Update(ctx, notification) +} + +// Get gets a notification by id +func (r *notificationRepository) Get(ctx context.Context, id string) (*Notification, error) { + return r.ormRepo.FindByID(ctx, id) +} + +// Search searches for notifications +func (r *notificationRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Notification, int64, error) { + // Build pagination + paginationBuilder := orm.NewPaginationBuilder(). + Limit(pagination.Limit). + Skip(pagination.Offset). + SortBy(pagination.SortBy, pagination.SortOrder) + + filter := bson.M{} + + if form.Search != nil { + filter["$text"] = bson.M{"$search": *form.Search} + } + + // Use ORM to find customers + result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder.Build()) + if err != nil { + r.logger.Error("Failed to search customers", map[string]interface{}{ + "error": err.Error(), + "search": form.Search, + }) + return nil, 0, err + } + + return result.Items, result.TotalCount, nil +} + +// Delete deletes a notification +func (r *notificationRepository) Delete(ctx context.Context, id string) error { + return r.ormRepo.Delete(ctx, id) +} diff --git a/internal/notification/service.go b/internal/notification/service.go new file mode 100644 index 0000000..0902611 --- /dev/null +++ b/internal/notification/service.go @@ -0,0 +1,199 @@ +package notification + +import ( + "context" + "errors" + "fmt" + + "tm/pkg/logger" + "tm/pkg/response" +) + +// NotificationService defines the interface for notification business logic +type NotificationService interface { + Create(ctx context.Context, req *NotificationRequest) (*NotificationResponse, error) + Update(ctx context.Context, id string, req *NotificationRequest) (*NotificationResponse, error) + Get(ctx context.Context, id string) (*NotificationResponse, error) + Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*NotificationsResponse, error) + Delete(ctx context.Context, id string) error +} + +// notificationService implements NotificationService interface +type notificationService struct { + repository NotificationRepository + logger logger.Logger +} + +// NewNotificationService creates a new notification service +func NewNotificationService( + repository NotificationRepository, + logger logger.Logger, +) NotificationService { + return ¬ificationService{ + repository: repository, + logger: logger, + } +} + +// Create creates a single notification +func (s *notificationService) Create(ctx context.Context, req *NotificationRequest) (*NotificationResponse, error) { + s.logger.Info("Creating notification", map[string]interface{}{ + "recipient": req.Recipient, + "type": req.Type, + "priority": req.Priority, + }) + + // Create notification entity + notification := &Notification{ + Recipient: req.Recipient, + Tender: req.Tender, + Channels: req.Channels, + Type: req.Type, + Priority: req.Priority, + Title: req.Title, + Description: req.Description, + Link: req.Link, + Status: DeliveryStatusPending, + Schedule: Schedule{ + Time: req.Schedule.Time, + }, + } + + // Create notification in repository + if err := s.repository.Create(ctx, notification); err != nil { + s.logger.Error("Failed to create notification", map[string]interface{}{ + "recipient": req.Recipient, + "error": err.Error(), + }) + return nil, fmt.Errorf("failed to create notification") + } + + s.logger.Info("Notification created", map[string]interface{}{ + "notification_id": notification.ID, + "recipient": req.Recipient, + "channels": req.Channels, + }) + + return notification.ToResponse(), nil +} + +// Update updates a notification +func (s *notificationService) Update(ctx context.Context, id string, req *NotificationRequest) (*NotificationResponse, error) { + s.logger.Info("Updating notification", map[string]interface{}{ + "notification_id": id, + }) + + notification, err := s.repository.Get(ctx, id) + if err != nil { + s.logger.Error("Failed to get notification", map[string]interface{}{ + "notification_id": id, + "error": err.Error(), + }) + return nil, fmt.Errorf("failed to get notification") + } + + if notification.Status == DeliveryStatusSent { + return nil, fmt.Errorf("notification already sent") + } + + // update notification fields + notification.Recipient = req.Recipient + notification.Tender = req.Tender + notification.Channels = req.Channels + notification.Type = req.Type + notification.Priority = req.Priority + notification.Title = req.Title + notification.Description = req.Description + notification.Link = req.Link + notification.Schedule = Schedule{ + Time: req.Schedule.Time, + } + notification.Status = DeliveryStatusPending + + // update notification in repository + if err := s.repository.Update(ctx, notification); err != nil { + s.logger.Error("Failed to update notification", map[string]interface{}{ + "notification_id": id, + "error": err.Error(), + }) + return nil, fmt.Errorf("failed to update notification") + } + + s.logger.Info("Notification updated", map[string]interface{}{ + "notification_id": id, + "recipient": req.Recipient, + "channels": req.Channels, + }) + + return notification.ToResponse(), nil +} + +// Get gets a notification +func (s *notificationService) Get(ctx context.Context, id string) (*NotificationResponse, error) { + s.logger.Info("Getting notification", map[string]interface{}{ + "notification_id": id, + }) + + notification, err := s.repository.Get(ctx, id) + if err != nil { + s.logger.Error("Failed to get notification", map[string]interface{}{ + "notification_id": id, + "error": err.Error(), + }) + return nil, fmt.Errorf("failed to get notification") + } + + return notification.ToResponse(), nil +} + +// Search searches for notifications +func (s *notificationService) Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*NotificationsResponse, error) { + notifications, total, err := s.repository.Search(ctx, req, pagination) + if err != nil { + s.logger.Error("Failed to search notifications", map[string]interface{}{ + "error": err.Error(), + }) + return nil, errors.New("failed to search notifications") + } + + var notificationResponses []*NotificationResponse + for _, notification := range notifications { + notificationResponses = append(notificationResponses, notification.ToResponse()) + } + + return &NotificationsResponse{ + Notifications: notificationResponses, + Meta: pagination.Response(total), + }, nil +} + +// Delete deletes a notification +func (s *notificationService) Delete(ctx context.Context, id string) error { + s.logger.Info("Deleting notification", map[string]interface{}{ + "notification_id": id, + }) + + notification, err := s.repository.Get(ctx, id) + if err != nil { + s.logger.Error("Failed to get notification", map[string]interface{}{ + "notification_id": id, + "error": err.Error(), + }) + return fmt.Errorf("failed to get notification") + } + + if notification.Status == DeliveryStatusSent { + return fmt.Errorf("notification already sent") + } + + err = s.repository.Delete(ctx, id) + if err != nil { + s.logger.Error("Failed to delete notification", map[string]interface{}{ + "notification_id": id, + "error": err.Error(), + }) + return err + } + + return nil +}