feat(apply): treat "Inserat offline" as its own outcome

Offline listings are a different class of result from real failures:
they mean "this ad is gone", not "the apply pipeline is broken".

- DB migration 0010 adds flats.offline_at; recent_flats() filters those
  out globally so they drop off every user's Wohnungen list
- _is_offline_result() matches the four known offline/deactivated
  phrases (DE + EN translations)
- On an offline result: mark the flat, reset the failure counter
  instead of incrementing, and skip the apply_fail notification
- Bewerbungen history renders a yellow "offline" chip in place of the
  red "fehlgeschlagen" one

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
EiSiMo 2026-04-23 09:56:04 +02:00
parent 64439fd42e
commit abd614604f
3 changed files with 53 additions and 5 deletions

View file

@ -202,6 +202,23 @@ def _has_running_application(user_id: int) -> bool:
return db.has_running_application(user_id)
# Apply returns success=False with one of these phrases when the listing has
# been taken down or deactivated. We treat that as "the flat is gone" rather
# than "the apply pipeline is broken" — no circuit-breaker hit, no fail
# notification, and the flat is hidden from every user's list.
_OFFLINE_MESSAGE_MARKERS = (
"inserat offline", "inserat deaktiviert", # de
"ad offline", "ad deactivated", # en
)
def _is_offline_result(message: str) -> bool:
if not message:
return False
m = message.lower()
return any(marker in m for marker in _OFFLINE_MESSAGE_MARKERS)
def _finish_apply_background(app_id: int, user_id: int, flat_id: str, url: str,
profile: dict, submit_forms: bool) -> None:
"""Called on a worker thread AFTER the application row already exists.
@ -218,8 +235,14 @@ def _finish_apply_background(app_id: int, user_id: int, flat_id: str, url: str,
db.finish_application(app_id, success=success, message=message,
provider=provider, forensics=forensics)
offline = not success and _is_offline_result(message)
if offline:
db.mark_flat_offline(flat_id)
prefs = db.get_preferences(user_id)
if success:
if success or offline:
# Offline isn't a pipeline failure — reset the counter so a streak of
# stale listings doesn't trip the circuit breaker.
db.update_preferences(user_id, {"apply_recent_failures": 0})
else:
failures = int(prefs["apply_recent_failures"] or 0) + 1
@ -241,10 +264,12 @@ def _finish_apply_background(app_id: int, user_id: int, flat_id: str, url: str,
"total_rent": flat["total_rent"] if flat else None}
if success:
notifications.on_apply_ok(user_id, flat_dict, message)
else:
elif not offline:
notifications.on_apply_fail(user_id, flat_dict, message)
db.log_audit("system", "apply_finished", f"app={app_id} success={success}", user_id=user_id)
outcome = "success" if success else ("offline" if offline else "failure")
db.log_audit("system", "apply_finished",
f"app={app_id} outcome={outcome}", user_id=user_id)
def _kick_apply(user_id: int, flat_id: str, url: str, triggered_by: str) -> None: