NutriCoach customer UX successor qualification diff

--- r39/gateway/platforms/nutrition_coaching.py
+++ ux-successor/gateway/platforms/nutrition_coaching.py
@@ -2857,6 +2857,11 @@
             except ValueError:
                 expires_at = None
             claim_time = (now or self._generation_now_provider()).astimezone(timezone.utc)
+            effective_kst_date = (
+                kst_date
+                if kst_date is not None
+                else claim_time.astimezone(ZoneInfo("Asia/Seoul")).date()
+            )
             if (
                 delivery.get("status") != "pending"
                 or delivery.get("customer_key") != customer_key
@@ -2914,7 +2919,7 @@
                 or not self.customer_transport_allowed(
                     customer_key,
                     canonical_destination,
-                    kst_date=kst_date,
+                    kst_date=effective_kst_date,
                 )
             ):
                 return False
--- r39/gateway/platforms/telegram.py
+++ ux-successor/gateway/platforms/telegram.py
@@ -5792,14 +5792,42 @@
                         len(claims) == 1
                         and session.role_claim(Role.CUSTOMER) is not None
                     )
-                    if not owner_v1 or getattr(session, "recovery_attempts", ()):
+                    if not owner_v1:
                         continue
+                    current = session
+                    recovery_attempts = getattr(session, "recovery_attempts", ())
+                    if recovery_attempts:
+                        if state != "AWAITING_CONSENT":
+                            continue
+                        try:
+                            current = (
+                                transport.store.recover_uncertain_consent_publication(
+                                    session.session_id,
+                                    expected_generation=session.generation,
+                                )
+                            )
+                        except (OSError, RuntimeError, TypeError, ValueError):
+                            logger.warning(
+                                "[%s] uncertain consent publication recovery failed",
+                                self.name,
+                            )
+                            continue
+                        if getattr(
+                            getattr(current, "state", None),
+                            "value",
+                            "",
+                        ) == "FAILED":
+                            logger.warning(
+                                "[%s] uncertain consent publication exhausted; "
+                                "a fresh invite is required",
+                                self.name,
+                            )
+                            continue
                     nutrition = self._get_nutrition_coaching()
                     refresher = getattr(nutrition, "refresh_live_registry", None)
                     if nutrition is None or not callable(refresher):
                         continue
                     try:
-                        current = session
                         if state == "REGISTERING":
                             from gateway.platforms.telegram_customer_bootstrap_registration import (
                                 TelegramCustomerBootstrapRegistration,
@@ -6698,6 +6726,7 @@
         data: str,
         message: object,
     ) -> None:
+        ingress_recorded_at = datetime.now(timezone.utc)
         coordinator = self._get_nutrition_coaching()
         if coordinator is None:
             await query.answer(text="고객 코칭 기능을 사용할 수 없습니다.")
@@ -6712,13 +6741,19 @@
         if store is not None:
             from gateway.platforms.telegram_customer_bootstrap import (
                 BootstrapState,
+                ConsentHandoff,
                 Role,
+                consent_handoff_digest,
             )
 
             for candidate in store.list_sessions():
                 customer = candidate.role_claim(Role.CUSTOMER)
                 if (
-                    candidate.state is BootstrapState.AWAITING_CONSENT
+                    candidate.state
+                    in {
+                        BootstrapState.AWAITING_CONSENT,
+                        BootstrapState.AWAITING_ACTIVATION,
+                    }
                     and customer is not None
                     and customer.user_id == str(address.user_id)
                     and customer.chat_id == str(address.chat_id)
@@ -6726,10 +6761,19 @@
                 ):
                     bootstrap_session = candidate
                     break
+        expected_consent_message_id = (
+            None
+            if bootstrap_session is None
+            else (
+                bootstrap_session.consent_card_message_id
+                if bootstrap_session.state is BootstrapState.AWAITING_CONSENT
+                else bootstrap_session.committed_consent_card_message_id
+            )
+        )
         if bootstrap_session is not None and (
-            bootstrap_session.consent_card_message_id is None
+            expected_consent_message_id is None
             or str(getattr(message, "message_id", ""))
-            != bootstrap_session.consent_card_message_id
+            != expected_consent_message_id
         ):
             await query.answer(
                 text="이 동의 카드는 만료됐습니다. ‘고객 체크인’ 토픽의 "
@@ -6740,6 +6784,51 @@
             query,
             gate="nutrition_customer_consent",
         )
