From 41cc7c857f444ca169ce99592b5fa2b1035162be Mon Sep 17 00:00:00 2001 From: ayuayuyu <147569295+ayuayuyu@users.noreply.github.com> Date: Sun, 14 Jun 2026 23:20:45 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=8D=E6=AD=A3=E3=81=AAJAN=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=82=92400=E3=80=81=E5=95=86=E5=93=81?= =?UTF-8?q?=E3=81=8C=E5=AD=98=E5=9C=A8=E3=81=97=E3=81=AA=E3=81=84=E5=A0=B4?= =?UTF-8?q?=E5=90=88=E3=82=92404=E3=81=A7=E8=BF=94=E3=81=99=E3=82=88?= =?UTF-8?q?=E3=81=86=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../object/item/get_item_by_jan_code.go | 11 ------- sysken-pay-backend/app/ui/api/item/item.go | 11 ++++++- .../app/ui/api/purchase/purchase.go | 5 +++ .../app/usecase/item/find_item_by_jan_code.go | 10 ++++++ .../app/usecase/item/item_test.go | 33 +++++++++++++++++++ .../app/usecase/purchase/create_purchase.go | 8 +++++ .../app/usecase/purchase/purchase_test.go | 18 ++++++++++ sysken-pay-backend/docs/openapi.yaml | 14 +++++++- 8 files changed, 97 insertions(+), 13 deletions(-) delete mode 100644 sysken-pay-backend/app/domain/object/item/get_item_by_jan_code.go diff --git a/sysken-pay-backend/app/domain/object/item/get_item_by_jan_code.go b/sysken-pay-backend/app/domain/object/item/get_item_by_jan_code.go deleted file mode 100644 index 67640d1..0000000 --- a/sysken-pay-backend/app/domain/object/item/get_item_by_jan_code.go +++ /dev/null @@ -1,11 +0,0 @@ -package item - -func GetItemByJanCode(janCode string) (*Item, error) { - p := &Item{} - - if err := p.SetJanCode(janCode); err != nil { - return nil, err - } - - return p, nil -} diff --git a/sysken-pay-backend/app/ui/api/item/item.go b/sysken-pay-backend/app/ui/api/item/item.go index e32c98f..c7bb5c6 100644 --- a/sysken-pay-backend/app/ui/api/item/item.go +++ b/sysken-pay-backend/app/ui/api/item/item.go @@ -2,6 +2,7 @@ package item import ( "encoding/json" + "errors" "log" "net/http" apierrors "sysken-pay-api/app/ui/api/pkg/errors" @@ -109,9 +110,17 @@ func (h *itemHandlerImpl) GetItemByJanCode(w http.ResponseWriter, r *http.Reques foundItem, err := h.findItemByJanCodeUseCase.GetItemByJanCode(ctx, janCode) if err != nil { log.Printf("Failed to find item by jan code: %v", err) + if errors.Is(err, item.ErrInvalidJanCode) { + apierrors.RespondError(w, http.StatusBadRequest, err.Error()) + return + } apierrors.RespondError(w, http.StatusInternalServerError, err.Error()) return } + if foundItem == nil { + apierrors.RespondError(w, http.StatusNotFound, "item not found") + return + } //レスポンスの作成 res := toGetItemResponse(foundItem) @@ -119,7 +128,7 @@ func (h *itemHandlerImpl) GetItemByJanCode(w http.ResponseWriter, r *http.Reques w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(res); err != nil { - apierrors.RespondError(w, http.StatusBadRequest, err.Error()) + apierrors.RespondError(w, http.StatusInternalServerError, err.Error()) return } } diff --git a/sysken-pay-backend/app/ui/api/purchase/purchase.go b/sysken-pay-backend/app/ui/api/purchase/purchase.go index 9aaa9f2..0c1bb50 100644 --- a/sysken-pay-backend/app/ui/api/purchase/purchase.go +++ b/sysken-pay-backend/app/ui/api/purchase/purchase.go @@ -2,6 +2,7 @@ package purchase import ( "encoding/json" + "errors" "log" "net/http" apierrors "sysken-pay-api/app/ui/api/pkg/errors" @@ -53,6 +54,10 @@ func (h *purchaseHandlerImpl) CreatePurchase(w http.ResponseWriter, r *http.Requ createdPurchase, err := h.createPurchaseUseCase.CreatePurchase(ctx, userID, inputs) if err != nil { log.Printf("Failed to create purchase: %v", err) + if errors.Is(err, purchase.ErrItemNotFound) { + apierrors.RespondError(w, http.StatusNotFound, "item not found") + return + } apierrors.RespondError(w, http.StatusInternalServerError, err.Error()) return } diff --git a/sysken-pay-backend/app/usecase/item/find_item_by_jan_code.go b/sysken-pay-backend/app/usecase/item/find_item_by_jan_code.go index d98633a..a21d082 100644 --- a/sysken-pay-backend/app/usecase/item/find_item_by_jan_code.go +++ b/sysken-pay-backend/app/usecase/item/find_item_by_jan_code.go @@ -2,12 +2,17 @@ package item import ( "context" + "errors" + "fmt" "sysken-pay-api/app/domain/object/item" "sysken-pay-api/app/domain/repository" ) //TODO ドメイン層のインターフェースに接続して処理を完成させる +// ErrInvalidJanCode は JANコードのフォーマットが不正な場合に返されます。 +var ErrInvalidJanCode = errors.New("invalid janCode") + type FindItemByJanCodeUseCase interface { GetItemByJanCode(ctx context.Context, janCode string) (*item.Item, error) } @@ -27,6 +32,11 @@ func NewFindItemByJanCodeUseCase( func (s *FindItemByJanCodeServiceImpl) GetItemByJanCode( ctx context.Context, janCode string) (*item.Item, error) { + // JANコードのフォーマットを検証する(userIDの学籍番号フォーマット等の不正値は400として扱う) + if err := (&item.Item{}).SetJanCode(janCode); err != nil { + return nil, fmt.Errorf("%w: %v", ErrInvalidJanCode, err) + } + foundItemByJanCode, err := s.itemFindRepo.GetItemByJanCode(ctx, janCode) if err != nil { return nil, err diff --git a/sysken-pay-backend/app/usecase/item/item_test.go b/sysken-pay-backend/app/usecase/item/item_test.go index 8846b19..16d9e19 100644 --- a/sysken-pay-backend/app/usecase/item/item_test.go +++ b/sysken-pay-backend/app/usecase/item/item_test.go @@ -192,6 +192,39 @@ func TestFindItemByJanCode_RepoError(t *testing.T) { } } +// userIDの学籍番号フォーマット等、JANコードとして不正な値はリポジトリに到達せず +// ErrInvalidJanCode を返すこと(ハンドラ側で400にマッピングされる) +func TestFindItemByJanCode_InvalidJanCode(t *testing.T) { + repo := &mockItemRepo{ + getByJanCodeFunc: func(_ context.Context, _ string) (*domainitem.Item, error) { + t.Fatal("repository must not be called for invalid janCode") + return nil, nil + }, + } + uc := NewFindItemByJanCodeUseCase(repo) + if _, err := uc.GetItemByJanCode(context.Background(), "20X24045"); !errors.Is(err, ErrInvalidJanCode) { + t.Errorf("GetItemByJanCode with student ID should return ErrInvalidJanCode, got: %v", err) + } +} + +// 存在しないJANコード(リポジトリが nil を返す)はそのまま nil を伝播し、 +// ハンドラ側で404にマッピングされること(500パニックにならない) +func TestFindItemByJanCode_NotFound(t *testing.T) { + repo := &mockItemRepo{ + getByJanCodeFunc: func(_ context.Context, _ string) (*domainitem.Item, error) { + return nil, nil + }, + } + uc := NewFindItemByJanCodeUseCase(repo) + result, err := uc.GetItemByJanCode(context.Background(), validJAN13) + if err != nil { + t.Fatalf("GetItemByJanCode should not error when item not found: %v", err) + } + if result != nil { + t.Errorf("GetItemByJanCode should return nil item when not found, got: %v", result) + } +} + // --- GetAllItems --- func TestGetAllItems_Success(t *testing.T) { diff --git a/sysken-pay-backend/app/usecase/purchase/create_purchase.go b/sysken-pay-backend/app/usecase/purchase/create_purchase.go index cd9b428..3e823b7 100644 --- a/sysken-pay-backend/app/usecase/purchase/create_purchase.go +++ b/sysken-pay-backend/app/usecase/purchase/create_purchase.go @@ -2,11 +2,16 @@ package purchase import ( "context" + "errors" + "fmt" "sysken-pay-api/app/domain/object/purchase" "sysken-pay-api/app/domain/repository" domainservice "sysken-pay-api/app/domain/service/purchase" ) +// ErrItemNotFound は購入対象の商品が存在しない場合に返されます(ハンドラ側で404にマッピングされる)。 +var ErrItemNotFound = errors.New("item not found") + type CreatePurchaseUseCase interface { CreatePurchase(ctx context.Context, userID string, inputs []PurchaseItemInput) (*purchase.Purchase, error) } @@ -49,6 +54,9 @@ func (s *CreatePurchaseServiceImpl) CreatePurchase( if err != nil { return nil, err } + if it == nil { + return nil, fmt.Errorf("%w: itemID=%d", ErrItemNotFound, input.ItemID) + } totalAmount += it.Price() * input.Quantity } diff --git a/sysken-pay-backend/app/usecase/purchase/purchase_test.go b/sysken-pay-backend/app/usecase/purchase/purchase_test.go index 9b75591..740100c 100644 --- a/sysken-pay-backend/app/usecase/purchase/purchase_test.go +++ b/sysken-pay-backend/app/usecase/purchase/purchase_test.go @@ -186,6 +186,24 @@ func TestCreatePurchase_ItemRepoError(t *testing.T) { } } +// リポジトリが商品なし(nil, nil)を返した場合、nil参照でpanic(500)せず +// ErrItemNotFound を返すこと(ハンドラ側で404にマッピングされる) +func TestCreatePurchase_ItemNotFound(t *testing.T) { + inputs := []PurchaseItemInput{{ItemID: 999, Quantity: 1}} + itemRepo := &mockItemRepo{ + getByIDFunc: func(_ context.Context, _ int) (*domainitem.Item, error) { + return nil, nil + }, + } + balanceRepo := &mockBalanceRepo{} + purchaseRepo := &mockPurchaseRepo{} + + uc := NewCreatePurchaseUseCase(purchaseRepo, itemRepo, balanceRepo, &mockTxManager{}) + if _, err := uc.CreatePurchase(context.Background(), "user-1", inputs); !errors.Is(err, ErrItemNotFound) { + t.Errorf("CreatePurchase with missing item should return ErrItemNotFound, got: %v", err) + } +} + func TestCreatePurchase_PurchaseRepoError(t *testing.T) { inputs := []PurchaseItemInput{{ItemID: 1, Quantity: 1}} itemRepo := &mockItemRepo{ diff --git a/sysken-pay-backend/docs/openapi.yaml b/sysken-pay-backend/docs/openapi.yaml index 7f72e9a..94d186a 100644 --- a/sysken-pay-backend/docs/openapi.yaml +++ b/sysken-pay-backend/docs/openapi.yaml @@ -221,6 +221,12 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorResponse" + "404": + description: 購入対象の商品が存在しない + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" "500": description: サーバーエラー content: @@ -419,7 +425,7 @@ paths: - name: jan_code in: path required: true - description: JANコード(8桁または13桁) + description: JANコード(8桁または13桁。チェックデジット検証あり。不正な場合は400を返す) schema: type: string pattern: "^[0-9]{8}$|^[0-9]{13}$" @@ -436,6 +442,12 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorResponse" + "404": + description: 商品が存在しない + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" "500": description: サーバーエラー content: