feat: method impl added for the notfication
This commit is contained in:
parent
dd1d05929a
commit
4b06c5386b
16
cmd/main.go
16
cmd/main.go
|
|
@ -2,11 +2,15 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
||||
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
|
|
@ -21,11 +25,21 @@ func main() {
|
|||
slog.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db, _, err := repository.OpenDB(cfg.DbUrl)
|
||||
if err != nil {
|
||||
fmt.Print(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger := customlogger.NewLogger("development", slog.LevelDebug, "1.0")
|
||||
|
||||
store := repository.NewStore(db)
|
||||
fmt.Println(store)
|
||||
notificationRepo := repository.NewNotificationRepository(store)
|
||||
notificationSvc := notificationservice.New(notificationRepo, logger)
|
||||
|
||||
app := httpserver.NewApp(cfg.Port, logger, notificationSvc)
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatal("Failed to start server with error: ", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
db/query/notification.sql
Normal file
18
db/query/notification.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
-- name: CreateNotification :one
|
||||
INSERT INTO notifications (
|
||||
id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, timestamp, metadata
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetNotification :one
|
||||
SELECT * FROM notifications WHERE id = $1 LIMIT 1;
|
||||
|
||||
-- name: ListNotifications :many
|
||||
SELECT * FROM notifications WHERE recipient_id = $1 ORDER BY timestamp DESC LIMIT $2 OFFSET $3;
|
||||
|
||||
-- name: UpdateNotificationStatus :one
|
||||
UPDATE notifications SET delivery_status = $2, is_read = $3, metadata = $4 WHERE id = $1 RETURNING *;
|
||||
|
||||
-- name: ListFailedNotifications :many
|
||||
SELECT * FROM notifications WHERE delivery_status = 'failed' AND timestamp < NOW() - INTERVAL '1 hour' ORDER BY timestamp ASC LIMIT $1;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.28.0
|
||||
|
||||
package dbgen
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.28.0
|
||||
|
||||
package dbgen
|
||||
|
||||
|
|
@ -8,6 +8,23 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type Notification struct {
|
||||
ID string
|
||||
RecipientID string
|
||||
Type string
|
||||
Level string
|
||||
ErrorSeverity pgtype.Text
|
||||
Reciever string
|
||||
IsRead bool
|
||||
DeliveryStatus string
|
||||
DeliveryChannel pgtype.Text
|
||||
Payload []byte
|
||||
Priority pgtype.Int4
|
||||
Version int32
|
||||
Timestamp pgtype.Timestamptz
|
||||
Metadata []byte
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
FirstName string
|
||||
|
|
|
|||
220
gen/db/notification.sql.go
Normal file
220
gen/db/notification.sql.go
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: notification.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const CreateNotification = `-- name: CreateNotification :one
|
||||
INSERT INTO notifications (
|
||||
id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, timestamp, metadata
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
|
||||
) RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
|
||||
`
|
||||
|
||||
type CreateNotificationParams struct {
|
||||
ID string
|
||||
RecipientID string
|
||||
Type string
|
||||
Level string
|
||||
ErrorSeverity pgtype.Text
|
||||
Reciever string
|
||||
IsRead bool
|
||||
DeliveryStatus string
|
||||
DeliveryChannel pgtype.Text
|
||||
Payload []byte
|
||||
Priority pgtype.Int4
|
||||
Timestamp pgtype.Timestamptz
|
||||
Metadata []byte
|
||||
}
|
||||
|
||||
func (q *Queries) CreateNotification(ctx context.Context, arg CreateNotificationParams) (Notification, error) {
|
||||
row := q.db.QueryRow(ctx, CreateNotification,
|
||||
arg.ID,
|
||||
arg.RecipientID,
|
||||
arg.Type,
|
||||
arg.Level,
|
||||
arg.ErrorSeverity,
|
||||
arg.Reciever,
|
||||
arg.IsRead,
|
||||
arg.DeliveryStatus,
|
||||
arg.DeliveryChannel,
|
||||
arg.Payload,
|
||||
arg.Priority,
|
||||
arg.Timestamp,
|
||||
arg.Metadata,
|
||||
)
|
||||
var i Notification
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.RecipientID,
|
||||
&i.Type,
|
||||
&i.Level,
|
||||
&i.ErrorSeverity,
|
||||
&i.Reciever,
|
||||
&i.IsRead,
|
||||
&i.DeliveryStatus,
|
||||
&i.DeliveryChannel,
|
||||
&i.Payload,
|
||||
&i.Priority,
|
||||
&i.Version,
|
||||
&i.Timestamp,
|
||||
&i.Metadata,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetNotification = `-- name: GetNotification :one
|
||||
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata FROM notifications WHERE id = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetNotification(ctx context.Context, id string) (Notification, error) {
|
||||
row := q.db.QueryRow(ctx, GetNotification, id)
|
||||
var i Notification
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.RecipientID,
|
||||
&i.Type,
|
||||
&i.Level,
|
||||
&i.ErrorSeverity,
|
||||
&i.Reciever,
|
||||
&i.IsRead,
|
||||
&i.DeliveryStatus,
|
||||
&i.DeliveryChannel,
|
||||
&i.Payload,
|
||||
&i.Priority,
|
||||
&i.Version,
|
||||
&i.Timestamp,
|
||||
&i.Metadata,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const ListFailedNotifications = `-- name: ListFailedNotifications :many
|
||||
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata FROM notifications WHERE delivery_status = 'failed' AND timestamp < NOW() - INTERVAL '1 hour' ORDER BY timestamp ASC LIMIT $1
|
||||
`
|
||||
|
||||
func (q *Queries) ListFailedNotifications(ctx context.Context, limit int32) ([]Notification, error) {
|
||||
rows, err := q.db.Query(ctx, ListFailedNotifications, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Notification
|
||||
for rows.Next() {
|
||||
var i Notification
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.RecipientID,
|
||||
&i.Type,
|
||||
&i.Level,
|
||||
&i.ErrorSeverity,
|
||||
&i.Reciever,
|
||||
&i.IsRead,
|
||||
&i.DeliveryStatus,
|
||||
&i.DeliveryChannel,
|
||||
&i.Payload,
|
||||
&i.Priority,
|
||||
&i.Version,
|
||||
&i.Timestamp,
|
||||
&i.Metadata,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const ListNotifications = `-- name: ListNotifications :many
|
||||
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata FROM notifications WHERE recipient_id = $1 ORDER BY timestamp DESC LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
type ListNotificationsParams struct {
|
||||
RecipientID string
|
||||
Limit int32
|
||||
Offset int32
|
||||
}
|
||||
|
||||
func (q *Queries) ListNotifications(ctx context.Context, arg ListNotificationsParams) ([]Notification, error) {
|
||||
rows, err := q.db.Query(ctx, ListNotifications, arg.RecipientID, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Notification
|
||||
for rows.Next() {
|
||||
var i Notification
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.RecipientID,
|
||||
&i.Type,
|
||||
&i.Level,
|
||||
&i.ErrorSeverity,
|
||||
&i.Reciever,
|
||||
&i.IsRead,
|
||||
&i.DeliveryStatus,
|
||||
&i.DeliveryChannel,
|
||||
&i.Payload,
|
||||
&i.Priority,
|
||||
&i.Version,
|
||||
&i.Timestamp,
|
||||
&i.Metadata,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const UpdateNotificationStatus = `-- name: UpdateNotificationStatus :one
|
||||
UPDATE notifications SET delivery_status = $2, is_read = $3, metadata = $4 WHERE id = $1 RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
|
||||
`
|
||||
|
||||
type UpdateNotificationStatusParams struct {
|
||||
ID string
|
||||
DeliveryStatus string
|
||||
IsRead bool
|
||||
Metadata []byte
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateNotificationStatus(ctx context.Context, arg UpdateNotificationStatusParams) (Notification, error) {
|
||||
row := q.db.QueryRow(ctx, UpdateNotificationStatus,
|
||||
arg.ID,
|
||||
arg.DeliveryStatus,
|
||||
arg.IsRead,
|
||||
arg.Metadata,
|
||||
)
|
||||
var i Notification
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.RecipientID,
|
||||
&i.Type,
|
||||
&i.Level,
|
||||
&i.ErrorSeverity,
|
||||
&i.Reciever,
|
||||
&i.IsRead,
|
||||
&i.DeliveryStatus,
|
||||
&i.DeliveryChannel,
|
||||
&i.Payload,
|
||||
&i.Priority,
|
||||
&i.Version,
|
||||
&i.Timestamp,
|
||||
&i.Metadata,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// sqlc v1.28.0
|
||||
// source: user.sql
|
||||
|
||||
package dbgen
|
||||
|
|
|
|||
5
go.mod
5
go.mod
|
|
@ -5,6 +5,8 @@ go 1.24.1
|
|||
require (
|
||||
github.com/bytedance/sonic v1.13.2
|
||||
github.com/gofiber/fiber/v2 v2.52.6
|
||||
github.com/gofiber/websocket/v2 v2.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.7.4
|
||||
github.com/joho/godotenv v1.5.1
|
||||
)
|
||||
|
|
@ -13,7 +15,7 @@ require (
|
|||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/fasthttp/websocket v1.5.3 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
|
|
@ -23,6 +25,7 @@ require (
|
|||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
|
|
|
|||
6
go.sum
6
go.sum
|
|
@ -11,8 +11,12 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
|
||||
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
|
||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
||||
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
|
|
@ -41,6 +45,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
|
|
|||
7
internal/pkgs/helpers/helpers.go
Normal file
7
internal/pkgs/helpers/helpers.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package helpers
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
func GenerateID() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
174
internal/repository/notification.go
Normal file
174
internal/repository/notification.go
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type NotificationRepository interface {
|
||||
CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error)
|
||||
UpdateNotificationStatus(ctx context.Context, id, status string, isRead bool, metadata []byte) (*domain.Notification, error)
|
||||
ListNotifications(ctx context.Context, recipientID string, limit, offset int) ([]domain.Notification, error)
|
||||
ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error)
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
func NewNotificationRepository(store *Store) NotificationRepository {
|
||||
return &Repository{store: store}
|
||||
}
|
||||
|
||||
func (r *Repository) CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error) {
|
||||
var errorSeverity pgtype.Text
|
||||
if notification.ErrorSeverity != nil {
|
||||
errorSeverity.String = string(*notification.ErrorSeverity)
|
||||
errorSeverity.Valid = true
|
||||
}
|
||||
|
||||
var deliveryChannel pgtype.Text
|
||||
if notification.DeliveryChannel != "" {
|
||||
deliveryChannel.String = string(notification.DeliveryChannel)
|
||||
deliveryChannel.Valid = true
|
||||
}
|
||||
|
||||
var priority pgtype.Int4
|
||||
if notification.Priority != 0 {
|
||||
priority.Int32 = int32(notification.Priority)
|
||||
priority.Valid = true
|
||||
}
|
||||
|
||||
params := dbgen.CreateNotificationParams{
|
||||
ID: notification.ID,
|
||||
RecipientID: notification.RecipientID,
|
||||
Type: string(notification.Type),
|
||||
Level: string(notification.Level),
|
||||
ErrorSeverity: errorSeverity,
|
||||
Reciever: string(notification.Reciever),
|
||||
IsRead: notification.IsRead,
|
||||
DeliveryStatus: string(notification.DeliveryStatus),
|
||||
DeliveryChannel: deliveryChannel,
|
||||
Payload: marshalPayload(notification.Payload),
|
||||
Priority: priority,
|
||||
Timestamp: pgtype.Timestamptz{Time: notification.Timestamp, Valid: true},
|
||||
Metadata: notification.Metadata,
|
||||
}
|
||||
|
||||
dbNotification, err := r.store.queries.CreateNotification(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.mapDBToDomain(&dbNotification), nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateNotificationStatus(ctx context.Context, id, status string, isRead bool, metadata []byte) (*domain.Notification, error) {
|
||||
params := dbgen.UpdateNotificationStatusParams{
|
||||
ID: id,
|
||||
DeliveryStatus: status,
|
||||
IsRead: isRead,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
dbNotification, err := r.store.queries.UpdateNotificationStatus(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.mapDBToDomain(&dbNotification), nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListNotifications(ctx context.Context, recipientID string, limit, offset int) ([]domain.Notification, error) {
|
||||
params := dbgen.ListNotificationsParams{
|
||||
RecipientID: recipientID,
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
}
|
||||
|
||||
dbNotifications, err := r.store.queries.ListNotifications(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []domain.Notification
|
||||
for _, dbNotif := range dbNotifications {
|
||||
domainNotif := r.mapDBToDomain(&dbNotif)
|
||||
result = append(result, *domainNotif)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error) {
|
||||
dbNotifications, err := r.store.queries.ListFailedNotifications(ctx, int32(limit))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []domain.Notification
|
||||
for _, dbNotif := range dbNotifications {
|
||||
domainNotif := r.mapDBToDomain(&dbNotif)
|
||||
result = append(result, *domainNotif)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) mapDBToDomain(dbNotif *dbgen.Notification) *domain.Notification {
|
||||
var errorSeverity *domain.NotificationErrorSeverity
|
||||
if dbNotif.ErrorSeverity.Valid {
|
||||
s := domain.NotificationErrorSeverity(dbNotif.ErrorSeverity.String)
|
||||
errorSeverity = &s
|
||||
}
|
||||
|
||||
var deliveryChannel domain.DeliveryChannel
|
||||
if dbNotif.DeliveryChannel.Valid {
|
||||
deliveryChannel = domain.DeliveryChannel(dbNotif.DeliveryChannel.String)
|
||||
} else {
|
||||
deliveryChannel = ""
|
||||
}
|
||||
|
||||
var priority int
|
||||
if dbNotif.Priority.Valid {
|
||||
priority = int(dbNotif.Priority.Int32)
|
||||
}
|
||||
|
||||
payload, err := unmarshalPayload(dbNotif.Payload)
|
||||
if err != nil {
|
||||
payload = domain.NotificationPayload{}
|
||||
}
|
||||
|
||||
return &domain.Notification{
|
||||
ID: dbNotif.ID,
|
||||
RecipientID: dbNotif.RecipientID,
|
||||
Type: domain.NotificationType(dbNotif.Type),
|
||||
Level: domain.NotificationLevel(dbNotif.Level),
|
||||
ErrorSeverity: errorSeverity,
|
||||
Reciever: domain.NotificationRecieverSide(dbNotif.Reciever),
|
||||
IsRead: dbNotif.IsRead,
|
||||
DeliveryStatus: domain.NotificationDeliveryStatus(dbNotif.DeliveryStatus),
|
||||
DeliveryChannel: deliveryChannel,
|
||||
Payload: payload,
|
||||
Priority: priority,
|
||||
Timestamp: dbNotif.Timestamp.Time,
|
||||
Metadata: dbNotif.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
func marshalPayload(payload domain.NotificationPayload) []byte {
|
||||
data, _ := json.Marshal(payload)
|
||||
return data
|
||||
}
|
||||
|
||||
func unmarshalPayload(data []byte) (domain.NotificationPayload, error) {
|
||||
var payload domain.NotificationPayload
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return domain.NotificationPayload{}, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
|
@ -1,4 +1,18 @@
|
|||
package notficationservice
|
||||
package notificationservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
)
|
||||
|
||||
type NotificationStore interface {
|
||||
SendNotification(ctx context.Context, notification *domain.Notification) error
|
||||
MarkAsRead(ctx context.Context, notificationID, recipientID string) error
|
||||
ListNotifications(ctx context.Context, recipientID string, limit, offset int) ([]domain.Notification, error)
|
||||
ConnectWebSocket(ctx context.Context, recipientID string, c *websocket.Conn) error
|
||||
DisconnectWebSocket(recipientID string)
|
||||
SendSMS(ctx context.Context, recipientID, message string) error
|
||||
SendEmail(ctx context.Context, recipientID, subject, message string) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,196 @@
|
|||
package notficationservice
|
||||
package notificationservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
store NotificationStore
|
||||
repo repository.NotificationRepository
|
||||
logger *slog.Logger
|
||||
connections sync.Map
|
||||
notificationCh chan *domain.Notification
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
func New(store NotificationStore) *Service {
|
||||
return &Service{
|
||||
store: store,
|
||||
func New(repo repository.NotificationRepository, logger *slog.Logger) NotificationStore {
|
||||
svc := &Service{
|
||||
repo: repo,
|
||||
logger: logger,
|
||||
connections: sync.Map{},
|
||||
notificationCh: make(chan *domain.Notification, 1000),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
go svc.startWorker()
|
||||
go svc.startRetryWorker()
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func (s *Service) addConnection(ctx context.Context, recipientID string, c *websocket.Conn) {
|
||||
if c == nil {
|
||||
s.logger.Warn("Attempted to add nil WebSocket connection", "recipientID", recipientID)
|
||||
return
|
||||
}
|
||||
|
||||
s.connections.Store(recipientID, c)
|
||||
s.logger.Info("Added WebSocket connection", "recipientID", recipientID)
|
||||
}
|
||||
|
||||
func (s *Service) SendNotification(ctx context.Context, notification *domain.Notification) error {
|
||||
notification.ID = helpers.GenerateID()
|
||||
notification.Timestamp = time.Now()
|
||||
notification.DeliveryStatus = domain.DeliveryStatusPending
|
||||
|
||||
created, err := s.repo.CreateNotification(ctx, notification)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notification = created
|
||||
|
||||
select {
|
||||
case s.notificationCh <- notification:
|
||||
default:
|
||||
s.logger.Error("Notification channel full, dropping notification", "id", notification.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) MarkAsRead(ctx context.Context, notificationID, recipientID string) error {
|
||||
_, err := s.repo.UpdateNotificationStatus(ctx, notificationID, string(domain.DeliveryStatusSent), true, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ListNotifications(ctx context.Context, recipientID string, limit, offset int) ([]domain.Notification, error) {
|
||||
return s.repo.ListNotifications(ctx, recipientID, limit, offset)
|
||||
}
|
||||
|
||||
func (s *Service) ConnectWebSocket(ctx context.Context, recipientID string, c *websocket.Conn) error {
|
||||
s.addConnection(ctx, recipientID, c)
|
||||
defer func() {
|
||||
s.DisconnectWebSocket(recipientID)
|
||||
}()
|
||||
|
||||
for {
|
||||
_, _, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
s.logger.Error("WebSocket error", "recipientID", recipientID, "error", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) DisconnectWebSocket(recipientID string) {
|
||||
s.connections.Delete(recipientID)
|
||||
if conn, loaded := s.connections.LoadAndDelete(recipientID); loaded {
|
||||
conn.(*websocket.Conn).Close()
|
||||
s.logger.Info("Disconnected WebSocket", "recipientID", recipientID)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) SendSMS(ctx context.Context, recipientID, message string) error {
|
||||
s.logger.Info("SMS notification requested", "recipientID", recipientID, "message", message)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) SendEmail(ctx context.Context, recipientID, subject, message string) error {
|
||||
s.logger.Info("Email notification requested", "recipientID", recipientID, "subject", subject)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) startWorker() {
|
||||
for {
|
||||
select {
|
||||
case notification := <-s.notificationCh:
|
||||
s.handleNotification(notification)
|
||||
case <-s.stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) handleNotification(notification *domain.Notification) {
|
||||
ctx := context.Background()
|
||||
|
||||
if conn, ok := s.connections.Load(notification.RecipientID); ok {
|
||||
data, err := notification.ToJSON()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to serialize notification", "id", notification.ID, "error", err)
|
||||
return
|
||||
}
|
||||
if err := conn.(*websocket.Conn).WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
s.logger.Error("Failed to send WebSocket message", "id", notification.ID, "error", err)
|
||||
notification.DeliveryStatus = domain.DeliveryStatusFailed
|
||||
} else {
|
||||
notification.DeliveryStatus = domain.DeliveryStatusSent
|
||||
}
|
||||
} else {
|
||||
s.logger.Warn("No WebSocket connection for recipient", "recipientID", notification.RecipientID)
|
||||
notification.DeliveryStatus = domain.DeliveryStatusFailed
|
||||
}
|
||||
|
||||
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
|
||||
s.logger.Error("Failed to update notification status", "id", notification.ID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) startRetryWorker() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.retryFailedNotifications()
|
||||
case <-s.stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) retryFailedNotifications() {
|
||||
ctx := context.Background()
|
||||
failedNotifications, err := s.repo.ListFailedNotifications(ctx, 100)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to list failed notifications", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, n := range failedNotifications {
|
||||
notification := &n
|
||||
go func(notification *domain.Notification) {
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
time.Sleep(time.Duration(attempt) * time.Second)
|
||||
if conn, ok := s.connections.Load(notification.RecipientID); ok {
|
||||
data, err := notification.ToJSON()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := conn.(*websocket.Conn).WriteMessage(websocket.TextMessage, data); err == nil {
|
||||
notification.DeliveryStatus = domain.DeliveryStatusSent
|
||||
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
|
||||
s.logger.Error("Failed to update after retry", "id", notification.ID, "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
s.logger.Error("Max retries reached for notification", "id", notification.ID)
|
||||
}(notification)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,21 @@ package httpserver
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
fiber *fiber.App
|
||||
port int
|
||||
fiber *fiber.App
|
||||
NotidicationStore notificationservice.NotificationStore
|
||||
port int
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewApp(port int) *App {
|
||||
func NewApp(port int, logger *slog.Logger, notidicationStore notificationservice.NotificationStore) *App {
|
||||
app := fiber.New(fiber.Config{
|
||||
CaseSensitive: true,
|
||||
DisableHeaderNormalizing: true,
|
||||
|
|
@ -20,8 +24,10 @@ func NewApp(port int) *App {
|
|||
JSONDecoder: sonic.Unmarshal,
|
||||
})
|
||||
s := &App{
|
||||
fiber: app,
|
||||
port: port,
|
||||
fiber: app,
|
||||
port: port,
|
||||
NotidicationStore: notidicationStore,
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
s.initAppRoutes()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,35 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/websocket/v2"
|
||||
)
|
||||
|
||||
func (a *App) initAppRoutes() {
|
||||
// a.fiber.Group("/users", users.CreateAccount(a.userAPI))
|
||||
|
||||
a.fiber.Get("/ws/:recipientID", func(c *fiber.Ctx) error {
|
||||
if websocket.IsWebSocketUpgrade(c) {
|
||||
c.Locals("allowed", true)
|
||||
return c.Next()
|
||||
}
|
||||
return fiber.ErrUpgradeRequired
|
||||
}, websocket.New(func(c *websocket.Conn) {
|
||||
recipientID := c.Params("recipientID")
|
||||
a.NotidicationStore.ConnectWebSocket(context.Background(), recipientID, c)
|
||||
|
||||
defer a.NotidicationStore.DisconnectWebSocket(recipientID)
|
||||
|
||||
for {
|
||||
_, _, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
a.Logger.Error("WebSocket error", "recipientID", recipientID, "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user