Backend: receipt upload -> LLM vision extraction (OpenAI-compatible, provider-agnostic), item review/edit, per-group splitting against Cospend projects/members, highlight+upload via WebDAV, public share link, bill creation via Cospend's OCS API (verified against real source, not just doc summaries). Frontend: capture -> review -> group -> summary flow as an installable PWA. install.sh / run.sh (venv + npm, tmux session) instead of Docker, per the ~/Projects/gain pattern.
100 lines
2.9 KiB
Python
100 lines
2.9 KiB
Python
"""Cospend's authenticated (NC-login) API - same app-password auth as
|
|
nc_client. This is an OCS API (same family as the Share API), confirmed
|
|
against the real routes/controller source in julien-nc/cospend-nc:
|
|
|
|
appinfo/routes.php ('ocs' section):
|
|
GET /api/{v}/projects -> api#getLocalProjects
|
|
GET /api/{v}/projects/{projectId}/members -> api#getMembers
|
|
POST /api/{v}/projects/{projectId}/bills -> api#createBill
|
|
|
|
lib/Controller/ApiController.php, createBill() signature:
|
|
string $projectId, ?string $date, ?string $what, ?int $payer,
|
|
?string $payedFor, ?float $amount, ... ?string $comment, ...
|
|
-> params are camelCase (payedFor, not payed_for), payer/payedFor are
|
|
numeric member ids (payedFor is a comma-separated string of ids).
|
|
|
|
Being an OCS controller, every call needs the OCS-APIRequest header and the
|
|
full path is under /ocs/v2.php/apps/cospend/...
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import requests
|
|
|
|
from .config import Config
|
|
|
|
_HEADERS = {"OCS-APIRequest": "true"}
|
|
|
|
|
|
def _auth() -> tuple[str, str]:
|
|
return (Config.NC_USERNAME, Config.NC_APP_PASSWORD)
|
|
|
|
|
|
def _base(project_id: str | None = None) -> str:
|
|
root = f"{Config.NC_BASE_URL}/ocs/v2.php/apps/cospend/api/v1"
|
|
if project_id is None:
|
|
return root
|
|
return f"{root}/projects/{project_id}"
|
|
|
|
|
|
def get_projects() -> list[dict[str, Any]]:
|
|
"""Lists Cospend projects visible to the authenticated user."""
|
|
resp = requests.get(
|
|
f"{_base()}/projects",
|
|
auth=_auth(),
|
|
headers=_HEADERS,
|
|
params={"format": "json"},
|
|
timeout=15,
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()["ocs"]["data"]
|
|
|
|
|
|
def get_members(project_id: str) -> list[dict[str, Any]]:
|
|
resp = requests.get(
|
|
f"{_base(project_id)}/members",
|
|
auth=_auth(),
|
|
headers=_HEADERS,
|
|
params={"format": "json"},
|
|
timeout=15,
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()["ocs"]["data"]
|
|
|
|
|
|
def create_bill(
|
|
*,
|
|
project_id: str,
|
|
what: str,
|
|
amount: float,
|
|
payer_id: int,
|
|
ower_ids: list[int],
|
|
date: str,
|
|
comment: str = "",
|
|
) -> int:
|
|
"""Creates a bill split evenly among `ower_ids`, paid by `payer_id`.
|
|
|
|
`amount` is the bill total; Cospend divides it across owers itself.
|
|
Returns the new bill's id - createBill's controller method returns just
|
|
the int id (`return new DataResponse($newBillId)`), not a bill object.
|
|
"""
|
|
resp = requests.post(
|
|
f"{_base(project_id)}/bills",
|
|
auth=_auth(),
|
|
headers=_HEADERS,
|
|
params={"format": "json"},
|
|
json={
|
|
"what": what,
|
|
"amount": amount,
|
|
"payer": payer_id,
|
|
"payedFor": ",".join(str(i) for i in ower_ids),
|
|
"comment": comment,
|
|
"date": date, # YYYY-MM-DD, the receipt's issue date, not today
|
|
},
|
|
timeout=15,
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()["ocs"]["data"]
|