Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
34 changes: 34 additions & 0 deletions sysken-pay-backend/app/domain/object/user/get_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package user

import "time"

//TODO モデル(データベースから取得する型を宣言する)
//データベースの制約通りになるようにエラーハンドリングをガチる
//ユーザーID、名前、作成日時、更新日時など

func NewUserFromDB(
userID string,
userName string,
createdAt time.Time,
updatedAt time.Time,
deletedAt time.Time,
) (*User, error) {
user, err := NewUser(userID, userName)
if err != nil {
return nil, err
}

if err := user.SetCreatedAt(createdAt); err != nil {
return nil, err
}
if err := user.SetUpdatedAt(updatedAt); err != nil {
return nil, err
}
if !deletedAt.IsZero() {
if err := user.SetDeletedAt(deletedAt); err != nil {
return nil, err
}
}

return user, nil
}
8 changes: 8 additions & 0 deletions sysken-pay-backend/app/domain/object/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package user

import (
"errors"
"regexp"
"time"
"unicode/utf8"
)

// 学籍番号フォーマット: 2桁の数字 + 1文字のアルファベット + 数字 (例: 20K23099)
var userIDPattern = regexp.MustCompile(`^\d{2}[A-Za-z]\d+$`)

//TODO モデル(データベースに入れる型を宣言する)
//データベースの制約通りになるようにエラーハンドリングをガチる
//ユーザーID、名前、作成日時、更新日時など
Expand All @@ -27,6 +31,10 @@ func (p *User) SetUserID(userID string) error {
if utf8.RuneCountInString(userID) > 20 {
return errors.New("userID must be 20 characters or less")
}
// 学籍番号フォーマット (例: 20K23099)
if !userIDPattern.MatchString(userID) {
return errors.New("userID must be in student ID format (e.g., 20K23099)")
}

p.userID = userID
return nil
Expand Down
45 changes: 26 additions & 19 deletions sysken-pay-backend/app/domain/object/user/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

func TestSetUserID_Valid(t *testing.T) {
u := &User{}
if err := u.SetUserID("user123"); err != nil {
if err := u.SetUserID("20K23099"); err != nil {
t.Errorf("SetUserID should succeed: %v", err)
}
}
Expand All @@ -23,31 +23,37 @@ func TestSetUserID_Empty(t *testing.T) {

func TestSetUserID_Exactly20Chars(t *testing.T) {
u := &User{}
id := strings.Repeat("a", 20)
// 学籍番号フォーマットで20文字 (2桁数字 + 1文字 + 17桁数字)
id := "20K" + strings.Repeat("1", 17)
if err := u.SetUserID(id); err != nil {
t.Errorf("SetUserID 20 chars should succeed: %v", err)
}
}

func TestSetUserID_Over20Chars(t *testing.T) {
u := &User{}
id := strings.Repeat("a", 21)
id := "20K" + strings.Repeat("1", 18) // 21 chars
if err := u.SetUserID(id); err == nil {
t.Error("SetUserID 21 chars should fail")
}
}

func TestSetUserID_MultibyteCounts(t *testing.T) {
func TestSetUserID_InvalidFormat(t *testing.T) {
u := &User{}
// 日本語20文字はOK
id := strings.Repeat("あ", 20)
if err := u.SetUserID(id); err != nil {
t.Errorf("SetUserID 20 multibyte chars should succeed: %v", err)
}
// 21文字はNG
id21 := strings.Repeat("あ", 21)
if err := u.SetUserID(id21); err == nil {
t.Error("SetUserID 21 multibyte chars should fail")
cases := []string{
"abcdef", // アルファベットのみ
"user123", // 数字2桁スタートでない
"123", // 全数字
"20K", // 末尾の数字なし
"20KK23099", // アルファベット2文字
"K20K23099", // 先頭が数字でない
"2-K23099", // 記号混入
strings.Repeat("あ", 8), // マルチバイトはフォーマット違反
}
for _, id := range cases {
if err := u.SetUserID(id); err == nil {
t.Errorf("SetUserID(%q) should fail format validation", id)
}
}
}

Expand Down Expand Up @@ -98,12 +104,12 @@ func TestSetUserName_MultibyteCounts(t *testing.T) {
// --- NewUser ---

func TestNewUser_Valid(t *testing.T) {
u, err := NewUser("student001", "田中 太郎")
u, err := NewUser("20K23099", "田中 太郎")
if err != nil {
t.Fatalf("NewUser should succeed: %v", err)
}
if u.ID() != "student001" {
t.Errorf("ID() = %s, want student001", u.ID())
if u.ID() != "20K23099" {
t.Errorf("ID() = %s, want 20K23099", u.ID())
}
if u.UserName() != "田中 太郎" {
t.Errorf("UserName() = %s, want 田中 太郎", u.UserName())
Expand All @@ -117,19 +123,20 @@ func TestNewUser_EmptyUserID(t *testing.T) {
}

func TestNewUser_EmptyUserName(t *testing.T) {
if _, err := NewUser("student001", ""); err == nil {
if _, err := NewUser("20K23099", ""); err == nil {
t.Error("NewUser empty userName should fail")
}
}

func TestNewUser_UserIDTooLong(t *testing.T) {
if _, err := NewUser(strings.Repeat("a", 21), "田中 太郎"); err == nil {
id := "20K" + strings.Repeat("1", 18) // 21 chars
if _, err := NewUser(id, "田中 太郎"); err == nil {
t.Error("NewUser userID > 20 chars should fail")
}
}

func TestNewUser_UserNameTooLong(t *testing.T) {
if _, err := NewUser("student001", strings.Repeat("a", 51)); err == nil {
if _, err := NewUser("20K23099", strings.Repeat("a", 51)); err == nil {
t.Error("NewUser userName > 50 chars should fail")
}
}
3 changes: 3 additions & 0 deletions sysken-pay-backend/app/domain/repository/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
//データベースで必要な入力と出力のインターフェースの作成

type UserRepository interface {
// ユーザーIDでユーザー情報を取得する
GetUserByID(ctx context.Context, userID string) (*user.User, error)

// ユーザーを新規作成して保存する
// 保存に成功した場合は保存したユーザーを返す
InsertUser(ctx context.Context, u *user.User) (*user.User, error)
Expand Down
31 changes: 31 additions & 0 deletions sysken-pay-backend/app/infra/repository/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,37 @@ func NewUserProfileRepository(db *sql.DB) *UserRepositoryImpl {
return &UserRepositoryImpl{db: db}
}

func (r *UserRepositoryImpl) GetUserByID(ctx context.Context, userID string) (*user.User, error) {
executor := getExecutor(ctx, r.db)

row := executor.QueryRowContext(ctx, `
SELECT id, name, created_at, updated_at, deleted_at
FROM `+"`user`"+`
WHERE id = ? AND deleted_at IS NULL
`, userID)

var (
id string
name string
createdAt time.Time
updatedAt time.Time
deletedAt sql.NullTime
)
if err := row.Scan(&id, &name, &createdAt, &updatedAt, &deletedAt); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}

var deleted time.Time
if deletedAt.Valid {
deleted = deletedAt.Time
}

return user.NewUserFromDB(id, name, createdAt, updatedAt, deleted)
}

func (r *UserRepositoryImpl) InsertUser(
ctx context.Context, u *user.User) (*user.User, error) {

Expand Down
6 changes: 4 additions & 2 deletions sysken-pay-backend/app/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func Run(db *sql.DB) error {
balanceRepo := repository.NewBalanceRepository(db)

// UseCase
getUserUseCase := user.NewGetUserUseCase(userRepo)
registerUserUseCase := user.NewRegisterUserUseCase(userRepo)
updateUserUseCase := user.NewUpdateUserUseCase(userRepo)
registerItemUseCase := item.NewRegisterItemUseCase(itemRepo)
Expand All @@ -70,7 +71,7 @@ func Run(db *sql.DB) error {
getPurchaseHistoriesUseCase := balance.NewGetPurchaseHistoriesUseCase(balanceRepo)

// Handler
userHandler := api_user.NewUserHandler(registerUserUseCase, updateUserUseCase)
userHandler := api_user.NewUserHandler(getUserUseCase, registerUserUseCase, updateUserUseCase)
itemHandler := api_item.NewItemHandler(registerItemUseCase, updateItemUseCase, findItemByJanCodeUseCase, getAllItemsUseCase)
chargeHandler := api_charge.NewChargeHandler(chargeAmountUseCase, chargeCancelUseCase)
purchaseHandler := api_purchase.NewPurchaseHandler(createPurchaseUseCase, cancelPurchaseUseCase)
Expand All @@ -93,8 +94,9 @@ func Run(db *sql.DB) error {
r.Route("/v1", func(r chi.Router) {
// ユーザー関連
r.Post("/user", userHandler.RegisterUser)
r.Patch("/user/{user_id}", userHandler.UpdateUser)
r.Route("/user/{user_id}", func(r chi.Router) {
r.Get("/", userHandler.GetUser)
r.Patch("/", userHandler.UpdateUser)
Comment on lines +98 to +99
r.Post("/charge", chargeHandler.ChargeAmount)
r.Post("/charge/cancel", chargeHandler.ChargeCancel)
r.Post("/purchase", purchaseHandler.CreatePurchase)
Expand Down
16 changes: 16 additions & 0 deletions sysken-pay-backend/app/ui/api/user/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ func toPostUserResponse(user *user.User) *PostUserResponse {
}
}

type GetUserResponse struct {
Status string `json:"status"`
UserID string `json:"user_id"`
UserName string `json:"user_name"`
CreatedAt string `json:"created_at"`
}

func toGetUserResponse(user *user.User) *GetUserResponse {
return &GetUserResponse{
Status: "success",
UserID: user.ID(),
UserName: user.UserName(),
CreatedAt: user.CreatedAt().Format("2006-01-02T15:04:05.000Z"),
}
Comment on lines +30 to +36
}

type PatchUserResponse struct {
Status string `json:"status"`
UserID string `json:"user_id"`
Expand Down
44 changes: 43 additions & 1 deletion sysken-pay-backend/app/ui/api/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package user

import (
"encoding/json"
"errors"
"log"
"net/http"
apierrors "sysken-pay-api/app/ui/api/pkg/errors"
Expand All @@ -12,12 +13,18 @@ import (

// TODO APIリクエストからデータを整形してユースケースに情報を渡すものを作成する
type Handler interface {
GetUser(w http.ResponseWriter, r *http.Request)
RegisterUser(w http.ResponseWriter, r *http.Request)
UpdateUser(w http.ResponseWriter, r *http.Request)
}

func NewUserHandler(registerUserUseCase user.RegisterUserUseCase, updateUserUseCase user.UpdateUserUseCase) Handler {
func NewUserHandler(
getUserUseCase user.GetUserUseCase,
registerUserUseCase user.RegisterUserUseCase,
updateUserUseCase user.UpdateUserUseCase,
) Handler {
return &userHandlerImpl{
getUserUseCase: getUserUseCase,
registerUserUseCase: registerUserUseCase,
updateUserUseCase: updateUserUseCase,
}
Expand All @@ -26,10 +33,45 @@ func NewUserHandler(registerUserUseCase user.RegisterUserUseCase, updateUserUseC
var _ Handler = (*userHandlerImpl)(nil)

type userHandlerImpl struct {
getUserUseCase user.GetUserUseCase
registerUserUseCase user.RegisterUserUseCase
updateUserUseCase user.UpdateUserUseCase
}

func (h *userHandlerImpl) GetUser(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "user_id")
if userID == "" {
log.Printf("user_id is missing in URL")
apierrors.RespondError(w, http.StatusBadRequest, "user_id is required")
return
}

ctx := r.Context()
foundUser, err := h.getUserUseCase.GetUser(ctx, userID)
if err != nil {
log.Printf("Failed to get user: %v", err)
if errors.Is(err, user.ErrInvalidUserID) {
apierrors.RespondError(w, http.StatusBadRequest, err.Error())
return
}
apierrors.RespondError(w, http.StatusInternalServerError, err.Error())
return
}
if foundUser == nil {
apierrors.RespondError(w, http.StatusNotFound, "user not found")
return
}

res := toGetUserResponse(foundUser)

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(res); err != nil {
apierrors.RespondError(w, http.StatusInternalServerError, err.Error())
return
}
}

func (h *userHandlerImpl) RegisterUser(w http.ResponseWriter, r *http.Request) {
//userRequestのパース
var req PostUserRequest
Expand Down
30 changes: 30 additions & 0 deletions sysken-pay-backend/app/usecase/user/get_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package user

import (
"context"
"errors"
"fmt"
domainuser "sysken-pay-api/app/domain/object/user"
"sysken-pay-api/app/domain/repository"
)

var ErrInvalidUserID = errors.New("invalid userID")

type GetUserUseCase interface {
GetUser(ctx context.Context, userID string) (*domainuser.User, error)
}

type GetUserServiceImpl struct {
userRepo repository.UserRepository
}

func NewGetUserUseCase(userRepo repository.UserRepository) *GetUserServiceImpl {
return &GetUserServiceImpl{userRepo: userRepo}
}

func (s *GetUserServiceImpl) GetUser(ctx context.Context, userID string) (*domainuser.User, error) {
if err := (&domainuser.User{}).SetUserID(userID); err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidUserID, err)
}
return s.userRepo.GetUserByID(ctx, userID)
}
Loading