Added activities module
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
DO
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'cleanup_activity_logs_hourly') THEN
|
||||||
|
PERFORM cron.unschedule((SELECT jobid FROM cron.job WHERE jobname = 'cleanup_activity_logs_hourly'));
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS activity_logs;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
CREATE UNLOGGED TABLE activity_logs
|
||||||
|
(
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
family_id BIGINT REFERENCES families (id) ON DELETE CASCADE,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
entity_type TEXT NOT NULL,
|
||||||
|
entity_id BIGINT,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_activity_logs_created_at ON activity_logs (created_at DESC);
|
||||||
|
CREATE INDEX idx_activity_logs_user_id ON activity_logs (user_id);
|
||||||
|
CREATE INDEX idx_activity_logs_family_id ON activity_logs (family_id);
|
||||||
|
|
||||||
|
DO
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'cleanup_activity_logs_hourly') THEN
|
||||||
|
PERFORM cron.unschedule((SELECT jobid FROM cron.job WHERE jobname = 'cleanup_activity_logs_hourly'));
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
SELECT cron.schedule(
|
||||||
|
'cleanup_activity_logs_hourly',
|
||||||
|
'0 * * * *',
|
||||||
|
$$DELETE FROM activity_logs WHERE created_at < NOW() - INTERVAL '1 day'$$
|
||||||
|
);
|
||||||
@@ -15,6 +15,67 @@ const docTemplate = `{
|
|||||||
"host": "{{.Host}}",
|
"host": "{{.Host}}",
|
||||||
"basePath": "{{.BasePath}}",
|
"basePath": "{{.BasePath}}",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/activities": {
|
||||||
|
"get": {
|
||||||
|
"description": "Возвращает список действий пользователей с пагинацией",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Activities"
|
||||||
|
],
|
||||||
|
"summary": "Получить активность пользователей",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Family ID",
|
||||||
|
"name": "family_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "User ID",
|
||||||
|
"name": "user_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Limit, default 10",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Offset",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ActivityListResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/families": {
|
"/families": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Создает новую семью",
|
"description": "Создает новую семью",
|
||||||
@@ -1158,6 +1219,52 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.ActivityListResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/dto.ActivityResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.ActivityResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"entity_type": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"family_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.CreateTransactionRequest": {
|
"dto.CreateTransactionRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -4,6 +4,67 @@
|
|||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/activities": {
|
||||||
|
"get": {
|
||||||
|
"description": "Возвращает список действий пользователей с пагинацией",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Activities"
|
||||||
|
],
|
||||||
|
"summary": "Получить активность пользователей",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Family ID",
|
||||||
|
"name": "family_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "User ID",
|
||||||
|
"name": "user_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Limit, default 10",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Offset",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ActivityListResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/families": {
|
"/families": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Создает новую семью",
|
"description": "Создает новую семью",
|
||||||
@@ -1147,6 +1208,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.ActivityListResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/dto.ActivityResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.ActivityResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"entity_type": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"family_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.CreateTransactionRequest": {
|
"dto.CreateTransactionRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -120,6 +120,36 @@ definitions:
|
|||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
dto.ActivityListResponse:
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/dto.ActivityResponse'
|
||||||
|
type: array
|
||||||
|
limit:
|
||||||
|
type: integer
|
||||||
|
offset:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
dto.ActivityResponse:
|
||||||
|
properties:
|
||||||
|
action:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
entity_id:
|
||||||
|
type: integer
|
||||||
|
entity_type:
|
||||||
|
type: string
|
||||||
|
family_id:
|
||||||
|
type: integer
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
user_id:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
dto.CreateTransactionRequest:
|
dto.CreateTransactionRequest:
|
||||||
properties:
|
properties:
|
||||||
amount:
|
amount:
|
||||||
@@ -210,6 +240,46 @@ definitions:
|
|||||||
info:
|
info:
|
||||||
contact: {}
|
contact: {}
|
||||||
paths:
|
paths:
|
||||||
|
/activities:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Возвращает список действий пользователей с пагинацией
|
||||||
|
parameters:
|
||||||
|
- description: Family ID
|
||||||
|
in: query
|
||||||
|
name: family_id
|
||||||
|
type: integer
|
||||||
|
- description: User ID
|
||||||
|
in: query
|
||||||
|
name: user_id
|
||||||
|
type: integer
|
||||||
|
- description: Limit, default 10
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
- description: Offset
|
||||||
|
in: query
|
||||||
|
name: offset
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ActivityListResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
|
summary: Получить активность пользователей
|
||||||
|
tags:
|
||||||
|
- Activities
|
||||||
/families:
|
/families:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"FamilyHub/src/domain"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityListQuery struct {
|
||||||
|
FamilyID *int64 `form:"family_id"`
|
||||||
|
UserID *int64 `form:"user_id"`
|
||||||
|
Limit int `form:"limit"`
|
||||||
|
Offset int `form:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityResponse struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
FamilyID *int64 `json:"family_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
EntityType string `json:"entity_type"`
|
||||||
|
EntityID *int64 `json:"entity_id"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityListResponse struct {
|
||||||
|
Items []ActivityResponse `json:"items"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ActivityToResponse(activity *domain.ActivityLog) ActivityResponse {
|
||||||
|
return ActivityResponse{
|
||||||
|
ID: activity.ID,
|
||||||
|
FamilyID: activity.FamilyID,
|
||||||
|
UserID: activity.UserID,
|
||||||
|
Action: activity.Action,
|
||||||
|
EntityType: activity.EntityType,
|
||||||
|
EntityID: activity.EntityID,
|
||||||
|
Description: activity.Description,
|
||||||
|
CreatedAt: activity.CreatedAt.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ActivitiesToListResponse(activities []*domain.ActivityLog, limit, offset int) ActivityListResponse {
|
||||||
|
items := make([]ActivityResponse, 0, len(activities))
|
||||||
|
for _, activity := range activities {
|
||||||
|
items = append(items, ActivityToResponse(activity))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ActivityListResponse{
|
||||||
|
Items: items,
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package routers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"FamilyHub/src/api/dto"
|
||||||
|
"FamilyHub/src/api/services"
|
||||||
|
"FamilyHub/src/domain"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivitiesRouter struct {
|
||||||
|
service services.ActivityService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActivitiesRouter(s services.ActivityService) *ActivitiesRouter {
|
||||||
|
return &ActivitiesRouter{service: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router *ActivitiesRouter) RegisterRoutes(r *gin.RouterGroup) {
|
||||||
|
activities := r.Group("/activities")
|
||||||
|
{
|
||||||
|
activities.GET("", router.List)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List GoDoc
|
||||||
|
// @Summary Получить активность пользователей
|
||||||
|
// @Description Возвращает список действий пользователей с пагинацией
|
||||||
|
// @Tags Activities
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param family_id query int false "Family ID"
|
||||||
|
// @Param user_id query int false "User ID"
|
||||||
|
// @Param limit query int false "Limit, default 10"
|
||||||
|
// @Param offset query int false "Offset"
|
||||||
|
// @Success 200 {object} dto.ActivityListResponse
|
||||||
|
// @Failure 400 {object} dto.ErrorResponse
|
||||||
|
// @Failure 500 {object} dto.ErrorResponse
|
||||||
|
// @Router /activities [get]
|
||||||
|
func (router *ActivitiesRouter) List(c *gin.Context) {
|
||||||
|
var query dto.ActivityListQuery
|
||||||
|
if err := c.ShouldBindQuery(&query); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
activities, filter, err := router.service.List(c.Request.Context(), domain.ActivityLogListFilter{
|
||||||
|
FamilyID: query.FamilyID,
|
||||||
|
UserID: query.UserID,
|
||||||
|
Limit: query.Limit,
|
||||||
|
Offset: query.Offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Message: "internal server error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, dto.ActivitiesToListResponse(activities, filter.Limit, filter.Offset))
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package routers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"FamilyHub/src/api/services"
|
||||||
|
"FamilyHub/src/domain"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type activityServiceMock struct {
|
||||||
|
listFn func(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, domain.ActivityLogListFilter, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *activityServiceMock) List(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, domain.ActivityLogListFilter, error) {
|
||||||
|
if m.listFn != nil {
|
||||||
|
return m.listFn(ctx, filter)
|
||||||
|
}
|
||||||
|
return nil, filter, errors.New("mock list is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupActivitiesRouter(mock services.ActivityService) *gin.Engine {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
r := gin.New()
|
||||||
|
apiV1 := r.Group("/api/v1")
|
||||||
|
router := NewActivitiesRouter(mock)
|
||||||
|
router.RegisterRoutes(apiV1)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActivitiesRouter_List(t *testing.T) {
|
||||||
|
t.Run("uses default pagination", func(t *testing.T) {
|
||||||
|
r := setupActivitiesRouter(&activityServiceMock{listFn: func(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, domain.ActivityLogListFilter, error) {
|
||||||
|
assert.Equal(t, 0, filter.Limit)
|
||||||
|
assert.Equal(t, 0, filter.Offset)
|
||||||
|
|
||||||
|
activity := &domain.ActivityLog{
|
||||||
|
ID: 1,
|
||||||
|
UserID: 2,
|
||||||
|
Action: "create",
|
||||||
|
EntityType: "transaction",
|
||||||
|
Description: "Created transaction 1",
|
||||||
|
CreatedAt: time.Date(2026, time.April, 11, 12, 0, 0, 0, time.UTC),
|
||||||
|
}
|
||||||
|
filter.Limit = 10
|
||||||
|
return []*domain.ActivityLog{activity}, filter, nil
|
||||||
|
}})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/activities", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "\"limit\":10")
|
||||||
|
assert.Contains(t, w.Body.String(), "Created transaction 1")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -90,6 +90,7 @@ func NewServer(cfg config.Config) *Server {
|
|||||||
apiV1 := router.Group("/api/v1")
|
apiV1 := router.Group("/api/v1")
|
||||||
|
|
||||||
transactionRepo := repositories.NewTransactionsSQLRepository(dbConn)
|
transactionRepo := repositories.NewTransactionsSQLRepository(dbConn)
|
||||||
|
activityRepo := repositories.NewActivitySQLRepository(dbConn)
|
||||||
|
|
||||||
ocrSvc, err := ocr.NewGoogleOCR(context.Background())
|
ocrSvc, err := ocr.NewGoogleOCR(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -101,10 +102,14 @@ func NewServer(cfg config.Config) *Server {
|
|||||||
receiptRouter := routers.NewReceiptRouter(receiptService_, ocrSvc)
|
receiptRouter := routers.NewReceiptRouter(receiptService_, ocrSvc)
|
||||||
receiptRouter.RegisterRoutes(apiV1)
|
receiptRouter.RegisterRoutes(apiV1)
|
||||||
|
|
||||||
transactionService := services.NewTransactionService(transactionRepo)
|
transactionService := services.NewTransactionService(transactionRepo, activityRepo)
|
||||||
transactionRouter := routers.NewTransactionsRouter(transactionService)
|
transactionRouter := routers.NewTransactionsRouter(transactionService)
|
||||||
transactionRouter.RegisterRoutes(apiV1)
|
transactionRouter.RegisterRoutes(apiV1)
|
||||||
|
|
||||||
|
activityService := services.NewActivityService(activityRepo)
|
||||||
|
activityRouter := routers.NewActivitiesRouter(activityService)
|
||||||
|
activityRouter.RegisterRoutes(apiV1)
|
||||||
|
|
||||||
usersRepo := repositories.NewUsersSQLRepository(dbConn)
|
usersRepo := repositories.NewUsersSQLRepository(dbConn)
|
||||||
usersService := services.NewUserService(usersRepo)
|
usersService := services.NewUserService(usersRepo)
|
||||||
usersRouter := routers.NewUsersRouter(usersService)
|
usersRouter := routers.NewUsersRouter(usersService)
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"FamilyHub/src/domain"
|
||||||
|
"FamilyHub/src/repositories"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityService interface {
|
||||||
|
List(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, domain.ActivityLogListFilter, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type activityService struct {
|
||||||
|
repo repositories.ActivityRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActivityService(repo repositories.ActivityRepository) ActivityService {
|
||||||
|
return &activityService{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *activityService) List(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, domain.ActivityLogListFilter, error) {
|
||||||
|
if filter.Limit <= 0 {
|
||||||
|
filter.Limit = 10
|
||||||
|
}
|
||||||
|
if filter.Limit > 100 {
|
||||||
|
filter.Limit = 100
|
||||||
|
}
|
||||||
|
if filter.Offset < 0 {
|
||||||
|
filter.Offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
activities, err := s.repo.List(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, filter, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return activities, filter, nil
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,10 +21,11 @@ type TransactionService interface {
|
|||||||
|
|
||||||
type transactionService struct {
|
type transactionService struct {
|
||||||
repo repositories.TransactionRepository
|
repo repositories.TransactionRepository
|
||||||
|
activityRepo repositories.ActivityRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransactionService(repo repositories.TransactionRepository) TransactionService {
|
func NewTransactionService(repo repositories.TransactionRepository, activityRepo repositories.ActivityRepository) TransactionService {
|
||||||
return &transactionService{repo: repo}
|
return &transactionService{repo: repo, activityRepo: activityRepo}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -58,6 +60,18 @@ func (s *transactionService) Create(ctx context.Context, req domain.CreateTransa
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.activityRepo != nil {
|
||||||
|
description := fmt.Sprintf("Created transaction %d", transaction.ID)
|
||||||
|
_ = s.activityRepo.Create(ctx, &domain.ActivityLog{
|
||||||
|
FamilyID: &transaction.FamilyID,
|
||||||
|
UserID: transaction.CreatedBy,
|
||||||
|
Action: "create",
|
||||||
|
EntityType: "transaction",
|
||||||
|
EntityID: &transaction.ID,
|
||||||
|
Description: description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return transaction, nil
|
return transaction, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type ActivityLog struct {
|
||||||
|
ID int64
|
||||||
|
FamilyID *int64
|
||||||
|
UserID int64
|
||||||
|
Action string
|
||||||
|
EntityType string
|
||||||
|
EntityID *int64
|
||||||
|
Description string
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityLogCreateRequest struct {
|
||||||
|
FamilyID *int64
|
||||||
|
UserID int64
|
||||||
|
Action string
|
||||||
|
EntityType string
|
||||||
|
EntityID *int64
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityLogListFilter struct {
|
||||||
|
FamilyID *int64
|
||||||
|
UserID *int64
|
||||||
|
Limit int
|
||||||
|
Offset int
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"FamilyHub/src/domain"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityRepository interface {
|
||||||
|
Create(ctx context.Context, activity *domain.ActivityLog) error
|
||||||
|
List(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivitySQLRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActivitySQLRepository(db *sql.DB) *ActivitySQLRepository {
|
||||||
|
return &ActivitySQLRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ActivitySQLRepository) Create(ctx context.Context, activity *domain.ActivityLog) error {
|
||||||
|
query := `
|
||||||
|
INSERT INTO activity_logs (family_id, user_id, action, entity_type, entity_id, description)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
RETURNING id, created_at
|
||||||
|
`
|
||||||
|
|
||||||
|
return r.db.QueryRowContext(
|
||||||
|
ctx,
|
||||||
|
query,
|
||||||
|
activity.FamilyID,
|
||||||
|
activity.UserID,
|
||||||
|
activity.Action,
|
||||||
|
activity.EntityType,
|
||||||
|
activity.EntityID,
|
||||||
|
activity.Description,
|
||||||
|
).Scan(&activity.ID, &activity.CreatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ActivitySQLRepository) List(ctx context.Context, filter domain.ActivityLogListFilter) ([]*domain.ActivityLog, error) {
|
||||||
|
var (
|
||||||
|
whereClauses []string
|
||||||
|
args []any
|
||||||
|
)
|
||||||
|
|
||||||
|
appendFilter := func(condition string, value any) {
|
||||||
|
args = append(args, value)
|
||||||
|
whereClauses = append(whereClauses, fmt.Sprintf(condition, len(args)))
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
SELECT id, family_id, user_id, action, entity_type, entity_id, description, created_at
|
||||||
|
FROM activity_logs
|
||||||
|
`
|
||||||
|
|
||||||
|
if filter.FamilyID != nil {
|
||||||
|
appendFilter("family_id = $%d", *filter.FamilyID)
|
||||||
|
}
|
||||||
|
if filter.UserID != nil {
|
||||||
|
appendFilter("user_id = $%d", *filter.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(whereClauses) > 0 {
|
||||||
|
query += " WHERE " + strings.Join(whereClauses, " AND ")
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, filter.Limit, filter.Offset)
|
||||||
|
query += fmt.Sprintf(" ORDER BY created_at DESC LIMIT $%d OFFSET $%d", len(args)-1, len(args))
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var activities []*domain.ActivityLog
|
||||||
|
for rows.Next() {
|
||||||
|
var activity domain.ActivityLog
|
||||||
|
if err := rows.Scan(
|
||||||
|
&activity.ID,
|
||||||
|
&activity.FamilyID,
|
||||||
|
&activity.UserID,
|
||||||
|
&activity.Action,
|
||||||
|
&activity.EntityType,
|
||||||
|
&activity.EntityID,
|
||||||
|
&activity.Description,
|
||||||
|
&activity.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
activities = append(activities, &activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return activities, rows.Err()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user