Added analytics by transactions
This commit is contained in:
@@ -504,6 +504,69 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/transactions/analytics": {
|
||||||
|
"get": {
|
||||||
|
"description": "Возвращает расходы, доходы и total за период. При фильтре по type второй тип возвращается как 0.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Transactions"
|
||||||
|
],
|
||||||
|
"summary": "Получить аналитику по транзакциям",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Family ID",
|
||||||
|
"name": "family_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Transaction type: income or expense",
|
||||||
|
"name": "type",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "RFC3339 start datetime",
|
||||||
|
"name": "date_from",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "RFC3339 end datetime",
|
||||||
|
"name": "date_to",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.TransactionAnalyticsResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/transactions/{id}": {
|
"/transactions/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Возвращает транзакцию по ее внутреннему ID",
|
"description": "Возвращает транзакцию по ее внутреннему ID",
|
||||||
@@ -1140,6 +1203,20 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.TransactionAnalyticsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expenses": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"incomes": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.TransactionListResponse": {
|
"dto.TransactionListResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -493,6 +493,69 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/transactions/analytics": {
|
||||||
|
"get": {
|
||||||
|
"description": "Возвращает расходы, доходы и total за период. При фильтре по type второй тип возвращается как 0.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Transactions"
|
||||||
|
],
|
||||||
|
"summary": "Получить аналитику по транзакциям",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Family ID",
|
||||||
|
"name": "family_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Transaction type: income or expense",
|
||||||
|
"name": "type",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "RFC3339 start datetime",
|
||||||
|
"name": "date_from",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "RFC3339 end datetime",
|
||||||
|
"name": "date_to",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.TransactionAnalyticsResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/transactions/{id}": {
|
"/transactions/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Возвращает транзакцию по ее внутреннему ID",
|
"description": "Возвращает транзакцию по ее внутреннему ID",
|
||||||
@@ -1129,6 +1192,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.TransactionAnalyticsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expenses": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"incomes": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.TransactionListResponse": {
|
"dto.TransactionListResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -151,6 +151,15 @@ definitions:
|
|||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
dto.TransactionAnalyticsResponse:
|
||||||
|
properties:
|
||||||
|
expenses:
|
||||||
|
type: number
|
||||||
|
incomes:
|
||||||
|
type: number
|
||||||
|
total:
|
||||||
|
type: number
|
||||||
|
type: object
|
||||||
dto.TransactionListResponse:
|
dto.TransactionListResponse:
|
||||||
properties:
|
properties:
|
||||||
items:
|
items:
|
||||||
@@ -630,6 +639,49 @@ paths:
|
|||||||
summary: Обновить транзакцию
|
summary: Обновить транзакцию
|
||||||
tags:
|
tags:
|
||||||
- Transactions
|
- Transactions
|
||||||
|
/transactions/analytics:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Возвращает расходы, доходы и total за период. При фильтре по type
|
||||||
|
второй тип возвращается как 0.
|
||||||
|
parameters:
|
||||||
|
- description: Family ID
|
||||||
|
in: query
|
||||||
|
name: family_id
|
||||||
|
type: integer
|
||||||
|
- description: 'Transaction type: income or expense'
|
||||||
|
in: query
|
||||||
|
name: type
|
||||||
|
type: string
|
||||||
|
- description: RFC3339 start datetime
|
||||||
|
in: query
|
||||||
|
name: date_from
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: RFC3339 end datetime
|
||||||
|
in: query
|
||||||
|
name: date_to
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.TransactionAnalyticsResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
|
summary: Получить аналитику по транзакциям
|
||||||
|
tags:
|
||||||
|
- Transactions
|
||||||
/users:
|
/users:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ type ListTransactionsQuery struct {
|
|||||||
Offset int `form:"offset"`
|
Offset int `form:"offset"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TransactionAnalyticsQuery struct {
|
||||||
|
FamilyID *int64 `form:"family_id"`
|
||||||
|
Type *string `form:"type"`
|
||||||
|
DateFrom string `form:"date_from" binding:"required"`
|
||||||
|
DateTo string `form:"date_to" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type TransactionResponse struct {
|
type TransactionResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
FamilyID int64 `json:"family_id"`
|
FamilyID int64 `json:"family_id"`
|
||||||
@@ -54,6 +61,12 @@ type TransactionListResponse struct {
|
|||||||
Items []TransactionResponse `json:"items"`
|
Items []TransactionResponse `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TransactionAnalyticsResponse struct {
|
||||||
|
Expenses float64 `json:"expenses"`
|
||||||
|
Incomes float64 `json:"incomes"`
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
func TransactionToResponse(transaction *domain.Transaction) TransactionResponse {
|
func TransactionToResponse(transaction *domain.Transaction) TransactionResponse {
|
||||||
return TransactionResponse{
|
return TransactionResponse{
|
||||||
ID: transaction.ID,
|
ID: transaction.ID,
|
||||||
@@ -77,3 +90,11 @@ func TransactionsToListResponse(transactions []*domain.Transaction) TransactionL
|
|||||||
|
|
||||||
return TransactionListResponse{Items: items}
|
return TransactionListResponse{Items: items}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TransactionAnalyticsToResponse(analytics domain.TransactionAnalytics) TransactionAnalyticsResponse {
|
||||||
|
return TransactionAnalyticsResponse{
|
||||||
|
Expenses: analytics.Expenses,
|
||||||
|
Incomes: analytics.Incomes,
|
||||||
|
Total: analytics.Total,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func (router *TransactionsRouter) RegisterRoutes(r *gin.RouterGroup) {
|
|||||||
{
|
{
|
||||||
transactions.POST("", router.Create)
|
transactions.POST("", router.Create)
|
||||||
transactions.GET("", router.List)
|
transactions.GET("", router.List)
|
||||||
|
transactions.GET("/analytics", router.Analytics)
|
||||||
transactions.GET("/:id", router.Read)
|
transactions.GET("/:id", router.Read)
|
||||||
transactions.PATCH("/:id", router.Update)
|
transactions.PATCH("/:id", router.Update)
|
||||||
transactions.DELETE("/:id", router.Delete)
|
transactions.DELETE("/:id", router.Delete)
|
||||||
@@ -114,6 +115,53 @@ func (router *TransactionsRouter) List(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, dto.TransactionsToListResponse(transactions))
|
c.JSON(http.StatusOK, dto.TransactionsToListResponse(transactions))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Analytics GoDoc
|
||||||
|
// @Summary Получить аналитику по транзакциям
|
||||||
|
// @Description Возвращает расходы, доходы и total за период. При фильтре по type второй тип возвращается как 0.
|
||||||
|
// @Tags Transactions
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param family_id query int false "Family ID"
|
||||||
|
// @Param type query string false "Transaction type: income or expense"
|
||||||
|
// @Param date_from query string true "RFC3339 start datetime"
|
||||||
|
// @Param date_to query string true "RFC3339 end datetime"
|
||||||
|
// @Success 200 {object} dto.TransactionAnalyticsResponse
|
||||||
|
// @Failure 400 {object} dto.ErrorResponse
|
||||||
|
// @Failure 500 {object} dto.ErrorResponse
|
||||||
|
// @Router /transactions/analytics [get]
|
||||||
|
func (router *TransactionsRouter) Analytics(c *gin.Context) {
|
||||||
|
var query dto.TransactionAnalyticsQuery
|
||||||
|
if err := c.ShouldBindQuery(&query); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dateFrom, err := time.Parse(time.RFC3339, query.DateFrom)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: "date_from must be RFC3339"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dateTo, err := time.Parse(time.RFC3339, query.DateTo)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: "date_to must be RFC3339"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
analytics, err := router.service.Analytics(c.Request.Context(), domain.TransactionAnalyticsFilter{
|
||||||
|
FamilyID: query.FamilyID,
|
||||||
|
Type: query.Type,
|
||||||
|
DateFrom: dateFrom,
|
||||||
|
DateTo: dateTo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
handleTransactionError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, dto.TransactionAnalyticsToResponse(analytics))
|
||||||
|
}
|
||||||
|
|
||||||
// Read GoDoc
|
// Read GoDoc
|
||||||
// @Summary Получить транзакцию по ID
|
// @Summary Получить транзакцию по ID
|
||||||
// @Description Возвращает транзакцию по ее внутреннему ID
|
// @Description Возвращает транзакцию по ее внутреннему ID
|
||||||
@@ -228,7 +276,8 @@ func handleTransactionError(c *gin.Context, err error) {
|
|||||||
c.JSON(http.StatusNotFound, dto.ErrorResponse{Message: err.Error()})
|
c.JSON(http.StatusNotFound, dto.ErrorResponse{Message: err.Error()})
|
||||||
case errors.Is(err, services.ErrTransactionPatch),
|
case errors.Is(err, services.ErrTransactionPatch),
|
||||||
errors.Is(err, services.ErrReceiptLinkConflict),
|
errors.Is(err, services.ErrReceiptLinkConflict),
|
||||||
errors.Is(err, services.ErrInvalidTransaction):
|
errors.Is(err, services.ErrInvalidTransaction),
|
||||||
|
errors.Is(err, services.ErrInvalidAnalytics):
|
||||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||||
case errors.Is(err, services.ErrReceiptNotFound):
|
case errors.Is(err, services.ErrReceiptNotFound):
|
||||||
c.JSON(http.StatusNotFound, dto.ErrorResponse{Message: err.Error()})
|
c.JSON(http.StatusNotFound, dto.ErrorResponse{Message: err.Error()})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type TransactionService interface {
|
|||||||
Create(ctx context.Context, req domain.CreateTransactionRequest) (*domain.Transaction, error)
|
Create(ctx context.Context, req domain.CreateTransactionRequest) (*domain.Transaction, error)
|
||||||
GetByID(ctx context.Context, id int64) (*domain.Transaction, error)
|
GetByID(ctx context.Context, id int64) (*domain.Transaction, error)
|
||||||
List(ctx context.Context, filter domain.TransactionListFilter) ([]*domain.Transaction, error)
|
List(ctx context.Context, filter domain.TransactionListFilter) ([]*domain.Transaction, error)
|
||||||
|
Analytics(ctx context.Context, filter domain.TransactionAnalyticsFilter) (domain.TransactionAnalytics, error)
|
||||||
Update(ctx context.Context, id int64, req domain.UpdateTransactionRequest) (*domain.Transaction, error)
|
Update(ctx context.Context, id int64, req domain.UpdateTransactionRequest) (*domain.Transaction, error)
|
||||||
Delete(ctx context.Context, id int64) error
|
Delete(ctx context.Context, id int64) error
|
||||||
}
|
}
|
||||||
@@ -31,6 +32,7 @@ var (
|
|||||||
ErrReceiptLinkConflict = errors.New("receipt_id and detach_receipt cannot be used together")
|
ErrReceiptLinkConflict = errors.New("receipt_id and detach_receipt cannot be used together")
|
||||||
ErrInvalidTransaction = errors.New("type and category are required")
|
ErrInvalidTransaction = errors.New("type and category are required")
|
||||||
ErrReceiptNotFound = errors.New("receipt not found")
|
ErrReceiptNotFound = errors.New("receipt not found")
|
||||||
|
ErrInvalidAnalytics = errors.New("type must be income or expense")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *transactionService) Create(ctx context.Context, req domain.CreateTransactionRequest) (*domain.Transaction, error) {
|
func (s *transactionService) Create(ctx context.Context, req domain.CreateTransactionRequest) (*domain.Transaction, error) {
|
||||||
@@ -85,6 +87,18 @@ func (s *transactionService) List(ctx context.Context, filter domain.Transaction
|
|||||||
return s.repo.List(ctx, filter)
|
return s.repo.List(ctx, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *transactionService) Analytics(ctx context.Context, filter domain.TransactionAnalyticsFilter) (domain.TransactionAnalytics, error) {
|
||||||
|
if filter.Type != nil {
|
||||||
|
typeValue := strings.TrimSpace(*filter.Type)
|
||||||
|
if typeValue != "income" && typeValue != "expense" {
|
||||||
|
return domain.TransactionAnalytics{}, ErrInvalidAnalytics
|
||||||
|
}
|
||||||
|
filter.Type = &typeValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.Analytics(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *transactionService) Update(ctx context.Context, id int64, req domain.UpdateTransactionRequest) (*domain.Transaction, error) {
|
func (s *transactionService) Update(ctx context.Context, id int64, req domain.UpdateTransactionRequest) (*domain.Transaction, error) {
|
||||||
if req.ReceiptID != nil && req.DetachReceipt {
|
if req.ReceiptID != nil && req.DetachReceipt {
|
||||||
return nil, ErrReceiptLinkConflict
|
return nil, ErrReceiptLinkConflict
|
||||||
|
|||||||
@@ -46,3 +46,16 @@ type TransactionListFilter struct {
|
|||||||
Limit int
|
Limit int
|
||||||
Offset int
|
Offset int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TransactionAnalyticsFilter struct {
|
||||||
|
FamilyID *int64
|
||||||
|
Type *string
|
||||||
|
DateFrom time.Time
|
||||||
|
DateTo time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransactionAnalytics struct {
|
||||||
|
Expenses float64
|
||||||
|
Incomes float64
|
||||||
|
Total float64
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type TransactionRepository interface {
|
|||||||
Create(ctx context.Context, transaction *domain.Transaction) error
|
Create(ctx context.Context, transaction *domain.Transaction) error
|
||||||
GetByID(ctx context.Context, id int64) (*domain.Transaction, error)
|
GetByID(ctx context.Context, id int64) (*domain.Transaction, error)
|
||||||
List(ctx context.Context, filter domain.TransactionListFilter) ([]*domain.Transaction, error)
|
List(ctx context.Context, filter domain.TransactionListFilter) ([]*domain.Transaction, error)
|
||||||
|
Analytics(ctx context.Context, filter domain.TransactionAnalyticsFilter) (domain.TransactionAnalytics, error)
|
||||||
Update(ctx context.Context, transaction *domain.Transaction, syncReceipt bool) error
|
Update(ctx context.Context, transaction *domain.Transaction, syncReceipt bool) error
|
||||||
Delete(ctx context.Context, id int64) error
|
Delete(ctx context.Context, id int64) error
|
||||||
}
|
}
|
||||||
@@ -188,6 +189,47 @@ func (r *TransactionsSQLRepository) List(ctx context.Context, filter domain.Tran
|
|||||||
return transactions, rows.Err()
|
return transactions, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *TransactionsSQLRepository) Analytics(ctx context.Context, filter domain.TransactionAnalyticsFilter) (domain.TransactionAnalytics, error) {
|
||||||
|
var (
|
||||||
|
whereClauses []string
|
||||||
|
args []any
|
||||||
|
)
|
||||||
|
|
||||||
|
appendFilter := func(condition string, value any) {
|
||||||
|
args = append(args, value)
|
||||||
|
whereClauses = append(whereClauses, fmt.Sprintf(condition, len(args)))
|
||||||
|
}
|
||||||
|
|
||||||
|
appendFilter("datetime >= $%d", filter.DateFrom)
|
||||||
|
appendFilter("datetime <= $%d", filter.DateTo)
|
||||||
|
|
||||||
|
if filter.FamilyID != nil {
|
||||||
|
appendFilter("family_id = $%d", *filter.FamilyID)
|
||||||
|
}
|
||||||
|
if filter.Type != nil {
|
||||||
|
appendFilter("type = $%d", *filter.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0),
|
||||||
|
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0)
|
||||||
|
FROM transactions
|
||||||
|
`
|
||||||
|
|
||||||
|
if len(whereClauses) > 0 {
|
||||||
|
query += " WHERE " + strings.Join(whereClauses, " AND ")
|
||||||
|
}
|
||||||
|
|
||||||
|
var analytics domain.TransactionAnalytics
|
||||||
|
if err := r.db.QueryRowContext(ctx, query, args...).Scan(&analytics.Expenses, &analytics.Incomes); err != nil {
|
||||||
|
return domain.TransactionAnalytics{}, err
|
||||||
|
}
|
||||||
|
analytics.Total = analytics.Incomes - analytics.Expenses
|
||||||
|
|
||||||
|
return analytics, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *TransactionsSQLRepository) Update(ctx context.Context, transaction *domain.Transaction, syncReceipt bool) error {
|
func (r *TransactionsSQLRepository) Update(ctx context.Context, transaction *domain.Transaction, syncReceipt bool) error {
|
||||||
tx, err := r.db.BeginTx(ctx, nil)
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user