diff --git a/api/api.go b/api/api.go index 11522da..42a8f99 100644 --- a/api/api.go +++ b/api/api.go @@ -2,6 +2,7 @@ package api import ( "database/sql" + "github.com/KKGo-Software-engineering/workshop-summer/api/summary" "github.com/KKGo-Software-engineering/workshop-summer/api/transaction" "github.com/KKGo-Software-engineering/workshop-summer/api/config" @@ -43,5 +44,11 @@ func New(db *sql.DB, cfg config.Config, logger *zap.Logger) *Server { v1.POST("/transactions", h.Create) } + { + h := summary.New(cfg.FeatureFlag, db) + v1.GET("/spenders/:id/expenses/summary", h.GetExpenseSummaryHandler) + //v1.GET("/spenders/:id/incomes/summary", h.GetIncomeSummaryHandler) + } + return &Server{e} } diff --git a/api/expense/summary/summary.go b/api/expense/summary/summary.go deleted file mode 100644 index 764af8a..0000000 --- a/api/expense/summary/summary.go +++ /dev/null @@ -1,80 +0,0 @@ -package summary - -import ( - "database/sql" - "errors" - "github.com/KKGo-Software-engineering/workshop-summer/api/config" - "github.com/kkgo-software-engineering/workshop/mlog" - "github.com/labstack/echo/v4" - "go.uber.org/zap" - "net/http" - "time" -) - -var ( - ErrInvalidSpender = errors.New("invalid spender") -) - -type Err struct { - Message string `json:"message"` -} - -type Spender struct { - ID int `param:"id"` -} - -type RawData struct { - Date time.Time - SumAmount float64 - CountExpenses int -} - -type Summary struct { - Total float64 `json:"total_amount"` - Average float64 `json:"average_per_day"` - Count int `json:"count_transaction"` -} - -type handler struct { - flag config.FeatureFlag - db *sql.DB -} - -func New(cfg config.FeatureFlag, db *sql.DB) *handler { - return &handler{cfg, db} -} - -func summary(data []RawData) Summary { - if len(data) == 0 { - return Summary{} - } - - var total float64 - var count int - for _, d := range data { - total += d.SumAmount - count += d.CountExpenses - } - - return Summary{ - Total: total, - Average: total / float64(len(data)), - Count: count, - } -} - -// /api/v1/spenders/{id}/expenses/summary -func (h *handler) GetExpenseSummaryHandler(c echo.Context) error { - logger := mlog.L(c) - //ctx := c.Request().Context() - _ = c.Request().Context() - - var spender Spender - err := c.Bind(&spender) - if err != nil { - logger.Error(ErrInvalidSpender.Error(), zap.Error(err)) - return c.JSON(http.StatusBadRequest, Err{Message: ErrInvalidSpender.Error()}) - } - - return c.JSON(http.StatusOK, Summary{}) -} diff --git a/api/summary/summary.go b/api/summary/summary.go new file mode 100644 index 0000000..215137c --- /dev/null +++ b/api/summary/summary.go @@ -0,0 +1,131 @@ +package summary + +import ( + "database/sql" + "errors" + "github.com/KKGo-Software-engineering/workshop-summer/api/config" + "github.com/kkgo-software-engineering/workshop/mlog" + "github.com/labstack/echo/v4" + "go.uber.org/zap" + "net/http" + "time" +) + +const ( + typeExpense = "expense" + typeIncome = "income" +) + +var ( + ErrInvalidSpender = errors.New("invalid spender") +) + +type Err struct { + Message string `json:"message"` +} + +type Spender struct { + ID int `param:"id"` +} + +type RawData struct { + Date time.Time + SumAmount float64 + CountExpenses int +} + +type Summary struct { + Total float64 `json:"total_amount"` + Average float64 `json:"average_per_day"` + Count int `json:"count_transaction"` +} + +type handler struct { + flag config.FeatureFlag + db *sql.DB +} + +func New(cfg config.FeatureFlag, db *sql.DB) *handler { + return &handler{cfg, db} +} + +func summary(data []RawData) Summary { + if len(data) == 0 { + return Summary{} + } + + var total float64 + var count int + for _, d := range data { + total += d.SumAmount + count += d.CountExpenses + } + + return Summary{ + Total: total, + Average: total / float64(len(data)), + Count: count, + } +} + +const ( + sumSQL = `SELECT + date_trunc('day', date)::date AS transaction_date, + SUM(amount) AS total_amount, + COUNT(*) AS record_count + FROM + "transaction" + WHERE + transaction_type = $1 AND spender_id = $2 + GROUP BY + date_trunc('day', date)::date + ORDER BY + transaction_date;` +) + +func getSummary(c echo.Context, db *sql.DB, tnxType string) error { + logger := mlog.L(c) + ctx := c.Request().Context() + + var spender Spender + err := c.Bind(&spender) + if err != nil { + logger.Error(ErrInvalidSpender.Error(), zap.Error(err)) + return c.JSON(http.StatusBadRequest, Err{Message: ErrInvalidSpender.Error()}) + } + + stmt, err := db.PrepareContext(ctx, sumSQL) + if err != nil { + logger.Error("prepare statement error", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, Err{Message: "prepare statement error"}) + } + + rows, err := stmt.QueryContext(ctx, tnxType, spender.ID) + if err != nil { + logger.Error("query error", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, Err{Message: "query error"}) + } + defer rows.Close() + + var raws []RawData + for rows.Next() { + var raw RawData + err := rows.Scan(&raw.Date, &raw.SumAmount, &raw.CountExpenses) + if err != nil { + logger.Error("scan error", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, Err{Message: "scan error"}) + } + raws = append(raws, raw) + } + + return c.JSON(http.StatusOK, summary(raws)) +} + +func (h *handler) GetExpenseSummaryHandler(c echo.Context) error { + return getSummary(c, h.db, typeExpense) +} + +// +//func (h *handler) GetIncomeSummaryHandler(c echo.Context) error { +// return getSummary(c, h.db, typeIncome) +//} diff --git a/api/expense/summary/summary_test.go b/api/summary/summary_test.go similarity index 100% rename from api/expense/summary/summary_test.go rename to api/summary/summary_test.go diff --git a/sql/add-sample-transaction.sql b/sql/add-sample-transaction.sql new file mode 100644 index 0000000..c9049b4 --- /dev/null +++ b/sql/add-sample-transaction.sql @@ -0,0 +1,35 @@ +INSERT INTO "transaction" (date, amount, category, transaction_type, note, image_url, spender_id) VALUES +-- Day 1: 2024-05-01 +('2024-05-01 10:15:00+00', 23, 'groceries', 'expense', 'Mock transaction 1', 'http://example.com/receipt1.jpg', 1), +('2024-05-01 12:30:00+00', 45, 'rent', 'income', 'Mock transaction 2', 'http://example.com/receipt2.jpg', 1), +('2024-05-01 15:45:00+00', 12, 'utilities', 'expense', 'Mock transaction 3', 'http://example.com/receipt3.jpg', 1), +('2024-05-01 18:00:00+00', 67, 'entertainment', 'income', 'Mock transaction 4', 'http://example.com/receipt4.jpg', 1), +('2024-05-01 20:15:00+00', 54, 'transport', 'expense', 'Mock transaction 5', 'http://example.com/receipt5.jpg', 1), + +-- Day 2: 2024-05-02 +('2024-05-02 09:00:00+00', 31, 'groceries', 'income', 'Mock transaction 6', 'http://example.com/receipt6.jpg', 1), +('2024-05-02 11:15:00+00', 29, 'rent', 'expense', 'Mock transaction 7', 'http://example.com/receipt7.jpg', 1), +('2024-05-02 13:30:00+00', 46, 'utilities', 'income', 'Mock transaction 8', 'http://example.com/receipt8.jpg', 1), +('2024-05-02 15:45:00+00', 18, 'entertainment', 'expense', 'Mock transaction 9', 'http://example.com/receipt9.jpg', 1), +('2024-05-02 18:00:00+00', 39, 'transport', 'income', 'Mock transaction 10', 'http://example.com/receipt10.jpg', 1), + +-- Day 3: 2024-05-03 +('2024-05-03 08:00:00+00', 22, 'groceries', 'expense', 'Mock transaction 11', 'http://example.com/receipt11.jpg', 1), +('2024-05-03 10:15:00+00', 53, 'rent', 'income', 'Mock transaction 12', 'http://example.com/receipt12.jpg', 1), +('2024-05-03 12:30:00+00', 44, 'utilities', 'expense', 'Mock transaction 13', 'http://example.com/receipt13.jpg', 1), +('2024-05-03 14:45:00+00', 19, 'entertainment', 'income', 'Mock transaction 14', 'http://example.com/receipt14.jpg', 1), +('2024-05-03 17:00:00+00', 61, 'transport', 'expense', 'Mock transaction 15', 'http://example.com/receipt15.jpg', 1), + +-- Day 4: 2024-05-04 +('2024-05-04 07:00:00+00', 38, 'groceries', 'income', 'Mock transaction 16', 'http://example.com/receipt16.jpg', 1), +('2024-05-04 09:15:00+00', 27, 'rent', 'expense', 'Mock transaction 17', 'http://example.com/receipt17.jpg', 1), +('2024-05-04 11:30:00+00', 49, 'utilities', 'income', 'Mock transaction 18', 'http://example.com/receipt18.jpg', 1), +('2024-05-04 13:45:00+00', 32, 'entertainment', 'expense', 'Mock transaction 19', 'http://example.com/receipt19.jpg', 1), +('2024-05-04 16:00:00+00', 55, 'transport', 'income', 'Mock transaction 20', 'http://example.com/receipt20.jpg', 1), + +-- Day 5: 2024-05-05 +('2024-05-05 06:00:00+00', 26, 'groceries', 'expense', 'Mock transaction 21', 'http://example.com/receipt21.jpg', 1), +('2024-05-05 08:15:00+00', 48, 'rent', 'income', 'Mock transaction 22', 'http://example.com/receipt22.jpg', 1), +('2024-05-05 10:30:00+00', 35, 'utilities', 'expense', 'Mock transaction 23', 'http://example.com/receipt23.jpg', 1), +('2024-05-05 12:45:00+00', 43, 'entertainment', 'income', 'Mock transaction 24', 'http://example.com/receipt24.jpg', 1), +('2024-05-05 15:00:00+00', 59, 'transport', 'expense', 'Mock transaction 25', 'http://example.com/receipt25.jpg', 1); diff --git a/sql/get-raw-summary-expense.sql b/sql/get-raw-summary-expense.sql new file mode 100644 index 0000000..dd3df76 --- /dev/null +++ b/sql/get-raw-summary-expense.sql @@ -0,0 +1,12 @@ +SELECT + date_trunc('day', date)::date AS transaction_date, + SUM(amount) AS total_amount, + COUNT(*) AS record_count +FROM + "transaction" +WHERE + transaction_type = 'expense' AND spender_id = 1 +GROUP BY + date_trunc('day', date)::date +ORDER BY + transaction_date;