Initial scaffold: Flask/SQLite backend, React/Vite PWA frontend
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.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""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"]
|
||||
Reference in New Issue
Block a user