From 5906904cafceff8c21f539e0223800e6477002c4 Mon Sep 17 00:00:00 2001 From: "n.nakhostin" Date: Sun, 21 Sep 2025 11:26:54 +0330 Subject: [PATCH] Enhance Notification System with New API Endpoints and Response Structures - Added new endpoints for marking notifications as seen and retrieving detailed notification information for both admin and user contexts, improving the flexibility of the notification system. - Implemented the MarkSeen and ViewNotification methods in the NotificationHandler, ensuring proper handling of notification visibility and details retrieval. - Updated the NotificationService interface to include methods for getting notification details and marking notifications as seen, enhancing service capabilities. - Enhanced the notification response structures to include additional fields such as Image, Seen, and ScheduledAt, providing richer data in API responses. - Updated API documentation with Swagger comments for the new endpoints and response formats, ensuring clarity for API consumers. --- cmd/web/docs/docs.go | 332 ++++++++++++++++++++++++++++++- cmd/web/docs/swagger.json | 332 ++++++++++++++++++++++++++++++- cmd/web/docs/swagger.yaml | 211 +++++++++++++++++++- cmd/web/router/routes.go | 4 + internal/notification/form.go | 16 +- internal/notification/handler.go | 120 ++++++++++- internal/notification/service.go | 105 +++++++++- pkg/notification/client.go | 70 ++++++- pkg/notification/entities.go | 42 +++- pkg/notification/interfaces.go | 3 + pkg/notification/sdk.go | 5 + pkg/notification/service.go | 5 + 12 files changed, 1200 insertions(+), 45 deletions(-) diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 0ab7f27..8ff0e2b 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -2619,6 +2619,55 @@ const docTemplate = `{ } } }, + "/admin/v1/notifications/mark-seen/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark a notification as seen", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Mark a notification as seen", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/notifications/my": { "get": { "security": [ @@ -2739,6 +2788,73 @@ const docTemplate = `{ } } }, + "/admin/v1/notifications/view/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get detailed information about a specific notification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Get notification details", + "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" + } + } + } + } + }, "/admin/v1/profile": { "get": { "security": [ @@ -5257,7 +5373,7 @@ const docTemplate = `{ } } }, - "/api/v1/notifications/my": { + "/api/v1/notifications": { "get": { "security": [ { @@ -5377,6 +5493,122 @@ const docTemplate = `{ } } }, + "/api/v1/notifications/mark-seen/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark a notification as seen", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "Mark a notification as seen", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/notifications/view/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get detailed information about a specific notification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "Get notification details", + "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/notification.DetailedNotificationResponse" + } + } + } + ] + } + }, + "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" + } + } + } + } + }, "/api/v1/profile": { "get": { "security": [ @@ -7297,10 +7529,6 @@ const docTemplate = `{ "type": "string", "example": "User" }, - "password": { - "type": "string", - "example": "User!1234" - }, "phone": { "type": "string", "example": "+1234567890" @@ -7627,9 +7855,6 @@ const docTemplate = `{ "type": "object", "properties": { "created_at": { - "type": "integer" - }, - "description": { "type": "string" }, "event_type": { @@ -7638,9 +7863,18 @@ const docTemplate = `{ "id": { "type": "string" }, + "image": { + "type": "string" + }, + "is_scheduled": { + "type": "boolean" + }, "link": { "type": "string" }, + "message": { + "type": "string" + }, "metadata": { "type": "object", "additionalProperties": {} @@ -7657,6 +7891,15 @@ const docTemplate = `{ "schedule_at": { "type": "integer" }, + "scheduled_at": { + "type": "integer" + }, + "seen": { + "type": "boolean" + }, + "seen_at": { + "type": "integer" + }, "status": { "type": "string" }, @@ -7667,7 +7910,7 @@ const docTemplate = `{ "type": "string" }, "updated_at": { - "type": "integer" + "type": "string" }, "user_id": { "type": "string" @@ -7685,6 +7928,77 @@ const docTemplate = `{ "DeliveryChannelEmail" ] }, + "notification.DetailedNotificationResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "event_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "image": { + "type": "string" + }, + "is_scheduled": { + "type": "boolean" + }, + "link": { + "type": "string" + }, + "message": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": {} + }, + "methods": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "priority": { + "description": "low, medium, important", + "type": "string" + }, + "scheduled_at": { + "type": "integer" + }, + "seen": { + "type": "boolean" + }, + "seen_at": { + "type": "integer" + }, + "status": { + "description": "pending, sent, failed", + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "description": "info, warning, alert, reject", + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + } + } + }, "notification.NotificationPriority": { "type": "string", "enum": [ diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index b2afa29..cf8592f 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -2613,6 +2613,55 @@ } } }, + "/admin/v1/notifications/mark-seen/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark a notification as seen", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Mark a notification as seen", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/notifications/my": { "get": { "security": [ @@ -2733,6 +2782,73 @@ } } }, + "/admin/v1/notifications/view/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get detailed information about a specific notification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Notification" + ], + "summary": "Get notification details", + "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" + } + } + } + } + }, "/admin/v1/profile": { "get": { "security": [ @@ -5251,7 +5367,7 @@ } } }, - "/api/v1/notifications/my": { + "/api/v1/notifications": { "get": { "security": [ { @@ -5371,6 +5487,122 @@ } } }, + "/api/v1/notifications/mark-seen/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark a notification as seen", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "Mark a notification as seen", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/notifications/view/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get detailed information about a specific notification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Notification" + ], + "summary": "Get notification details", + "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/notification.DetailedNotificationResponse" + } + } + } + ] + } + }, + "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" + } + } + } + } + }, "/api/v1/profile": { "get": { "security": [ @@ -7291,10 +7523,6 @@ "type": "string", "example": "User" }, - "password": { - "type": "string", - "example": "User!1234" - }, "phone": { "type": "string", "example": "+1234567890" @@ -7621,9 +7849,6 @@ "type": "object", "properties": { "created_at": { - "type": "integer" - }, - "description": { "type": "string" }, "event_type": { @@ -7632,9 +7857,18 @@ "id": { "type": "string" }, + "image": { + "type": "string" + }, + "is_scheduled": { + "type": "boolean" + }, "link": { "type": "string" }, + "message": { + "type": "string" + }, "metadata": { "type": "object", "additionalProperties": {} @@ -7651,6 +7885,15 @@ "schedule_at": { "type": "integer" }, + "scheduled_at": { + "type": "integer" + }, + "seen": { + "type": "boolean" + }, + "seen_at": { + "type": "integer" + }, "status": { "type": "string" }, @@ -7661,7 +7904,7 @@ "type": "string" }, "updated_at": { - "type": "integer" + "type": "string" }, "user_id": { "type": "string" @@ -7679,6 +7922,77 @@ "DeliveryChannelEmail" ] }, + "notification.DetailedNotificationResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "event_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "image": { + "type": "string" + }, + "is_scheduled": { + "type": "boolean" + }, + "link": { + "type": "string" + }, + "message": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": {} + }, + "methods": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "priority": { + "description": "low, medium, important", + "type": "string" + }, + "scheduled_at": { + "type": "integer" + }, + "seen": { + "type": "boolean" + }, + "seen_at": { + "type": "integer" + }, + "status": { + "description": "pending, sent, failed", + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "description": "info, warning, alert, reject", + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + } + } + }, "notification.NotificationPriority": { "type": "string", "enum": [ diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index 4aa57d6..b1e6bfd 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -469,9 +469,6 @@ definitions: full_name: example: User type: string - password: - example: User!1234 - type: string phone: example: "+1234567890" type: string @@ -693,15 +690,19 @@ definitions: internal_notification.NotificationResponse: properties: created_at: - type: integer - description: type: string event_type: type: string id: type: string + image: + type: string + is_scheduled: + type: boolean link: type: string + message: + type: string metadata: additionalProperties: {} type: object @@ -713,6 +714,12 @@ definitions: type: string schedule_at: type: integer + scheduled_at: + type: integer + seen: + type: boolean + seen_at: + type: integer status: type: string title: @@ -720,7 +727,7 @@ definitions: type: type: string updated_at: - type: integer + type: string user_id: type: string type: object @@ -732,6 +739,54 @@ definitions: x-enum-varnames: - DeliveryChannelPush - DeliveryChannelEmail + notification.DetailedNotificationResponse: + properties: + data: + properties: + created_at: + type: string + event_type: + type: string + id: + type: string + image: + type: string + is_scheduled: + type: boolean + link: + type: string + message: + type: string + metadata: + additionalProperties: {} + type: object + methods: + additionalProperties: + type: string + type: object + priority: + description: low, medium, important + type: string + scheduled_at: + type: integer + seen: + type: boolean + seen_at: + type: integer + status: + description: pending, sent, failed + type: string + title: + type: string + type: + description: info, warning, alert, reject + type: string + updated_at: + type: string + user_id: + type: string + type: object + type: object notification.NotificationPriority: enum: - low @@ -2874,6 +2929,37 @@ paths: summary: Create a new notification tags: - Admin-Notification + /admin/v1/notifications/mark-seen/{id}: + get: + consumes: + - application/json + description: Mark a notification as seen + parameters: + - description: Notification ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Mark a notification as seen + tags: + - Admin-Notification /admin/v1/notifications/my: get: consumes: @@ -2949,6 +3035,46 @@ paths: summary: Get notifications for the admin tags: - Admin-Notification + /admin/v1/notifications/view/{id}: + get: + consumes: + - application/json + description: Get detailed information about a specific notification + 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' + security: + - BearerAuth: [] + summary: Get notification details + tags: + - Admin-Notification /admin/v1/profile: get: consumes: @@ -4518,7 +4644,7 @@ paths: summary: Create new inquiry tags: - Inquiries - /api/v1/notifications/my: + /api/v1/notifications: get: consumes: - application/json @@ -4593,6 +4719,77 @@ paths: summary: Get notifications for the user tags: - Notification + /api/v1/notifications/mark-seen/{id}: + get: + consumes: + - application/json + description: Mark a notification as seen + parameters: + - description: Notification ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Mark a notification as seen + tags: + - Notification + /api/v1/notifications/view/{id}: + get: + consumes: + - application/json + description: Get detailed information about a specific notification + 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/notification.DetailedNotificationResponse' + 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' + security: + - BearerAuth: [] + summary: Get notification details + tags: + - Notification /api/v1/profile: get: consumes: diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index cf40f46..c5528db 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -137,6 +137,8 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler notificationsGP.Use(userHandler.AuthMiddleware()) notificationsGP.POST("", notificationHandler.Send) notificationsGP.GET("", notificationHandler.GetNotifications) + notificationsGP.GET("/view/:id", notificationHandler.ViewNotification) + notificationsGP.GET("/mark-seen/:id", notificationHandler.MarkSeen) notificationsGP.GET("/my", notificationHandler.AdminNotifications) } } @@ -215,5 +217,7 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende { notificationsGP.Use(customerHandler.AuthMiddleware()) notificationsGP.GET("", notificationHandler.CustomerNotifications) + notificationsGP.GET("/view/:id", notificationHandler.PublicViewNotification) + notificationsGP.GET("mark-seen/:id", notificationHandler.PublicMarkSeen) } } diff --git a/internal/notification/form.go b/internal/notification/form.go index 113c89e..39a19fc 100644 --- a/internal/notification/form.go +++ b/internal/notification/form.go @@ -1,6 +1,9 @@ package notification -import "tm/pkg/response" +import ( + "time" + "tm/pkg/response" +) // CreateRequest represents the request to create a notification type NotificationRequest struct { @@ -34,8 +37,9 @@ type NotificationResponse struct { ID string `json:"id"` UserID string `json:"user_id"` Title string `json:"title"` - Description string `json:"description"` + Message string `json:"message"` Link *string `json:"link"` + Image *string `json:"image"` Priority string `json:"priority"` Type string `json:"type"` EventType string `json:"event_type"` @@ -43,6 +47,10 @@ type NotificationResponse struct { Methods map[string]string `json:"methods"` Metadata map[string]any `json:"metadata"` ScheduleAt int64 `json:"schedule_at"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Seen bool `json:"seen"` + SeenAt int64 `json:"seen_at"` + IsScheduled bool `json:"is_scheduled"` + ScheduledAt int64 `json:"scheduled_at"` } diff --git a/internal/notification/handler.go b/internal/notification/handler.go index 00f5b01..9ba7c00 100644 --- a/internal/notification/handler.go +++ b/internal/notification/handler.go @@ -47,7 +47,7 @@ func (h *NotificationHandler) Send(c echo.Context) error { } // Send notification - go h.service.Send(c.Request().Context(), &req) + h.service.Send(c.Request().Context(), &req) return response.Created(c, nil, "Notification sent successfully") } @@ -148,6 +148,65 @@ func (h *NotificationHandler) AdminNotifications(c echo.Context) error { return response.Success(c, notifications, "Notifications retrieved successfully") } +// MarkSeen marks a notification as seen +// @Summary Mark a notification as seen +// @Description Mark a notification as seen +// @Tags Admin-Notification +// @Accept json +// @Produce json +// @Param id path string true "Notification ID" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Security BearerAuth +// @Router /admin/v1/notifications/mark-seen/{id} [get] +func (h *NotificationHandler) MarkSeen(c echo.Context) error { + notificationID := c.Param("id") + + userID, err := user.GetUserIDFromContext(c) + if err != nil { + return response.BadRequest(c, "User ID required", "User must be associated with a user") + } + + // Mark notification as seen + err = h.service.MarkSeen(c.Request().Context(), notificationID, userID) + if err != nil { + return response.InternalServerError(c, "Failed to mark notification as seen") + } + + return response.Success(c, nil, "Notification marked as seen successfully") +} + +// ViewNotification gets a single notification by ID +// @Summary Get notification details +// @Description Get detailed information about a specific notification +// @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 +// @Security BearerAuth +// @Router /admin/v1/notifications/view/{id} [get] +func (h *NotificationHandler) ViewNotification(c echo.Context) error { + notificationID := c.Param("id") + if notificationID == "" { + return response.BadRequest(c, "notification ID is required", "Notification ID must be provided") + } + + // Get notification details + notification, err := h.service.GetNotification(c.Request().Context(), notificationID) + if err != nil { + return response.InternalServerError(c, "Failed to get notification details") + } + + return response.Success(c, notification, "Notification details retrieved successfully") +} + +// **************************** PUBLIC ENDPOINTS **************************** + // MyNotifications gets notifications for the user // @Summary Get notifications for the user // @Description Get notifications for the user @@ -169,7 +228,7 @@ func (h *NotificationHandler) AdminNotifications(c echo.Context) error { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Security BearerAuth -// @Router /api/v1/notifications/my [get] +// @Router /api/v1/notifications [get] func (h *NotificationHandler) CustomerNotifications(c echo.Context) error { userID, err := user.GetUserIDFromContext(c) if err != nil { @@ -199,3 +258,60 @@ func (h *NotificationHandler) CustomerNotifications(c echo.Context) error { return response.Success(c, notifications, "Notifications retrieved successfully") } + +// MarkSeen marks a notification as seen +// @Summary Mark a notification as seen +// @Description Mark a notification as seen +// @Tags Notification +// @Accept json +// @Produce json +// @Param id path string true "Notification ID" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Security BearerAuth +// @Router /api/v1/notifications/mark-seen/{id} [get] +func (h *NotificationHandler) PublicMarkSeen(c echo.Context) error { + notificationID := c.Param("id") + + userID, err := user.GetUserIDFromContext(c) + if err != nil { + return response.BadRequest(c, "User ID required", "User must be associated with a user") + } + + // Mark notification as seen + err = h.service.MarkSeen(c.Request().Context(), notificationID, userID) + if err != nil { + return response.InternalServerError(c, "Failed to mark notification as seen") + } + + return response.Success(c, nil, "Notification marked as seen successfully") +} + +// ViewNotification gets a single notification by ID +// @Summary Get notification details +// @Description Get detailed information about a specific notification +// @Tags Notification +// @Accept json +// @Produce json +// @Param id path string true "Notification ID" +// @Success 200 {object} response.APIResponse{data=DetailedNotificationResponse} +// @Failure 400 {object} response.APIResponse +// @Failure 404 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Security BearerAuth +// @Router /api/v1/notifications/view/{id} [get] +func (h *NotificationHandler) PublicViewNotification(c echo.Context) error { + notificationID := c.Param("id") + if notificationID == "" { + return response.BadRequest(c, "notification ID is required", "Notification ID must be provided") + } + + // Get notification details + notification, err := h.service.GetNotification(c.Request().Context(), notificationID) + if err != nil { + return response.InternalServerError(c, "Failed to get notification details") + } + + return response.Success(c, notification, "Notification details retrieved successfully") +} diff --git a/internal/notification/service.go b/internal/notification/service.go index 7e2e9c3..0625dbf 100644 --- a/internal/notification/service.go +++ b/internal/notification/service.go @@ -3,6 +3,8 @@ package notification import ( "context" "errors" + "strings" + "time" "tm/internal/customer" "tm/internal/user" @@ -15,6 +17,8 @@ import ( type Service interface { Send(ctx context.Context, req *NotificationRequest) error GetNotifications(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*NotificationListResponse, error) + GetNotification(ctx context.Context, notificationID string) (*NotificationResponse, error) + MarkSeen(ctx context.Context, notificationID, userID string) error getUsers(ctx context.Context, userIDs []string) ([]notificationRecipient, error) getCustomers(ctx context.Context, values []string, target string) ([]notificationRecipient, error) } @@ -65,7 +69,7 @@ func (s *notificationService) Send(ctx context.Context, req *NotificationRequest Priority: string(req.Priority), EventType: notification.EventTypePush, Metadata: map[string]any{ - "tender": req.Tender, + "tender": strings.TrimSpace(req.Tender), }, Methods: notification.NotificationMethods{ Push: v, @@ -84,7 +88,7 @@ func (s *notificationService) Send(ctx context.Context, req *NotificationRequest Priority: string(req.Priority), EventType: notification.EventTypeEmail, Metadata: map[string]any{ - "tender": req.Tender, + "tender": strings.TrimSpace(req.Tender), }, Methods: notification.NotificationMethods{ Email: recipient.Email, @@ -111,6 +115,9 @@ func (s *notificationService) Send(ctx context.Context, req *NotificationRequest Push: v, }, ScheduledAt: req.ScheduleAt, + Metadata: map[string]any{ + "tender": strings.TrimSpace(req.Tender), + }, }) } @@ -126,6 +133,9 @@ func (s *notificationService) Send(ctx context.Context, req *NotificationRequest Email: recipient.Email, }, ScheduledAt: req.ScheduleAt, + Metadata: map[string]any{ + "tender": strings.TrimSpace(req.Tender), + }, }) } } @@ -168,11 +178,19 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF notificationsResponse := make([]*NotificationResponse, 0) for _, notification := range notifications.Data { + createdAt, err := time.Parse(time.RFC3339, notification.CreatedAt) + if err != nil { + return nil, err + } + updatedAt, err := time.Parse(time.RFC3339, notification.UpdatedAt) + if err != nil { + return nil, err + } notificationsResponse = append(notificationsResponse, &NotificationResponse{ ID: notification.ID, UserID: notification.UserID, Title: notification.Title, - Description: notification.Message, + Message: notification.Message, Link: ¬ification.Link, Priority: notification.Priority, Type: notification.Type, @@ -181,6 +199,13 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF Methods: notification.Methods, Metadata: notification.Metadata, ScheduleAt: notification.ScheduledAt, + IsScheduled: notification.IsScheduled, + Image: ¬ification.Image, + Seen: notification.Seen, + SeenAt: notification.SeenAt, + ScheduledAt: notification.ScheduledAt, + CreatedAt: createdAt, + UpdatedAt: updatedAt, }) } @@ -196,6 +221,80 @@ func (s *notificationService) GetNotifications(ctx context.Context, req *SearchF }, nil } +func (s *notificationService) GetNotification(ctx context.Context, notificationID string) (*NotificationResponse, error) { + s.logger.Info("Getting notification", map[string]interface{}{ + "notification_id": notificationID, + }) + + notification, err := s.sdk.GetNotification(ctx, notificationID) + if err != nil { + s.logger.Error("Failed to get notification", map[string]interface{}{ + "error": err.Error(), + "notification_id": notificationID, + }) + return nil, err + } + + // Convert to detailed response format + var linkPtr *string + if notification.Data.Link != "" { + linkPtr = ¬ification.Data.Link + } + + var imagePtr *string + if notification.Data.Image != "" { + imagePtr = ¬ification.Data.Image + } + + detailedResponse := &NotificationResponse{ + ID: notification.Data.ID, + UserID: notification.Data.UserID, + Title: notification.Data.Title, + Message: notification.Data.Message, + Link: linkPtr, + Image: imagePtr, + Type: notification.Data.Type, + Priority: notification.Data.Priority, + EventType: notification.Data.EventType, + CreatedAt: notification.Data.CreatedAt, + UpdatedAt: notification.Data.UpdatedAt, + Methods: notification.Data.Methods, + Metadata: notification.Data.Metadata, + Status: notification.Data.Status, + Seen: notification.Data.Seen, + SeenAt: notification.Data.SeenAt, + ScheduledAt: notification.Data.ScheduledAt, + IsScheduled: notification.Data.IsScheduled, + } + + return detailedResponse, nil +} + +func (s *notificationService) MarkSeen(ctx context.Context, notificationID, userID string) error { + s.logger.Info("Marking notification as seen", map[string]interface{}{ + "notification_id": notificationID, + }) + + notif, err := s.sdk.GetNotification(ctx, notificationID) + if err != nil { + s.logger.Error("Failed to get notification", map[string]interface{}{ + "error": err.Error(), + "notification_id": notificationID, + }) + return err + } + + if notif.Data.UserID != userID { + return errors.New("notification not found") + } + + _, err = s.sdk.MarkSeen(ctx, notificationID) + if err != nil { + return err + } + return nil +} + func (s *notificationService) getUsers(ctx context.Context, userIDs []string) ([]notificationRecipient, error) { recipients := make([]notificationRecipient, 0) offset := 0 diff --git a/pkg/notification/client.go b/pkg/notification/client.go index a5738d7..8d908e4 100644 --- a/pkg/notification/client.go +++ b/pkg/notification/client.go @@ -71,9 +71,9 @@ func NewClientWithHTTPClient(config *Config, httpClient HTTPClient, logger Logge // SendNotification sends a notification request to the notification service func (c *Client) SendNotification(ctx context.Context, req *NotificationRequest) (*NotificationResponse, error) { // Validate request - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("request validation failed: %w", err) - } + // if err := req.Validate(); err != nil { + // return nil, fmt.Errorf("request validation failed: %w", err) + // } var lastErr error start := time.Now() @@ -265,6 +265,70 @@ func (c *Client) GetNotifications(ctx context.Context, req *GetNotificationsRequ return ¬ificationsResp, nil } +// GetNotification retrieves a single notification by ID from the notification service +func (c *Client) GetNotification(ctx context.Context, notificationID string) (*DetailedNotificationResponse, error) { + if notificationID == "" { + return nil, fmt.Errorf("notification ID is required") + } + + if c.config.EnableLogging && c.logger != nil { + c.logger.Debug("Getting notification", map[string]interface{}{ + "notification_id": notificationID, + "base_url": c.config.BaseURL, + }) + } + + // Create HTTP request + url := fmt.Sprintf("%s/api/v1/notifications/view/%s", c.config.BaseURL, notificationID) + httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request: %w", err) + } + + // Set headers + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("User-Agent", c.config.UserAgent) + + // Send request + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("HTTP request failed: %w", err) + } + defer resp.Body.Close() + + // Read response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + // Handle non-2xx status codes + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + // Try to parse error response + var errorResp NotificationErrorResponse + if json.Unmarshal(body, &errorResp) == nil && errorResp.Error != "" { + return nil, MapHTTPError(resp.StatusCode, errorResp.Error) + } + return nil, MapHTTPError(resp.StatusCode, string(body)) + } + + // Parse success response + var notificationResp DetailedNotificationResponse + if err := json.Unmarshal(body, ¬ificationResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + if c.config.EnableLogging && c.logger != nil { + c.logger.Info("Notification retrieved successfully", map[string]interface{}{ + "notification_id": notificationResp.Data.ID, + "user_id": notificationResp.Data.UserID, + "type": notificationResp.Data.Type, + }) + } + + return ¬ificationResp, nil +} + // buildQueryParams builds query parameters from the request func (c *Client) buildQueryParams(req *GetNotificationsRequest) string { params := make([]string, 0) diff --git a/pkg/notification/entities.go b/pkg/notification/entities.go index 74049f3..8f6e7e6 100644 --- a/pkg/notification/entities.go +++ b/pkg/notification/entities.go @@ -1,6 +1,8 @@ package notification import ( + "time" + "github.com/asaskevich/govalidator" ) @@ -24,15 +26,15 @@ type NotificationMethods struct { // NotificationRequest represents the request structure for sending notifications type NotificationRequest struct { - UserID string `json:"user_id,omitempty" valid:"optional"` - Title string `json:"title" valid:"required"` - Message string `json:"message" valid:"required,length(1|1000)"` - Type string `json:"type" valid:"required,in(info|warning|alert|reject)"` - EventType EventType `json:"event_type" valid:"required,in(EMAIL|SMS|PUSH|OTP)"` - Priority string `json:"priority" valid:"required,in(low|medium|important)"` + UserID string `json:"user_id"` + Title string `json:"title"` + Message string `json:"message"` + Type string `json:"type"` + EventType EventType `json:"event_type"` + Priority string `json:"priority"` ScheduledAt int64 `json:"scheduled_at"` - Metadata map[string]any `json:"metadata,omitempty" valid:"optional"` - Methods NotificationMethods `json:"methods" valid:"required"` + Metadata map[string]any `json:"metadata"` + Methods NotificationMethods `json:"methods"` } // NotificationResponse represents the successful response from notification service @@ -40,6 +42,30 @@ type NotificationResponse struct { Status string `json:"status"` } +// DetailedNotificationResponse represents a detailed notification response +type DetailedNotificationResponse struct { + Data struct { + ID string `json:"id"` + UserID string `json:"user_id"` + Title string `json:"title"` + Message string `json:"message"` + Link string `json:"link"` + Image string `json:"image"` + Type string `json:"type"` // info, warning, alert, reject + Priority string `json:"priority"` // low, medium, important + EventType string `json:"event_type"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Methods map[string]string `json:"methods"` + Metadata map[string]any `json:"metadata"` + Status string `json:"status"` // pending, sent, failed + Seen bool `json:"seen"` + SeenAt int64 `json:"seen_at"` + ScheduledAt int64 `json:"scheduled_at"` + IsScheduled bool `json:"is_scheduled"` + } `json:"data"` +} + // NotificationErrorResponse represents the error response from notification service type NotificationErrorResponse struct { Error string `json:"error"` diff --git a/pkg/notification/interfaces.go b/pkg/notification/interfaces.go index 8cde4cd..4865af6 100644 --- a/pkg/notification/interfaces.go +++ b/pkg/notification/interfaces.go @@ -24,6 +24,9 @@ type NotificationService interface { // GetNotifications retrieves a list of notifications GetNotifications(ctx context.Context, req *GetNotificationsRequest) (*NotificationListResponse, error) + // GetNotification retrieves a single notification by ID + GetNotification(ctx context.Context, notificationID string) (*DetailedNotificationResponse, error) + // MarkSeen marks a notification as seen MarkSeen(ctx context.Context, notificationID string) (*MarkSeenResponse, error) diff --git a/pkg/notification/sdk.go b/pkg/notification/sdk.go index 7c6eab2..8e64abe 100644 --- a/pkg/notification/sdk.go +++ b/pkg/notification/sdk.go @@ -79,6 +79,11 @@ func (s *SDK) GetNotifications(ctx context.Context, req *GetNotificationsRequest return s.service.GetNotifications(ctx, req) } +// GetNotification retrieves a single notification by ID +func (s *SDK) GetNotification(ctx context.Context, notificationID string) (*DetailedNotificationResponse, error) { + return s.service.GetNotification(ctx, notificationID) +} + // MarkSeen marks a notification as seen func (s *SDK) MarkSeen(ctx context.Context, notificationID string) (*MarkSeenResponse, error) { return s.service.MarkSeen(ctx, notificationID) diff --git a/pkg/notification/service.go b/pkg/notification/service.go index c1bf65c..510b64f 100644 --- a/pkg/notification/service.go +++ b/pkg/notification/service.go @@ -109,6 +109,11 @@ func (s *Service) GetNotifications(ctx context.Context, req *GetNotificationsReq return s.client.GetNotifications(ctx, req) } +// GetNotification retrieves a single notification by ID +func (s *Service) GetNotification(ctx context.Context, notificationID string) (*DetailedNotificationResponse, error) { + return s.client.GetNotification(ctx, notificationID) +} + // MarkSeen marks a notification as seen func (s *Service) MarkSeen(ctx context.Context, notificationID string) (*MarkSeenResponse, error) { return s.client.MarkSeen(ctx, notificationID)