Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions api/externals/handler/campus_donation_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package handler

import (
"net/http"

"github.com/labstack/echo/v4"
)

// router.GET(baseURL+"/campus_donations/years/:year/buildings/:building_id/floors/:floor_number", wrapper.GetCampusDonationsYearsYearBuildingsBuildingIdFloorsFloorNumber)
func (h *Handler) GetCampusDonationsYearsYearBuildingsBuildingIdFloorsFloorNumber(
c echo.Context,
year int,
buildingId int,
floorNumber string,
) error {
buildingFloors, err := h.campusDonationUseCase.GetBuildingFloorDonationsByYear(
c.Request().Context(),
year,
buildingId,
floorNumber,
)
if err != nil {
return err
}

return c.JSON(http.StatusOK, buildingFloors)
}
3 changes: 3 additions & 0 deletions api/externals/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Handler struct {
activityStyleUseCase usecase.ActivityStyleUseCase
bureauUseCase usecase.BureauUseCase
buyReportUseCase usecase.BuyReportUseCase
campusDonationUseCase usecase.CampusDonationUseCase
departmentUseCase usecase.DepartmentUseCase
divisionUseCase usecase.DivisionUseCase
festivalItemUseCase usecase.FestivalItemUseCase
Expand All @@ -33,6 +34,7 @@ func NewHandler(
activityStyleUseCase usecase.ActivityStyleUseCase,
bureauUseCase usecase.BureauUseCase,
buyReportUseCase usecase.BuyReportUseCase,
campusDonationUseCase usecase.CampusDonationUseCase,
departmentUseCase usecase.DepartmentUseCase,
divisionUseCase usecase.DivisionUseCase,
festivalItemUseCase usecase.FestivalItemUseCase,
Expand All @@ -55,6 +57,7 @@ func NewHandler(
activityStyleUseCase: activityStyleUseCase,
bureauUseCase: bureauUseCase,
buyReportUseCase: buyReportUseCase,
campusDonationUseCase: campusDonationUseCase,
departmentUseCase: departmentUseCase,
divisionUseCase: divisionUseCase,
festivalItemUseCase: festivalItemUseCase,
Expand Down
90 changes: 90 additions & 0 deletions api/externals/repository/campus_donation_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package repository

import (
"context"
"database/sql"

"github.com/NUTFes/FinanSu/api/drivers/db"
goqu "github.com/doug-martin/goqu/v9"
)

type campusDonationRepository struct {
client db.Client
}

type CampusDonationRepository interface {
GetBuildingFloorDonationsByYear(context.Context, int, int, string) (*sql.Rows, error)
}

func NewCampusDonationRepository(c db.Client) CampusDonationRepository {
return &campusDonationRepository{client: c}
}

func (cdr *campusDonationRepository) GetBuildingFloorDonationsByYear(
ctx context.Context,
year int,
buildingID int,
floorNumber string,
) (*sql.Rows, error) {
donationTotalsByTeacher := dialect.
Select(
goqu.I("campus_donations.teacher_id").As("teacher_id"),
goqu.SUM(goqu.I("campus_donations.price")).As("total_price"),
).
From(goqu.T("campus_donations")).
InnerJoin(
goqu.T("years"),
goqu.On(goqu.I("campus_donations.year_id").Eq(goqu.I("years.id"))),
).
Where(goqu.I("years.year").Eq(year)).
GroupBy(goqu.I("campus_donations.teacher_id"))
Comment on lines +108 to +119
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The subquery donationTotalsByTeacher filters by year but does not filter by building or floor. While this is logically correct because it's joined later on teacher_id, it might be more efficient to include building/floor filters in the subquery if the campus_donations table is very large, to reduce the number of rows processed in the aggregation.


query, args, err := dialect.
From(goqu.T("buildings")).
InnerJoin(
goqu.T("buildings").As("selected_building"),
goqu.On(
goqu.I("buildings.name").Eq(goqu.I("selected_building.name")),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Joining on buildings.name to group units assumes that building names are unique identifiers for a group of units. If there's any risk of different buildings having the same name, this could lead to incorrect data aggregation. Consider if a more robust grouping mechanism (like a parent_building_id) is available or needed in the future.

goqu.I("selected_building.id").Eq(buildingID),
),
).
InnerJoin(
goqu.T("rooms"),
goqu.On(goqu.I("rooms.building_id").Eq(goqu.I("buildings.id"))),
).
InnerJoin(
goqu.T("room_teachers"),
goqu.On(goqu.I("room_teachers.room_id").Eq(goqu.I("rooms.id"))),
).
InnerJoin(
goqu.T("teachers"),
goqu.On(goqu.I("teachers.id").Eq(goqu.I("room_teachers.teacher_id"))),
).
LeftJoin(
donationTotalsByTeacher.As("donation_totals"),
goqu.On(goqu.I("donation_totals.teacher_id").Eq(goqu.I("teachers.id"))),
).
Select(
goqu.I("buildings.id").As("building_id"),
goqu.I("buildings.name").As("building_name"),
goqu.I("buildings.unit_number").As("unit_number"),
goqu.I("rooms.floor_number").As("floor_number"),
goqu.I("rooms.room_name").As("room_name"),
goqu.I("teachers.id").As("teacher_id"),
goqu.I("teachers.name").As("teacher_name"),
goqu.COALESCE(goqu.I("donation_totals.total_price"), 0).As("total_price"),
Copy link
Copy Markdown
Collaborator

@Chikuwa0141 Chikuwa0141 May 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

仕様書

### 回収済み/未回収の判定
- 金額が登録済み(または募金レコードが存在)なら回収済み
- 未登録なら未回収

みたいに回収済みの0円なのか未回収なのかわかるようにしたいからnullのままはどう?
それともisDonatedとか追加する?

goqu.COALESCE(goqu.I("teachers.is_black"), false).As("is_black"),
).
Where(goqu.I("rooms.floor_number").Eq(floorNumber)).
Order(
goqu.I("buildings.unit_number").Asc(),
goqu.I("rooms.room_name").Asc(),
goqu.I("teachers.name").Asc(),
).
ToSQL()
if err != nil {
return nil, err
}

return cdr.client.DB().QueryContext(ctx, query, args...)
}
1 change: 1 addition & 0 deletions api/externals/repository/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var RepositoryProviderSet = wire.NewSet(
NewBudgetRepository,
NewBureauRepository,
NewBuyReportRepository,
NewCampusDonationRepository,
NewDepartmentRepository,
NewDivisionRepository,
NewExpenseRepository,
Expand Down
54 changes: 54 additions & 0 deletions api/generated/openapi_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion api/internals/di/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions api/internals/domain/campus_donation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package domain

type CampusDonationBuildingFloorRow struct {
BuildingID int
BuildingName string
UnitNumber int
FloorNumber string
RoomName string
TeacherID int
TeacherName string
TotalPrice int
IsBlack bool
}
90 changes: 90 additions & 0 deletions api/internals/usecase/campus_donation_usecase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package usecase

import (
"context"
"log"

rep "github.com/NUTFes/FinanSu/api/externals/repository"
"github.com/NUTFes/FinanSu/api/generated"
"github.com/NUTFes/FinanSu/api/internals/domain"
"github.com/pkg/errors"
)

type campusDonationUseCase struct {
rep rep.CampusDonationRepository
}

type CampusDonationUseCase interface {
GetBuildingFloorDonationsByYear(context.Context, int, int, string) ([]generated.CampusDonationBuildingFloor, error)
}

func NewCampusDonationUseCase(rep rep.CampusDonationRepository) CampusDonationUseCase {
return &campusDonationUseCase{rep: rep}
}

func (cdu *campusDonationUseCase) GetBuildingFloorDonationsByYear(
ctx context.Context,
year int,
buildingID int,
floorNumber string,
) ([]generated.CampusDonationBuildingFloor, error) {
rows, err := cdu.rep.GetBuildingFloorDonationsByYear(ctx, year, buildingID, floorNumber)
if err != nil {
return nil, err
}
defer func() {
if err := rows.Close(); err != nil {
log.Println(err)
}
}()

buildingFloors := make([]generated.CampusDonationBuildingFloor, 0)
buildingIndexByID := make(map[int]int)

for rows.Next() {
var row domain.CampusDonationBuildingFloorRow
if err := rows.Scan(
&row.BuildingID,
&row.BuildingName,
&row.UnitNumber,
&row.FloorNumber,
&row.RoomName,
&row.TeacherID,
&row.TeacherName,
&row.TotalPrice,
&row.IsBlack,
); err != nil {
return nil, errors.Wrap(err, "failed to scan campus donation building floor row")
}

index, ok := buildingIndexByID[row.BuildingID]
if !ok {
buildingFloors = append(buildingFloors, generated.CampusDonationBuildingFloor{
BuildingId: row.BuildingID,
BuildingName: row.BuildingName,
UnitNumber: row.UnitNumber,
FloorNumber: row.FloorNumber,
Donations: []generated.CampusDonationTeacher{},
})
index = len(buildingFloors) - 1
buildingIndexByID[row.BuildingID] = index
}

buildingFloors[index].Donations = append(
buildingFloors[index].Donations,
generated.CampusDonationTeacher{
RoomName: row.RoomName,
TeacherId: row.TeacherID,
TeacherName: row.TeacherName,
TotalPrice: row.TotalPrice,
IsBlack: row.IsBlack,
},
)
}

if err := rows.Err(); err != nil {
return nil, errors.Wrap(err, "failed to iterate campus donation building floors")
}

return buildingFloors, nil
}
1 change: 1 addition & 0 deletions api/internals/usecase/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var UseCaseProviderSet = wire.NewSet(
NewBudgetUseCase,
NewBureauUseCase,
NewBuyReportUseCase,
NewCampusDonationUseCase,
NewDepartmentUseCase,
NewDivisionUseCase,
NewExpenseUseCase,
Expand Down
11 changes: 11 additions & 0 deletions api/test/fixtures/buildings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
- id: 1
name: 学内募金API確認棟
unit_number: 1

- id: 2
name: 学内募金API確認棟
unit_number: 2

- id: 3
name: 学内募金API確認別棟
unit_number: 1
Loading
Loading