Files
han-app/VM1_app/codebase/backend/deployment/scripts/seed-personal-notifications-test.sh

496 lines
24 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Создаёт 9 персональных уведомлений всех видов контура P для одного user_id.
# Запускать на ВМ1 под root/admin (sudo -i). Секреты не читаются из /etc/han/vm1.env.
#
# sed -i 's/\r$//' /opt/han-chat/current/backend/deployment/scripts/seed-personal-notifications-test.sh
# chmod +x /opt/han-chat/current/backend/deployment/scripts/seed-personal-notifications-test.sh
# /opt/han-chat/current/backend/deployment/scripts/seed-personal-notifications-test.sh
#
# Токен: runtime-файл NOTIFICATIONS_TOKEN_PRODUCER_TEST
# (/run/han-chat/secrets/…, тот же, что у api-backend). Hash в notification_sources
# синхронизируется при старте api-backend.
set -euo pipefail
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
if [[ -f "${SCRIPT_DIR}/../../docker-compose.yml" ]]; then
cd "${SCRIPT_DIR}/../.."
else
cd "${HAN_DEPLOY_DIR:-/opt/han-chat/current/backend}"
fi
CONFIG_FILE="${CONFIG_FILE:-/etc/han/vm1.env}"
SECRETS_LAUNCHER="${SECRETS_LAUNCHER:-/usr/local/lib/han-secrets/han-secrets}"
if [[ ! -x "$SECRETS_LAUNCHER" ]]; then
SECRETS_LAUNCHER="deployment/secrets/han-secrets"
fi
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "Не найден конфиг ${CONFIG_FILE} (ожидается /etc/han/vm1.env)." >&2
exit 1
fi
if [[ "${HAN_SECRETS_ACTIVE:-0}" != "1" ]]; then
[[ -x "$SECRETS_LAUNCHER" ]] || {
echo "Secret launcher is required: $SECRETS_LAUNCHER" >&2
exit 66
}
exec "$SECRETS_LAUNCHER" run --config "$CONFIG_FILE" -- "$0" "$@"
fi
compose() {
if [[ -x /usr/local/sbin/han-vm1-compose ]]; then
/usr/local/sbin/han-vm1-compose "$@"
else
docker compose --env-file "$CONFIG_FILE" "$@"
fi
}
trim_token() {
printf '%s' "$1" | tr -d '\r\n\t '
}
read_secret_file() {
local path="${1:-}"
[[ -n "$path" && -r "$path" ]] || return 1
python3 - "$path" <<'PY'
from pathlib import Path
import sys
print(Path(sys.argv[1]).read_text(encoding="utf-8").strip())
PY
}
TOKEN="$(trim_token "${NOTIFICATIONS_TOKEN_PRODUCER_TEST:-}")"
if [[ -z "$TOKEN" ]]; then
TOKEN="$(trim_token "$(read_secret_file "${NOTIFICATIONS_TOKEN_PRODUCER_TEST_FILE:-}" 2>/dev/null || true)")"
fi
if [[ -z "$TOKEN" ]]; then
TOKEN="$(trim_token "$(read_secret_file "${HAN_RUNTIME_SECRET_DIR:-/run/han-chat/secrets}/NOTIFICATIONS_TOKEN_PRODUCER_TEST" 2>/dev/null || true)")"
fi
if [[ -z "$TOKEN" ]]; then
TOKEN="$(trim_token "$(compose exec -T api-backend python3 -c 'import os; print(os.getenv("NOTIFICATIONS_TOKEN_PRODUCER_TEST") or "")' 2>/dev/null || true)")"
fi
if [[ -z "$TOKEN" ]]; then
read -rsp "NOTIFICATIONS_TOKEN_PRODUCER_TEST (runtime secret не найден): " TOKEN
echo
TOKEN="$(trim_token "$TOKEN")"
fi
read -rp "USER_ID клиента: " USER_ID
USER_ID="$(trim_token "$USER_ID")"
if [[ -z "${TOKEN}" || -z "${USER_ID}" ]]; then
echo "TOKEN и USER_ID обязательны." >&2
exit 1
fi
echo "Токен: ${#TOKEN} символов (значение не печатается)."
PAYLOAD_DIR="$(mktemp -d)"
trap 'rm -rf "$PAYLOAD_DIR"' EXIT
BASE_TS="$(date +%s)"
RUN_ID="manual-test-${BASE_TS}"
NOW="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
DT_URGENT="$(date -u -d "${NOW} +0 seconds" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v+0S +"%Y-%m-%dT%H:%M:%SZ")"
DT_PAYMENT="$(date -u -d "${NOW} - 60 seconds" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v-60S +"%Y-%m-%dT%H:%M:%SZ")"
DT_DOCS_REQ="$(date -u -d "${NOW} - 120 seconds" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v-120S +"%Y-%m-%dT%H:%M:%SZ")"
DT_DOCS_READY="$(date -u -d "${NOW} - 180 seconds" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v-180S +"%Y-%m-%dT%H:%M:%SZ")"
DT_STATUS="$(date -u -d "${NOW} - 240 seconds" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v-240S +"%Y-%m-%dT%H:%M:%SZ")"
DT_REMINDER="$(date -u -d "${NOW} - 300 seconds" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v-300S +"%Y-%m-%dT%H:%M:%SZ")"
DT_NEWS="$(date -u -d "${NOW} - 360 seconds" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v-360S +"%Y-%m-%dT%H:%M:%SZ")"
DT_PROMO="$(date -u -d "${NOW} - 420 seconds" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v-420S +"%Y-%m-%dT%H:%M:%SZ")"
DT_ADS="$(date -u -d "${NOW} - 480 seconds" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v-480S +"%Y-%m-%dT%H:%M:%SZ")"
DEADLINE_URGENT="$(date -u -d "${NOW} + 2 days" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v+2d +"%Y-%m-%dT%H:%M:%SZ")"
DEADLINE_DOCS="$(date -u -d "${NOW} + 5 days" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v+5d +"%Y-%m-%dT%H:%M:%SZ")"
DEADLINE_REMINDER="$(date -u -d "${NOW} + 1 day" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v+1d +"%Y-%m-%dT%H:%M:%SZ")"
EXPIRE_PAYMENT="$(date -u -d "${NOW} + 3 days" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v+3d +"%Y-%m-%dT%H:%M:%SZ")"
write_payload() {
local name="$1"
cat > "${PAYLOAD_DIR}/${name}.json"
}
create_notification() {
local label="$1"
local payload_file="${PAYLOAD_DIR}/${label}.json"
if [[ ! -f "$payload_file" ]]; then
echo "Нет payload: ${payload_file}" >&2
return 1
fi
echo
echo "========== ${label} =========="
compose exec -T \
-e "NOTIFICATIONS_PRODUCER_TOKEN=${TOKEN}" \
api-backend python3 -c "
import os
import sys
import urllib.error
import urllib.request
payload = sys.stdin.buffer.read()
request = urllib.request.Request(
'http://127.0.0.1:8000/internal/notifications/v1/notifications',
data=payload,
method='POST',
headers={
'Authorization': 'Bearer ' + os.environ['NOTIFICATIONS_PRODUCER_TOKEN'],
'Content-Type': 'application/json; charset=utf-8',
},
)
try:
with urllib.request.urlopen(request, timeout=60) as response:
sys.stdout.write('HTTP/1.1 %s\n' % response.status)
for key, value in response.headers.items():
sys.stdout.write('%s: %s\n' % (key, value))
sys.stdout.write('\n')
sys.stdout.write(response.read().decode('utf-8', errors='replace'))
sys.stdout.write('\n')
except urllib.error.HTTPError as exc:
sys.stdout.write('HTTP/1.1 %s\n' % exc.code)
for key, value in exc.headers.items():
sys.stdout.write('%s: %s\n' % (key, value))
sys.stdout.write('\n')
sys.stdout.write(exc.read().decode('utf-8', errors='replace'))
sys.stdout.write('\n')
if exc.code >= 500 or exc.code in (401, 403):
raise SystemExit(1)
" <"$payload_file"
}
prepare_docs_in_s3() {
echo >&2
echo "========== PREP: загрузка тестовых документов в S3 для docs_ready ==========" >&2
compose exec -T \
-e "USER_ID=${USER_ID}" \
-e "RUN_ID=${RUN_ID}" \
api-backend python3 - <<'PY'
import hashlib
import os
import uuid
import boto3
from botocore.client import Config
from app.settings import Settings
USER_ID = os.environ["USER_ID"]
settings = Settings()
client = boto3.client(
"s3",
endpoint_url=str(settings.selectel_s3_endpoint_url),
aws_access_key_id=settings.selectel_s3_access_key.get_secret_value(),
aws_secret_access_key=settings.selectel_s3_secret_key.get_secret_value(),
config=Config(signature_version="s3v4", s3={"addressing_style": "virtual"}),
)
bucket = settings.selectel_s3_bucket_documents
fixtures = [
{
"prefix": "CONTRACT",
"title": "Уведомление о постановке на миграционный учёт.pdf",
"body": b"""%PDF-1.4
1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R>>endobj
xref
0 4
trailer<</Size 4/Root 1 0 R>>
startxref
100
%%EOF
""",
},
{
"prefix": "RESULTS",
"title": "Справка о соблюдении миграционного законодательства.pdf",
"body": b"""%PDF-1.4
1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R>>endobj
xref
0 4
trailer<</Size 4/Root 1 0 R>>
startxref
100
%%EOF
""",
},
]
lines: list[str] = []
for item in fixtures:
doc_id = str(uuid.uuid4())
key = f"documents/users/{USER_ID}/{doc_id}"
body = item["body"]
checksum = hashlib.sha256(body).hexdigest()
client.put_object(
Bucket=bucket,
Key=key,
Body=body,
ContentType="application/pdf",
)
p = item["prefix"]
lines.append(
f"{p}_KEY={key}\n"
f"{p}_TITLE={item['title']}\n"
f"{p}_SIZE={len(body)}\n"
f"{p}_SHA256={checksum}"
)
print("\n".join(lines))
PY
}
echo "Run ID: ${RUN_ID}"
echo "User ID: ${USER_ID}"
read -rp "Подготовить документы в S3 для docs_ready? [Y/n]: " PREP_DOCS
PREP_DOCS="${PREP_DOCS:-Y}"
CONTRACT_KEY=""
CONTRACT_TITLE=""
CONTRACT_SIZE=""
CONTRACT_SHA256=""
RESULTS_KEY=""
RESULTS_TITLE=""
RESULTS_SIZE=""
RESULTS_SHA256=""
if [[ "${PREP_DOCS^^}" != "N" ]]; then
PREP_OUT="$(prepare_docs_in_s3)"
echo "${PREP_OUT}"
while IFS= read -r line; do
[[ "$line" =~ ^([A-Z][A-Z0-9_]*)=(.*)$ ]] || continue
declare "${BASH_REMATCH[1]}=${BASH_REMATCH[2]}"
done <<< "${PREP_OUT}"
fi
# --- payloads ---
write_payload urgent <<JSON
{
"user_id": "${USER_ID}",
"notification_type": "urgent",
"source": "producer_test",
"external_id": "${RUN_ID}-urgent",
"notification_datetime": "${DT_URGENT}",
"header": "Истекает срок постановки на учёт",
"text": "По данным сервиса, срок уведомления о месте пребывания истекает 29 июля — просрочка влечёт административную ответственность.",
"priority_override": 1,
"date_expired": "${DEADLINE_URGENT}",
"details": {
"deadline": "${DEADLINE_URGENT}",
"details_header": "Срочно: соблюдение сроков миграционного учёта",
"details_text": "Федеральный закон № 109-ФЗ обязывает иностранного гражданина в течение 7 рабочих дней с даты въезда подать уведомление о прибытии в место пребывания (если иное не предусмотрено для вашего правового статуса).\n\nПо имеющимся данным крайний срок для вашего случая — 29.07.2026. Нарушение сроков может повлечь штраф от 2 000 до 5 000 ₽ и, при повторном нарушении, более серьёзные последствия, включая административное выдворение.\n\nЕсли уведомление уже подано — отметьте «Готово»; если нужна помощь — напишите оператору в чат.",
"todo_header": "Что проверить сейчас",
"todo_plan": [
{"number": 1, "text": "Сверьте дату въезда и адрес фактического проживания в анкете личного кабинета."},
{"number": 2, "text": "Подготовьте копии паспорта, миграционной карты и документа о праве пребывания (виза, РВП, ВНЖ, патент)."},
{"number": 3, "text": "Нажмите «Готово», если уведомление уже подано через МВД или принимающую сторону."},
{"number": 4, "text": "При сомнениях выберите «Сделаю позже» и задайте вопрос оператору — укажите город и тип документа."}
]
}
}
JSON
write_payload payment_pending <<JSON
{
"user_id": "${USER_ID}",
"notification_type": "payment_pending",
"source": "producer_test",
"external_id": "${RUN_ID}-payment_pending",
"notification_datetime": "${DT_PAYMENT}",
"header": "Оплата сопровождения по миграционному учёту",
"text": "Счёт №МИГ-2026-0718 на 18 500 ₽ за подготовку пакета и подачу уведомления — оплатите до 30 июля.",
"price": "18500.00",
"old_price": "22000.00",
"date_expired": "${EXPIRE_PAYMENT}",
"payment_url": "https://pay.han0107.ru/checkout/test-${RUN_ID}"
}
JSON
write_payload docs_required <<JSON
{
"user_id": "${USER_ID}",
"notification_type": "docs_required",
"source": "producer_test",
"external_id": "${RUN_ID}-docs_required",
"notification_datetime": "${DT_DOCS_REQ}",
"header": "Загрузите документы для миграционного учёта",
"text": "Для проверки соблюдения миграционного законодательства нужен комплект документов — загрузите до 01 августа.",
"date_expired": "${DEADLINE_DOCS}",
"details": {
"deadline": "${DEADLINE_DOCS}",
"details_header": "Документы для постановки на миграционный учёт",
"details_text": "Специалист проверит комплект в течение 1 рабочего дня после отправки. Принимаются чёткие фото или сканы в JPG, PNG, PDF. Каждый файл — до 10 МБ, не более 10 файлов за одну отправку.\n\nВсе документы должны быть действительными на дату проверки. Если какого-то документа пока нет (например, договор найма ещё не подписан), загрузите доступные — оператор подскажет порядок действий и допустимые альтернативы.",
"todo_header": "Необходимый пакет",
"todo_plan": [
{"number": 1, "text": "Паспорт: страница с фото, действующая виза или иной документ на право пребывания."},
{"number": 2, "text": "Миграционная карта с отметкой о въезде (обе стороны)."},
{"number": 3, "text": "Документ о месте пребывания: договор найма, свидетельство собственности или письмо принимающей стороны."},
{"number": 4, "text": "При трудовой деятельности — копия патента или разрешения на работу (если применимо)."},
{"number": 5, "text": "Нажмите «Отправить документы», когда все файлы приложены."}
],
"send_documents": true
}
}
JSON
if [[ -n "${CONTRACT_KEY}" && -n "${RESULTS_KEY}" ]]; then
write_payload docs_ready <<JSON
{
"user_id": "${USER_ID}",
"notification_type": "docs_ready",
"source": "producer_test",
"external_id": "${RUN_ID}-docs_ready",
"notification_datetime": "${DT_DOCS_READY}",
"header": "Миграционные документы готовы",
"text": "Уведомление о постановке на учёт и справка о соблюдении требований закона сформированы — скачайте в деталях.",
"details": {
"details_header": "Документы по миграционному учёту",
"details_text": "Документы подготовлены на основании переданных вами данных и проверены специалистом. Сохраните копии на устройство — они могут понадобиться при проверке или продлении статуса пребывания.\n\nСсылки на скачивание действуют ограниченное время. После первого скачивания карточка скроется с главной, но останется в Центре уведомлений до нажатия «Понятно».",
"documents": [
{
"object_key": "${CONTRACT_KEY}",
"title": "${CONTRACT_TITLE}",
"mime_type": "application/pdf",
"size_bytes": ${CONTRACT_SIZE},
"checksum_sha256": "${CONTRACT_SHA256}"
},
{
"object_key": "${RESULTS_KEY}",
"title": "${RESULTS_TITLE}",
"mime_type": "application/pdf",
"size_bytes": ${RESULTS_SIZE},
"checksum_sha256": "${RESULTS_SHA256}"
}
]
}
}
JSON
fi
write_payload status_changed <<JSON
{
"user_id": "${USER_ID}",
"notification_type": "status_changed",
"source": "producer_test",
"external_id": "${RUN_ID}-status_changed",
"notification_datetime": "${DT_STATUS}",
"header": "Статус миграционной заявки обновлён",
"text": "Заявка №МГ-8841 переведена на этап «Проверка комплектности документов».",
"details": {
"details_header": "Ход рассмотрения заявки №МГ-8841",
"details_text": "27.07.2026 в 11:42 специалист принял ваш пакет документов в работу. Сейчас выполняется проверка на соответствие требованиям миграционного законодательства РФ: сроки пребывания, адрес учёта, основания для трудовой деятельности.\n\nОжидаемое время на этом этапе: 1–2 рабочих дня. При выявлении недостающих документов вы получите отдельное уведомление с перечнем. При успешной проверке будет сформировано уведомление о постановке на учёт.",
"todo_header": "Этапы обработки",
"todo_plan": [
{"number": 1, "text": "Документы получены — выполнено 25.07.2026"},
{"number": 2, "text": "Первичная верификация — выполнено 27.07.2026"},
{"number": 3, "text": "Проверка комплектности и сроков — в работе"},
{"number": 4, "text": "Формирование уведомления / рекомендаций — ожидает"}
]
}
}
JSON
write_payload reminder <<JSON
{
"user_id": "${USER_ID}",
"notification_type": "reminder",
"source": "producer_test",
"external_id": "${RUN_ID}-reminder",
"notification_datetime": "${DT_REMINDER}",
"header": "Напоминание: продление патента",
"text": "28 июля истекает срок действия патента — подайте заявление на продление заранее.",
"date_expired": "${DEADLINE_REMINDER}",
"details": {
"deadline": "${DEADLINE_REMINDER}",
"details_header": "Сроки продления документа на право работы",
"details_text": "Патент на работу необходимо продлевать заблаговременно — подача заявления рекомендуется не позднее чем за 10–15 рабочих дней до даты окончания действия. Просрочка означает прекращение права на трудовую деятельность и риск штрафа по ст. 18.15 КоАП РФ.\n\nДля продления потребуются: действующий патент, чеки об оплате авансовых платежей по НДФЛ, полис ДМС, сертификат о знании русского языка (если срок действия истекает), договор найма или иной документ о месте пребывания.",
"todo_header": "Чек-лист перед подачей",
"todo_plan": [
{"number": 1, "text": "Проверьте дату окончания патента в личном кабинете или на бланке документа."},
{"number": 2, "text": "Убедитесь, что авансовые платежи по НДФЛ оплачены без просрочки."},
{"number": 3, "text": "Подготовьте сканы документов — при необходимости загрузите через уведомление «Требуются документы»."},
{"number": 4, "text": "При вопросах напишите оператору — укажите регион и номер патента."}
]
}
}
JSON
write_payload news <<JSON
{
"user_id": "${USER_ID}",
"notification_type": "news",
"source": "producer_test",
"external_id": "${RUN_ID}-news",
"notification_datetime": "${DT_NEWS}",
"header": "Изменения в правилах миграционного учёта",
"text": "С 1 августа 2026 уточнены сроки подачи уведомлений при смене адреса пребывания.",
"details": {
"details_header": "Что изменилось для иностранных граждан",
"details_text": "С 01.08.2026 при смене адреса фактического проживания в том же субъекте РФ уведомление необходимо подать в течение 3 рабочих дней (ранее — 7). При переезде в другой регион срок остаётся 7 рабочих дней с даты регистрации по новому адресу.\n\nСервис HAN напомнит о приближающихся сроках через Центр уведомлений. Рекомендуем заранее подготовить копии договора найма и отметку о регистрации — это ускорит проверку оператором.\n\nПодробности процедуры — в чате с оператором или на официальном портале МВД России."
}
}
JSON
write_payload promo_personal <<JSON
{
"user_id": "${USER_ID}",
"notification_type": "promo_personal",
"source": "producer_test",
"external_id": "${RUN_ID}-promo_personal",
"notification_datetime": "${DT_PROMO}",
"header": "Скидка 15% на годовое сопровождение",
"text": "Персональное предложение: контроль сроков патента, учёта и уведомлений — до конца месяца.",
"price": "15725.00",
"old_price": "18500.00",
"date_expired": "${EXPIRE_PAYMENT}",
"chat_message_text": "Здравствуйте! Хочу подключить годовое сопровождение по соблюдению миграционного законодательства со скидкой 15%. Подскажите, что входит в пакет и как оформить."
}
JSON
write_payload ads_personal <<JSON
{
"user_id": "${USER_ID}",
"notification_type": "ads_personal",
"source": "producer_test",
"external_id": "${RUN_ID}-ads_personal",
"notification_datetime": "${DT_ADS}",
"header": "Бесплатная проверка миграционного статуса",
"text": "15 минут с экспертом: оценим риски и составим чек-лист обязательных действий.",
"chat_message_text": "Здравствуйте! Хочу воспользоваться бесплатной проверкой миграционного статуса. Подскажите, как записаться и какие документы подготовить к консультации."
}
JSON
# --- отправка ---
create_notification urgent
create_notification payment_pending
create_notification docs_required
if [[ -f "${PAYLOAD_DIR}/docs_ready.json" ]]; then
create_notification docs_ready
else
echo
echo "========== docs_ready — ПРОПУЩЕН =========="
fi
create_notification status_changed
create_notification reminder
create_notification news
create_notification promo_personal
create_notification ads_personal
echo
echo "========== Готово =========="
echo "External ID prefix: ${RUN_ID}-*"
echo
echo "Если видите 401: проверьте runtime-секрет NOTIFICATIONS_TOKEN_PRODUCER_TEST"
echo "и hash в han_app.notification_sources, затем пересоздайте api-backend:"
echo " test -r /run/han-chat/secrets/NOTIFICATIONS_TOKEN_PRODUCER_TEST"
echo " /usr/local/sbin/han-vm1-compose up -d --force-recreate api-backend"