diff --git a/sysken-pay-backend/app/domain/object/user/get_user.go b/sysken-pay-backend/app/domain/object/user/get_user.go new file mode 100644 index 0000000..5a7964c --- /dev/null +++ b/sysken-pay-backend/app/domain/object/user/get_user.go @@ -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 +} diff --git a/sysken-pay-backend/app/domain/object/user/user.go b/sysken-pay-backend/app/domain/object/user/user.go index e4f434e..1094dd4 100644 --- a/sysken-pay-backend/app/domain/object/user/user.go +++ b/sysken-pay-backend/app/domain/object/user/user.go @@ -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、名前、作成日時、更新日時など @@ -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 diff --git a/sysken-pay-backend/app/domain/object/user/user_test.go b/sysken-pay-backend/app/domain/object/user/user_test.go index 9543afb..5bf191b 100644 --- a/sysken-pay-backend/app/domain/object/user/user_test.go +++ b/sysken-pay-backend/app/domain/object/user/user_test.go @@ -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) } } @@ -23,7 +23,8 @@ 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) } @@ -31,23 +32,28 @@ func TestSetUserID_Exactly20Chars(t *testing.T) { 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) + } } } @@ -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()) @@ -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") } } diff --git a/sysken-pay-backend/app/domain/repository/user.go b/sysken-pay-backend/app/domain/repository/user.go index f7d974d..254c9ea 100644 --- a/sysken-pay-backend/app/domain/repository/user.go +++ b/sysken-pay-backend/app/domain/repository/user.go @@ -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) diff --git a/sysken-pay-backend/app/infra/repository/user.go b/sysken-pay-backend/app/infra/repository/user.go index 1dbb06c..b754074 100644 --- a/sysken-pay-backend/app/infra/repository/user.go +++ b/sysken-pay-backend/app/infra/repository/user.go @@ -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) { diff --git a/sysken-pay-backend/app/server/server.go b/sysken-pay-backend/app/server/server.go index db4c643..efb650c 100644 --- a/sysken-pay-backend/app/server/server.go +++ b/sysken-pay-backend/app/server/server.go @@ -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) @@ -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) @@ -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) r.Post("/charge", chargeHandler.ChargeAmount) r.Post("/charge/cancel", chargeHandler.ChargeCancel) r.Post("/purchase", purchaseHandler.CreatePurchase) diff --git a/sysken-pay-backend/app/ui/api/user/response.go b/sysken-pay-backend/app/ui/api/user/response.go index 3ae110d..722de18 100644 --- a/sysken-pay-backend/app/ui/api/user/response.go +++ b/sysken-pay-backend/app/ui/api/user/response.go @@ -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"), + } +} + type PatchUserResponse struct { Status string `json:"status"` UserID string `json:"user_id"` diff --git a/sysken-pay-backend/app/ui/api/user/user.go b/sysken-pay-backend/app/ui/api/user/user.go index 22198cd..1037ac2 100644 --- a/sysken-pay-backend/app/ui/api/user/user.go +++ b/sysken-pay-backend/app/ui/api/user/user.go @@ -2,6 +2,7 @@ package user import ( "encoding/json" + "errors" "log" "net/http" apierrors "sysken-pay-api/app/ui/api/pkg/errors" @@ -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, } @@ -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 diff --git a/sysken-pay-backend/app/usecase/user/get_user.go b/sysken-pay-backend/app/usecase/user/get_user.go new file mode 100644 index 0000000..a5102f7 --- /dev/null +++ b/sysken-pay-backend/app/usecase/user/get_user.go @@ -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) +} diff --git a/sysken-pay-backend/app/usecase/user/user_test.go b/sysken-pay-backend/app/usecase/user/user_test.go index 0883d89..892f085 100644 --- a/sysken-pay-backend/app/usecase/user/user_test.go +++ b/sysken-pay-backend/app/usecase/user/user_test.go @@ -12,10 +12,15 @@ import ( // --- mock --- type mockUserRepo struct { + getFunc func(ctx context.Context, userID string) (*domainuser.User, error) insertFunc func(ctx context.Context, u *domainuser.User) (*domainuser.User, error) updateFunc func(ctx context.Context, u *domainuser.User) (*domainuser.User, error) } +func (m *mockUserRepo) GetUserByID(ctx context.Context, userID string) (*domainuser.User, error) { + return m.getFunc(ctx, userID) +} + func (m *mockUserRepo) InsertUser(ctx context.Context, u *domainuser.User) (*domainuser.User, error) { return m.insertFunc(ctx, u) } @@ -24,6 +29,86 @@ func (m *mockUserRepo) UpdateUser(ctx context.Context, u *domainuser.User) (*dom return m.updateFunc(ctx, u) } +// --- GetUser --- + +func TestGetUser_Success(t *testing.T) { + want, err := domainuser.NewUser("20K23099", "田中 太郎") + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + repo := &mockUserRepo{ + getFunc: func(_ context.Context, userID string) (*domainuser.User, error) { + if userID != "20K23099" { + t.Errorf("userID = %s, want 20K23099", userID) + } + return want, nil + }, + } + uc := NewGetUserUseCase(repo) + result, err := uc.GetUser(context.Background(), "20K23099") + if err != nil { + t.Fatalf("GetUser should succeed: %v", err) + } + if result.ID() != "20K23099" { + t.Errorf("ID() = %s, want 20K23099", result.ID()) + } + if result.UserName() != "田中 太郎" { + t.Errorf("UserName() = %s, want 田中 太郎", result.UserName()) + } +} + +func TestGetUser_NotFound(t *testing.T) { + repo := &mockUserRepo{ + getFunc: func(_ context.Context, userID string) (*domainuser.User, error) { + return nil, nil + }, + } + uc := NewGetUserUseCase(repo) + result, err := uc.GetUser(context.Background(), "20K23099") + if err != nil { + t.Fatalf("GetUser should not fail when user is not found: %v", err) + } + if result != nil { + t.Error("GetUser should return nil when user is not found") + } +} + +func TestGetUser_EmptyUserID(t *testing.T) { + repo := &mockUserRepo{} + uc := NewGetUserUseCase(repo) + if _, err := uc.GetUser(context.Background(), ""); err == nil { + t.Error("GetUser with empty userID should fail") + } +} + +func TestGetUser_UserIDTooLong(t *testing.T) { + repo := &mockUserRepo{} + uc := NewGetUserUseCase(repo) + if _, err := uc.GetUser(context.Background(), strings.Repeat("a", 21)); err == nil { + t.Error("GetUser with userID > 20 chars should fail") + } +} + +func TestGetUser_ItemID(t *testing.T) { + repo := &mockUserRepo{} + uc := NewGetUserUseCase(repo) + if _, err := uc.GetUser(context.Background(), "1"); err == nil { + t.Error("GetUser with itemID should fail") + } +} + +func TestGetUser_RepoError(t *testing.T) { + repo := &mockUserRepo{ + getFunc: func(_ context.Context, userID string) (*domainuser.User, error) { + return nil, errors.New("db error") + }, + } + uc := NewGetUserUseCase(repo) + if _, err := uc.GetUser(context.Background(), "20K23099"); err == nil { + t.Error("GetUser should propagate repo error") + } +} + // --- RegisterUser --- func TestRegisterUser_Success(t *testing.T) { @@ -33,12 +118,12 @@ func TestRegisterUser_Success(t *testing.T) { }, } uc := NewRegisterUserUseCase(repo) - result, err := uc.RegisterUser(context.Background(), "student001", "田中 太郎") + result, err := uc.RegisterUser(context.Background(), "20K23099", "田中 太郎") if err != nil { t.Fatalf("RegisterUser should succeed: %v", err) } - if result.ID() != "student001" { - t.Errorf("ID() = %s, want student001", result.ID()) + if result.ID() != "20K23099" { + t.Errorf("ID() = %s, want 20K23099", result.ID()) } if result.UserName() != "田中 太郎" { t.Errorf("UserName() = %s, want 田中 太郎", result.UserName()) @@ -56,7 +141,7 @@ func TestRegisterUser_EmptyUserID(t *testing.T) { func TestRegisterUser_EmptyUserName(t *testing.T) { repo := &mockUserRepo{} uc := NewRegisterUserUseCase(repo) - if _, err := uc.RegisterUser(context.Background(), "student001", ""); err == nil { + if _, err := uc.RegisterUser(context.Background(), "20K23099", ""); err == nil { t.Error("RegisterUser with empty userName should fail") } } @@ -72,7 +157,7 @@ func TestRegisterUser_UserIDTooLong(t *testing.T) { func TestRegisterUser_UserNameTooLong(t *testing.T) { repo := &mockUserRepo{} uc := NewRegisterUserUseCase(repo) - if _, err := uc.RegisterUser(context.Background(), "student001", strings.Repeat("a", 51)); err == nil { + if _, err := uc.RegisterUser(context.Background(), "20K23099", strings.Repeat("a", 51)); err == nil { t.Error("RegisterUser with userName > 50 chars should fail") } } @@ -84,7 +169,7 @@ func TestRegisterUser_RepoError(t *testing.T) { }, } uc := NewRegisterUserUseCase(repo) - if _, err := uc.RegisterUser(context.Background(), "student001", "田中 太郎"); err == nil { + if _, err := uc.RegisterUser(context.Background(), "20K23099", "田中 太郎"); err == nil { t.Error("RegisterUser should propagate repo error") } } @@ -96,7 +181,8 @@ func TestRegisterUser_MaxLengthUserID(t *testing.T) { }, } uc := NewRegisterUserUseCase(repo) - if _, err := uc.RegisterUser(context.Background(), strings.Repeat("a", 20), "田中 太郎"); err != nil { + id := "20K" + strings.Repeat("1", 17) // 20 chars in valid format + if _, err := uc.RegisterUser(context.Background(), id, "田中 太郎"); err != nil { t.Errorf("RegisterUser with 20-char userID should succeed: %v", err) } } @@ -110,12 +196,12 @@ func TestUpdateUser_Success(t *testing.T) { }, } uc := NewUpdateUserUseCase(repo) - result, err := uc.UpdateUser(context.Background(), "student001", "佐藤 花子") + result, err := uc.UpdateUser(context.Background(), "20K23099", "佐藤 花子") if err != nil { t.Fatalf("UpdateUser should succeed: %v", err) } - if result.ID() != "student001" { - t.Errorf("ID() = %s, want student001", result.ID()) + if result.ID() != "20K23099" { + t.Errorf("ID() = %s, want 20K23099", result.ID()) } if result.UserName() != "佐藤 花子" { t.Errorf("UserName() = %s, want 佐藤 花子", result.UserName()) @@ -125,7 +211,7 @@ func TestUpdateUser_Success(t *testing.T) { func TestUpdateUser_EmptyUserName(t *testing.T) { repo := &mockUserRepo{} uc := NewUpdateUserUseCase(repo) - if _, err := uc.UpdateUser(context.Background(), "student001", ""); err == nil { + if _, err := uc.UpdateUser(context.Background(), "20K23099", ""); err == nil { t.Error("UpdateUser with empty userName should fail") } } @@ -141,7 +227,7 @@ func TestUpdateUser_EmptyUserID(t *testing.T) { func TestUpdateUser_UserNameTooLong(t *testing.T) { repo := &mockUserRepo{} uc := NewUpdateUserUseCase(repo) - if _, err := uc.UpdateUser(context.Background(), "student001", strings.Repeat("a", 51)); err == nil { + if _, err := uc.UpdateUser(context.Background(), "20K23099", strings.Repeat("a", 51)); err == nil { t.Error("UpdateUser with userName > 50 chars should fail") } } @@ -153,7 +239,7 @@ func TestUpdateUser_RepoError(t *testing.T) { }, } uc := NewUpdateUserUseCase(repo) - if _, err := uc.UpdateUser(context.Background(), "student001", "田中 太郎"); err == nil { + if _, err := uc.UpdateUser(context.Background(), "20K23099", "田中 太郎"); err == nil { t.Error("UpdateUser should propagate repo error") } } diff --git a/sysken-pay-backend/docs/openapi.yaml b/sysken-pay-backend/docs/openapi.yaml index 7353b9b..61adf8c 100644 --- a/sysken-pay-backend/docs/openapi.yaml +++ b/sysken-pay-backend/docs/openapi.yaml @@ -58,6 +58,37 @@ paths: $ref: "#/components/schemas/ErrorResponse" /v1/user/{user_id}: + get: + tags: [user] + summary: ユーザー取得 + operationId: getUser + parameters: + - $ref: "#/components/parameters/UserId" + responses: + "200": + description: 取得成功 + content: + application/json: + schema: + $ref: "#/components/schemas/GetUserResponse" + "400": + description: リクエスト不正 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "404": + description: ユーザーが存在しない + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "500": + description: サーバーエラー + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" patch: tags: [user] summary: ユーザー更新 @@ -559,6 +590,21 @@ components: format: date-time example: "2025-01-01T00:00:00.000Z" + GetUserResponse: + type: object + properties: + status: + type: string + example: "success" + user_id: + type: string + user_name: + type: string + created_at: + type: string + format: date-time + example: "2025-01-01T00:00:00.000Z" + PatchUserResponse: type: object properties: diff --git a/sysken-pay-front/src/adapter/repository/UserRepositoryImpl.ts b/sysken-pay-front/src/adapter/repository/UserRepositoryImpl.ts index 32459c1..f49007f 100644 --- a/sysken-pay-front/src/adapter/repository/UserRepositoryImpl.ts +++ b/sysken-pay-front/src/adapter/repository/UserRepositoryImpl.ts @@ -2,6 +2,17 @@ import { apiClient } from "../api/client"; import type { components } from "../../types/api-schema"; export const UserRepositoryImpl = { + getUser: async ( + userId: string + ): Promise => { + const { data, error } = await apiClient.GET("/v1/user/{user_id}", { + params: { path: { user_id: userId } }, + }); + if (error) throw new Error(error.message); + if (!data?.user_id) throw new Error("ユーザーが見つかりませんでした"); + return data; + }, + registerUser: async ( body: components["schemas"]["PostUserRequest"] ): Promise => { diff --git a/sysken-pay-front/src/pages/admin/user-register/index.tsx b/sysken-pay-front/src/pages/admin/user-register/index.tsx index f79fedb..c172b5c 100644 --- a/sysken-pay-front/src/pages/admin/user-register/index.tsx +++ b/sysken-pay-front/src/pages/admin/user-register/index.tsx @@ -22,13 +22,13 @@ export default function UserRegisterPage(): JSX.Element { setError(null); clearScannedUser(); try { - await UserRepositoryImpl.getBalance(barcode); + await UserRepositoryImpl.getUser(barcode); setError("このユーザーはすでに登録済みです"); } catch { setScannedUser({ user_id: barcode }); navigate("/admin/user-register/name"); } - } + }; return (
diff --git a/sysken-pay-front/src/pages/admin/user-update/index.tsx b/sysken-pay-front/src/pages/admin/user-update/index.tsx index 7b02ade..33f12ae 100644 --- a/sysken-pay-front/src/pages/admin/user-update/index.tsx +++ b/sysken-pay-front/src/pages/admin/user-update/index.tsx @@ -22,13 +22,13 @@ export default function UserUpdatePage(): JSX.Element { setError(null); clearScannedUser(); try { - await UserRepositoryImpl.getBalance(barcode); + await UserRepositoryImpl.getUser(barcode); setScannedUser({ user_id: barcode }); navigate("/admin/user-update/name"); } catch { setError("このユーザーは登録されていません"); } - } + }; return (
diff --git a/sysken-pay-front/src/test/repository/PurchaseRepositoryImpl.test.ts b/sysken-pay-front/src/test/repository/PurchaseRepositoryImpl.test.ts index 35ed384..3eb086a 100644 --- a/sysken-pay-front/src/test/repository/PurchaseRepositoryImpl.test.ts +++ b/sysken-pay-front/src/test/repository/PurchaseRepositoryImpl.test.ts @@ -39,18 +39,19 @@ describe("PurchaseRepositoryImpl", () => { const response = { status: "success", purchase_id: 1, user_id: "k24000", balance: 1000 }; mockPost.mockResolvedValue({ data: response, error: undefined }); - const result = await PurchaseRepositoryImpl.cancelPurchase("k24000", { purchase_id: 1 }); + const body = { items: [{ item_id: 1, quantity: 1 }] }; + const result = await PurchaseRepositoryImpl.cancelPurchase("k24000", body); expect(result).toEqual(response); expect(mockPost).toHaveBeenCalledWith("/v1/user/{user_id}/purchase/cancel", { params: { path: { user_id: "k24000" } }, - body: { purchase_id: 1 }, + body, }); }); it("エラー時に例外を投げる", async () => { mockPost.mockResolvedValue({ data: undefined, error: { message: "キャンセル失敗" } }); await expect( - PurchaseRepositoryImpl.cancelPurchase("k24000", { purchase_id: 99 }) + PurchaseRepositoryImpl.cancelPurchase("k24000", { items: [{ item_id: 99, quantity: 1 }] }) ).rejects.toThrow("キャンセル失敗"); }); }); diff --git a/sysken-pay-front/src/test/repository/UserRepositoryImpl.test.ts b/sysken-pay-front/src/test/repository/UserRepositoryImpl.test.ts index 80739f8..9958f65 100644 --- a/sysken-pay-front/src/test/repository/UserRepositoryImpl.test.ts +++ b/sysken-pay-front/src/test/repository/UserRepositoryImpl.test.ts @@ -16,6 +16,24 @@ beforeEach(() => { }); describe("UserRepositoryImpl", () => { + describe("getUser", () => { + it("ユーザー取得が成功する", async () => { + const response = { status: "success", user_id: "k24000", user_name: "シス研太郎" }; + mockGet.mockResolvedValue({ data: response, error: undefined }); + + const result = await UserRepositoryImpl.getUser("k24000"); + expect(result).toEqual(response); + expect(mockGet).toHaveBeenCalledWith("/v1/user/{user_id}", { + params: { path: { user_id: "k24000" } }, + }); + }); + + it("エラー時に例外を投げる", async () => { + mockGet.mockResolvedValue({ data: undefined, error: { message: "ユーザーが見つかりません" } }); + await expect(UserRepositoryImpl.getUser("k99999")).rejects.toThrow("ユーザーが見つかりません"); + }); + }); + describe("registerUser", () => { it("ユーザー登録が成功する", async () => { const response = { status: "success", user_id: "k24000", user_name: "シス研太郎" }; diff --git a/sysken-pay-front/src/types/api-schema.d.ts b/sysken-pay-front/src/types/api-schema.d.ts index 3cfb5f0..5e88a90 100644 --- a/sysken-pay-front/src/types/api-schema.d.ts +++ b/sysken-pay-front/src/types/api-schema.d.ts @@ -28,7 +28,8 @@ export interface paths { path?: never; cookie?: never; }; - get?: never; + /** ユーザー取得 */ + get: operations["getUser"]; put?: never; post?: never; delete?: never; @@ -260,6 +261,17 @@ export interface components { */ created_at?: string; }; + GetUserResponse: { + /** @example success */ + status?: string; + user_id?: string; + user_name?: string; + /** + * Format: date-time + * @example 2025-01-01T00:00:00.000Z + */ + created_at?: string; + }; PatchUserResponse: { /** @example success */ status?: string; @@ -463,6 +475,56 @@ export interface operations { }; }; }; + getUser: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ユーザーID */ + user_id: components["parameters"]["UserId"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description 取得成功 */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetUserResponse"]; + }; + }; + /** @description リクエスト不正 */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description ユーザーが存在しない */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description サーバーエラー */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; updateUser: { parameters: { query?: never;