diff --git a/cmd/web/bootstrap/bootstrap.go b/cmd/web/bootstrap/bootstrap.go index faf9c87..c2d2bbb 100644 --- a/cmd/web/bootstrap/bootstrap.go +++ b/cmd/web/bootstrap/bootstrap.go @@ -88,7 +88,7 @@ func InitHTTPServer(conf Config, log logger.Logger) *echo.Echo { e.Use(middleware.Logger()) e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"*"}, - AllowMethods: []string{echo.GET, echo.POST, echo.PUT, echo.DELETE, echo.OPTIONS}, + AllowMethods: []string{echo.GET, echo.POST, echo.PUT, echo.DELETE, echo.OPTIONS, echo.PATCH}, })) // Add custom middleware for logging diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 828c896..bcd2ec0 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -1460,6 +1460,70 @@ const docTemplate = `{ } } }, + "/admin/v1/customers/{id}/companies": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Assign companies to a customer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Customers" + ], + "summary": "Assign companies to a customer", + "parameters": [ + { + "type": "string", + "description": "Customer ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Companies to assign", + "name": "companies", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.AssignCompaniesForm" + } + } + ], + "responses": { + "200": { + "description": "Companies assigned successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "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}/role": { "patch": { "security": [ @@ -2387,112 +2451,6 @@ 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": [ @@ -2538,173 +2496,6 @@ const docTemplate = `{ } } }, - "/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": [ @@ -6898,6 +6689,17 @@ const docTemplate = `{ } } }, + "customer.AssignCompaniesForm": { + "type": "object", + "properties": { + "companies": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "customer.AssignRoleForm": { "type": "object", "properties": { @@ -6951,7 +6753,7 @@ const docTemplate = `{ "customer.CreateCustomerForm": { "type": "object", "properties": { - "company_ids": { + "companies": { "type": "array", "items": { "type": "string" @@ -7007,12 +6809,6 @@ const docTemplate = `{ "$ref": "#/definitions/customer.CompanySummary" } }, - "company_ids": { - "type": "array", - "items": { - "type": "string" - } - }, "created_at": { "type": "integer" }, @@ -7124,7 +6920,7 @@ const docTemplate = `{ "customer.UpdateCustomerForm": { "type": "object", "properties": { - "company_ids": { + "companies": { "type": "array", "items": { "type": "string" @@ -7433,46 +7229,8 @@ const docTemplate = `{ "type": "string" } }, - "schedule": { - "$ref": "#/definitions/notification.ScheduleRequest" - }, - "target": { - "$ref": "#/definitions/notification.TargetAudienceType" - }, - "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" + "schedule_at": { + "type": "integer" }, "target": { "$ref": "#/definitions/notification.TargetAudienceType" @@ -7527,33 +7285,6 @@ const docTemplate = `{ "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" - } - } - }, "notification.TargetAudienceType": { "type": "string", "enum": [ diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index 2528c5a..375d454 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -1454,6 +1454,70 @@ } } }, + "/admin/v1/customers/{id}/companies": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Assign companies to a customer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Customers" + ], + "summary": "Assign companies to a customer", + "parameters": [ + { + "type": "string", + "description": "Customer ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Companies to assign", + "name": "companies", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.AssignCompaniesForm" + } + } + ], + "responses": { + "200": { + "description": "Companies assigned successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "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}/role": { "patch": { "security": [ @@ -2381,112 +2445,6 @@ } }, "/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": [ @@ -2532,173 +2490,6 @@ } } }, - "/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": [ @@ -6892,6 +6683,17 @@ } } }, + "customer.AssignCompaniesForm": { + "type": "object", + "properties": { + "companies": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "customer.AssignRoleForm": { "type": "object", "properties": { @@ -6945,7 +6747,7 @@ "customer.CreateCustomerForm": { "type": "object", "properties": { - "company_ids": { + "companies": { "type": "array", "items": { "type": "string" @@ -7001,12 +6803,6 @@ "$ref": "#/definitions/customer.CompanySummary" } }, - "company_ids": { - "type": "array", - "items": { - "type": "string" - } - }, "created_at": { "type": "integer" }, @@ -7118,7 +6914,7 @@ "customer.UpdateCustomerForm": { "type": "object", "properties": { - "company_ids": { + "companies": { "type": "array", "items": { "type": "string" @@ -7427,46 +7223,8 @@ "type": "string" } }, - "schedule": { - "$ref": "#/definitions/notification.ScheduleRequest" - }, - "target": { - "$ref": "#/definitions/notification.TargetAudienceType" - }, - "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" + "schedule_at": { + "type": "integer" }, "target": { "$ref": "#/definitions/notification.TargetAudienceType" @@ -7521,33 +7279,6 @@ "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" - } - } - }, "notification.TargetAudienceType": { "type": "string", "enum": [ diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index c3d937d..c9085eb 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -303,6 +303,13 @@ definitions: updated_at: type: integer type: object + customer.AssignCompaniesForm: + properties: + companies: + items: + type: string + type: array + type: object customer.AssignRoleForm: properties: role: @@ -338,7 +345,7 @@ definitions: type: object customer.CreateCustomerForm: properties: - company_ids: + companies: items: type: string type: array @@ -377,10 +384,6 @@ definitions: items: $ref: '#/definitions/customer.CompanySummary' type: array - company_ids: - items: - type: string - type: array created_at: type: integer email: @@ -456,7 +459,7 @@ definitions: type: object customer.UpdateCustomerForm: properties: - company_ids: + companies: items: type: string type: array @@ -667,33 +670,8 @@ definitions: items: type: string type: array - schedule: - $ref: '#/definitions/notification.ScheduleRequest' - target: - $ref: '#/definitions/notification.TargetAudienceType' - 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' + schedule_at: + type: integer target: $ref: '#/definitions/notification.TargetAudienceType' tender: @@ -733,23 +711,6 @@ definitions: - 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 notification.TargetAudienceType: enum: - all_users @@ -2149,6 +2110,47 @@ paths: summary: Update customer information tags: - Admin-Customers + /admin/v1/customers/{id}/companies: + patch: + consumes: + - application/json + description: Assign companies to a customer + parameters: + - description: Customer ID + in: path + name: id + required: true + type: string + - description: Companies to assign + in: body + name: companies + required: true + schema: + $ref: '#/definitions/customer.AssignCompaniesForm' + produces: + - application/json + responses: + "200": + description: Companies assigned successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid input data + 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 companies to a customer + tags: + - Admin-Customers /admin/v1/customers/{id}/role: patch: consumes: @@ -2724,75 +2726,6 @@ paths: 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 @@ -2822,114 +2755,6 @@ paths: 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: diff --git a/cmd/web/main.go b/cmd/web/main.go index 1a8bdef..03a336e 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -142,9 +142,8 @@ 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", "notification"}, + "repositories": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry"}, }) // Initialize validation service @@ -160,7 +159,7 @@ 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) + notificationService := notification.NewNotificationService(notificationSDK, userService, customerService, logger) logger.Info("Services initialized successfully", map[string]interface{}{ "services": []string{"customer", "user", "company", "category", "tender", "feedback", "tender_approval", "inquiry", "flag", "notification"}, }) diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index 4ed6820..e4e547c 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -132,19 +132,11 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler } // 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) - // } + notificationsGP := adminV1.Group("/notifications") + { + // notificationsGP.Use(userHandler.AuthMiddleware()) + notificationsGP.POST("", notificationHandler.Send) + } } 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) { diff --git a/internal/customer/form.go b/internal/customer/form.go index de42288..32686a5 100644 --- a/internal/customer/form.go +++ b/internal/customer/form.go @@ -60,6 +60,7 @@ type CustomerResponse struct { CreatedAt int64 `json:"created_at"` LastLoginAt *int64 `json:"last_login_at"` Companies []*CompanySummary `json:"companies"` + DeviceToken []string `json:"-"` } // ToResponse converts Customer to CustomerResponse diff --git a/internal/customer/repository.go b/internal/customer/repository.go index 42dce33..aa93226 100644 --- a/internal/customer/repository.go +++ b/internal/customer/repository.go @@ -20,6 +20,9 @@ type Repository interface { GetByID(ctx context.Context, id string) (*Customer, error) GetByEmail(ctx context.Context, email string) (*Customer, error) GetByUsername(ctx context.Context, username string) (*Customer, error) + GetByIDs(ctx context.Context, ids []string) ([]Customer, error) + GetByRole(ctx context.Context, role CustomerRole) ([]Customer, error) + GetByCompanies(ctx context.Context, companies []string) ([]Customer, error) GetByCompanyID(ctx context.Context, companyID string, pagination *response.Pagination) ([]*Customer, int64, error) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) ([]Customer, int64, error) Delete(ctx context.Context, id string) error @@ -168,6 +171,48 @@ func (r *customerRepository) GetByUsername(ctx context.Context, username string) return customer, nil } +// GetByIDs retrieves customers by IDs +func (r *customerRepository) GetByIDs(ctx context.Context, ids []string) ([]Customer, error) { + customers, err := r.ormRepo.FindAll(ctx, bson.M{"_id": bson.M{"$in": ids}}, orm.NewPaginationBuilder().Limit(len(ids)).Build()) + if err != nil { + if errors.Is(err, orm.ErrDocumentNotFound) { + return nil, errors.New("customers not found") + } + r.logger.Error("Failed to get customers by IDs", map[string]interface{}{ + "error": err.Error(), + "ids": ids, + }) + return nil, err + } + return customers.Items, nil +} + +// GetByRole retrieves customers by role +func (r *customerRepository) GetByRole(ctx context.Context, role CustomerRole) ([]Customer, error) { + customers, err := r.ormRepo.FindAll(ctx, bson.M{"role": role}, orm.NewPaginationBuilder().Build()) + if err != nil { + if errors.Is(err, orm.ErrDocumentNotFound) { + return nil, errors.New("customers not found") + } + r.logger.Error("Failed to get customers by role", map[string]interface{}{ + "error": err.Error(), + "role": role, + }) + return nil, err + } + return customers.Items, nil +} + +// GetByCompanies retrieves customers by companies + +func (r *customerRepository) GetByCompanies(ctx context.Context, companies []string) ([]Customer, error) { + customers, err := r.ormRepo.FindAll(ctx, bson.M{"companies": bson.M{"$in": companies}}, orm.NewPaginationBuilder().Build()) + if err != nil { + return nil, err + } + return customers.Items, nil +} + // GetByCompanyID retrieves customers by company ID with pagination func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID string, pagination *response.Pagination) ([]*Customer, int64, error) { // Build pagination diff --git a/internal/customer/service.go b/internal/customer/service.go index d95b62e..e2d0280 100644 --- a/internal/customer/service.go +++ b/internal/customer/service.go @@ -29,6 +29,9 @@ type Service interface { Register(ctx context.Context, form *CreateCustomerForm) (*CustomerResponse, error) GetByID(ctx context.Context, id string) (*CustomerResponse, error) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*CustomerListResponse, error) + GetCustomersByIDs(ctx context.Context, ids []string) (*CustomerListResponse, error) + GetCustomersByRole(ctx context.Context, role string) (*CustomerListResponse, error) + GetCustomersByCompanies(ctx context.Context, companies []string) (*CustomerListResponse, error) Update(ctx context.Context, id string, form *UpdateCustomerForm) (*CustomerResponse, error) Delete(ctx context.Context, id string) error UpdateStatus(ctx context.Context, id string, form *UpdateStatusForm) error @@ -206,6 +209,65 @@ func (s *customerService) Search(ctx context.Context, form *SearchCustomersForm, }, nil } +// GetCustomersByIDs retrieves customers by IDs +func (s *customerService) GetCustomersByIDs(ctx context.Context, ids []string) (*CustomerListResponse, error) { + customers, err := s.repository.GetByIDs(ctx, ids) + if err != nil { + return nil, err + } + + var customerResponses []*CustomerResponse + for _, customer := range customers { + customerResponses = append(customerResponses, customer.ToResponse(nil)) + } + + return &CustomerListResponse{ + Customers: customerResponses, + }, nil +} + +// GetCustomersByRole retrieves customers by role +func (s *customerService) GetCustomersByRole(ctx context.Context, role string) (*CustomerListResponse, error) { + customers, err := s.repository.GetByRole(ctx, CustomerRole(role)) + if err != nil { + s.logger.Error("Failed to get customers by role", map[string]interface{}{ + "error": err.Error(), + "role": role, + }) + return nil, err + } + + var customerResponses []*CustomerResponse + for _, customer := range customers { + customerResponses = append(customerResponses, customer.ToResponse(nil)) + } + + return &CustomerListResponse{ + Customers: customerResponses, + }, nil +} + +// GetCustomersByCompanies retrieves customers by companies +func (s *customerService) GetCustomersByCompanies(ctx context.Context, companies []string) (*CustomerListResponse, error) { + customers, err := s.repository.GetByCompanies(ctx, companies) + if err != nil { + s.logger.Error("Failed to get customers by companies", map[string]interface{}{ + "error": err.Error(), + "companies": companies, + }) + return nil, err + } + + var customerResponses []*CustomerResponse + for _, customer := range customers { + customerResponses = append(customerResponses, customer.ToResponse(nil)) + } + + return &CustomerListResponse{ + Customers: customerResponses, + }, nil +} + // Update updates a customer func (s *customerService) Update(ctx context.Context, id string, form *UpdateCustomerForm) (*CustomerResponse, error) { // Get existing customer diff --git a/internal/notification/entity.go b/internal/notification/entity.go index 4ec3e1a..d835ddd 100644 --- a/internal/notification/entity.go +++ b/internal/notification/entity.go @@ -1,11 +1,5 @@ package notification -import ( - "tm/pkg/mongo" - - "go.mongodb.org/mongo-driver/v2/bson" -) - // NotificationType represents the type of notification type NotificationType string @@ -65,46 +59,8 @@ const ( 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 +type notificationRecipient struct { + UserID string + Email string + DeviceTokens []string } diff --git a/internal/notification/form.go b/internal/notification/form.go index 588c0e3..ae59705 100644 --- a/internal/notification/form.go +++ b/internal/notification/form.go @@ -1,70 +1,26 @@ 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)"` + Recipient []string `json:"recipient"` + Tender string `json:"tender" valid:"optional,stringlength(1|100)"` + Channels []DeliveryChannel `json:"channels" valid:"required"` + Type NotificationType `json:"type" valid:"required"` + Priority NotificationPriority `json:"priority" valid:"required"` + Target TargetAudienceType `json:"target" valid:"required"` + Title string `json:"title" valid:"required"` + Description string `json:"description" valid:"required"` 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, - }, - } + ScheduleAt int64 `json:"schedule_at" valid:"optional"` } // SearchForm represents the form for searching notifications type SearchForm struct { - Search *string `query:"q" valid:"optional"` + Search *string `query:"q" valid:"optional"` + Status NotificationStatus `query:"status" valid:"optional"` + + SortBy *string `query:"sort_by" valid:"optional"` + SortOrder *string `query:"sort_order" valid:"optional"` + Limit *int `query:"limit" valid:"optional"` + Offset *int `query:"offset" valid:"optional"` } diff --git a/internal/notification/handler.go b/internal/notification/handler.go index d1564b5..e8b754d 100644 --- a/internal/notification/handler.go +++ b/internal/notification/handler.go @@ -10,12 +10,12 @@ import ( // NotificationHandler handles HTTP requests for notification operations type NotificationHandler struct { - service NotificationService + service Service logger logger.Logger } // NewNotificationHandler creates a new notification handler -func NewHandler(service NotificationService, logger logger.Logger) *NotificationHandler { +func NewHandler(service Service, logger logger.Logger) *NotificationHandler { return &NotificationHandler{ service: service, logger: logger, @@ -33,7 +33,7 @@ func NewHandler(service NotificationService, logger logger.Logger) *Notification // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /admin/v1/notifications [post] -func (h *NotificationHandler) Create(c echo.Context) error { +func (h *NotificationHandler) Send(c echo.Context) error { var req NotificationRequest if err := c.Bind(&req); err != nil { return response.BadRequest(c, "Invalid request format", err.Error()) @@ -44,145 +44,10 @@ func (h *NotificationHandler) Create(c echo.Context) error { 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") + // Send notification + if err := h.service.Send(c.Request().Context(), &req); err != nil { + return response.InternalServerError(c, "Failed to send 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") + return response.Created(c, nil, "Notification sent successfully") } diff --git a/internal/notification/repository.go b/internal/notification/repository.go deleted file mode 100644 index 4b70765..0000000 --- a/internal/notification/repository.go +++ /dev/null @@ -1,103 +0,0 @@ -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 index 0902611..385b0e3 100644 --- a/internal/notification/service.go +++ b/internal/notification/service.go @@ -3,197 +3,242 @@ package notification import ( "context" "errors" - "fmt" + "tm/internal/customer" + "tm/internal/user" "tm/pkg/logger" + "tm/pkg/notification" "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 +type Service interface { + Send(ctx context.Context, req *NotificationRequest) error + getUsers(ctx context.Context, userIDs []string) ([]notificationRecipient, error) + getCustomers(ctx context.Context, values []string, target string) ([]notificationRecipient, error) } // notificationService implements NotificationService interface type notificationService struct { - repository NotificationRepository - logger logger.Logger + sdk notification.SDK + userService user.Service + customerService customer.Service + logger logger.Logger } // NewNotificationService creates a new notification service func NewNotificationService( - repository NotificationRepository, + sdk notification.SDK, + userService user.Service, + customerService customer.Service, logger logger.Logger, -) NotificationService { +) Service { return ¬ificationService{ - repository: repository, - logger: logger, + sdk: sdk, + userService: userService, + customerService: customerService, + 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{}{ +// Send sends a multiple notification +func (s *notificationService) Send(ctx context.Context, req *NotificationRequest) error { + s.logger.Info("Sending 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, - }, - } + if req.Target == TargetAudienceAllUsers || req.Target == TargetAudienceSpecificUsers { + recipients, err := s.getUsers(ctx, req.Recipient) + if err != nil { + return err + } + for _, recipient := range recipients { + for _, v := range recipient.DeviceTokens { + s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{ + UserID: recipient.UserID, + Title: req.Title, + Message: req.Description, + Type: string(req.Type), + Priority: string(req.Priority), + EventType: notification.EventTypePush, + Metadata: map[string]any{ + "tender": req.Tender, + }, + Methods: notification.NotificationMethods{ + Push: v, + }, - // Create notification in repository - if err := s.repository.Create(ctx, notification); err != nil { - s.logger.Error("Failed to create notification", map[string]interface{}{ + ScheduledAt: req.ScheduleAt, + }) + } + + if recipient.Email != "" { + s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{ + UserID: recipient.UserID, + Title: req.Title, + Message: req.Description, + Type: string(req.Type), + Priority: string(req.Priority), + EventType: notification.EventTypeEmail, + Metadata: map[string]any{ + "tender": req.Tender, + }, + Methods: notification.NotificationMethods{ + Email: recipient.Email, + }, + ScheduledAt: req.ScheduleAt, + }) + } + } + } else { + recipients, err := s.getCustomers(ctx, req.Recipient, string(req.Target)) + if err != nil { + return err + } + for _, recipient := range recipients { + for _, v := range recipient.DeviceTokens { + s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{ + UserID: recipient.UserID, + Title: req.Title, + Message: req.Description, + Type: string(req.Type), + Priority: string(req.Priority), + EventType: notification.EventTypePush, + Methods: notification.NotificationMethods{ + Push: v, + }, + ScheduledAt: req.ScheduleAt, + }) + } + + if recipient.Email != "" { + s.sdk.SendNotification(ctx, ¬ification.NotificationRequest{ + UserID: recipient.UserID, + Title: req.Title, + Message: req.Description, + Type: string(req.Type), + Priority: string(req.Priority), + EventType: notification.EventTypeEmail, + Methods: notification.NotificationMethods{ + Email: recipient.Email, + }, + ScheduledAt: req.ScheduleAt, + }) + } + } + s.logger.Info("Notification sent", map[string]interface{}{ "recipient": req.Recipient, - "error": err.Error(), + "channels": req.Channels, }) - return nil, fmt.Errorf("failed to create notification") + + return nil } - - 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 } + +func (s *notificationService) getUsers(ctx context.Context, userIDs []string) ([]notificationRecipient, error) { + recipients := make([]notificationRecipient, 0) + offset := 0 + for { + var users *user.UserListResponse + var err error + if len(userIDs) > 0 { + users, err = s.userService.GetUsersByIDs(ctx, userIDs) + if err != nil { + return nil, err + } + } else { + users, err = s.userService.Search(ctx, &user.SearchUsersForm{}, &response.Pagination{Limit: 100, Offset: offset}) + if err != nil { + return nil, err + } + offset += 100 + if len(users.Users) == 0 { + break + } + } + + for _, user := range users.Users { + recipients = append(recipients, notificationRecipient{ + UserID: user.ID, + Email: user.Email, + DeviceTokens: user.DeviceToken, + }) + } + + if len(recipients) == 0 { + return nil, errors.New("no users found") + } + } + + return recipients, nil +} + +func (s *notificationService) getCustomers(ctx context.Context, values []string, target string) ([]notificationRecipient, error) { + recipients := make([]notificationRecipient, 0) + + if target == string(TargetAudienceSpecificRole) { + customers, err := s.customerService.GetCustomersByRole(ctx, values[0]) + if err != nil { + return nil, err + } + for _, customer := range customers.Customers { + recipients = append(recipients, notificationRecipient{ + UserID: customer.ID, + Email: customer.Email, + DeviceTokens: customer.DeviceToken, + }) + } + } else if target == string(TargetAudienceSpecificCompany) { + customers, err := s.customerService.GetCustomersByCompanies(ctx, values) + if err != nil { + return nil, err + } + for _, customer := range customers.Customers { + recipients = append(recipients, notificationRecipient{ + UserID: customer.ID, + Email: customer.Email, + DeviceTokens: customer.DeviceToken, + }) + } + } else if target == string(TargetAudienceSpecificCustomer) { + customers, err := s.customerService.GetCustomersByIDs(ctx, values) + if err != nil { + return nil, err + } + for _, customer := range customers.Customers { + recipients = append(recipients, notificationRecipient{ + UserID: customer.ID, + Email: customer.Email, + DeviceTokens: customer.DeviceToken, + }) + } + } else { + offset := 0 + for { + customers, err := s.customerService.Search(ctx, &customer.SearchCustomersForm{}, &response.Pagination{Limit: 100, Offset: offset}) + if err != nil { + return nil, err + } + for _, customer := range customers.Customers { + recipients = append(recipients, notificationRecipient{ + UserID: customer.ID, + Email: customer.Email, + DeviceTokens: customer.DeviceToken, + }) + } + offset += 100 + if len(customers.Customers) == 0 { + break + } + } + } + + if len(recipients) == 0 { + return nil, errors.New("no customers found") + } + + return recipients, nil +} diff --git a/internal/user/form.go b/internal/user/form.go index 3569f31..dce0e6f 100644 --- a/internal/user/form.go +++ b/internal/user/form.go @@ -85,20 +85,21 @@ type UpdateUserRoleForm struct { // Response DTOs type UserResponse struct { - ID string `json:"id"` - FullName string `json:"full_name"` - Username string `json:"username"` - Email string `json:"email"` - Role string `json:"role"` - Status string `json:"status"` - Department *string `json:"department"` - Position *string `json:"position"` - Phone *string `json:"phone"` - ProfileImage *string `json:"profile_image"` - IsVerified bool `json:"is_verified"` - LastLoginAt *int64 `json:"last_login_at"` - UpdatedAt int64 `json:"updated_at"` - CreatedAt int64 `json:"created_at"` + ID string `json:"id"` + FullName string `json:"full_name"` + Username string `json:"username"` + Email string `json:"email"` + Role string `json:"role"` + Status string `json:"status"` + Department *string `json:"department"` + Position *string `json:"position"` + Phone *string `json:"phone"` + ProfileImage *string `json:"profile_image"` + IsVerified bool `json:"is_verified"` + LastLoginAt *int64 `json:"last_login_at"` + UpdatedAt int64 `json:"updated_at"` + CreatedAt int64 `json:"created_at"` + DeviceToken []string `json:"-"` } type AuthResponse struct { diff --git a/internal/user/repository.go b/internal/user/repository.go index 8444f41..bd50b12 100644 --- a/internal/user/repository.go +++ b/internal/user/repository.go @@ -20,6 +20,7 @@ type Repository interface { GetByID(ctx context.Context, id string) (*User, error) GetByEmail(ctx context.Context, email string) (*User, error) GetByUsername(ctx context.Context, username string) (*User, error) + GetByIDs(ctx context.Context, userIDs []string) ([]User, error) Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) ([]User, int64, error) } @@ -215,6 +216,16 @@ func (r *userRepository) Delete(ctx context.Context, id string) error { return nil } +// GetByIDs retrieves users by IDs +func (r *userRepository) GetByIDs(ctx context.Context, userIDs []string) ([]User, error) { + users, err := r.ormRepo.FindAll(ctx, bson.M{"_id": bson.M{"$in": userIDs}}, orm.NewPaginationBuilder().Limit(len(userIDs)).Build()) + if err != nil { + return nil, err + } + + return users.Items, nil +} + // List retrieves users with pagination func (r *userRepository) Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) ([]User, int64, error) { filter := bson.M{} diff --git a/internal/user/service.go b/internal/user/service.go index cccc43f..722321e 100644 --- a/internal/user/service.go +++ b/internal/user/service.go @@ -23,6 +23,7 @@ type Service interface { Update(ctx context.Context, id string, form *UpdateUserForm) (*UserResponse, error) UpdateStatus(ctx context.Context, id string, form *UpdateUserStatusForm) error GetUserByID(ctx context.Context, userID string) (*UserResponse, error) + GetUsersByIDs(ctx context.Context, userIDs []string) (*UserListResponse, error) Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*UserListResponse, error) Delete(ctx context.Context, id string) error @@ -250,6 +251,23 @@ func (s *userService) GetUserByID(ctx context.Context, id string) (*UserResponse return user.ToResponse(), nil } +// GetUsersByIDs retrieves users by IDs +func (s *userService) GetUsersByIDs(ctx context.Context, userIDs []string) (*UserListResponse, error) { + users, err := s.repository.GetByIDs(ctx, userIDs) + if err != nil { + return nil, err + } + + var userResponses []*UserResponse + for _, user := range users { + userResponses = append(userResponses, user.ToResponse()) + } + + return &UserListResponse{ + Users: userResponses, + }, nil +} + // Search searches users with search and filters func (s *userService) Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*UserListResponse, error) { // Get users diff --git a/pkg/notification/entities.go b/pkg/notification/entities.go index d9a191a..61a8581 100644 --- a/pkg/notification/entities.go +++ b/pkg/notification/entities.go @@ -31,6 +31,7 @@ type NotificationRequest struct { EventType EventType `json:"event_type" valid:"required,in(EMAIL|SMS|PUSH|OTP)"` Priority string `json:"priority" valid:"required,in(low|medium|important)"` ScheduledAt int64 `json:"scheduled_at"` + Metadata map[string]any `json:"metadata,omitempty" valid:"optional"` Methods NotificationMethods `json:"methods" valid:"required"` }