hh-applicant-tool 1.4.7__py3-none-any.whl → 1.5.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. hh_applicant_tool/__main__.py +1 -1
  2. hh_applicant_tool/ai/__init__.py +1 -0
  3. hh_applicant_tool/ai/openai.py +30 -14
  4. hh_applicant_tool/api/__init__.py +4 -2
  5. hh_applicant_tool/api/client.py +32 -17
  6. hh_applicant_tool/{constants.py → api/client_keys.py} +3 -3
  7. hh_applicant_tool/{datatypes.py → api/datatypes.py} +2 -0
  8. hh_applicant_tool/api/errors.py +8 -2
  9. hh_applicant_tool/{utils → api}/user_agent.py +1 -1
  10. hh_applicant_tool/main.py +63 -38
  11. hh_applicant_tool/operations/apply_similar.py +136 -52
  12. hh_applicant_tool/operations/authorize.py +97 -28
  13. hh_applicant_tool/operations/call_api.py +3 -3
  14. hh_applicant_tool/operations/{check_negotiations.py → clear_negotiations.py} +40 -26
  15. hh_applicant_tool/operations/list_resumes.py +5 -7
  16. hh_applicant_tool/operations/query.py +5 -3
  17. hh_applicant_tool/operations/refresh_token.py +9 -2
  18. hh_applicant_tool/operations/reply_employers.py +80 -40
  19. hh_applicant_tool/operations/settings.py +2 -2
  20. hh_applicant_tool/operations/update_resumes.py +5 -4
  21. hh_applicant_tool/operations/whoami.py +3 -3
  22. hh_applicant_tool/storage/__init__.py +5 -1
  23. hh_applicant_tool/storage/facade.py +2 -2
  24. hh_applicant_tool/storage/models/base.py +9 -4
  25. hh_applicant_tool/storage/models/contacts.py +42 -0
  26. hh_applicant_tool/storage/queries/schema.sql +23 -10
  27. hh_applicant_tool/storage/repositories/base.py +69 -15
  28. hh_applicant_tool/storage/repositories/contacts.py +5 -10
  29. hh_applicant_tool/storage/repositories/employers.py +1 -0
  30. hh_applicant_tool/storage/repositories/errors.py +19 -0
  31. hh_applicant_tool/storage/repositories/negotiations.py +1 -0
  32. hh_applicant_tool/storage/repositories/resumes.py +2 -7
  33. hh_applicant_tool/storage/repositories/settings.py +1 -0
  34. hh_applicant_tool/storage/repositories/vacancies.py +1 -0
  35. hh_applicant_tool/storage/utils.py +12 -15
  36. hh_applicant_tool/utils/__init__.py +3 -3
  37. hh_applicant_tool/utils/config.py +1 -1
  38. hh_applicant_tool/utils/log.py +6 -3
  39. hh_applicant_tool/utils/mixins.py +28 -46
  40. hh_applicant_tool/utils/string.py +15 -0
  41. hh_applicant_tool/utils/terminal.py +115 -0
  42. {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.5.7.dist-info}/METADATA +384 -162
  43. hh_applicant_tool-1.5.7.dist-info/RECORD +68 -0
  44. {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.5.7.dist-info}/WHEEL +1 -1
  45. hh_applicant_tool/storage/models/contact.py +0 -16
  46. hh_applicant_tool-1.4.7.dist-info/RECORD +0 -67
  47. /hh_applicant_tool/utils/{dateutil.py → date.py} +0 -0
  48. /hh_applicant_tool/utils/{jsonutil.py → json.py} +0 -0
  49. {hh_applicant_tool-1.4.7.dist-info → hh_applicant_tool-1.5.7.dist-info}/entry_points.txt +0 -0
@@ -6,12 +6,12 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  from prettytable import PrettyTable
8
8
 
9
- from ..datatypes import PaginatedItems
9
+ from ..api.datatypes import PaginatedItems
10
10
  from ..main import BaseNamespace, BaseOperation
