Skip to content

feat: アンケートを即座に終了するエンドポイントの追加#1536

Open
Eraxyso wants to merge 8 commits intomainfrom
claude/questionnaire-close-endpoint-4ca4N
Open

feat: アンケートを即座に終了するエンドポイントの追加#1536
Eraxyso wants to merge 8 commits intomainfrom
claude/questionnaire-close-endpoint-4ca4N

Conversation

@Eraxyso
Copy link
Copy Markdown
Contributor

@Eraxyso Eraxyso commented Apr 16, 2026

概要

アンケートを即座に終了するための専用エンドポイント POST /questionnaires/{questionnaireID}/close を追加します。

背景・問題

これまでアンケートを即座に終了するには、フロントエンドから res_time_limit に現在時刻を設定してリクエストを送る必要がありました。しかし、フロントエンドとバックエンドの時刻がわずかにずれている場合、5秒の許容範囲(responseDueDateTimeTolerance)を超えてしまい、400 Bad Request が返るという問題がありました。

解決策

サーバー側の現在時刻を使って res_time_limit を更新する専用の close エンドポイントを追加することで、クライアントの時刻に依存せずにアンケートを即座に終了できるようにしました。

変更内容

  • POST /questionnaires/{questionnaireID}/close エンドポイントを追加(管理者のみ)
  • model.IQuestionnaireUpdateQuestionnaireLimit メソッドを追加(res_time_limit のみを更新)
  • OpenAPI スペックおよびコード生成ファイルを更新
  • QuestionnaireAdministratorAuthenticate ミドルウェアを新エンドポイントに適用

テスト方法

  1. アンケートを作成し、res_time_limit を設定する
  2. POST /api/questionnaires/{questionnaireID}/close を呼び出す
  3. アンケートの res_time_limit がサーバー時刻で更新され、それ以降の回答が拒否されることを確認する

Summary by CodeRabbit

  • 新機能
    • 管理者向けにアンケートを「閉鎖」する新しいPOSTエンドポイントを追加しました。
  • バグ修正
    • 回答期限に過去日時を受け付けないバリデーションを導入し、過去日時でのリクエストは400エラーになります。
  • ドキュメント
    • API仕様を更新し、閉鎖エンドポイントの経路とレスポンス情報を追記しました。
  • テスト
    • 閉鎖処理と期限更新の振る舞いを検証する自動テストを追加しました。

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4a82b576-ad68-427c-95f5-dd299a8ae596

📥 Commits

Reviewing files that changed from the base of the PR and between d325af3 and 9be9468.

📒 Files selected for processing (2)
  • controller/questionnaire.go
  • model/questionnaires_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • controller/questionnaire.go
  • model/questionnaires_test.go

📝 Walkthrough

Walkthrough

POST /questionnaires/{questionnaireID}/close エンドポイントを追加し、ハンドラー→コントローラー→リポジトリでアンケートを「閉じる」処理(res_time_limit を現在時刻に設定または NULL に更新)を実装。既存の期限正規化トレランスは削除され、OpenAPI/spec、ルーティング、関連テストを追加/更新。

変更

Cohort / File(s) Summary
ハンドラー
handler/questionnaire.go
新しい CloseQuestionnaire POST ハンドラを追加。コントローラー呼び出し、成功時は NoContent(200) を返す。
コントローラー
controller/questionnaire.go
CloseQuestionnaire を追加。トランザクション内で UpdateQuestionnaireLimit を呼び出し、ErrNoRecordUpdated を 404 にマップ。既存の due-date 正規化関数と定数を削除し、過去日時は 400 エラーで拒否する簡潔な検証に変更。DeleteReminder は失敗してもログのみ。
モデル(インターフェース)
model/questionnaires.go
IQuestionnaireUpdateQuestionnaireLimit(ctx context.Context, questionnaireID int, resTimeLimit null.Time) error を追加。
モデル(実装)
model/questionnaires_impl.go
UpdateQuestionnaireLimit 実装追加。レコード取得後に modified_at を更新し、res_time_limit を与値または NULL に設定。更新件数ゼロは ErrNoRecordUpdated を返す。
ルーティング / サーバー
main.go, openapi/server.go
POST /api/questionnaires/:questionnaireID/close をルート登録。ServerInterface とラッパーに CloseQuestionnaire を追加し、パスパラメータのバインドを実装。
OpenAPI / ドキュメント
docs/swagger/swagger.yaml, openapi/spec.go
OpenAPI に POST /questionnaires/{questionnaireID}/close を追加。埋め込み swaggerSpec を更新し、新エンドポイントのレスポンス仕様(200/400/403/404/500)を含める。
テスト
controller/questionnaire_test.go, model/questionnaires_test.go
due-date 正規化関連テストを簡素化(トレランス削除)。TestCloseQuestionnaireUpdateQuestionnaireLimit の単体テストを追加し、時刻近接性と不存在 ID のエラーを検証。

シーケンス図