+        if (
+            store is not None
+            and bootstrap_session is not None
+            and str(data).endswith(":g")
+        ):
+            current = store.get(bootstrap_session.session_id)
+            handoff = current.consent_handoff
+            if handoff is None:
+                update_id = current_telegram_update_id()
+                if type(update_id) is not int or update_id < 0:
+                    logger.warning(
+                        "telegram_customer_consent_handoff_unavailable "
+                        "reason=update_id"
+                    )
+                    return
+                unsigned_handoff = ConsentHandoff(
+                    update_id=update_id,
+                    actor_id=int(address.user_id),
+                    chat_id=int(address.chat_id),
+                    topic_id=int(address.topic_id),
+                    message_id=int(getattr(message, "message_id")),
+                    callback_data=data,
+                    bootstrap_generation=current.generation,
+                    recorded_at=ingress_recorded_at,
+                    provenance_digest="",
+                )
+                handoff = dataclasses.replace(
+                    unsigned_handoff,
+                    provenance_digest=consent_handoff_digest(
+                        current,
+                        unsigned_handoff,
+                    ),
+                )
+            try:
+                current = store.bind_consent_handoff(
+                    current.session_id,
+                    expected_generation=current.generation,
+                    handoff=handoff,
+                )
+            except Exception as exc:
+                logger.warning(
+                    "telegram_customer_consent_handoff_unavailable error=%s",
+                    type(exc).__name__,
+                )
+                return
         transition = coordinator.handle_customer_consent_callback(address, data)
         if transition.reply.accepted:
             try:
@@ -6754,30 +6843,43 @@
                     update_id if update_id is not None else "unknown",
                     type(exc).__name__,
                 )
-        if not transition.reply.accepted or not str(data).endswith(":g"):
+        if not str(data).endswith(":g"):
             return
         if store is None or bootstrap_session is None:
             return
         current = store.get(bootstrap_session.session_id)
-        if (
-            current.state is BootstrapState.AWAITING_CONSENT
-            and current.generation == bootstrap_session.generation
-            and current.consent_card_message_id
-            == bootstrap_session.consent_card_message_id
+        if not transition.reply.accepted and not self._room_bootstrap_has_current_consent(
+            coordinator,
+            address,
+            current.customer_key,
         ):
+            return
+        if current.state is BootstrapState.AWAITING_CONSENT:
             activated = store.reconcile_committed_consent(
                 current.session_id,
                 expected_generation=current.generation,
                 publication_attempt=current.consent_publication_attempt,
                 consent_card_message_id=current.consent_card_message_id,
             )
-            runtime = self._get_nutrition_onboarding_runtime()
-            if runtime is not None:
-                await runtime.start_after_consent(
-                    session=activated,
-                    query=query,
-                    message=message,
-                )
+        elif current.state is BootstrapState.AWAITING_ACTIVATION:
+            activated = current
+        else:
+            return
+        handoff = activated.consent_handoff
+        if handoff is None:
+            return
+        activated = store.mark_consent_recovery_reconciled(
+            activated.session_id,
+            expected_generation=activated.generation,
+            provenance_digest=handoff.provenance_digest,
+        )
+        runtime = self._get_nutrition_onboarding_runtime()
+        if runtime is not None:
+            await runtime.start_after_consent(
+                session=activated,
+                query=query,
+                message=message,
+            )
 
     async def _handle_nutrition_customer_pause_callback(
         self,
@@ -10478,6 +10580,9 @@
         allow_weekend: bool = False,
     ) -> bool:
         """Independently prove a profile-emitted task is today's eligible work."""
+        if local_now.tzinfo is None:
+            return False
+        local_now = local_now.astimezone(ZoneInfo("Asia/Seoul"))
         spec = getattr(customer, "spec", None)
         plan = getattr(spec, "plan", None)
         schedule = getattr(spec, "schedule", None)
@@ -10666,7 +10771,13 @@
         if not isinstance(profile_root, _Path):
             return SendResult(success=False, error="schedule delivery profile is unavailable")
 
-        local_now = now or datetime.now(timezone.utc).astimezone(ZoneInfo("Asia/Seoul"))
+        supplied_now = now or datetime.now(timezone.utc)
+        if supplied_now.tzinfo is None:
+            return SendResult(
+                success=False,
+                error="schedule delivery clock is timezone-naive",
+            )
+        local_now = supplied_now.astimezone(ZoneInfo("Asia/Seoul"))
         try:
             weekly_plan = self._weekly_schedule_plan(local_now)
             weekly_policy = weekly_plan.policy
