change bot logic

This commit is contained in:
o.vodianov 2025-07-27 14:33:36 +04:00
parent 9d6b772777
commit e9dc210786
17 changed files with 594 additions and 84 deletions

3
.gitignore vendored
View File

@ -9,3 +9,6 @@ db/
__pycache__ __pycache__
src/docbot/conversations.pkl src/docbot/conversations.pkl
conversations.pkl conversations.pkl
*.jpg
*.png
*.jpeg

4
ai_example.py Normal file
View File

@ -0,0 +1,4 @@
from core.ai.ai_bot import detect_eyes
detect_eyes("/Users/o.vodianov/IdeaProjects/docbot/image.png")

161
poetry.lock generated
View File

@ -20,6 +20,41 @@ typing-extensions = ">=4.12"
[package.extras] [package.extras]
tz = ["tzdata"] tz = ["tzdata"]
[[package]]
name = "alembic-postgresql-enum"
version = "1.7.0"
description = "Alembic autogenerate support for creation, alteration and deletion of enums"
optional = false
python-versions = "<4.0,>=3.7"
groups = ["main"]
files = [
{file = "alembic_postgresql_enum-1.7.0-py3-none-any.whl", hash = "sha256:2a64260f3f1ed96f1bf984f55cb838e5d915693b8478b7b6ffea13cc09184ae0"},
{file = "alembic_postgresql_enum-1.7.0.tar.gz", hash = "sha256:1a12a2b25f3f49440f419821888530496511573875438b6e9a08cbcb8a6f006f"},
]
[package.dependencies]
alembic = ">=1.7"
SQLAlchemy = ">=1.4"
[[package]]
name = "alembic-utils"
version = "0.8.8"
description = "A sqlalchemy/alembic extension for migrating procedures and views"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "alembic_utils-0.8.8-py3-none-any.whl", hash = "sha256:2c2545dc545833c5deb63bce2c3cde01c1807bf99da5efab2497bc8d817cb86e"},
{file = "alembic_utils-0.8.8.tar.gz", hash = "sha256:99de5d13194f26536bc0322f0c1660020a305015700d8447ccfc20e7d1494e5b"},
]
[package.dependencies]
alembic = ">=1.9"
flupy = "*"
parse = ">=1.8.4"
sqlalchemy = ">=1.4"
typing_extensions = ">=0.1.0"
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
version = "0.7.0" version = "0.7.0"
@ -241,6 +276,21 @@ mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.13.0,<2.14.0" pycodestyle = ">=2.13.0,<2.14.0"
pyflakes = ">=3.3.0,<3.4.0" pyflakes = ">=3.3.0,<3.4.0"
[[package]]
name = "flupy"
version = "1.2.3"
description = "Fluent data processing in Python - a chainable stream processing library for expressive data manipulation using method chaining"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "flupy-1.2.3-py3-none-any.whl", hash = "sha256:be0f5a393bad2b3534697fbab17081993cd3f5817169dd3a61e8b2e0887612e6"},
{file = "flupy-1.2.3.tar.gz", hash = "sha256:220b6d40dea238cd2d66784c0d4d2a5483447a48acd343385768e0c740af9609"},
]
[package.dependencies]
typing_extensions = ">=4"
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.2.2" version = "3.2.2"
@ -567,6 +617,91 @@ files = [
{file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
] ]
[[package]]
name = "numpy"
version = "2.2.6"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"},
{file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"},
{file = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"},
{file = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"},
{file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"},
{file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"},
{file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"},
{file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"},
{file = "numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"},
{file = "numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"},
{file = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"},
{file = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"},
{file = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"},
{file = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"},
{file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"},
{file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"},
{file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"},
{file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"},
{file = "numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"},
{file = "numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"},
{file = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"},
{file = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"},
{file = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"},
{file = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"},
{file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"},
{file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"},
{file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"},
{file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"},
{file = "numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"},
{file = "numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"},
{file = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"},
{file = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"},
{file = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"},
{file = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"},
{file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"},
{file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"},
{file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"},
{file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"},
{file = "numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"},
{file = "numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"},
{file = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"},
{file = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"},
{file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"},
{file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"},
{file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"},
{file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"},
{file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"},
{file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"},
{file = "numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"},
{file = "numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"},
{file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"},
{file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"},
{file = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"},
{file = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"},
{file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"},
]
[[package]]
name = "opencv-python"
version = "4.12.0.88"
description = "Wrapper package for OpenCV python bindings."
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "opencv-python-4.12.0.88.tar.gz", hash = "sha256:8b738389cede219405f6f3880b851efa3415ccd674752219377353f017d2994d"},
{file = "opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:f9a1f08883257b95a5764bf517a32d75aec325319c8ed0f89739a57fae9e92a5"},
{file = "opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:812eb116ad2b4de43ee116fcd8991c3a687f099ada0b04e68f64899c09448e81"},
{file = "opencv_python-4.12.0.88-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:51fd981c7df6af3e8f70b1556696b05224c4e6b6777bdd2a46b3d4fb09de1a92"},
{file = "opencv_python-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:092c16da4c5a163a818f120c22c5e4a2f96e0db4f24e659c701f1fe629a690f9"},
{file = "opencv_python-4.12.0.88-cp37-abi3-win32.whl", hash = "sha256:ff554d3f725b39878ac6a2e1fa232ec509c36130927afc18a1719ebf4fbf4357"},
{file = "opencv_python-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:d98edb20aa932fd8ebd276a72627dad9dc097695b3d435a4257557bbb49a79d2"},
]
[package.dependencies]
numpy = {version = ">=2,<2.3.0", markers = "python_version >= \"3.9\""}
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "25.0" version = "25.0"
@ -579,6 +714,18 @@ files = [
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
] ]
[[package]]
name = "parse"
version = "1.20.2"
description = "parse() is the opposite of format()"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558"},
{file = "parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce"},
]
[[package]] [[package]]
name = "pathspec" name = "pathspec"
version = "0.12.1" version = "0.12.1"
@ -965,6 +1112,18 @@ rate-limiter = ["aiolimiter (>=1.1,<1.3)"]
socks = ["httpx[socks]"] socks = ["httpx[socks]"]
webhooks = ["tornado (>=6.4,<7.0)"] webhooks = ["tornado (>=6.4,<7.0)"]
[[package]]
name = "pytz"
version = "2025.2"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"},
{file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"},
]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.2" version = "6.0.2"
@ -1426,4 +1585,4 @@ files = [
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "004653e87a533d6d6e4471fd4a57d2ff49110aa6adaf4ba590394fcaf0aef46b" content-hash = "c50f083c47c32cf0ca58d1ccfec01aec63db5dc07be39cac76bb1e792c255951"

View File

@ -31,6 +31,8 @@ greenlet = "3.2.2"
psycopg2-binary = "2.9.10" psycopg2-binary = "2.9.10"
alembic_utils = "0.8.8" alembic_utils = "0.8.8"
alembic-postgresql-enum = "1.7.0" alembic-postgresql-enum = "1.7.0"
pytz = "2025.2"
opencv-python = "4.12.0.88"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]

28
src/core/ai/ai_bot.py Normal file
View File

@ -0,0 +1,28 @@
import cv2
def detect_eyes(image: str) -> None:
# Load the image
image = cv2.imread(image)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Detect faces in the grayscale image
eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')
eyes = eye_cascade.detectMultiScale(gray, scaleFactor=1.3, minNeighbors=5)
for (ex, ey, ew, eh) in eyes:
# Extract the eye region
eye_roi = image[ey:ey+eh, ex:ex+ew]
# Apply a Gaussian blur to the eye region
# Kernel size must be odd, e.g., (25, 25)
blurred_eye_roi = cv2.GaussianBlur(eye_roi, (599, 599), 50)
# Replace the original eye region with the blurred one
image[ey:ey+eh, ex:ex+ew] = blurred_eye_roi
output_path = 'blurred_eyes_image.jpg' # Name for the output image
cv2.imwrite(output_path, image)
print(f"Image with blurred eyes saved to {output_path}")

View File

@ -2,7 +2,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings): class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file='../../.env', env_file_encoding='utf-8') model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8')
BOT_TOKEN: str BOT_TOKEN: str
DATABASE_URL: str DATABASE_URL: str

View File

@ -1,4 +1,7 @@
import uuid import uuid
import hashlib
import base64
from datetime import datetime
def UUID_code_generator() -> str: def UUID_code_generator() -> str:
@ -7,3 +10,22 @@ def UUID_code_generator() -> str:
Возвращает строку с кодом. Возвращает строку с кодом.
""" """
return str(uuid.uuid4().hex[:8]) return str(uuid.uuid4().hex[:8])
def generate_session_code(telegram_id: int, phone: str, consultation_date_time: str) -> str:
"""Генерация 8-символьного уникального кода на основе 3 параметров."""
combined = f"{telegram_id}|{phone}|{consultation_date_time}"
hash_digest = hashlib.sha256(combined.encode()).digest()
b64 = base64.urlsafe_b64encode(hash_digest).decode()
return b64[:8].upper()
def date_time_formatter(date_time: str) -> datetime:
"""
Форматирует дату и время в строку формата ДД.ММ.ГГГГ ЧЧ:ММ.
:param date_time: Дата и время в формате ISO 8601.
:return: Строка с отформатированной датой и временем.
"""
dt = datetime.strptime(date_time, '%d.%m.%y %H:%M')
return dt

View File

@ -11,17 +11,50 @@ class Base(DeclarativeBase):
pass pass
class SessionCode(Base): class Sessions(Base):
__tablename__ = "session_codes" __tablename__ = "sessions"
id: Mapped[uuid.UUID] = mapped_column( id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
code: Mapped[str] = mapped_column(String(8), unique=True, nullable=False) code: Mapped[str] = mapped_column(String(8), unique=True, nullable=False)
patient_telegram_id: Mapped[int] = mapped_column(nullable=False) patient_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("patients.id", ondelete="CASCADE"), nullable=False)
patient: Mapped["Patients"] = relationship(back_populates="sessions")
sent_at: Mapped[datetime] = mapped_column(nullable=False) sent_at: Mapped[datetime] = mapped_column(nullable=False)
consulted_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) consulted_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)
doctor_id: Mapped[Optional[uuid.UUID]] = mapped_column( doctor_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True), ForeignKey("doctors.id")) UUID(as_uuid=True), ForeignKey("doctors.id"))
session_status_history: Mapped[List["SessionStatusHistory"]] = relationship(
back_populates="sessions", cascade="all, delete-orphan")
session_date_time_history: Mapped[List["SessionDateTimeHistory"]] = relationship(
back_populates="sessions", cascade="all, delete-orphan")
paid_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)
class SessionDateTimeHistory(Base):
__tablename__ = "session_date_time_history"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
sessions_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False)
sessions: Mapped["Sessions"] = relationship(back_populates="session_date_time_history")
updated_at: Mapped[datetime] = mapped_column(nullable=False, default=datetime.utcnow)
consultation_date_time: Mapped[Optional[datetime]] = mapped_column(nullable=True)
who_updated: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
class SessionStatusHistory(Base):
__tablename__ = "session_status_history"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
sessions_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False)
sessions: Mapped["Sessions"] = relationship(back_populates="session_status_history")
updated_at: Mapped[datetime] = mapped_column(nullable=False, default=datetime.utcnow)
status: Mapped[str] = mapped_column(String(50), nullable=False, default="pending")
who_updated: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
class Admins(Base): class Admins(Base):
@ -35,6 +68,20 @@ class Admins(Base):
ARRAY(String), nullable=True) ARRAY(String), nullable=True)
class Patients(Base):
__tablename__ = "patients"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
telegram_id: Mapped[int] = mapped_column(unique=True, nullable=False)
phone: Mapped[Optional[str]] = mapped_column(nullable=True)
created_at: Mapped[datetime] = mapped_column(nullable=False)
updated_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)
accepted_terms: Mapped[bool] = mapped_column(default=False, nullable=True)
sessions: Mapped[List["Sessions"]] = relationship(
back_populates="patient", cascade="all, delete-orphan")
class Doctors(Base): class Doctors(Base):
__tablename__ = "doctors" __tablename__ = "doctors"
@ -54,7 +101,7 @@ class Doctors(Base):
referral: Mapped["ReferralCode"] = relationship( referral: Mapped["ReferralCode"] = relationship(
back_populates="doctor", uselist=False) back_populates="doctor", uselist=False)
session_codes: Mapped[List["SessionCode"]] = relationship() sessions: Mapped[List["Sessions"]] = relationship()
form_links: Mapped[List["FormLink"]] = relationship( form_links: Mapped[List["FormLink"]] = relationship(
back_populates="doctor", cascade="all, delete-orphan") back_populates="doctor", cascade="all, delete-orphan")
payment_methods: Mapped[List["PaymentMethod"]] = relationship( payment_methods: Mapped[List["PaymentMethod"]] = relationship(

View File

@ -4,14 +4,14 @@ from telegram.ext import (
MessageHandler, filters MessageHandler, filters
) )
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from core.logging import logger from core.logging import logger
from core.enums.dialog_helpers import Confirmation from core.enums.dialog_helpers import Confirmation
from core.texts import NO_PERMISSIONS_TO_USE_COMMAND from core.texts import NO_PERMISSIONS_TO_USE_COMMAND
from docbot.services.admins_service import ( from docbot.services.admins_service import (
get_admin_info, get_doctors_waiting_authorization, approve_doctor, get_admin_info, get_doctors_waiting_authorization, approve_doctor,
reject_doctor, get_payment_link reject_doctor, get_payment_methods
) )
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
SELECT_DOCTOR, VERIFY_DOCTOR, SEND_PAYMENT_LINK = range(3) SELECT_DOCTOR, VERIFY_DOCTOR, SEND_PAYMENT_LINK = range(3)
@ -20,7 +20,7 @@ SELECT_DOCTOR_TO_VERIFY = "👀 Выберите врача для автори
CHOOSE_DOCTORS_DESTINY = "🙄 Врач с ID {doctor_telegram_id}. Что сделать?" CHOOSE_DOCTORS_DESTINY = "🙄 Врач с ID {doctor_telegram_id}. Что сделать?"
VERIFIED = "Врач с ID {doctor_telegram_id} верифицирован ✅" VERIFIED = "Врач с ID {doctor_telegram_id} верифицирован ✅"
REJECTED = "Верификация врача с ID {doctor_telegram_id} отклонена ❌" REJECTED = "Верификация врача с ID {doctor_telegram_id} отклонена ❌"
PAYMENT_LINK = "Ссылка на оплату для врача с ID {doctor_telegram_id}: {payment_link}" PAYMENT_LINK = "Ссылка на оплату для врача с ID {doctor_telegram_id}: {payment_link} отправлена."
async def verify(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def verify(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
@ -31,13 +31,13 @@ async def verify(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
user_id = update.effective_user.id user_id = update.effective_user.id
if not await get_admin_info(user_id): if not await get_admin_info(user_id):
logger.warning( logger.warning(
f"Пользователь {user_id} пытался проверить верификацию без прав.") "Пользователь %s пытался проверить верификацию без прав.", user_id)
await update.message.reply_text(NO_PERMISSIONS_TO_USE_COMMAND) await update.message.reply_text(NO_PERMISSIONS_TO_USE_COMMAND)
return return
# Здесь должна быть логика проверки верификации врача # Здесь должна быть логика проверки верификации врача
# Например, можно запросить информацию о врачах и их статусе верификации # Например, можно запросить информацию о врачах и их статусе верификации
logger.info(f"Admin {user_id} запрашивает список врачей для верификации.") logger.info("Admin %s запрашивает список врачей для верификации.", user_id)
doctors = await get_doctors_waiting_authorization() doctors = await get_doctors_waiting_authorization()
if not doctors: if not doctors:
await update.message.reply_text(NO_DOCTORS) await update.message.reply_text(NO_DOCTORS)
@ -93,6 +93,12 @@ async def doctor_selected(update: Update, context: ContextTypes.DEFAULT_TYPE):
async def doctor_action(update: Update, context: ContextTypes.DEFAULT_TYPE): async def doctor_action(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Обрабатывает действия администратора по верификации врача.
Действия: approve (верифицировать) или reject (отклонить).
После успешной верификации отправляет ссылку на оплату.
"""
logger.info("Обработка действия администратора по верификации врача.")
query = update.callback_query query = update.callback_query
await query.answer() await query.answer()
data = query.data.split("_") data = query.data.split("_")
@ -103,7 +109,7 @@ async def doctor_action(update: Update, context: ContextTypes.DEFAULT_TYPE):
if action == "approve": if action == "approve":
# Здесь твоя логика верификации врача (например, запись в базу) # Здесь твоя логика верификации врача (например, запись в базу)
approve_doctor(doctor_telegram_id) approve_doctor(doctor_telegram_id)
logger.info(f"Врач с ID {doctor_telegram_id} верифицирован.") logger.info("Врач с ID %s верифицирован.", doctor_telegram_id)
# Отправляем сообщение об успешной верификации # Отправляем сообщение об успешной верификации
await query.edit_message_text( await query.edit_message_text(
VERIFIED.format(doctor_telegram_id=doctor_telegram_id) VERIFIED.format(doctor_telegram_id=doctor_telegram_id)
@ -111,7 +117,7 @@ async def doctor_action(update: Update, context: ContextTypes.DEFAULT_TYPE):
elif action == "reject": elif action == "reject":
# Логика отклонения # Логика отклонения
reject_doctor(doctor_telegram_id) reject_doctor(doctor_telegram_id)
logger.info(f"Врач с ID {doctor_telegram_id} отклонен.") logger.info("Врач с ID %s отклонен.", doctor_telegram_id)
# Отправляем сообщение об отклонении # Отправляем сообщение об отклонении
await query.edit_message_text( await query.edit_message_text(
REJECTED.format(doctor_telegram_id=doctor_telegram_id) REJECTED.format(doctor_telegram_id=doctor_telegram_id)
@ -120,7 +126,7 @@ async def doctor_action(update: Update, context: ContextTypes.DEFAULT_TYPE):
return SEND_PAYMENT_LINK return SEND_PAYMENT_LINK
def send_payment_link(update: Update, context: ContextTypes.DEFAULT_TYPE): async def send_payment_link(update: Update, context: ContextTypes.DEFAULT_TYPE):
""" """
Отправляет ссылку на оплату после верификации врача. Отправляет ссылку на оплату после верификации врача.
""" """
@ -130,7 +136,12 @@ def send_payment_link(update: Update, context: ContextTypes.DEFAULT_TYPE):
return return
# Здесь должна быть логика получения ссылки на оплату # Здесь должна быть логика получения ссылки на оплату
payment_link = get_payment_link(doctor_telegram_id) payment_link = get_payment_methods(doctor_telegram_id)
await context.bot.send_message(
chat_id=doctor_telegram_id,
text=f"Ваша верификация подтверждена! Вот ваша ссылка: {payment_link}"
)
# Отправляем ссылку на оплату # Отправляем ссылку на оплату
update.message.reply_text( update.message.reply_text(

View File

@ -41,6 +41,7 @@ WAIT_FOR_ACTIVATION_TEXT = (
VERIFICATION_IN_PROGRESS_TEXT = ( VERIFICATION_IN_PROGRESS_TEXT = (
"📝 Пока идет верификация, вы можете ознакомиться с форматами работы и условиями сервиса\n" "📝 Пока идет верификация, вы можете ознакомиться с форматами работы и условиями сервиса\n"
"Формат: название форматов + пакеты." "Формат: название форматов + пакеты."
# TODO добавить ссылку на формат работы и пакеты (сразу отправить в чате инфу)
) )
ALL_INFORMATION_RECEIVED_TEXT = ( ALL_INFORMATION_RECEIVED_TEXT = (
"✅ Спасибо за информацию, мы с вами свяжемся. До свидания. 👋" "✅ Спасибо за информацию, мы с вами свяжемся. До свидания. 👋"

View File

@ -13,13 +13,38 @@ from telegram.ext import (
from docbot.handlers.utils.cancel_handler import get_cancel_handler from docbot.handlers.utils.cancel_handler import get_cancel_handler
from docbot.handlers.start_handler import get_start_handler from docbot.handlers.start_handler import get_start_handler
from core.enums.dialog_helpers import ConfirmationMessage from core.enums.dialog_helpers import ConfirmationMessage
from docbot.services.patients_service import (
create_patient, update_patient_phone, get_patient_by_telegram_id,
is_user_has_phone
)
from docbot.services.session_service import create_session
from core.logging import logger
SEND_ACKNOWLEDGEMENT_INFO = 1 SEND_ACKNOWLEDGEMENT_INFO = 1
PROCEED_WITH_CONSULTATION = 2 PROCEED_WITH_CONSULTATION = 2
CHOOSE_CONSULTATION_TYPE = 3 SELECT_CONSULTATION_TYPE = 3
ENTER_PATIENT_PHONE = 4
ENTER_DOCTOR_NUMBER = 5
ENTER_CONSULTATION_DATE = 6
PAY_CONSULTATION = 7
STOPPING = 99 STOPPING = 99
keyboard = [
[
InlineKeyboardButton(
"Записаться на консультацию",
callback_data="proceed_with_consultation"
),
InlineKeyboardButton(
"Частые вопросы",
callback_data="frequent_questions"
),
]
]
ACCEPT_PERSONAL_DATA_AGREEMENT_TEXT = ( ACCEPT_PERSONAL_DATA_AGREEMENT_TEXT = (
"📝 Пожалуйста, подтвердите, что вы согласны с обработкой ваших персональных данных.\n" "📝 Пожалуйста, подтвердите, что вы согласны с обработкой ваших персональных данных.\n"
@ -28,8 +53,7 @@ ACCEPT_PERSONAL_DATA_AGREEMENT_TEXT = (
async def accept_personal_data_agreement(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: async def accept_personal_data_agreement(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
keyboard_accept = [
keyboard = [
[ [
InlineKeyboardButton( InlineKeyboardButton(
ConfirmationMessage.PROCEED.value, ConfirmationMessage.PROCEED.value,
@ -42,32 +66,38 @@ async def accept_personal_data_agreement(update: Update, context: ContextTypes.D
] ]
] ]
user_id = update.effective_user.id
user_data = context.user_data
user_data['telegram_id'] = user_id
logger.info(f"User {user_id} initiated consultation process.")
logger.info(f" user exists? {await get_patient_by_telegram_id(user_id)}")
if await get_patient_by_telegram_id(user_id):
await update.message.reply_text(
"Вы уже зарегистрированы как пациент. Пожалуйста, продолжайте с записью на консультацию.",
parse_mode="Markdown",
reply_markup=InlineKeyboardMarkup(keyboard),
)
return PROCEED_WITH_CONSULTATION
await update.message.reply_text( await update.message.reply_text(
ACCEPT_PERSONAL_DATA_AGREEMENT_TEXT, ACCEPT_PERSONAL_DATA_AGREEMENT_TEXT,
parse_mode="Markdown", parse_mode="Markdown",
reply_markup=InlineKeyboardMarkup(keyboard), reply_markup=InlineKeyboardMarkup(keyboard_accept),
) )
return SEND_ACKNOWLEDGEMENT_INFO return SEND_ACKNOWLEDGEMENT_INFO
async def receive_patient_aceptance(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: async def receive_patient_aceptance(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
# Здесь можно добавить логику обработки согласия пациента # Здесь можно добавить логику обработки согласия пациента
keyboard = [
[
InlineKeyboardButton(
"Записаться на консультацию",
callback_data="proceed_with_consultation"
),
InlineKeyboardButton(
"Частые вопросы",
callback_data="frequent_questions"
),
]
]
user_data = context.user_data user_data = context.user_data
await update.callback_query.answer() await update.callback_query.answer()
if update.callback_query.data == "accepted": if update.callback_query.data == "accepted":
user_data['accepted'] = True user_data['accepted'] = True
user_id = user_data['telegram_id']
await create_patient(telegram_id=user_id, terms_acceptance=True) # Создаем пациента в БД
await update.callback_query.edit_message_text( await update.callback_query.edit_message_text(
text="✅ Спасибо за подтверждение. Вы можете продолжить запись на консультацию.", text="✅ Спасибо за подтверждение. Вы можете продолжить запись на консультацию.",
parse_mode="Markdown", parse_mode="Markdown",
@ -98,12 +128,105 @@ async def choose_consultation_type(update: Update, context: ContextTypes.DEFAULT
] ]
await update.callback_query.answer() await update.callback_query.answer()
if update.callback_query.data == "proceed_with_consultation":
await update.callback_query.edit_message_text(
text="Выберите тип консультации:",
reply_markup=InlineKeyboardMarkup(keyboard),
)
return SELECT_CONSULTATION_TYPE
async def enter_patient_phone(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
await update.callback_query.answer()
user_data = context.user_data
user_id = user_data['telegram_id']
patient = await get_patient_by_telegram_id(user_id)
has_phone = False
if patient:
has_phone = await is_user_has_phone(user_id)
logger.info(f"User {user_id} has phone: {has_phone}")
if update.callback_query.data in ["initial_reception"] and not has_phone:
await update.callback_query.edit_message_text( await update.callback_query.edit_message_text(
text="Выберите тип консультации:", text="Пожалуйста, введите ваш номер телефона для записи на консультацию:"
reply_markup=InlineKeyboardMarkup(keyboard),
) )
return CHOOSE_CONSULTATION_TYPE return ENTER_PATIENT_PHONE
elif has_phone:
await update.callback_query.edit_message_text(
text="Введите серийный номер врача, к которому вы хотите записаться на консультацию:",
parse_mode="Markdown"
)
return ENTER_DOCTOR_NUMBER
async def receive_patient_phone(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
phone = update.message.text.strip()
user_id = context.user_data['telegram_id']
# Здесь можно добавить логику для сохранения номера телефона пациента в БД
# Например, вызов сервиса для обновления информации о пациенте
# await update_patient_phone(telegram_id=user_id, phone=phone)
logger.info((f"receive_patient_phone User {user_id} provided phone: {phone}"))
await update_patient_phone(telegram_id=user_id, phone=phone)
context.user_data['phone'] = phone
logger.info((f"receive_patient_phone2 User {user_id} provided phone: {phone}"))
await update.message.reply_text(
f"Ваш номер телефона: {phone}\n"
"Введите серийный номер врача, к которому вы хотите записаться на консультацию:",
parse_mode="Markdown"
)
return ENTER_DOCTOR_NUMBER
async def receive_doctor_number(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
doctor_number = update.message.text.strip()
await update.message.reply_text(
f"Вы ввели серийный номер врача: {doctor_number}\n"
"Введите дату и время консультации в формате ДД.ММ.ГГ ЧЧ:ММ, например, 01.01.23 12:00.",
parse_mode="Markdown"
)
logger.info(f"phone is {context.user_data['phone']}")
return ENTER_CONSULTATION_DATE
async def receive_consultation_date(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
consultation_date_time = update.message.text.strip()
# Здесь можно добавить логику для сохранения даты консультации в БД
# Например, вызов сервиса для создания записи о консультации
patient = await get_patient_by_telegram_id(context.user_data['telegram_id'])
await create_session(
telegram_id=context.user_data['telegram_id'],
phone=context.user_data['phone'],
consultation_date_time=consultation_date_time,
patient=patient
)
link = f"https://example.com/consultation/{context.user_data['telegram_id']}"
await update.message.reply_text(
f"Чтобы оплатить консультацию перейдите по ссылке {link}.",
parse_mode="Markdown",
reply_markup=ReplyKeyboardRemove()
)
return PAY_CONSULTATION
async def pay_consultation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
await update.message.reply_text(
"💳 Пожалуйста, оплатите консультацию по следующей ссылке: [Оплата](https://example.com/pay)",
parse_mode="Markdown"
)
return ConversationHandler.END
def consultation_handler() -> CommandHandler: def consultation_handler() -> CommandHandler:
@ -117,9 +240,13 @@ def get_consultation_handler() -> ConversationHandler:
states={ states={
SEND_ACKNOWLEDGEMENT_INFO: [CallbackQueryHandler(receive_patient_aceptance)], SEND_ACKNOWLEDGEMENT_INFO: [CallbackQueryHandler(receive_patient_aceptance)],
PROCEED_WITH_CONSULTATION: [CallbackQueryHandler(choose_consultation_type)], PROCEED_WITH_CONSULTATION: [CallbackQueryHandler(choose_consultation_type)],
SELECT_CONSULTATION_TYPE: [CallbackQueryHandler(enter_patient_phone)],
ENTER_PATIENT_PHONE: [MessageHandler(filters=None, callback=receive_patient_phone)],
ENTER_DOCTOR_NUMBER: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_doctor_number)],
ENTER_CONSULTATION_DATE: [MessageHandler(filters=None, callback=receive_consultation_date)],
STOPPING: [get_start_handler()], STOPPING: [get_start_handler()],
}, },
fallbacks=[get_cancel_handler()], fallbacks=[get_cancel_handler()],
name="consultation_dialog", # для тестов/логирования name="consultation_dialog",
persistent=True, # если используете хранение состояний persistent=True,
) )

View File

@ -6,7 +6,7 @@ from telegram.ext import (
MessageHandler, MessageHandler,
filters, filters,
) )
from docbot.services.session_service import create_session_code, get_pending_session from docbot.services.session_service import create_session, get_pending_session
from docbot.handlers.utils.cancel_handler import get_cancel_handler from docbot.handlers.utils.cancel_handler import get_cancel_handler
@ -40,7 +40,7 @@ async def receive_link(update: Update, context: ContextTypes.DEFAULT_TYPE) -> in
user_id = update.effective_user.id user_id = update.effective_user.id
# вызываем сервис, который создаст код сессии и запишет в БД # вызываем сервис, который создаст код сессии и запишет в БД
code = await create_session_code(telegram_id=user_id, form_link=link) code = await create_session(telegram_id=user_id, form_link=link)
await update.message.reply_text( await update.message.reply_text(
f"Ваш код для консультации: *{code}*\n" f"Ваш код для консультации: *{code}*\n"

View File

@ -9,9 +9,8 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
""" """
text = ( text = (
"👋 Добро пожаловать в DocBot!\n\n" "👋 Добро пожаловать в DocBot!\n\n"
"Используйте команду /form чтобы прислать ссылку на анкету, если вы пациент.\n" "Используйте команду /consultation, чтобы произвести запись на консультацию, если вы пациент.\n"
"После заполнения анкеты мы пришлем код для консультации врача.\n" "Используйте команду /register, если вы врач и у вас есть реферальный код."
"Используйте команду /register, если у вас есть реферальный код."
) )
await context.bot.send_message(chat_id=update.effective_chat.id, text=text) await context.bot.send_message(chat_id=update.effective_chat.id, text=text)

View File

@ -6,11 +6,11 @@ from telegram.ext import (
) )
async def __cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
await update.message.reply_text("❌ Отменено.") await update.message.reply_text("❌ Отменено.")
return ConversationHandler.END return ConversationHandler.END
def get_cancel_handler() -> CommandHandler: def get_cancel_handler() -> CommandHandler:
"""Фабрика для регистрации в Application.""" """Фабрика для регистрации в Application."""
return CommandHandler("cancel", __cancel) return CommandHandler("cancel", cancel)

View File

@ -6,6 +6,9 @@ from db.models import Admins, Doctors, VerificationRequests
async def get_admin_info(telegram_id: int) -> Admins | None: async def get_admin_info(telegram_id: int) -> Admins | None:
""" Получает информацию об администраторе по его Telegram ID.
Возвращает объект Admins или None, если администратор не найден.
"""
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
result = await session.execute( result = await session.execute(
select(Admins.telegram_id) select(Admins.telegram_id)
@ -15,15 +18,23 @@ async def get_admin_info(telegram_id: int) -> Admins | None:
async def mark_doctor_inactive(telegram_id: int) -> Admins | None: async def mark_doctor_inactive(telegram_id: int) -> Admins | None:
# TODO: Добавить логику для пометки врача как неактивного.
""" Помечает врача как неактивного.
Устанавливает флаг is_active в False.
Возвращает информацию о враче или None, если не найден.
"""
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
result = await session.execute( result = await session.execute(
select(Admins.telegram_id) select(Doctors.telegram_id)
.where(Admins.telegram_id == telegram_id) .where(Doctors.telegram_id == telegram_id)
) )
return result.scalar_one_or_none() return result.scalar_one_or_none()
async def get_doctors_waiting_authorization() -> Sequence[Row[Tuple[VerificationRequests, Doctors]]] | None: async def get_doctors_waiting_authorization() -> Sequence[Row[Tuple[VerificationRequests, Doctors]]] | None:
""" Получает список врачей, ожидающих верификации.
Возвращает список кортежей (VerificationRequests, Doctors) или None, если нет врачей.
"""
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
result = await session.execute( result = await session.execute(
select(VerificationRequests, Doctors) select(VerificationRequests, Doctors)
@ -36,6 +47,10 @@ async def get_doctors_waiting_authorization() -> Sequence[Row[Tuple[Verification
async def approve_doctor(doctor_id: int) -> bool: async def approve_doctor(doctor_id: int) -> bool:
""" Верифицирует врача.
Устанавливает флаги is_verified и is_active в True.
Возвращает True, если врач найден и обновлен, иначе False.
"""
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
async with session.begin(): async with session.begin():
result = await session.execute( result = await session.execute(
@ -51,6 +66,10 @@ async def approve_doctor(doctor_id: int) -> bool:
async def reject_doctor(doctor_id: int) -> bool: async def reject_doctor(doctor_id: int) -> bool:
""" Отклоняет верификацию врача.
Устанавливает флаги is_verified и is_active в False.
Возвращает True, если врач найден и обновлен, иначе False.
"""
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
async with session.begin(): async with session.begin():
result = await session.execute( result = await session.execute(

View File

@ -0,0 +1,69 @@
from __future__ import annotations
from datetime import datetime
from sqlalchemy import select
from db.session import AsyncSessionLocal
from db.models import Patients
async def update_patient_phone(telegram_id: int, phone: str) -> bool:
"""
Обновляет номер телефона пациента в базе данных.
:param telegram_id: ID Telegram пациента.
:param phone: Новый номер телефона для обновления.
:return: True, если обновление прошло успешно, иначе False.
"""
async with AsyncSessionLocal() as session:
async with session.begin():
result = await session.execute(
select(Patients).where(Patients.telegram_id == telegram_id)
)
patient = result.scalar_one_or_none()
if patient:
patient.phone = phone
patient.updated_at = datetime.utcnow()
return True
return False
async def create_patient(telegram_id: int, terms_acceptance: bool) -> Patients:
"""
Создает нового пациента в базе данных.
:param telegram_id: ID Telegram пациента.
:return: Объект Patients с информацией о новом пациенте.
"""
async with AsyncSessionLocal() as session:
async with session.begin():
new_patient = Patients(
telegram_id=telegram_id,
created_at=datetime.utcnow(),
accepted_terms=terms_acceptance
)
session.add(new_patient)
return new_patient
async def get_patient_by_telegram_id(telegram_id: int) -> Patients | None:
"""
Получает информацию о пациенте по его Telegram ID.
:param telegram_id: ID Telegram пациента.
:return: Объект Patients или None, если пациент не найден.
"""
async with AsyncSessionLocal() as session:
result = await session.execute(
select(Patients).where(Patients.telegram_id == telegram_id)
)
return result.scalar_one_or_none()
async def is_user_has_phone(telegram_id: int) -> bool:
"""
Проверяет, есть ли у пользователя номер телефона.
:param telegram_id: ID Telegram пользователя.
:return: True, если номер телефона есть, иначе False.
"""
patient = await get_patient_by_telegram_id(telegram_id)
return patient is not None and patient.phone is not None

View File

@ -6,23 +6,42 @@ from datetime import datetime
from sqlalchemy import select from sqlalchemy import select
from db.session import AsyncSessionLocal from db.session import AsyncSessionLocal
from db.models import SessionCode from db.models import Sessions, Patients, SessionStatusHistory, SessionDateTimeHistory
from core.utils import UUID_code_generator from core.utils import (
UUID_code_generator, generate_session_code, date_time_formatter
)
async def create_session_code(telegram_id: int, form_link: str) -> str: async def create_session(telegram_id: int, phone: str, consultation_date_time: str, patient: Patients) -> str:
""" """
Генерирует уникальный код, сохраняет его вместе с Telegram ID. Генерирует уникальный код, сохраняет его вместе с Telegram ID.
Возвращает этот код. Возвращает этот код.
""" """
code = UUID_code_generator() code = generate_session_code(telegram_id=telegram_id, phone=phone, consultation_date_time=consultation_date_time)
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
async with session.begin(): async with session.begin():
session.add(SessionCode( sessions = Sessions(
code=code, code=code,
patient_telegram_id=telegram_id, patient=patient,
sent_at=datetime.utcnow() sent_at=datetime.utcnow()
)) )
sessions_code_history = SessionStatusHistory(
sessions=sessions,
updated_at=datetime.utcnow(),
status="created",
who_updated="bot"
)
sessions_date_time_history = SessionDateTimeHistory(
sessions=sessions,
updated_at=datetime.utcnow(),
consultation_date_time=date_time_formatter(consultation_date_time),
who_updated="bot"
)
session.add_all([sessions_code_history, sessions_date_time_history])
return code return code
@ -34,26 +53,26 @@ async def mark_consulted(code: str) -> bool:
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
async with session.begin(): async with session.begin():
result = await session.execute( result = await session.execute(
select(SessionCode).where(SessionCode.code.match(code)) select(Sessions).where(Sessions.code.match(code))
) )
sc: SessionCode | None = result.scalar_one_or_none() sc: Sessions | None = result.scalar_one_or_none()
if not sc: if not sc:
return False return False
sc.consulted_at = datetime.utcnow() sc.consulted_at = datetime.utcnow()
return True return True
async def get_pending_session(telegram_id: int) -> SessionCode | None: async def get_pending_session(telegram_id: int) -> Sessions | None:
""" """
Ищет самую «свежую» сессию по telegram_id, где consulted_at ещё не заполнен. Ищет самую «свежую» сессию по telegram_id, где consulted_at ещё не заполнен.
Вернёт объект SessionCode или None. Вернёт объект Sessions или None.
""" """
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
result = await session.execute( result = await session.execute(
select(SessionCode) select(Sessions)
.where(SessionCode.patient_telegram_id.match(telegram_id)) .where(Sessions.patient_telegram_id.match(telegram_id))
.where(SessionCode.consulted_at.is_(None)) .where(Sessions.consulted_at.is_(None))
.order_by(SessionCode.sent_at.desc()) .order_by(Sessions.sent_at.desc())
.limit(1) .limit(1)
) )
return result.scalar_one_or_none() return result.scalar_one_or_none()
@ -66,9 +85,9 @@ async def get_session_info(code: str) -> dict | None:
""" """
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
result = await session.execute( result = await session.execute(
select(SessionCode).where(SessionCode.code.match(code)) select(Sessions).where(Sessions.code.match(code))
) )
sc: SessionCode | None = result.scalar_one_or_none() sc: Sessions | None = result.scalar_one_or_none()
if not sc: if not sc:
return None return None
return { return {