11
11
  from ..utils.string import shorten
12
12
 
13
13
  if TYPE_CHECKING:
14
- from .. import datatypes
14
+ from ..api import datatypes
15
15
  from ..main import HHApplicantTool
16
16
 
17
17
 
@@ -32,9 +32,8 @@ class Operation(BaseOperation):
32
32
 
33
33
  def run(self, tool: HHApplicantTool) -> None:
34
34
  resumes: PaginatedItems[datatypes.Resume] = tool.get_resumes()
35
- storage = tool.storage
36
- for resume in resumes["items"]:
37
- storage.resumes.save(resume)
35
+ logger.debug(resumes)
36
+ tool.storage.resumes.save_batch(resumes)
38
37
 
39
38
  t = PrettyTable(
40
39
  field_names=["ID", "Название", "Статус"], align="l", valign="t"
@@ -46,8 +45,7 @@ class Operation(BaseOperation):
46
45
  shorten(x["title"]),
47
46
  x["status"]["name"].title(),
48
47
  )
49
- for x in resumes["items"]
48
+ for x in resumes
50
49
  ]
51
50
  )
52
51
  print(t)
53
- print(f"\nНайдено резюме: {resumes['found']}")
@@ -68,7 +68,7 @@ class Operation(BaseOperation):
68
68
  writer.writerows(cursor.fetchall())
69
69
 
70
70
  if tool.args.output:
71
- print(f"✅ Exported to {tool.args.output.name}")
71
+ print(f"✅ Exported to {tool.args.output.name}")
72
72
  return
73
73
 
74
74
  rows = cursor.fetchmany(MAX_RESULTS + 1)
@@ -88,10 +88,12 @@ class Operation(BaseOperation):
88
88
  )
89
89
  else:
90
90
  tool.db.commit()
91
- print(f"OK. Rows affected: {cursor.rowcount}")
91
+
92
+ if cursor.rowcount > 0:
93
+ print(f"Rows affected: {cursor.rowcount}")
92
94
 
93
95
  except sqlite3.Error as ex:
94
- print(f"❌ SQL Error: {ex}")
96
+ print(f"❌ SQL Error: {ex}")
95
97
  return 1
96
98
 
97
99
  if initial_sql := tool.args.sql:
@@ -26,5 +26,12 @@ class Operation(BaseOperation):
26
26
  pass
27
27
 
28
28
  def run(self, tool: HHApplicantTool) -> None:
29
- tool.api_client.refresh_access_token()
30
- print("✅ Токен обновлен!")
29
+ if tool.api_client.is_access_expired:
30
+ tool.api_client.refresh_access_token()
31
+ if not tool.save_token():
32
+ print("⚠️ Токен не был обновлен!")
33
+ return 1
34
+ print("✅ Токен успешно обновлен.")
35
+ else:
36
+ # logger.debug("Токен валиден, игнорируем обновление.")
37
+ print("ℹ️ Токен не истек, обновление не требуется.")
@@ -3,13 +3,13 @@ from __future__ import annotations
3
3
  import argparse
4
4
  import logging
5
5
  import random
6
+ from datetime import datetime
6
7
  from typing import TYPE_CHECKING
7
8
 
8
- from .. import datatypes
9
9
  from ..ai.base import AIError
10
- from ..api import ApiError
10
+ from ..api import ApiError, datatypes
11
11
  from ..main import BaseNamespace, BaseOperation
12
- from ..utils.dateutil import parse_api_datetime
12
+ from ..utils.date import parse_api_datetime
13
13
  from ..utils.string import rand_text
14
14
 
15
15
  if TYPE_CHECKING:
@@ -37,21 +37,30 @@ class Namespace(BaseNamespace):
37
37
  use_ai: bool
38
38
  first_prompt: str
39
39
  prompt: str
40
+ period: int
40
41
 