sequenceDiagram
    actor Client
    participant Handler as "Handler\n(handler/questionnaire.go)"
    participant Controller as "Controller\n(controller/questionnaire.go)"
    participant Repository as "IQuestionnaire\n(model/questionnaires_impl.go)"
    participant DB as "Database"

    Client->>Handler: POST /api/questionnaires/{id}/close
    Handler->>Handler: パスパラメータをバインド
    Handler->>Controller: CloseQuestionnaire(ctx, questionnaireID)
    activate Controller
    Controller->>Controller: now := null.TimeFrom(time.Now())
    Controller->>Controller: q.ITransaction.Do(txFn)
    Controller->>Repository: UpdateQuestionnaireLimit(ctx, id, now)
    activate Repository
    Repository->>DB: SELECT ... WHERE id=?
    DB-->>Repository: record / not found
    alt record found
        Repository->>DB: UPDATE questionnaires SET res_time_limit=?, modified_at=NOW() WHERE id=?
        DB-->>Repository: rows affected > 0
        Repository-->>Controller: nil
    else not found
        Repository-->>Controller: ErrNoRecordUpdated
    end
    deactivate Repository
    alt ErrNoRecordUpdated
        Controller-->>Handler: echo.HTTPError(404)
    else other error
        Controller-->>Handler: echo.HTTPError(500)
    else success
        Controller->>Controller: q.DeleteReminder(questionnaireID) (ログのみ)
        Controller-->>Handler: nil
    end
    deactivate Controller
    alt error
        Handler-->>Client: HTTP Error (400/404/500)
    else success
        Handler-->>Client: 200 No Content
    end
Loading

推定コードレビュー工数

🎯 4 (Complex) | ⏱️ ~45 分

Suggested labels

v3

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed プルリクエストのタイトルは、変更の主要な内容であるアンケートを終了するエンドポイントの追加を明確に説明している。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/questionnaire-close-endpoint-4ca4N

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 69.23077% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.49%. Comparing base (e27ac58) to head (9be9468).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
controller/questionnaire.go 71.42% 5 Missing and 1 partial ⚠️
model/questionnaires_impl.go 66.66% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1536      +/-   ##
==========================================
- Coverage   63.52%   63.49%   -0.04%     
==========================================
  Files          26       26              
  Lines        4025     4054      +29     
