From 69951ef05bae93692ce6e1dac95a6d7ae03c33de Mon Sep 17 00:00:00 2001 From: fezzlk Date: Wed, 4 Mar 2026 09:13:48 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E3=82=B5=E3=83=BC=E3=83=90=E3=83=BC500?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E4=BF=AE=E6=AD=A3=20+=20=E7=99=BB?= =?UTF-8?q?=E9=8C=B2=E5=BE=8C=E3=81=AE=E9=80=9A=E7=9F=A5=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=83=95=E3=82=A9=E3=83=AD=E3=83=BC=E3=82=A2=E3=83=83=E3=83=97?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: stock.notify_enabled → notify_days_before is not None (Stock エンティティに存在しない属性参照) - fix: url_for('stock_trash') → url_for('view_deleted_stock_list') (誤ったエンドポイント名) - feat: アイテム登録後に通知設定をフォローアップメッセージで変更可能に - 期限あり登録確認後に update_recent_notify コンテキストを保存 - update_recent_expiry 解決後も同様にチェーン - アイテム名を補完して NLP に渡すことで「通知は3日前から」などを認識 - DummyIntentParserService に extra_responses 対応を追加 Co-Authored-By: Claude Sonnet 4.6 --- .../Line/HandleIntentOperationUseCase.py | 53 +++++++- .../test_handle_intent_operation_use_case.py | 120 +++++++++++++++++- src/routes/web/index.py | 2 +- src/templates/pages/stock/index.html | 37 +----- 4 files changed, 172 insertions(+), 40 deletions(-) diff --git a/src/UseCases/Line/HandleIntentOperationUseCase.py b/src/UseCases/Line/HandleIntentOperationUseCase.py index dd7aaf4..ee8daab 100644 --- a/src/UseCases/Line/HandleIntentOperationUseCase.py +++ b/src/UseCases/Line/HandleIntentOperationUseCase.py @@ -200,6 +200,12 @@ def _execute_pending(self, line_user_id: str) -> None: self._line_response_service.add_message( f'"{item_name}" を期限{parsed_expiry_date.strftime("%Y年%m月%d日")}で登録しました。' ) + self._pending_operation_service.clear(line_user_id) + self._pending_operation_service.save( + line_user_id, + {"intent": "update_recent_notify", "item_name": item_name}, + ) + return else: self._line_response_service.add_message(f'"{item_name}" を登録しました。') self._line_response_service.add_message( @@ -410,11 +416,20 @@ def _handle_recent_register_expiry_update(self, line_user_id: str, message: str) return False operation = pending["operation"] + if operation.get("intent") == "update_recent_notify": + return self._handle_recent_notify_update(line_user_id, message, operation) + if operation.get("intent") != "update_recent_expiry": return False + item_name = operation.get("item_name") + if message in ("なし", "不要", "いらない", "NO", "no"): self._pending_operation_service.clear(line_user_id) + self._pending_operation_service.save( + line_user_id, + {"intent": "update_recent_notify", "item_name": item_name}, + ) self._line_response_service.add_message("期限なしのままにしました。") return True @@ -425,7 +440,6 @@ def _handle_recent_register_expiry_update(self, line_user_id: str, message: str) ) return True - item_name = operation.get("item_name") stocks = self._stock_repository.find( query={"owner_id": line_user_id, "item_name": item_name, "status": 1}, sort=[("created_at", "desc")], @@ -441,11 +455,48 @@ def _handle_recent_register_expiry_update(self, line_user_id: str, message: str) new_values={"expiry_date": parsed_date}, ) self._pending_operation_service.clear(line_user_id) + self._pending_operation_service.save( + line_user_id, + {"intent": "update_recent_notify", "item_name": item_name}, + ) self._line_response_service.add_message( f'"{item_name}" の期限を{parsed_date.strftime("%Y年%m月%d日")}に更新しました。' ) return True + def _handle_recent_notify_update(self, line_user_id: str, message: str, operation: dict) -> bool: + """直近登録アイテムへの通知設定フォローアップを処理する。""" + item_name = operation.get("item_name") + + # アイテム名を補完してNLPに渡す(例: "確定申告の通知は3日前から") + parsed = self._intent_parser_service.parse(f"{item_name}の{message}") + + if parsed.get("intent") != "update_stock_notify": + # 通知設定変更として解釈できない → コンテキスト破棄して通常処理へ + self._pending_operation_service.clear(line_user_id) + return False + + self._update_recent_item_notify(line_user_id, item_name, parsed.get("notify_days_before")) + self._pending_operation_service.clear(line_user_id) + return True + + def _update_recent_item_notify(self, line_user_id: str, item_name: str, notify_days_before) -> None: + stocks = self._stock_repository.find( + query={"owner_id": line_user_id, "item_name": item_name, "status": 1}, + sort=[("created_at", "desc")], + ) + if not stocks: + self._line_response_service.add_message(f'"{item_name}" が見つかりませんでした。') + return + self._stock_repository.update( + query={"_id": stocks[0]._id}, + new_values={"notify_days_before": notify_days_before}, + ) + label = "常に通知" if notify_days_before is None else f"{notify_days_before}日前から通知" + self._line_response_service.add_message( + f'"{item_name}" の通知を{label}に設定しました。' + ) + def _parse_followup_expiry_date(self, message: str): text = message.strip() if text == "": diff --git a/src/UseCases/Line/tests/test_handle_intent_operation_use_case.py b/src/UseCases/Line/tests/test_handle_intent_operation_use_case.py index 03023bb..d9a586f 100644 --- a/src/UseCases/Line/tests/test_handle_intent_operation_use_case.py +++ b/src/UseCases/Line/tests/test_handle_intent_operation_use_case.py @@ -99,10 +99,15 @@ def find(self, query=None, sort=None) -> list[Stock]: class DummyIntentParserService: - def __init__(self, parsed): + def __init__(self, parsed, extra_responses=None): self.parsed = parsed + # メッセージ部分文字列 → 返却値 のマッピング(テスト用) + self.extra_responses = extra_responses or {} def parse(self, message: str): + for substring, response in self.extra_responses.items(): + if substring in message: + return response return self.parsed @@ -302,7 +307,8 @@ def test_followup_expiry_date_updates_recently_registered_item(): assert repo.updated_query == {"_id": "S1"} assert repo.updated_values is not None assert repo.updated_values["expiry_date"] is not None - assert pending.get("U1") is None + # 期限更新後は通知フォローアップコンテキストが保存される + assert pending.get("U1")["operation"]["intent"] == "update_recent_notify" def test_register_with_notify_days_before(): @@ -630,3 +636,113 @@ def test_update_habit_log_not_found_creates(): assert log_repo.created.result == "done" assert log_repo.created.habit_task_id == "HT1" assert any("修正しました" in m for m in res.messages) + + +def test_followup_notify_after_register_with_expiry(): + """期限あり登録の直後に「通知は3日前から」で通知設定が反映される。""" + req = DummyLineRequestService(message="確定申告は3/15まで") + res = DummyLineResponseService() + repo = DummyStockRepository() + repo.found_stocks = [Stock(_id="S1", item_name="確定申告", owner_id="U1", status=1)] + # 「通知は3日前から」を含む補完済みメッセージが来たら update_stock_notify を返す + parser = DummyIntentParserService( + {"intent": "register", "item_name": "確定申告", "expiry_date": "2026-03-15"}, + extra_responses={ + "通知は3日前から": { + "intent": "update_stock_notify", + "item_name": "確定申告", + "notify_days_before": 3, + } + }, + ) + pending = DummyPendingOperationService() + use_case = HandleIntentOperationUseCase( + stock_repository=repo, + line_request_service=req, + line_response_service=res, + intent_parser_service=parser, + pending_operation_service=pending, + ) + + use_case.execute() + req.message = "はい" + use_case.execute() + + # 登録完了後、通知フォローアップコンテキストが保存される + assert pending.get("U1")["operation"]["intent"] == "update_recent_notify" + assert pending.get("U1")["operation"]["item_name"] == "確定申告" + + # 「通知は3日前から」を送ると通知設定が更新される + req.message = "通知は3日前から" + use_case.execute() + + assert repo.updated_values["notify_days_before"] == 3 + assert any("3日前から通知" in m for m in res.messages) + assert pending.get("U1") is None + + +def test_followup_notify_after_expiry_set(): + """期限なし登録 → 期限設定 → 「通知は3日前から」で通知設定が反映される。""" + res = DummyLineResponseService() + repo = DummyStockRepository() + repo.found_stocks = [Stock(_id="S1", item_name="卵", owner_id="U1", status=1)] + parser = DummyIntentParserService( + {"intent": "none", "item_name": None, "expiry_date": None}, + extra_responses={ + "通知は3日前から": { + "intent": "update_stock_notify", + "item_name": "卵", + "notify_days_before": 3, + } + }, + ) + pending = DummyPendingOperationService() + pending.save("U1", {"intent": "update_recent_expiry", "item_name": "卵"}) + req = DummyLineRequestService(message="明日で") + use_case = HandleIntentOperationUseCase( + stock_repository=repo, + line_request_service=req, + line_response_service=res, + intent_parser_service=parser, + pending_operation_service=pending, + ) + + use_case.execute() + + # 期限設定後、通知フォローアップコンテキストに切り替わる + assert pending.get("U1")["operation"]["intent"] == "update_recent_notify" + + # 「通知は3日前から」を送ると通知設定が更新される + req.message = "通知は3日前から" + use_case.execute() + + assert repo.updated_values["notify_days_before"] == 3 + assert any("3日前から通知" in m for m in res.messages) + assert pending.get("U1") is None + + +def test_followup_notify_unrelated_message_falls_through(): + """update_recent_notify 中に無関係なメッセージを送るとコンテキストが破棄される。""" + res = DummyLineResponseService() + repo = DummyStockRepository() + # NLPは「none」を返す(通知設定変更として解釈できない) + parser = DummyIntentParserService( + {"intent": "none", "item_name": None, "expiry_date": None} + ) + pending = DummyPendingOperationService() + pending.save("U1", {"intent": "update_recent_notify", "item_name": "牛乳"}) + req = DummyLineRequestService(message="ありがとう") + use_case = HandleIntentOperationUseCase( + stock_repository=repo, + line_request_service=req, + line_response_service=res, + intent_parser_service=parser, + pending_operation_service=pending, + ) + + use_case.execute() + + # コンテキスト破棄 + assert pending.get("U1") is None + # notify_days_before は更新されない + assert repo.updated_values is None diff --git a/src/routes/web/index.py b/src/routes/web/index.py index 443ef3a..3877ddd 100644 --- a/src/routes/web/index.py +++ b/src/routes/web/index.py @@ -41,7 +41,7 @@ def index(): near_due_count = 0 notify_on_count = 0 for stock in stocks: - if stock.notify_enabled: + if stock.notify_days_before is not None: notify_on_count += 1 if stock.expiry_date is None: continue diff --git a/src/templates/pages/stock/index.html b/src/templates/pages/stock/index.html index 10cafac..6f87fe7 100644 --- a/src/templates/pages/stock/index.html +++ b/src/templates/pages/stock/index.html @@ -3,39 +3,4 @@ - - -
-
- - - -
-
- -
-
- {% import 'components/_macros.html' as ui with context %} {{ - ui.stock_table(page_contents, form) }} -
-
- -{% endblock %} + \ No newline at end of file