How I kept the system's brain and replaced its transport
After the job seeker API closed, it became clear what the project actually rests on.
If its logic had been sewn to one way of accessing HH, the system would have had to be thrown away. But by then the project already had its own internal frame. So what had to change wasn’t the meaning, only the transport.
You can see this in the models through which the project describes its work:
class VacancyCard:
vacancy_id: str
title: str
company: str
url: str
class VacancyDetails:
vacancy_id: str
title: str
company: str
description_text: str
apply_available: bool
class ApplyDraft:
vacancy_id: str
resume_id: str
cover_letter: str
class ApplyResult:
vacancy_id: str
success: bool
status: str
class PrepareApplyOutcome(str, Enum):
PREPARED_DRAFT = "PREPARED_DRAFT"
NEEDS_HUMAN = "NEEDS_HUMAN"
SKIP = "SKIP"
RETRYABLE_TRANSIENT = "RETRYABLE_TRANSIENT"
RATE_LIMITED_EXTERNAL = "RATE_LIMITED_EXTERNAL"
VacancyCard is a short model of a vacancy at the level of search results. vacancy_id is the internal anchor through which the system links search, application, logs and duplicate protection. title is the vacancy name that classification and relevance logic then work with. company is the employer, so the project understands the context and records where the application is going. url is the vacancy link through which the transport opens the card in the browser.
VacancyDetails is the same vacancy, but after it has been fully opened and its contents extracted. description_text is the full vacancy text on which analysis, the letter and answers to questions are built. apply_available is a flag that immediately separates a live vacancy from one no longer open for applications.
ApplyDraft is no longer a vacancy but a prepared draft of a specific application. resume_id states which resume the system should use to answer this vacancy. cover_letter is the finished cover letter text, assembled for the specific position, not taken from a template.
ApplyResult is the final record of how an application attempt ended. success is a binary outcome that quickly separates a successful pass from a failed one. status is a more precise layer of the result, so the system distinguishes not just success and failure but the specific outcome.
PrepareApplyOutcome is the most important part, because here the system distinguishes types of states before the final submit. PREPARED_DRAFT: the application draft is assembled and the system can move on. NEEDS_HUMAN: the page or form needs a manual decision and must not break the whole pipeline. SKIP: the vacancy is deliberately discarded as irrelevant or unsuitable. RETRYABLE_TRANSIENT: a temporary technical failure happened, after which it can try again. RATE_LIMITED_EXTERNAL: HH’s external circuit has started limiting the process, and the system needs a cooldown, not aggression.
The whole value of this code is that it describes not an integration but the project’s inner world: entities, stages and outcomes. So after the API closed, the project didn’t have to be rebuilt from scratch: the old transport disappeared, but the logic, the models and the state remained.