==========================================
+ Hits         2557     2574      +17     
- Misses       1078     1086       +8     
- Partials      390      394       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/questionnaire.go`:
- Around line 1068-1090: CloseQuestionnaire currently only updates the limit but
doesn't disable or remove scheduled reminders, causing post-close notifications;
inside the transaction callback in CloseQuestionnaire (after calling
UpdateQuestionnaireLimit and before returning nil) invoke the same reminder
cleanup used by EditQuestionnaire/DeleteQuestionnaire (e.g., the function that
removes/disables scheduled reminders for a questionnaire), propagate/log any
error similarly (respecting the model.ErrNoRecordUpdated handling), and ensure
failures return the appropriate error to the outer handler so reminders are
reliably cleared on close.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da265e0f-877b-46f1-810a-b5824b1ae2f4

📥 Commits

Reviewing files that changed from the base of the PR and between e27ac58 and 38fa42b.

📒 Files selected for processing (8)
  • controller/questionnaire.go
  • docs/swagger/swagger.yaml
  • handler/questionnaire.go
  • main.go
  • model/questionnaires.go
  • model/questionnaires_impl.go
  • openapi/server.go
  • openapi/spec.go

Comment thread controller/questionnaire.go
Add a dedicated close endpoint that sets res_time_limit to the current
server time, avoiding front/back clock skew issues that occurred when
the client sent a timestamp near the tolerance boundary.

- Add UpdateQuestionnaireLimit model method for targeted limit update
- Add CloseQuestionnaire controller and handler
- Require QuestionnaireAdministratorAuthenticate middleware
- Update OpenAPI spec and regenerated server/spec code

Co-authored-by: Eraxyso <130852025+Eraxyso@users.noreply.github.com>
@Eraxyso Eraxyso force-pushed the claude/questionnaire-close-endpoint-4ca4N branch from 38fa42b to e0927ef Compare April 16, 2026 14:35
claude and others added 5 commits April 16, 2026 14:37
- CloseQuestionnaire: call DeleteReminder inside the transaction after
  updating the limit so scheduled notifications are cancelled when a
  questionnaire is closed immediately
- Remove normalizeResponseDueDateTime and responseDueDateTimeTolerance;
  replace call sites with a direct past-time rejection now that the
  dedicated /close endpoint removes the need to accept near-past timestamps

Co-authored-by: Eraxyso <130852025+Eraxyso@users.noreply.github.com>
…tion

Remove the "within tolerance" success case and its normalizedDueDate
expect field/assertion. Repurpose the test case to verify that even a
slightly-past timestamp (formerly accepted within the 5s window) is now
rejected with an error.

Co-authored-by: Eraxyso <130852025+Eraxyso@users.noreply.github.com>
- TestCloseQuestionnaire: valid ID sets res_time_limit to ~now,
  invalid ID returns not-found error
- updateQuestionnaireLimitTest: set limit to now, clear to null,
  and invalid ID returns ErrNoRecordUpdated

Co-authored-by: Eraxyso <130852025+Eraxyso@users.noreply.github.com>
… test

The "clear limit (null)" case previously created a questionnaire with no
limit (NULL) and then updated it to NULL again. MariaDB reports
RowsAffected=0 when no column actually changes, which made the
implementation return ErrNoRecordUpdated and failed the test. Seed the
questionnaire with a non-null limit in the past so every update is a
real change. Also drop t.Parallel() to keep ordering consistent with the
sibling tests in TestQuestionnaires.

Co-authored-by: Eraxyso <130852025+Eraxyso@users.noreply.github.com>
…ed=0 false negative

When res_time_limit is already NULL and we update to NULL (or when modified_at
doesn't change within the same second), GORM may report RowsAffected=0 even
though the questionnaire exists, causing ErrNoRecordUpdated to be returned
incorrectly. Replace RowsAffected check with an explicit SELECT to verify
existence before updating.

https://claude.ai/code/session_01UVfZTySABd5YYU1LP2ULiZ
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
controller/questionnaire_test.go (1)

1984-2075: クローズ後の回答拒否もこのテストで押さえておきたいです。

今の TestCloseQuestionnaireres_time_limit 更新しか見ていないので、将来 PostQuestionnaireResponse 側の期限チェックが壊れてもこのPRの主目的が検知できません。クローズ成功後に1件回答を投げて、422 相当のエラーになることまで確認しておくと回帰に強くなります。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/questionnaire_test.go` around lines 1984 - 2075, After closing the
questionnaire in TestCloseQuestionnaire, add an assertion that attempting to
submit a response via q.PostQuestionnaireResponse (or the HTTP POST to
/questionnaires/{id}/responses) fails with a 422-equivalent error; specifically
call q.PostQuestionnaireResponse (or invoke the handler) using the closed
questionnaireID and assert the call returns an error/HTTP 422, so the test
verifies both IQuestionnaire.GetQuestionnaireLimit updates and that
PostQuestionnaireResponse enforces the closed-time rejection.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/questionnaire.go`:
- Around line 1054-1068: The DeleteReminder call should be moved out of the
ITransaction.Do closure so you only remove the in-memory scheduler entry after
the DB transaction (UpdateQuestionnaireLimit) has committed; in the function
containing ITransaction.Do, call UpdateQuestionnaireLimit inside the transaction
(handling model.ErrNoRecordUpdated as before), return nil on success, and then
after ITransaction.Do returns without error invoke
DeleteReminder(questionnaireID) (logging any delete error) to avoid inconsistent
state between res_time_limit and the scheduler.

---

Nitpick comments:
In `@controller/questionnaire_test.go`:
- Around line 1984-2075: After closing the questionnaire in
TestCloseQuestionnaire, add an assertion that attempting to submit a response
via q.PostQuestionnaireResponse (or the HTTP POST to
/questionnaires/{id}/responses) fails with a 422-equivalent error; specifically
call q.PostQuestionnaireResponse (or invoke the handler) using the closed
questionnaireID and assert the call returns an error/HTTP 422, so the test
verifies both IQuestionnaire.GetQuestionnaireLimit updates and that
PostQuestionnaireResponse enforces the closed-time rejection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 16f10560-c07b-4e49-8060-0f23ff9eb8e3

📥 Commits

Reviewing files that changed from the base of the PR and between 38fa42b and d325af3.

📒 Files selected for processing (10)
  • controller/questionnaire.go
  • controller/questionnaire_test.go
  • docs/swagger/swagger.yaml
  • handler/questionnaire.go
  • main.go
  • model/questionnaires.go
  • model/questionnaires_impl.go
  • model/questionnaires_test.go
  • openapi/server.go
  • openapi/spec.go
✅ Files skipped from review due to trivial changes (1)
  • model/questionnaires_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • main.go
  • model/questionnaires.go
  • openapi/spec.go
  • model/questionnaires_impl.go
  • docs/swagger/swagger.yaml
  • openapi/server.go

Comment thread controller/questionnaire.go
claude added 2 commits April 16, 2026 18:40
Questionnaires created during the test were left in the DB, disrupting
the GetQuestionnaires sort-by-title test due to collation differences
between Go string comparison and MariaDB utf8mb4_general_ci for ASCII
vs CJK character ordering. Use t.Cleanup to soft-delete them after
the subtest completes, before parallel subtests run.

https://claude.ai/code/session_01UVfZTySABd5YYU1LP2ULiZ
DeleteReminder modifies in-memory scheduler state. Calling it inside the
ITransaction.Do closure means the scheduler entry could be removed before
the DB transaction commits; if the commit were to fail, the DB and
scheduler would be inconsistent. Move it after Do returns without error.

https://claude.ai/code/session_01UVfZTySABd5YYU1LP2ULiZ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants