Workflow-App/Workflow/scripts/README.md

163 lines
4.1 KiB
Markdown
Raw Permalink Normal View History

# BizPortal 2.0 — Python Playwright service
Sidecar that drives `https://www.bizportal.gov.za` via a real headless
Chromium. The Next.js side talks to it over HTTP — no LLM agent, no
file queue, no polling. Sits next to the existing `/bizportal` page
without touching it.
## Why this exists
The existing `/bizportal` page queues searches to `/tmp/bizportal_queue/`
and an LLM agent reads the page snapshot, decides what to click, and
writes the result back. Fast enough when it works, but the agent is
the brittle bit — it misreads the page, times out, or clicks the
wrong button. This service replaces that with a deterministic Python
script.
## Architecture
```
Browser ─► /bizportal-2 ─► /api/bizportal-2/* ─► this service ─► BizPortal
│
└─► PocketBase (via service)
```
The Python service holds a persistent `storage_state.json` so the
user is logged in once and every subsequent request reuses the
session.
## Setup
```bash
cd scripts
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
playwright install chromium
cp .env.example .env # fill in real credentials
```
## Run
```bash
# Dev
uvicorn bizportal_service:app --host 127.0.0.1 --port 7001 --reload
# Production (Coolify / systemd)
uvicorn bizportal_service:app --host 127.0.0.1 --port 7001 --workers 1
```
Keep `--workers 1` — the script uses a single shared browser context
to preserve the logged-in session.
## Endpoints
### `GET /health`
```json
{
"ok": true,
"service": "bizportal-2",
"playwright_ready": true,
"logged_in_cached": true,
"headless": true
}
```
### `POST /search`
```json
{ "type": "ck", "query": "2022/808158/08" }
```
```json
{
"ok": true,
"query": "2022/808158/08",
"type": "ck",
"results": [
{
"enterprise_no": "2022/808158/08",
"enterprise_name": "AMPLEFORTH TRADING (PTY) LTD",
"enterprise_type": "Private Company",
"enterprise_status": "Business is in Business",
"directors": [
{
"id_number": "870922XXXX08X",
"names": "DAVID",
"surname": "ELS",
"status": "Active",
"type": "Director"
}
]
}
],
"log": ["Navigating to login", "..."]
}
```
### `POST /reserve`
```json
{ "names": ["AMPLEFORTH PTY LTD", "AMPLEFORTH TRADING", "AMPLEFORTH SA", "AMPLEFORTH HOLDINGS"], "dry_run": false }
```
```json
{
"ok": true,
"balance_before": 412.50,
"status": "bizportal_done",
"log": ["..."],
"message": "Submitted via Bizportal. The R50 fee will be debited later."
}
```
## Tunable selectors
BizPortal uses ASP.NET WebForms (`cntMain_*` IDs). The known-stable
IDs for login and name reservation are baked in. The **search** field
IDs are not 100% verified — try a search after starting the service
once and tune the `DIRECTOR_SEARCH_FIELD_CANDIDATES` /
`ENTERPRISE_SEARCH_FIELD_CANDIDATES` lists at the top of
`bizportal_service.py` if you get a "Could not find …" error.
The page URL is logged in the error so you can inspect the live page
in your browser and grab the right field ID.
## Resetting the session
```bash
rm scripts/storage_state.json
# Restart the service — it will log in once and re-cache the session.
```
## Running as a systemd service
```ini
[Unit]
Description=BizPortal 2.0 Python service
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/workflow/scripts
EnvironmentFile=/var/www/workflow/scripts/.env
ExecStart=/var/www/workflow/scripts/.venv/bin/uvicorn bizportal_service:app --host 127.0.0.1 --port 7001 --workers 1
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
```
## What this does NOT replace
- The existing `/bizportal` page (kept untouched)
- The `/api/bizportal/search/*` queue routes (kept untouched)
- The `/api/register/[id]/apply-bizportal` route (kept untouched)
- The PocketBase collections and bulk-add / link logic
If you decide to retire the old flow, see
`scripts/README.rollback.md` (TODO if you want one).