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
11 changes: 0 additions & 11 deletions sysken-pay-backend/app/domain/object/item/get_item_by_jan_code.go

This file was deleted.

11 changes: 10 additions & 1 deletion sysken-pay-backend/app/ui/api/item/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package item

import (
"encoding/json"
"errors"
"log"
"net/http"
apierrors "sysken-pay-api/app/ui/api/pkg/errors"
Expand Down Expand Up @@ -109,17 +110,25 @@ 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)

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
}
}
Expand Down
5 changes: 5 additions & 0 deletions sysken-pay-backend/app/ui/api/purchase/purchase.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package purchase

import (
"encoding/json"
"errors"
"log"
"net/http"
apierrors "sysken-pay-api/app/ui/api/pkg/errors"
Expand Down Expand Up @@ -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
}
Expand Down
10 changes: 10 additions & 0 deletions sysken-pay-backend/app/usecase/item/find_item_by_jan_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
Expand Down
33 changes: 33 additions & 0 deletions sysken-pay-backend/app/usecase/item/item_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 8 additions & 0 deletions sysken-pay-backend/app/usecase/purchase/create_purchase.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}

Expand Down
18 changes: 18 additions & 0 deletions sysken-pay-backend/app/usecase/purchase/purchase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
14 changes: 13 additions & 1 deletion sysken-pay-backend/docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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}$"
Expand All @@ -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:
Expand Down