41
42
 
42
43
  class Operation(BaseOperation):
43
44
  """Ответ всем работодателям."""
44
45
 
45
- __aliases__ = ["reply-chats", "reply-all"]
46
+ __aliases__ = ["reply-empls", "reply-chats", "reall"]
46
47
 
47
48
  def setup_parser(self, parser: argparse.ArgumentParser) -> None:
48
- parser.add_argument("--resume-id", help="Идентификатор резюме")
49
+ parser.add_argument(
50
+ "--resume-id",
51
+ help="Идентификатор резюме. Если не указан, то просматриваем чаты для всех резюме",
52
+ )
49
53
  parser.add_argument(
50
54
  "-m",
51
55
  "--reply-message",
52
56
  "--reply",
53
57
  help="Отправить сообщение во все чаты. Если не передать сообщение, то нужно будет вводить его в интерактивном режиме.", # noqa: E501
54
58
  )
59
+ parser.add_argument(
60
+ "--period",
61
+ type=int,
62
+ help="Игнорировать отклики, которые не обновлялись больше N дней",
63
+ )
55
64
  parser.add_argument(
56
65
  "-p",
57
66
  "--max-pages",
@@ -92,7 +101,7 @@ class Operation(BaseOperation):
92
101
 
93
102
  def run(self, tool: HHApplicantTool) -> None:
94
103
  args: Namespace = tool.args
95
- self.applicant_tool = tool
104
+ self.tool = tool
96
105
  self.api_client = tool.api_client
97
106
  self.resume_id = tool.first_resume_id()
98
107
  self.reply_message = args.reply_message or tool.config.get(
@@ -106,27 +115,60 @@ class Operation(BaseOperation):
106
115
  self.openai_chat = (
107
116
  tool.get_openai_chat(args.first_prompt) if args.use_ai else None
108
117
  )
118
+ self.period = args.period
109
119
 
110
120
  logger.debug(f"{self.reply_message = }")
111
- self.reply_chats()
112
-
113
- def reply_chats(self) -> None:
114
- blacklisted = self.applicant_tool.get_blacklisted()
115
- logger.debug(f"{blacklisted = }")
116
- me: datatypes.User = self.applicant_tool.get_me()
117
-
118
- basic_message_placeholders = {
119
- "first_name": me.get("first_name", ""),
120
- "last_name": me.get("last_name", ""),
121
- "email": me.get("email", ""),
122
- "phone": me.get("phone", ""),
121
+ self.reply_employers()
122
+
123
+ def reply_employers(self):
124
+ blacklist = set(self.tool.get_blacklisted())
125
+ me: datatypes.User = self.tool.get_me()
126
+ resumes = self.tool.get_resumes()
127
+ resumes = (
128
+ list(filter(lambda x: x["id"] == self.resume_id, resumes))
129
+ if self.resume_id
130
+ else resumes
131
+ )
132
+ resumes = list(
133
+ filter(
134
+ lambda resume: resume["status"]["id"] == "published", resumes
135
+ )
136
+ )
137
+ self._reply_chats(user=me, resumes=resumes, blacklist=blacklist)
138
+
139
+ def _reply_chats(
140
+ self,
141
+ user: datatypes.User,
142
+ resumes: list[datatypes.Resume],
143
+ blacklist: set[str],
144
+ ) -> None:
145
+ resume_map = {r["id"]: r for r in resumes}
146
+
147
+ base_placeholders = {
148
+ "first_name": user.get("first_name") or "",
149
+ "last_name": user.get("last_name") or "",
150
+ "email": user.get("email") or "",
151
+ "phone": user.get("phone") or "",
123
152
  }
124
153
 
125
- for negotiation in self.applicant_tool.get_negotiations():
154
+ for negotiation in self.tool.get_negotiations():
126
155
  try:
127
- self.applicant_tool.storage.negotiations.save(negotiation)
156
+ # try:
157
+ # self.tool.storage.negotiations.save(negotiation)
158
+ # except RepositoryError as e:
159
+ # logger.exception(e)
128
160
 
129
- if self.resume_id != negotiation["resume"]["id"]:
161
+ if not (resume := resume_map.get(negotiation["resume"]["id"])):
162
+ continue
163
+
164
+ updated_at = parse_api_datetime(negotiation["updated_at"])
165
+
166
+ # Пропуск откликов, которые не обновлялись более N дней (при просмотре они обновляются вроде)
167
+ if (
168
+ self.period
169
+ and (datetime().now(updated_at.tzinfo) - updated_at).days
170
+ > self.period
171
+ ):
130
172
  continue
131
173
 
132
174
  state_id = negotiation["state"]["id"]
@@ -141,26 +183,27 @@ class Operation(BaseOperation):
141
183
  employer = vacancy.get("employer") or {}
142
184
  salary = vacancy.get("salary") or {}
143
185
 
144
- if employer.get("id") in blacklisted:
186
+ if employer.get("id") in blacklist:
145
187
  print(
146
188
  "🚫 Пропускаем заблокированного работодателя",
147
189
  employer.get("alternate_url"),
148
190
  )
149
191
  continue
150
192
 
151
- message_placeholders = {
193
+ placeholders = {
152
194
  "vacancy_name": vacancy.get("name", ""),
153
195
  "employer_name": employer.get("name", ""),
154
- **basic_message_placeholders,
196
+ "resume_title": resume.get("title") or "",
197
+ **base_placeholders,
155
198
  }
156
199
 
157
200
  logger.debug(
158
201
  "Вакансия %(vacancy_name)s от %(employer_name)s"
159
- % message_placeholders
202
+ % placeholders
160
203
  )
161
204
 
162
205
  page: int = 0
163
- last_message: dict | None = None
206
+ last_message: datatypes.Message | None = None
164
207
  message_history: list[str] = []
165
208
  while True:
166
209
  messages_res: datatypes.PaginatedItems[
@@ -183,7 +226,8 @@ class Operation(BaseOperation):
183
226
  )
184
227
  message_date = parse_api_datetime(
185
228
  message.get("created_at")
186
- ).isoformat()
229
+ ).strftime("%d.%m.%Y %H:%M:%S")
230
+
187
231
  message_history.append(
188
232
  f"[ {message_date} ] {author}: {message['text']}"
189
233
  )
@@ -205,13 +249,13 @@ class Operation(BaseOperation):
205
249
  send_message = ""
206
250
  if self.reply_message:
207
251
  send_message = (
208
- rand_text(self.reply_message) % message_placeholders
252
+ rand_text(self.reply_message) % placeholders
209
253
  )
210
254
  logger.debug(f"Template message: {send_message}")
211
255
  elif self.openai_chat:
212
256
  try:
213
257
  ai_query = (
214
- f"Вакансия: {message_placeholders['vacancy_name']}\n"
258
+ f"Вакансия: {placeholders['vacancy_name']}\n"
215
259
  f"История переписки:\n"
216
260
  + "\n".join(message_history[-10:])
217
261
  + f"\n\nИнструкция: {self.pre_prompt}"
@@ -226,12 +270,8 @@ class Operation(BaseOperation):
226
270
  )
227
271
  continue
228
272
  else:
229
- print(
230
- "\n🏢",
231
- message_placeholders["employer_name"],
232
- "| 💼",
233
- message_placeholders["vacancy_name"],
234
- )
273
+ print("🏢", placeholders["employer_name"])
274
+ print("💼", placeholders["vacancy_name"])
235
275
  if salary:
236
276
  print(
237
277
  "💵 от",
@@ -251,9 +291,10 @@ class Operation(BaseOperation):
251
291
  print(msg)
252
292
 
253
293
  try:
254
- print("-" * 10)
294
+ print("-" * 40)
295
+ print("Активное резюме:", resume.get("title") or "")
255
296
  print(
256
- "Команды: /ban, /cancel <опционально сообщение для отмены>"
297
+ "/ban, /cancel необязательное сообщение для отмены"
257
298
  )
258
299
  send_message = input("Ваше сообщение: ").strip()
259
300
  except EOFError:
@@ -264,13 +305,12 @@ class Operation(BaseOperation):
264
305
  continue
265
306
 
266
307
  if send_message.startswith("/ban"):
267
- self.applicant_tool.storage.employers.save(employer)
268
308
  self.api_client.put(
269
309
  f"/employers/blacklisted/{employer['id']}"
270
310
  )
271
- blacklisted.append(employer["id"])
311
+ blacklist.add(employer["id"])
272
312
  print(
273
- "🚫 Работодатель в ЧС",
313
+ "🚫 Работодатель заблокирован",
274
314
  employer.get("alternate_url"),
275
315
  )
276
316
  continue
@@ -7,8 +7,8 @@ from typing import TYPE_CHECKING
7
7
 
8
8
  from prettytable import PrettyTable
9
9
 
10
+ from .. import utils
10
11
  from ..main import BaseNamespace, BaseOperation
11
- from ..utils import jsonutil
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from ..main import HHApplicantTool
@@ -28,7 +28,7 @@ class Namespace(BaseNamespace):
28
28
 
29
29
  def parse_value(v):
30
30
  try:
31
- return jsonutil.loads(v)
31
+ return utils.json.loads(v)
32
32
  except json.JSONDecodeError:
33
33
  return v
34
34
 
@@ -5,8 +5,7 @@ import argparse
5
5
  import logging
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- from ..api import ApiError
9
- from ..datatypes import PaginatedItems
8
+ from ..api import ApiError, datatypes
10
9
  from ..main import BaseNamespace, BaseOperation
11
10
  from ..utils import print_err
12
11
  from ..utils.string import shorten
@@ -31,8 +30,10 @@ class Operation(BaseOperation):
31
30
  pass
32
31
 
33
32
  def run(self, tool: HHApplicantTool) -> None:
34
- resumes: PaginatedItems = tool.get_resumes()
35
- for resume in resumes["items"]:
33
+ resumes: list[datatypes.Resume] = tool.get_resumes()
34
+ # Там вызов API меняет поля
35
+ # tool.storage.resumes.save_batch(resumes)
36
+ for resume in resumes:
36
37
  try:
37
38
  res = tool.api_client.post(
38
39
  f"/resumes/{resume['id']}/publish",
@@ -5,7 +5,7 @@ import argparse
5
5
  import logging
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- from .. import datatypes
8
+ from ..api import datatypes
9
9
  from ..main import BaseNamespace, BaseOperation
10
10
 
11
11
  if TYPE_CHECKING:
@@ -53,6 +53,6 @@ class Operation(BaseOperation):
53
53
  print(
54
54
  f"🆔 {result['id']} {full_name or 'Анонимный аккаунт'} "
55
55
  f"[ 📄 {counters['resumes_count']} "
56
- f"| 👁️ {fmt_plus(counters['new_resume_views'])} "
57
- f"| ✉️ {fmt_plus(counters['unread_negotiations'])} ]"
56
+ f"| 👁️ {fmt_plus(counters['new_resume_views'])} "
57
+ f"| ✉️ {fmt_plus(counters['unread_negotiations'])} ]"
58
58
  )
@@ -1,4 +1,8 @@
1
1
  from .facade import StorageFacade
2
2
  from .utils import apply_migration, list_migrations
3
3
 
4
- __all__ = ["StorageFacade", "apply_migration", "list_migrations"]
4
+ __all__ = [
5
+ "StorageFacade",
6
+ "apply_migration",
7
+ "list_migrations",
8
+ ]
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import sqlite3
4
4
 
5
- from .repositories.contacts import EmployerContactsRepository
5
+ from .repositories.contacts import VacancyContactsRepository
6
6
  from .repositories.employers import EmployersRepository
7
7
  from .repositories.negotiations import NegotiationRepository
8
8
  from .repositories.resumes import ResumesRepository
@@ -18,7 +18,7 @@ class StorageFacade:
18
18
  init_db(conn)
19
19
  self.employers = EmployersRepository(conn)
20
20
  self.vacancies = VacanciesRepository(conn)
21
- self.employer_contacts = EmployerContactsRepository(conn)
21
+ self.vacancy_contacts = VacancyContactsRepository(conn)
22
22
  self.negotiations = NegotiationRepository(conn)
23
23
  self.settings = SettingsRepository(conn)
24
24
  self.resumes = ResumesRepository(conn)
@@ -4,8 +4,8 @@ from datetime import datetime
4
4
  from logging import getLogger
5
5
  from typing import Any, Callable, Mapping, Self, dataclass_transform, get_origin
6
6
 
7
- from hh_applicant_tool.utils import jsonutil
8
- from hh_applicant_tool.utils.dateutil import try_parse_datetime
7
+ from hh_applicant_tool.utils import json
8
+ from hh_applicant_tool.utils.date import try_parse_datetime
9
9
 
10
10
  logger = getLogger(__package__)
11
11
 
@@ -13,12 +13,15 @@ MISSING = object()
13
13
 
14
14
 
15
15
  def mapped(
16
+ *,
17
+ skip_src: bool = False,
16
18
  path: str | None = None,
17
19
  transform: Callable[[Any], Any] | None = None,
18
20
  store_json: bool = False,
19
21
  **kwargs: Any,
20
22
  ):
21
23
  metadata = kwargs.get("metadata", {})
24
+ metadata.setdefault("skip_src", skip_src)
22
25
  metadata.setdefault("path", path)
23
26
  metadata.setdefault("transform", transform)
24
27
  metadata.setdefault("store_json", store_json)
@@ -48,7 +51,7 @@ class BaseModel:
48
51
  if value is MISSING:
49
52
  continue
50
53
  if f.metadata.get("store_json"):
51
- value = jsonutil.dumps(value)
54
+ value = json.dumps(value)
52
55
  # Точно не нужно типы приводить перед сохранением
53
56
  # else:
54
57
  # value = self._coerce_type(value, f)
@@ -89,6 +92,8 @@ class BaseModel:
89
92
  kwargs = {}
90
93
  for f in fields(cls):
91
94
  if from_source:
95
+ if f.metadata.get("skip_src") and f.name in data:
96
+ continue
92
97
  if path := f.metadata.get("path"):
93
98
  found = True
94
99
  v = data
@@ -118,7 +123,7 @@ class BaseModel:
118
123
  continue
119
124
 
120
125
  if f.metadata.get("store_json"):
121
- value = jsonutil.loads(value)
126
+ value = json.loads(value)
122
127
  else:
123
128
  value = cls._coerce_type(value, f)
124
129
 
@@ -0,0 +1,42 @@
1
+ import secrets
2
+
3
+ from .base import BaseModel, mapped
4
+
5
+
6
+ # Из вакансии извлекается
7
+ class VacancyContactsModel(BaseModel):
8
+ # При вызове from_api на вакансии нужно игнорировать ее id
9
+ id: str = mapped(
10
+ skip_src=True,
11
+ default_factory=lambda: secrets.token_hex(16),
12
+ )
13
+ vacancy_id: int = mapped(path="id")
14
+
15
+ vacancy_name: str = mapped(path="name")
16
+ vacancy_alternate_url: str = mapped(path="alternate_url", default=None)
17
+ vacancy_area_id: int = mapped(path="area.id", default=None)
18
+ vacancy_area_name: str = mapped(path="area.name", default=None)
19
+ vacancy_salary_from: int = mapped(path="salary.from", default=0)
20
+ vacancy_salary_to: int = mapped(path="salary.to", default=0)
21
+ vacancy_currency: str = mapped(path="salary.currency", default="RUR")
22
+ vacancy_gross: bool = mapped(path="salary.gross", default=False)
23
+
24
+ employer_id: int = mapped(path="employer.id", default=None)
25
+ employer_name: str = mapped(path="employer.name", default=None)
26
+ email: str = mapped(path="contacts.email")
27
+ name: str = mapped(path="contacts.name", default=None)
28
+ phone_numbers: str = mapped(
29
+ path="contacts.phones",
30
+ transform=lambda phones: ", ".join(
31
+ p["formatted"] for p in phones if p.get("number")
32
+ ),
33
+ default=None,
34
+ )
35
+
36
+ def __post_init__(self):
37
+ self.vacancy_salary_from = (
38
+ self.vacancy_salary_from or self.vacancy_salary_to or 0
39
+ )
40
+ self.vacancy_salary_to = (
41
+ self.vacancy_salary_to or self.vacancy_salary_from or 0
42
+ )
@@ -14,17 +14,29 @@ CREATE TABLE IF NOT EXISTS employers (
14
14
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
15
15
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
16
16
  );
17
- /* ===================== employer_contacts ===================== */
18
- CREATE TABLE IF NOT EXISTS employer_contacts (
19
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
20
- employer_id INTEGER NOT NULL,
21
- -- Просто поле, без REFERENCES
17
+ /* ===================== contacts ===================== */
18
+ CREATE TABLE IF NOT EXISTS vacancy_contacts (
19
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))) NOT NULL,
20
+ vacancy_id INTEGER NOT NULL,
21
+ -- Все это избыточные поля
22
+ vacancy_alternate_url TEXT,
23
+ vacancy_name TEXT,
24
+ vacancy_area_id INTEGER,
25
+ vacancy_area_name TEXT,
26
+ vacancy_salary_from INTEGER,
27
+ vacancy_salary_to INTEGER,
28
+ vacancy_currency VARCHAR(3),
29
+ vacancy_gross BOOLEAN,
30
+ --
31
+ employer_id INTEGER,
32
+ employer_name TEXT,
33
+ --
22
34
  name TEXT,
23
35
  email TEXT,
24
36
  phone_numbers TEXT NOT NULL,
25
37
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
26
38
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
27
- UNIQUE (employer_id, email)
39
+ UNIQUE (vacancy_id, email)
28
40
  );
29
41
  /* ===================== vacancies ===================== */
30
42
  CREATE TABLE IF NOT EXISTS vacancies (
@@ -49,7 +61,8 @@ CREATE TABLE IF NOT EXISTS negotiations (
49
61
  id INTEGER PRIMARY KEY,
50
62
  state TEXT NOT NULL,
51
63
  vacancy_id INTEGER NOT NULL,
52
- employer_id INTEGER NOT NULL,
64
+ employer_id INTEGER,
65
+ -- Может обнулиться при блокировке раб-о-тодателя
53
66
  chat_id INTEGER NOT NULL,
54
67
  resume_id TEXT,
55
68
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -95,10 +108,10 @@ UPDATE employers
95
108
  SET updated_at = CURRENT_TIMESTAMP
96
109
  WHERE id = OLD.id;
97
110
  END;
98
- CREATE TRIGGER IF NOT EXISTS trg_employer_contacts_updated
111
+ CREATE TRIGGER IF NOT EXISTS trg_vacancy_contacts_updated
99
112
  AFTER
100
- UPDATE ON employer_contacts BEGIN
101
- UPDATE employer_contacts
113
+ UPDATE ON vacancy_contacts BEGIN
114
+ UPDATE vacancy_contacts
102
115
  SET updated_at = CURRENT_TIMESTAMP
103
116
  WHERE id = OLD.id;
104
117
  END;