Integrate Notification Management into Tender Management System

- Added a new notification domain, including entities, services, handlers, and repositories to manage notifications effectively.
- Implemented CRUD operations for notifications, allowing for creation, retrieval, updating, and deletion of notifications via the API.
- Enhanced API documentation with Swagger comments for new notification endpoints, ensuring clarity for API consumers.
- Updated routes to include notification management, improving administrative capabilities within the web panel.
- Introduced validation for notification requests and responses, ensuring data integrity and proper error handling.
- Updated the go.mod file to include the latest version of the go-redis library, ensuring compatibility with the notification service.
This commit is contained in:
n.nakhostin
2025-09-17 16:01:49 +03:30
parent 6906577f6e
commit a06dc5097e
12 changed files with 2244 additions and 16 deletions
+576 -2
View File
@@ -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"
+576 -2
View File
@@ -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"
+379 -2
View File
@@ -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
+12 -5
View File
@@ -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)
+28 -2
View File
@@ -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)
// }
}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+110
View File
@@ -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
}
+70
View File
@@ -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"`
}
+188
View File
@@ -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")
}
+103
View File
@@ -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 &notificationRepository{
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)
}
+199
View File
@@ -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 &notificationService{
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
}