The problem
Support tickets live in Movidesk, but the team tracked equipment and "who has what" on a Notion board that was updated by hand. Every ticket that touched a piece of equipment had to be copied across; tickets that came in with no assignee were easy to miss; and the equipment status (busy / free) drifted out of date within a day.
What it does, each run
- Pull every open ticket from Movidesk, paging through all results.
- For a ticket with an assignee and a linked asset, create or update its Notion page.
- For a ticket that is unassigned, has no asset and matches a keyword (notebook, meeting room, microphone, …) → send one Telegram message.
- Archive Notion pages whose ticket has been closed or has left Movidesk.
A second mode (--mode equipamentos) reuses the same ticket data to mark each asset Ocupado / Disponível.
Decisions & trade-offs
- Idempotent sync, not a one-way push
- Each run first indexes the Notion side by ticket number, then decides create vs update vs archive. Running it twice changes nothing the second time; a closed ticket's card is archived, not left behind.
- Alert once, with a tiny state file
- A
NotifiedStorekeeps a set of tags in a JSON file. Without it, every run would re-ping Telegram about the same ticket. A corrupt file just starts fresh. - I/O passed in as dependencies
sync.run()takes the Notion client, the Telegram sender and the state store as arguments. That is the whole reason it has 16 tests that drive it with fakes and assert the create / update / archive / alert behaviour — no network, no mocking ofrequests.- Retry + backoff at the session level
- Movidesk and Notion both return
429under load. Onerequests.Sessionwith aurllib3retry policy handles that for every call, instead of a try/except around each request. - Config — including the keyword list — from the environment
- Which tickets "matter" is a moving target.
KEYWORDSis an env var, so tuning it never needs a code change or a redeploy. - A
--dry-runflag - A job that writes to a shared team board should let you see the plan first. Dry-run logs every create / update / archive it would do and touches nothing.
Where it landed
- The manual copy step is gone; the board reflects Movidesk on every run.
- Unassigned tickets that match a keyword surface in Telegram within one run.
- 16 pytest tests over the pure helpers, the state store and the orchestration; CI on every push.
- Refactored from one 250-line dated script into a small package (
config,http_client,movidesk,notion,telegram,state,tickets,sync).
Stack
- Python 3.12
- requests (Session + urllib3 Retry)
- python-dotenv
- Movidesk REST API
- Notion API
- Telegram Bot API
- pytest
- GitHub Actions
What I'd do next
Validate the Notion database's property schema on startup (fail fast with a clear message instead of a 400 mid-run); structured logging to a file for the scheduled runs; and package it so it installs with pip and runs as a console command.