feat: Add PostgreSQL fields and workflow for form without files
Database changes: - Add unified_id, contact_id, phone columns to clpr_claims table - Create indexes for fast lookup by these fields - Migrate existing data from payload to new columns - SQL migration: docs/SQL_ALTER_CLPR_CLAIMS_ADD_FIELDS.sql SQL improvements: - Simplify claimsave query: remove complex claim_lookup logic - Use UPSERT (INSERT ON CONFLICT) with known claim_id - Always return claim (fix NULL issue) - Store unified_id, contact_id, phone directly in table columns - SQL: docs/SQL_CLAIMSAVE_UPSERT_SIMPLE.sql Workflow enhancements: - Add branch for form submissions WITHOUT files - Create 6 new nodes: extract, prepare, save, redis, respond - Separate flow for has_files=false in IF node - Instructions: docs/N8N_FORM_GET_NO_FILES_INSTRUCTIONS.md - Node config: docs/N8N_FORM_GET_NO_FILES_BRANCH.json Migration stats: - Total claims: 81 - With unified_id: 77 - Migrated from payload: 2 Next steps: 1. Add 6 nodes to n8n workflow form_get (ID: 8ZVMTsuH7Cmw7snw) 2. Connect TRUE branch of IF node to extract_webhook_data_no_files 3. Test form submission without files 4. Verify PostgreSQL save and Redis event
This commit is contained in:
@@ -342,3 +342,4 @@ TTL: 86400 секунд
|
|||||||
**Дата:** 2025-11-20
|
**Дата:** 2025-11-20
|
||||||
**Статус:** ✅ Завершено
|
**Статус:** ✅ Завершено
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -208,3 +208,4 @@ $2 = {{ $json.claim_id }} (TEXT, строка "CLM-2025-11-18-GEQ3K
|
|||||||
|
|
||||||
Оба запроса теперь используют строковый `claim_id` и правильно находят UUID.
|
Оба запроса теперь используют строковый `claim_id` и правильно находят UUID.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -101,3 +101,4 @@ function mapCombinedDocs(cds = []) {
|
|||||||
|
|
||||||
Но для `mapDialogHistory` это критично, т.к. она вызывается первой и падает.
|
Но для `mapDialogHistory` это критично, т.к. она вызывается первой и падает.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -210,3 +210,4 @@ const results = arr
|
|||||||
|
|
||||||
return results.length ? results : [{ json: null }];
|
return results.length ? results : [{ json: null }];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -181,3 +181,4 @@ clpr_user_accounts (channel='telegram', channel_user_id=telegram_id)
|
|||||||
clpr_users (id)
|
clpr_users (id)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,3 +36,4 @@ return {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -45,3 +45,4 @@ return {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
261
docs/N8N_FORM_GET_NO_FILES_BRANCH.json
Normal file
261
docs/N8N_FORM_GET_NO_FILES_BRANCH.json
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"description": "Ноды для обработки формы БЕЗ файлов в workflow form_get",
|
||||||
|
"date": "2025-11-21",
|
||||||
|
"action": "Добавить в TRUE ветку IF-ноды 'проверка наличия файлов'"
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "extract_webhook_data_no_files",
|
||||||
|
"description": "Извлекаем данные из webhook для случая без файлов",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [-320, 400],
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "session_id",
|
||||||
|
"name": "session_id",
|
||||||
|
"value": "={{ $('Webhook').item.json.body.session_id }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "claim_id",
|
||||||
|
"name": "claim_id",
|
||||||
|
"value": "={{ $('Webhook').item.json.body.claim_id }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "unified_id",
|
||||||
|
"name": "unified_id",
|
||||||
|
"value": "={{ $('Webhook').item.json.body.unified_id }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "contact_id",
|
||||||
|
"name": "contact_id",
|
||||||
|
"value": "={{ $('Webhook').item.json.body.contact_id }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "phone",
|
||||||
|
"name": "phone",
|
||||||
|
"value": "={{ $('Webhook').item.json.body.phone }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wizard_plan",
|
||||||
|
"name": "wizard_plan",
|
||||||
|
"value": "={{ $('Webhook').item.json.body.wizard_plan }}",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wizard_answers",
|
||||||
|
"name": "wizard_answers",
|
||||||
|
"value": "={{ $('Webhook').item.json.body.wizard_answers }}",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "type_code",
|
||||||
|
"name": "type_code",
|
||||||
|
"value": "={{ $('Webhook').item.json.body.type_code || 'consumer' }}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "prepare_payload_no_files",
|
||||||
|
"description": "Формируем payload для PostgreSQL",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [-80, 400],
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "payload_partial_json",
|
||||||
|
"name": "payload_partial_json",
|
||||||
|
"value": "={{ {\n session_id: $json.session_id,\n unified_id: $json.unified_id,\n contact_id: $json.contact_id,\n phone: $json.phone,\n type_code: $json.type_code,\n wizard_plan: $json.wizard_plan,\n wizard_answers: $json.wizard_answers,\n documents_meta: []\n} }}",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "claim_id",
|
||||||
|
"name": "claim_id",
|
||||||
|
"value": "={{ $json.claim_id }}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "save_claim_no_files",
|
||||||
|
"description": "Сохраняем claim без файлов в PostgreSQL",
|
||||||
|
"type": "n8n-nodes-base.postgres",
|
||||||
|
"typeVersion": 2.6,
|
||||||
|
"position": [180, 400],
|
||||||
|
"parameters": {
|
||||||
|
"operation": "executeQuery",
|
||||||
|
"query": "WITH partial AS (\n SELECT \n $1::jsonb AS p, \n $2::text AS claim_id_str\n),\n\n-- Парсим wizard_answers\nwizard_answers_parsed AS (\n SELECT \n CASE \n WHEN partial.p->>'wizard_answers' IS NOT NULL \n THEN (partial.p->>'wizard_answers')::jsonb\n WHEN partial.p->'wizard_answers' IS NOT NULL \n AND jsonb_typeof(partial.p->'wizard_answers') = 'object'\n THEN partial.p->'wizard_answers'\n ELSE '{}'::jsonb\n END AS answers\n FROM partial\n),\n\n-- Парсим wizard_plan\nwizard_plan_parsed AS (\n SELECT \n CASE \n WHEN partial.p->>'wizard_plan' IS NOT NULL \n THEN (partial.p->>'wizard_plan')::jsonb\n WHEN partial.p->'wizard_plan' IS NOT NULL \n AND jsonb_typeof(partial.p->'wizard_plan') = 'object'\n THEN partial.p->'wizard_plan'\n ELSE NULL\n END AS wizard_plan\n FROM partial\n),\n\n-- UPSERT claim\nclaim_upsert AS (\n INSERT INTO clpr_claims (\n id,\n session_token,\n unified_id,\n contact_id,\n phone,\n channel,\n type_code,\n status_code,\n payload,\n created_at,\n updated_at,\n expires_at\n )\n SELECT \n partial.claim_id_str::uuid,\n COALESCE(partial.p->>'session_id', 'sess-unknown'),\n partial.p->>'unified_id',\n partial.p->>'contact_id',\n partial.p->>'phone',\n 'web_form',\n COALESCE(partial.p->>'type_code', 'consumer'),\n 'draft',\n jsonb_build_object(\n 'claim_id', partial.claim_id_str,\n 'answers', (SELECT answers FROM wizard_answers_parsed),\n 'documents_meta', '[]'::jsonb,\n 'wizard_plan', (SELECT wizard_plan FROM wizard_plan_parsed)\n ),\n COALESCE(\n (SELECT created_at FROM clpr_claims WHERE id = partial.claim_id_str::uuid),\n now()\n ),\n now(),\n now() + interval '14 days'\n FROM partial\n ON CONFLICT (id) DO UPDATE SET\n session_token = EXCLUDED.session_token,\n unified_id = COALESCE(EXCLUDED.unified_id, clpr_claims.unified_id),\n contact_id = COALESCE(EXCLUDED.contact_id, clpr_claims.contact_id),\n phone = COALESCE(EXCLUDED.phone, clpr_claims.phone),\n status_code = 'draft',\n payload = (\n clpr_claims.payload \n - 'answers' \n - 'documents_meta' \n - 'wizard_plan' \n - 'claim_id'\n ) || EXCLUDED.payload,\n updated_at = now(),\n expires_at = now() + interval '14 days'\n RETURNING id, status_code, payload, unified_id, contact_id, phone, session_token\n)\n\nSELECT\n (SELECT jsonb_build_object(\n 'claim_id', cu.id::text,\n 'claim_id_str', (cu.payload->>'claim_id'),\n 'status_code', cu.status_code,\n 'unified_id', cu.unified_id,\n 'contact_id', cu.contact_id,\n 'phone', cu.phone,\n 'session_token', cu.session_token,\n 'payload', cu.payload\n ) FROM claim_upsert cu) AS claim;",
|
||||||
|
"options": {
|
||||||
|
"queryReplacement": "={{ $json.payload_partial_json }}, {{ $json.claim_id }}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"credentials": {
|
||||||
|
"postgres": {
|
||||||
|
"id": "sGJ0fJhU8rz88w3k",
|
||||||
|
"name": "timeweb_bd"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "prepare_redis_event_no_files",
|
||||||
|
"description": "Готовим событие для публикации в Redis",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [440, 400],
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "redis_key",
|
||||||
|
"name": "redis_key",
|
||||||
|
"value": "=ocr_events:{{ $('extract_webhook_data_no_files').item.json.session_id }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "redis_value",
|
||||||
|
"name": "redis_value",
|
||||||
|
"value": "={{ {\n event_type: 'form_saved',\n claim_id: $json.claim.claim_id,\n status_code: $json.claim.status_code,\n unified_id: $json.claim.unified_id,\n contact_id: $json.claim.contact_id,\n phone: $json.claim.phone,\n session_token: $json.claim.session_token,\n has_files: false,\n timestamp: new Date().toISOString()\n} }}",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "publish_to_redis_no_files",
|
||||||
|
"description": "Публикуем событие в Redis",
|
||||||
|
"type": "n8n-nodes-base.redis",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [700, 400],
|
||||||
|
"parameters": {
|
||||||
|
"operation": "publish",
|
||||||
|
"channel": "={{ $json.redis_key }}",
|
||||||
|
"value": "={{ JSON.stringify($json.redis_value) }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"credentials": {
|
||||||
|
"redis": {
|
||||||
|
"id": "RKICQB2ZaisVK4WS",
|
||||||
|
"name": "Local Redis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "respond_no_files",
|
||||||
|
"description": "Возвращаем ответ клиенту",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"position": [960, 400],
|
||||||
|
"parameters": {
|
||||||
|
"options": {
|
||||||
|
"responseCode": 200
|
||||||
|
},
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ {\n success: true,\n claim_id: $('save_claim_no_files').item.json.claim.claim_id,\n status_code: $('save_claim_no_files').item.json.claim.status_code,\n has_files: false,\n message: 'Заявка сохранена без файлов'\n} }}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"проверка наличия файлов": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "extract_webhook_data_no_files",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "set_token1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extract_webhook_data_no_files": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "prepare_payload_no_files",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prepare_payload_no_files": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "save_claim_no_files",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"save_claim_no_files": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "prepare_redis_event_no_files",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prepare_redis_event_no_files": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "publish_to_redis_no_files",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"publish_to_redis_no_files": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "respond_no_files",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"step1": "Открыть workflow 'form_get' (ID: 8ZVMTsuH7Cmw7snw) в n8n",
|
||||||
|
"step2": "Найти IF-ноду 'проверка наличия файлов' (ID: b7497f29-dab3-41cd-aaa3-a43ee83e607c)",
|
||||||
|
"step3": "TRUE ветка (index 0) сейчас пустая - туда нужно добавить новые ноды",
|
||||||
|
"step4": "Импортировать ноды из этого JSON или создать вручную по схеме",
|
||||||
|
"step5": "Подключить TRUE ветку IF к первой ноде: extract_webhook_data_no_files",
|
||||||
|
"step6": "Сохранить и активировать workflow",
|
||||||
|
"step7": "Протестировать отправку формы БЕЗ файлов"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
401
docs/N8N_FORM_GET_NO_FILES_INSTRUCTIONS.md
Normal file
401
docs/N8N_FORM_GET_NO_FILES_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
# Инструкция: Добавление обработки формы БЕЗ файлов в workflow form_get
|
||||||
|
|
||||||
|
**Дата:** 2025-11-21
|
||||||
|
**Workflow ID:** `8ZVMTsuH7Cmw7snw`
|
||||||
|
**Workflow Name:** `form_get`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Цель
|
||||||
|
|
||||||
|
Добавить ветку обработки для случая, когда форма отправляется **без файлов** (`has_files === false`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📍 Где добавлять
|
||||||
|
|
||||||
|
В workflow `form_get` есть IF-нода **"проверка наличия файлов"** (ID: `b7497f29-dab3-41cd-aaa3-a43ee83e607c`):
|
||||||
|
|
||||||
|
- **TRUE ветка** (index 0) — файлов НЕТ → **ПУСТАЯ** ❌
|
||||||
|
- **FALSE ветка** (index 1) — файлы ЕСТЬ → существующий flow ✅
|
||||||
|
|
||||||
|
**Задача:** Добавить ноды в TRUE ветку.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Пошаговая инструкция
|
||||||
|
|
||||||
|
### Шаг 1: Открыть workflow
|
||||||
|
|
||||||
|
1. Перейти в n8n: https://n8n.clientright.pro
|
||||||
|
2. Открыть workflow **"form_get"**
|
||||||
|
3. Найти ноду **"проверка наличия файлов"** (IF)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Шаг 2: Добавить ноду #1 - Extract Data
|
||||||
|
|
||||||
|
**Название:** `extract_webhook_data_no_files`
|
||||||
|
**Тип:** `Edit Fields` (Set)
|
||||||
|
**Позиция:** справа от IF-ноды, выше основного flow
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
|
||||||
|
| Field Name | Type | Value |
|
||||||
|
|------------|------|-------|
|
||||||
|
| `session_id` | String | `={{ $('Webhook').item.json.body.session_id }}` |
|
||||||
|
| `claim_id` | String | `={{ $('Webhook').item.json.body.claim_id }}` |
|
||||||
|
| `unified_id` | String | `={{ $('Webhook').item.json.body.unified_id }}` |
|
||||||
|
| `contact_id` | String | `={{ $('Webhook').item.json.body.contact_id }}` |
|
||||||
|
| `phone` | String | `={{ $('Webhook').item.json.body.phone }}` |
|
||||||
|
| `wizard_plan` | Object | `={{ $('Webhook').item.json.body.wizard_plan }}` |
|
||||||
|
| `wizard_answers` | Object | `={{ $('Webhook').item.json.body.wizard_answers }}` |
|
||||||
|
| `type_code` | String | `={{ $('Webhook').item.json.body.type_code || 'consumer' }}` |
|
||||||
|
|
||||||
|
**Подключение:**
|
||||||
|
- Из ноды **"проверка наличия файлов"** → TRUE (верхний выход)
|
||||||
|
- К ноде **"extract_webhook_data_no_files"**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Шаг 3: Добавить ноду #2 - Prepare Payload
|
||||||
|
|
||||||
|
**Название:** `prepare_payload_no_files`
|
||||||
|
**Тип:** `Edit Fields` (Set)
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
|
||||||
|
| Field Name | Type | Value |
|
||||||
|
|------------|------|-------|
|
||||||
|
| `payload_partial_json` | Object | См. ниже ⬇️ |
|
||||||
|
| `claim_id` | String | `={{ $json.claim_id }}` |
|
||||||
|
|
||||||
|
**Значение для `payload_partial_json`:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
={{ {
|
||||||
|
session_id: $json.session_id,
|
||||||
|
unified_id: $json.unified_id,
|
||||||
|
contact_id: $json.contact_id,
|
||||||
|
phone: $json.phone,
|
||||||
|
type_code: $json.type_code,
|
||||||
|
wizard_plan: $json.wizard_plan,
|
||||||
|
wizard_answers: $json.wizard_answers,
|
||||||
|
documents_meta: []
|
||||||
|
} }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Подключение:**
|
||||||
|
- Из ноды **"extract_webhook_data_no_files"**
|
||||||
|
- К ноде **"prepare_payload_no_files"**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Шаг 4: Добавить ноду #3 - Save to PostgreSQL
|
||||||
|
|
||||||
|
**Название:** `save_claim_no_files`
|
||||||
|
**Тип:** `Postgres`
|
||||||
|
**Operation:** `Execute Query`
|
||||||
|
|
||||||
|
**Credentials:** `timeweb_bd` (существующие)
|
||||||
|
|
||||||
|
**Query:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
WITH partial AS (
|
||||||
|
SELECT
|
||||||
|
$1::jsonb AS p,
|
||||||
|
$2::text AS claim_id_str
|
||||||
|
),
|
||||||
|
|
||||||
|
wizard_answers_parsed AS (
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN partial.p->>'wizard_answers' IS NOT NULL
|
||||||
|
THEN (partial.p->>'wizard_answers')::jsonb
|
||||||
|
WHEN partial.p->'wizard_answers' IS NOT NULL
|
||||||
|
AND jsonb_typeof(partial.p->'wizard_answers') = 'object'
|
||||||
|
THEN partial.p->'wizard_answers'
|
||||||
|
ELSE '{}'::jsonb
|
||||||
|
END AS answers
|
||||||
|
FROM partial
|
||||||
|
),
|
||||||
|
|
||||||
|
wizard_plan_parsed AS (
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN partial.p->>'wizard_plan' IS NOT NULL
|
||||||
|
THEN (partial.p->>'wizard_plan')::jsonb
|
||||||
|
WHEN partial.p->'wizard_plan' IS NOT NULL
|
||||||
|
AND jsonb_typeof(partial.p->'wizard_plan') = 'object'
|
||||||
|
THEN partial.p->'wizard_plan'
|
||||||
|
ELSE NULL
|
||||||
|
END AS wizard_plan
|
||||||
|
FROM partial
|
||||||
|
),
|
||||||
|
|
||||||
|
claim_upsert AS (
|
||||||
|
INSERT INTO clpr_claims (
|
||||||
|
id,
|
||||||
|
session_token,
|
||||||
|
unified_id,
|
||||||
|
contact_id,
|
||||||
|
phone,
|
||||||
|
channel,
|
||||||
|
type_code,
|
||||||
|
status_code,
|
||||||
|
payload,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
expires_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
partial.claim_id_str::uuid,
|
||||||
|
COALESCE(partial.p->>'session_id', 'sess-unknown'),
|
||||||
|
partial.p->>'unified_id',
|
||||||
|
partial.p->>'contact_id',
|
||||||
|
partial.p->>'phone',
|
||||||
|
'web_form',
|
||||||
|
COALESCE(partial.p->>'type_code', 'consumer'),
|
||||||
|
'draft',
|
||||||
|
jsonb_build_object(
|
||||||
|
'claim_id', partial.claim_id_str,
|
||||||
|
'answers', (SELECT answers FROM wizard_answers_parsed),
|
||||||
|
'documents_meta', '[]'::jsonb,
|
||||||
|
'wizard_plan', (SELECT wizard_plan FROM wizard_plan_parsed)
|
||||||
|
),
|
||||||
|
COALESCE(
|
||||||
|
(SELECT created_at FROM clpr_claims WHERE id = partial.claim_id_str::uuid),
|
||||||
|
now()
|
||||||
|
),
|
||||||
|
now(),
|
||||||
|
now() + interval '14 days'
|
||||||
|
FROM partial
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
session_token = EXCLUDED.session_token,
|
||||||
|
unified_id = COALESCE(EXCLUDED.unified_id, clpr_claims.unified_id),
|
||||||
|
contact_id = COALESCE(EXCLUDED.contact_id, clpr_claims.contact_id),
|
||||||
|
phone = COALESCE(EXCLUDED.phone, clpr_claims.phone),
|
||||||
|
status_code = 'draft',
|
||||||
|
payload = (
|
||||||
|
clpr_claims.payload
|
||||||
|
- 'answers'
|
||||||
|
- 'documents_meta'
|
||||||
|
- 'wizard_plan'
|
||||||
|
- 'claim_id'
|
||||||
|
) || EXCLUDED.payload,
|
||||||
|
updated_at = now(),
|
||||||
|
expires_at = now() + interval '14 days'
|
||||||
|
RETURNING id, status_code, payload, unified_id, contact_id, phone, session_token
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
(SELECT jsonb_build_object(
|
||||||
|
'claim_id', cu.id::text,
|
||||||
|
'claim_id_str', (cu.payload->>'claim_id'),
|
||||||
|
'status_code', cu.status_code,
|
||||||
|
'unified_id', cu.unified_id,
|
||||||
|
'contact_id', cu.contact_id,
|
||||||
|
'phone', cu.phone,
|
||||||
|
'session_token', cu.session_token,
|
||||||
|
'payload', cu.payload
|
||||||
|
) FROM claim_upsert cu) AS claim;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Replacement:** `={{ $json.payload_partial_json }}, {{ $json.claim_id }}`
|
||||||
|
|
||||||
|
**Подключение:**
|
||||||
|
- Из ноды **"prepare_payload_no_files"**
|
||||||
|
- К ноде **"save_claim_no_files"**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Шаг 5: Добавить ноду #4 - Prepare Redis Event
|
||||||
|
|
||||||
|
**Название:** `prepare_redis_event_no_files`
|
||||||
|
**Тип:** `Edit Fields` (Set)
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
|
||||||
|
| Field Name | Type | Value |
|
||||||
|
|------------|------|-------|
|
||||||
|
| `redis_key` | String | `=ocr_events:{{ $('extract_webhook_data_no_files').item.json.session_id }}` |
|
||||||
|
| `redis_value` | Object | См. ниже ⬇️ |
|
||||||
|
|
||||||
|
**Значение для `redis_value`:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
={{ {
|
||||||
|
event_type: 'form_saved_no_files',
|
||||||
|
claim_id: $json.claim.claim_id,
|
||||||
|
status_code: $json.claim.status_code,
|
||||||
|
unified_id: $json.claim.unified_id,
|
||||||
|
contact_id: $json.claim.contact_id,
|
||||||
|
phone: $json.claim.phone,
|
||||||
|
session_token: $json.claim.session_token,
|
||||||
|
has_files: false,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
} }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Подключение:**
|
||||||
|
- Из ноды **"save_claim_no_files"**
|
||||||
|
- К ноде **"prepare_redis_event_no_files"**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Шаг 6: Добавить ноду #5 - Publish to Redis
|
||||||
|
|
||||||
|
**Название:** `publish_to_redis_no_files`
|
||||||
|
**Тип:** `Redis`
|
||||||
|
**Operation:** `Publish`
|
||||||
|
|
||||||
|
**Credentials:** `Local Redis` (существующие)
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
|
||||||
|
| Parameter | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| Channel | `={{ $json.redis_key }}` |
|
||||||
|
| Value | `={{ JSON.stringify($json.redis_value) }}` |
|
||||||
|
|
||||||
|
**Подключение:**
|
||||||
|
- Из ноды **"prepare_redis_event_no_files"**
|
||||||
|
- К ноде **"publish_to_redis_no_files"**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Шаг 7: Добавить ноду #6 - Respond to Webhook
|
||||||
|
|
||||||
|
**Название:** `respond_no_files`
|
||||||
|
**Тип:** `Respond to Webhook`
|
||||||
|
**Response Code:** `200`
|
||||||
|
|
||||||
|
**Response Body:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
={{ {
|
||||||
|
success: true,
|
||||||
|
claim_id: $('save_claim_no_files').item.json.claim.claim_id,
|
||||||
|
status_code: $('save_claim_no_files').item.json.claim.status_code,
|
||||||
|
has_files: false,
|
||||||
|
message: 'Заявка сохранена без файлов'
|
||||||
|
} }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Подключение:**
|
||||||
|
- Из ноды **"publish_to_redis_no_files"**
|
||||||
|
- К ноде **"respond_no_files"** (финальная нода)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Финальная структура workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
Webhook → Code17 (парсинг файлов)
|
||||||
|
↓
|
||||||
|
IF "проверка наличия файлов"
|
||||||
|
│
|
||||||
|
├─ TRUE (файлов НЕТ) → [НОВАЯ ВЕТКА]
|
||||||
|
│ ↓
|
||||||
|
│ extract_webhook_data_no_files
|
||||||
|
│ ↓
|
||||||
|
│ prepare_payload_no_files
|
||||||
|
│ ↓
|
||||||
|
│ save_claim_no_files (PostgreSQL)
|
||||||
|
│ ↓
|
||||||
|
│ prepare_redis_event_no_files
|
||||||
|
│ ↓
|
||||||
|
│ publish_to_redis_no_files
|
||||||
|
│ ↓
|
||||||
|
│ respond_no_files
|
||||||
|
│
|
||||||
|
└─ FALSE (файлы ЕСТЬ) → существующий flow
|
||||||
|
↓
|
||||||
|
set_token1 → get_data1 → Upload → ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Проверка
|
||||||
|
|
||||||
|
После добавления нод:
|
||||||
|
|
||||||
|
1. **Сохранить workflow** (Ctrl+S)
|
||||||
|
2. **Активировать workflow** (если не активен)
|
||||||
|
3. **Протестировать:**
|
||||||
|
- Отправить форму БЕЗ файлов
|
||||||
|
- Проверить, что заявка сохранилась в PostgreSQL
|
||||||
|
- Проверить событие в Redis: `redis-cli GET "ocr_events:sess-xxx"`
|
||||||
|
- Проверить ответ webhook: `success: true, has_files: false`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Ожидаемый результат
|
||||||
|
|
||||||
|
**Вход (webhook body):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "sess_xxx",
|
||||||
|
"claim_id": "uuid",
|
||||||
|
"unified_id": "usr_xxx",
|
||||||
|
"contact_id": "12345",
|
||||||
|
"phone": "79262306381",
|
||||||
|
"wizard_answers": {"q1": "answer1"},
|
||||||
|
"wizard_plan": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Выход (claim в PostgreSQL):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"claim": {
|
||||||
|
"claim_id": "uuid",
|
||||||
|
"status_code": "draft",
|
||||||
|
"unified_id": "usr_xxx",
|
||||||
|
"contact_id": "12345",
|
||||||
|
"phone": "79262306381",
|
||||||
|
"session_token": "sess_xxx",
|
||||||
|
"payload": {
|
||||||
|
"claim_id": "uuid",
|
||||||
|
"answers": {"q1": "answer1"},
|
||||||
|
"documents_meta": [],
|
||||||
|
"wizard_plan": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Redis событие:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "form_saved_no_files",
|
||||||
|
"claim_id": "uuid",
|
||||||
|
"status_code": "draft",
|
||||||
|
"has_files": false,
|
||||||
|
"timestamp": "2025-11-21T15:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
|
### Проблема 1: "session_token": "sess-unknown"
|
||||||
|
**Причина:** В payload не передан `session_id`
|
||||||
|
**Решение:** Проверить, что фронтенд отправляет `session_id` в body
|
||||||
|
|
||||||
|
### Проблема 2: "contact_id": null
|
||||||
|
**Причина:** Поле не извлекается из webhook
|
||||||
|
**Решение:** Проверить путь в expression: `$('Webhook').item.json.body.contact_id`
|
||||||
|
|
||||||
|
### Проблема 3: Ошибка PostgreSQL
|
||||||
|
**Причина:** Неправильный формат данных
|
||||||
|
**Решение:** Проверить логи n8n и формат `payload_partial_json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Автор:** AI Assistant
|
||||||
|
**Дата:** 2025-11-21
|
||||||
|
**Статус:** Готово к внедрению ✅
|
||||||
|
|
||||||
@@ -92,3 +92,4 @@ updateFormData({
|
|||||||
4. **PostgreSQL: Find or Create User** → выполняет SQL запрос → возвращает `unified_id`
|
4. **PostgreSQL: Find or Create User** → выполняет SQL запрос → возвращает `unified_id`
|
||||||
5. **Response** → возвращает полный ответ с `unified_id`
|
5. **Response** → возвращает полный ответ с `unified_id`
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -142,3 +142,4 @@ return {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -131,3 +131,4 @@ WHERE ua.channel = 'web_form'
|
|||||||
|
|
||||||
Должна быть запись с `unified_id` в формате `usr_...`.
|
Должна быть запись с `unified_id` в формате `usr_...`.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -429,3 +429,4 @@ return claim;
|
|||||||
- ✅ Возобновление заполнения формы
|
- ✅ Возобновление заполнения формы
|
||||||
- ✅ Быстрая загрузка состояния формы
|
- ✅ Быстрая загрузка состояния формы
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -189,3 +189,4 @@ if (channel === 'telegram') {
|
|||||||
|
|
||||||
Все данные уже в PostgreSQL, и этого достаточно. Redis используется только для Pub/Sub событий (`ocr_events:{claim_id}`).
|
Все данные уже в PostgreSQL, и этого достаточно. Redis используется только для Pub/Sub событий (`ocr_events:{claim_id}`).
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -196,3 +196,4 @@ if (channel === 'web_form' && enable_cache === true) {
|
|||||||
|
|
||||||
Но это опционально и не обязательно для веб-формы.
|
Но это опционально и не обязательно для веб-формы.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -70,3 +70,4 @@
|
|||||||
3. Если API вызывается, но возвращает 0 - проверить SQL запрос в backend
|
3. Если API вызывается, но возвращает 0 - проверить SQL запрос в backend
|
||||||
4. Если SQL работает, но asyncpg не возвращает данные - проверить формат параметров
|
4. Если SQL работает, но asyncpg не возвращает данные - проверить формат параметров
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -112,3 +112,4 @@ final_claim_id = row.get('claim_id') or claim_id_from_payload
|
|||||||
2. Протестировать загрузку черновика из Telegram формата
|
2. Протестировать загрузку черновика из Telegram формата
|
||||||
3. Убедиться, что все данные корректно восстанавливаются в форму
|
3. Убедиться, что все данные корректно восстанавливаются в форму
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
55
docs/SQL_ALTER_CLPR_CLAIMS_ADD_FIELDS.sql
Normal file
55
docs/SQL_ALTER_CLPR_CLAIMS_ADD_FIELDS.sql
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
-- Миграция для добавления полей unified_id, contact_id, phone в таблицу clpr_claims
|
||||||
|
-- Дата: 2025-11-21
|
||||||
|
-- Описание: Добавляем поля для хранения идентификаторов пользователя напрямую в таблице заявок
|
||||||
|
|
||||||
|
-- 1. Добавляем unified_id (уникальный идентификатор пользователя из CRM)
|
||||||
|
ALTER TABLE clpr_claims
|
||||||
|
ADD COLUMN IF NOT EXISTS unified_id TEXT;
|
||||||
|
|
||||||
|
-- 2. Добавляем contact_id (ID контакта в RetailCRM)
|
||||||
|
ALTER TABLE clpr_claims
|
||||||
|
ADD COLUMN IF NOT EXISTS contact_id TEXT;
|
||||||
|
|
||||||
|
-- 3. Добавляем phone (номер телефона пользователя)
|
||||||
|
ALTER TABLE clpr_claims
|
||||||
|
ADD COLUMN IF NOT EXISTS phone TEXT;
|
||||||
|
|
||||||
|
-- 4. Создаём индексы для быстрого поиска
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_clpr_claims_unified_id
|
||||||
|
ON clpr_claims(unified_id)
|
||||||
|
WHERE unified_id IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_clpr_claims_contact_id
|
||||||
|
ON clpr_claims(contact_id)
|
||||||
|
WHERE contact_id IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_clpr_claims_phone
|
||||||
|
ON clpr_claims(phone)
|
||||||
|
WHERE phone IS NOT NULL;
|
||||||
|
|
||||||
|
-- 5. Комментарии к полям
|
||||||
|
COMMENT ON COLUMN clpr_claims.unified_id IS 'Уникальный идентификатор пользователя из CRM (формат: usr_xxx)';
|
||||||
|
COMMENT ON COLUMN clpr_claims.contact_id IS 'ID контакта в RetailCRM';
|
||||||
|
COMMENT ON COLUMN clpr_claims.phone IS 'Номер телефона пользователя (формат: 79262306381)';
|
||||||
|
|
||||||
|
-- 6. Опционально: заполняем существующие записи из payload (если есть)
|
||||||
|
UPDATE clpr_claims
|
||||||
|
SET
|
||||||
|
unified_id = payload->>'unified_id',
|
||||||
|
contact_id = payload->>'contact_id',
|
||||||
|
phone = payload->>'phone'
|
||||||
|
WHERE unified_id IS NULL
|
||||||
|
AND (
|
||||||
|
payload->>'unified_id' IS NOT NULL
|
||||||
|
OR payload->>'contact_id' IS NOT NULL
|
||||||
|
OR payload->>'phone' IS NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Проверка результата
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_claims,
|
||||||
|
COUNT(unified_id) as with_unified_id,
|
||||||
|
COUNT(contact_id) as with_contact_id,
|
||||||
|
COUNT(phone) as with_phone
|
||||||
|
FROM clpr_claims;
|
||||||
|
|
||||||
227
docs/SQL_CLAIMSAVE_UPSERT_SIMPLE.sql
Normal file
227
docs/SQL_CLAIMSAVE_UPSERT_SIMPLE.sql
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
-- Упрощённый UPSERT для сохранения claim с известным claim_id
|
||||||
|
-- Используется в n8n workflow: form_get (нода claimsave)
|
||||||
|
-- Дата: 2025-11-21
|
||||||
|
-- Описание: Простой INSERT/UPDATE для claim, т.к. claim_id уже известен
|
||||||
|
|
||||||
|
-- Входные параметры:
|
||||||
|
-- $1: payload_partial_json (jsonb) - данные формы с wizard_answers, wizard_plan, documents_meta
|
||||||
|
-- $2: claim_id (text) - UUID заявки
|
||||||
|
|
||||||
|
WITH partial AS (
|
||||||
|
SELECT
|
||||||
|
$1::jsonb AS p,
|
||||||
|
$2::text AS claim_id_str
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Парсим wizard_answers
|
||||||
|
wizard_answers_parsed AS (
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN partial.p->>'wizard_answers' IS NOT NULL
|
||||||
|
THEN (partial.p->>'wizard_answers')::jsonb
|
||||||
|
WHEN partial.p->'wizard_answers' IS NOT NULL
|
||||||
|
AND jsonb_typeof(partial.p->'wizard_answers') = 'object'
|
||||||
|
THEN partial.p->'wizard_answers'
|
||||||
|
ELSE '{}'::jsonb
|
||||||
|
END AS answers
|
||||||
|
FROM partial
|
||||||
|
),
|
||||||
|
|
||||||
|
-- Парсим wizard_plan
|
||||||
|
wizard_plan_parsed AS (
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN partial.p->>'wizard_plan' IS NOT NULL
|
||||||
|
THEN (partial.p->>'wizard_plan')::jsonb
|
||||||
|
WHEN partial.p->'wizard_plan' IS NOT NULL
|
||||||
|
AND jsonb_typeof(partial.p->'wizard_plan') = 'object'
|
||||||
|
THEN partial.p->'wizard_plan'
|
||||||
|
ELSE NULL
|
||||||
|
END AS wizard_plan
|
||||||
|
FROM partial
|
||||||
|
),
|
||||||
|
|
||||||
|
-- UPSERT claim
|
||||||
|
claim_upsert AS (
|
||||||
|
INSERT INTO clpr_claims (
|
||||||
|
id,
|
||||||
|
session_token,
|
||||||
|
unified_id,
|
||||||
|
contact_id,
|
||||||
|
phone,
|
||||||
|
channel,
|
||||||
|
type_code,
|
||||||
|
status_code,
|
||||||
|
payload,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
expires_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
partial.claim_id_str::uuid,
|
||||||
|
COALESCE(partial.p->>'session_id', 'sess-unknown'),
|
||||||
|
partial.p->>'unified_id',
|
||||||
|
partial.p->>'contact_id',
|
||||||
|
partial.p->>'phone',
|
||||||
|
'web_form',
|
||||||
|
COALESCE(partial.p->>'type_code', 'consumer'),
|
||||||
|
CASE
|
||||||
|
WHEN (SELECT answers->>'docs_exist' FROM wizard_answers_parsed) = 'true'
|
||||||
|
THEN 'in_work'
|
||||||
|
ELSE 'draft'
|
||||||
|
END,
|
||||||
|
jsonb_build_object(
|
||||||
|
'claim_id', partial.claim_id_str,
|
||||||
|
'answers', (SELECT answers FROM wizard_answers_parsed),
|
||||||
|
'documents_meta', COALESCE(partial.p->'documents_meta', '[]'::jsonb),
|
||||||
|
'wizard_plan', (SELECT wizard_plan FROM wizard_plan_parsed)
|
||||||
|
),
|
||||||
|
COALESCE(
|
||||||
|
(SELECT created_at FROM clpr_claims WHERE id = partial.claim_id_str::uuid),
|
||||||
|
now()
|
||||||
|
),
|
||||||
|
now(),
|
||||||
|
now() + interval '14 days'
|
||||||
|
FROM partial
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
session_token = EXCLUDED.session_token,
|
||||||
|
unified_id = COALESCE(EXCLUDED.unified_id, clpr_claims.unified_id),
|
||||||
|
contact_id = COALESCE(EXCLUDED.contact_id, clpr_claims.contact_id),
|
||||||
|
phone = COALESCE(EXCLUDED.phone, clpr_claims.phone),
|
||||||
|
status_code = EXCLUDED.status_code,
|
||||||
|
payload = (
|
||||||
|
-- Сохраняем старые поля, которых нет в новом payload
|
||||||
|
clpr_claims.payload
|
||||||
|
- 'answers'
|
||||||
|
- 'documents_meta'
|
||||||
|
- 'wizard_plan'
|
||||||
|
- 'claim_id'
|
||||||
|
) || EXCLUDED.payload,
|
||||||
|
updated_at = now(),
|
||||||
|
expires_at = now() + interval '14 days'
|
||||||
|
RETURNING id, status_code, payload, unified_id, contact_id, phone, session_token
|
||||||
|
),
|
||||||
|
|
||||||
|
-- UPSERT documents (если есть)
|
||||||
|
docs_upsert AS (
|
||||||
|
INSERT INTO clpr_claim_documents (
|
||||||
|
claim_id,
|
||||||
|
field_name,
|
||||||
|
file_id,
|
||||||
|
uploaded_at,
|
||||||
|
file_name,
|
||||||
|
original_file_name
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
partial.claim_id_str AS claim_id,
|
||||||
|
doc.field_name,
|
||||||
|
doc.file_id,
|
||||||
|
COALESCE((doc.uploaded_at)::timestamptz, now()),
|
||||||
|
doc.file_name,
|
||||||
|
doc.original_file_name
|
||||||
|
FROM partial
|
||||||
|
CROSS JOIN LATERAL jsonb_to_recordset(
|
||||||
|
COALESCE(partial.p->'documents_meta', '[]'::jsonb)
|
||||||
|
) AS doc(
|
||||||
|
field_name text,
|
||||||
|
file_id text,
|
||||||
|
file_name text,
|
||||||
|
original_file_name text,
|
||||||
|
uploaded_at text
|
||||||
|
)
|
||||||
|
WHERE partial.p->'documents_meta' IS NOT NULL
|
||||||
|
AND jsonb_array_length(partial.p->'documents_meta') > 0
|
||||||
|
ON CONFLICT (claim_id, field_name) DO UPDATE SET
|
||||||
|
file_id = EXCLUDED.file_id,
|
||||||
|
uploaded_at = EXCLUDED.uploaded_at,
|
||||||
|
file_name = EXCLUDED.file_name,
|
||||||
|
original_file_name = EXCLUDED.original_file_name
|
||||||
|
RETURNING id, claim_id, field_name, file_id, file_name, original_file_name
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Возвращаем результат
|
||||||
|
SELECT
|
||||||
|
(SELECT jsonb_build_object(
|
||||||
|
'claim_id', cu.id::text,
|
||||||
|
'claim_id_str', (cu.payload->>'claim_id'),
|
||||||
|
'status_code', cu.status_code,
|
||||||
|
'unified_id', cu.unified_id,
|
||||||
|
'contact_id', cu.contact_id,
|
||||||
|
'phone', cu.phone,
|
||||||
|
'session_token', cu.session_token,
|
||||||
|
'payload', cu.payload
|
||||||
|
) FROM claim_upsert cu) AS claim,
|
||||||
|
|
||||||
|
(SELECT jsonb_agg(jsonb_build_object(
|
||||||
|
'id', id,
|
||||||
|
'field_name', field_name,
|
||||||
|
'file_id', file_id,
|
||||||
|
'file_name', file_name,
|
||||||
|
'original_file_name', original_file_name
|
||||||
|
)) FROM docs_upsert) AS documents;
|
||||||
|
|
||||||
|
/*
|
||||||
|
ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
|
||||||
|
|
||||||
|
1. Вызов с wizard_answers и wizard_plan:
|
||||||
|
|
||||||
|
SELECT * FROM ... WHERE ... = (
|
||||||
|
'{
|
||||||
|
"session_id": "sess_xxx",
|
||||||
|
"unified_id": "usr_xxx",
|
||||||
|
"contact_id": "12345",
|
||||||
|
"phone": "79262306381",
|
||||||
|
"wizard_answers": "{\\"q1\\": \\"answer1\\"}",
|
||||||
|
"wizard_plan": "{\\"questions\\": [...]}",
|
||||||
|
"documents_meta": [
|
||||||
|
{
|
||||||
|
"field_name": "uploads[0][0]",
|
||||||
|
"file_id": "clientright/0/file.pdf",
|
||||||
|
"file_name": "file.pdf",
|
||||||
|
"original_file_name": "original.pdf",
|
||||||
|
"uploaded_at": "2025-11-21T12:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'::jsonb,
|
||||||
|
'uuid-here'::text
|
||||||
|
);
|
||||||
|
|
||||||
|
2. Вызов БЕЗ файлов (только answers):
|
||||||
|
|
||||||
|
SELECT * FROM ... WHERE ... = (
|
||||||
|
'{
|
||||||
|
"session_id": "sess_xxx",
|
||||||
|
"unified_id": "usr_xxx",
|
||||||
|
"contact_id": "12345",
|
||||||
|
"phone": "79262306381",
|
||||||
|
"wizard_answers": "{\\"q1\\": \\"answer1\\"}",
|
||||||
|
"wizard_plan": null,
|
||||||
|
"documents_meta": []
|
||||||
|
}'::jsonb,
|
||||||
|
'uuid-here'::text
|
||||||
|
);
|
||||||
|
|
||||||
|
РЕЗУЛЬТАТ:
|
||||||
|
{
|
||||||
|
"claim": {
|
||||||
|
"claim_id": "uuid",
|
||||||
|
"claim_id_str": "uuid",
|
||||||
|
"status_code": "draft" or "in_work",
|
||||||
|
"unified_id": "usr_xxx",
|
||||||
|
"contact_id": "12345",
|
||||||
|
"phone": "79262306381",
|
||||||
|
"session_token": "sess_xxx",
|
||||||
|
"payload": {...}
|
||||||
|
},
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"field_name": "uploads[0][0]",
|
||||||
|
"file_id": "clientright/0/file.pdf",
|
||||||
|
"file_name": "file.pdf",
|
||||||
|
"original_file_name": "original.pdf"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
@@ -129,3 +129,4 @@ WITH existing AS (
|
|||||||
2. Вернуть `unified_id` в ответе frontend (в `result.unified_id`)
|
2. Вернуть `unified_id` в ответе frontend (в `result.unified_id`)
|
||||||
3. При создании/обновлении черновика заполнять `clpr_claims.unified_id = unified_id`
|
3. При создании/обновлении черновика заполнять `clpr_claims.unified_id = unified_id`
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -70,3 +70,4 @@ WHERE c.session_token = $1 -- session_id
|
|||||||
ORDER BY c.updated_at DESC
|
ORDER BY c.updated_at DESC
|
||||||
LIMIT 20;
|
LIMIT 20;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -209,3 +209,4 @@ SELECT
|
|||||||
- ✅ Все подзапросы используют `LIMIT 1` для гарантии одной строки
|
- ✅ Все подзапросы используют `LIMIT 1` для гарантии одной строки
|
||||||
- ✅ Правильное слияние `answers` и `documents_meta`
|
- ✅ Правильное слияние `answers` и `documents_meta`
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -111,3 +111,4 @@
|
|||||||
|
|
||||||
Выполни задачу прямо сейчас и верни JSON согласно схеме.
|
Выполни задачу прямо сейчас и верни JSON согласно схеме.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user