Spaces:
Running
Running
Fix paid personal invoice client storage
Browse files
db.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
| 1 |
-
import os
|
| 2 |
-
|
| 3 |
-
from
|
|
|
|
| 4 |
|
| 5 |
import psycopg2
|
| 6 |
from psycopg2.extras import RealDictCursor
|
| 7 |
|
| 8 |
-
DATABASE_URL = os.environ.get("NEON_DATABASE_URL")
|
|
|
|
| 9 |
|
| 10 |
if not DATABASE_URL:
|
| 11 |
raise RuntimeError(
|
|
@@ -125,37 +127,50 @@ def update_business_logo(account_id: int, mime: Optional[str], data_base64: Opti
|
|
| 125 |
|
| 126 |
def upsert_client(account_id: int, payload: Dict[str, str]) -> int:
|
| 127 |
tax_id = (payload.get("tax_id") or "").strip()
|
| 128 |
-
stored_tax_id = tax_id or None
|
| 129 |
if tax_id:
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
"""
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
""",
|
| 135 |
-
(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
)
|
| 137 |
-
|
| 138 |
-
client_id = row["id"]
|
| 139 |
-
execute(
|
| 140 |
-
"""
|
| 141 |
-
UPDATE clients
|
| 142 |
-
SET name = %s,
|
| 143 |
-
address_line = %s,
|
| 144 |
-
postal_code = %s,
|
| 145 |
-
city = %s,
|
| 146 |
-
phone = %s
|
| 147 |
-
WHERE id = %s
|
| 148 |
-
""",
|
| 149 |
-
(
|
| 150 |
-
payload["name"],
|
| 151 |
-
payload["address_line"],
|
| 152 |
-
payload["postal_code"],
|
| 153 |
-
payload["city"],
|
| 154 |
-
payload.get("phone"),
|
| 155 |
-
client_id,
|
| 156 |
-
),
|
| 157 |
-
)
|
| 158 |
-
return client_id
|
| 159 |
|
| 160 |
with db_conn() as conn, conn.cursor() as cur:
|
| 161 |
cur.execute(
|
|
@@ -182,7 +197,9 @@ def search_clients(account_id: int, term: str, limit: int = 10) -> List[Dict[str
|
|
| 182 |
like = f"%{query}%"
|
| 183 |
return fetch_all(
|
| 184 |
"""
|
| 185 |
-
SELECT name,
|
|
|
|
|
|
|
| 186 |
FROM clients
|
| 187 |
WHERE account_id = %s
|
| 188 |
AND (
|
|
@@ -193,7 +210,7 @@ def search_clients(account_id: int, term: str, limit: int = 10) -> List[Dict[str
|
|
| 193 |
ORDER BY LOWER(COALESCE(name, tax_id, '')) ASC
|
| 194 |
LIMIT %s
|
| 195 |
""",
|
| 196 |
-
(account_id, query, like, like, limit),
|
| 197 |
)
|
| 198 |
|
| 199 |
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import hashlib
|
| 3 |
+
from contextlib import contextmanager
|
| 4 |
+
from typing import Any, Dict, List, Optional, Sequence
|
| 5 |
|
| 6 |
import psycopg2
|
| 7 |
from psycopg2.extras import RealDictCursor
|
| 8 |
|
| 9 |
+
DATABASE_URL = os.environ.get("NEON_DATABASE_URL")
|
| 10 |
+
PRIVATE_TAX_ID_PREFIX = "__PRIVATE__:"
|
| 11 |
|
| 12 |
if not DATABASE_URL:
|
| 13 |
raise RuntimeError(
|
|
|
|
| 127 |
|
| 128 |
def upsert_client(account_id: int, payload: Dict[str, str]) -> int:
|
| 129 |
tax_id = (payload.get("tax_id") or "").strip()
|
|
|
|
| 130 |
if tax_id:
|
| 131 |
+
stored_tax_id = tax_id
|
| 132 |
+
else:
|
| 133 |
+
identity = "|".join(
|
| 134 |
+
[
|
| 135 |
+
str(account_id),
|
| 136 |
+
(payload.get("name") or "").strip().lower(),
|
| 137 |
+
(payload.get("address_line") or "").strip().lower(),
|
| 138 |
+
(payload.get("postal_code") or "").strip().lower(),
|
| 139 |
+
(payload.get("city") or "").strip().lower(),
|
| 140 |
+
(payload.get("phone") or "").strip().lower(),
|
| 141 |
+
]
|
| 142 |
+
)
|
| 143 |
+
stored_tax_id = f"{PRIVATE_TAX_ID_PREFIX}{hashlib.sha1(identity.encode('utf-8')).hexdigest()[:20]}"
|
| 144 |
+
|
| 145 |
+
row = fetch_one(
|
| 146 |
+
"""
|
| 147 |
+
SELECT id FROM clients
|
| 148 |
+
WHERE account_id = %s AND tax_id = %s
|
| 149 |
+
""",
|
| 150 |
+
(account_id, stored_tax_id),
|
| 151 |
+
)
|
| 152 |
+
if row:
|
| 153 |
+
client_id = row["id"]
|
| 154 |
+
execute(
|
| 155 |
"""
|
| 156 |
+
UPDATE clients
|
| 157 |
+
SET name = %s,
|
| 158 |
+
address_line = %s,
|
| 159 |
+
postal_code = %s,
|
| 160 |
+
city = %s,
|
| 161 |
+
phone = %s
|
| 162 |
+
WHERE id = %s
|
| 163 |
""",
|
| 164 |
+
(
|
| 165 |
+
payload["name"],
|
| 166 |
+
payload["address_line"],
|
| 167 |
+
payload["postal_code"],
|
| 168 |
+
payload["city"],
|
| 169 |
+
payload.get("phone"),
|
| 170 |
+
client_id,
|
| 171 |
+
),
|
| 172 |
)
|
| 173 |
+
return client_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
with db_conn() as conn, conn.cursor() as cur:
|
| 176 |
cur.execute(
|
|
|
|
| 197 |
like = f"%{query}%"
|
| 198 |
return fetch_all(
|
| 199 |
"""
|
| 200 |
+
SELECT name,
|
| 201 |
+
CASE WHEN tax_id LIKE %s THEN '' ELSE tax_id END AS tax_id,
|
| 202 |
+
address_line, postal_code, city, phone
|
| 203 |
FROM clients
|
| 204 |
WHERE account_id = %s
|
| 205 |
AND (
|
|
|
|
| 210 |
ORDER BY LOWER(COALESCE(name, tax_id, '')) ASC
|
| 211 |
LIMIT %s
|
| 212 |
""",
|
| 213 |
+
(f"{PRIVATE_TAX_ID_PREFIX}%", account_id, query, like, like, limit),
|
| 214 |
)
|
| 215 |
|
| 216 |
|
server.py
CHANGED
|
@@ -31,7 +31,8 @@ DATA_FILE = DATA_DIR / "web_invoice_store.json"
|
|
| 31 |
INVOICE_HISTORY_LIMIT = 200
|
| 32 |
MAX_LOGO_SIZE = 512 * 1024 # 512 KB
|
| 33 |
TOKEN_TTL = timedelta(hours=12)
|
| 34 |
-
ALLOWED_LOGO_MIME_TYPES = {"image/png", "image/jpeg"}
|
|
|
|
| 35 |
|
| 36 |
DATABASE_AVAILABLE = bool(os.environ.get("NEON_DATABASE_URL"))
|
| 37 |
|
|
@@ -503,11 +504,18 @@ def api_logo() -> Any:
|
|
| 503 |
save_store(data)
|
| 504 |
return jsonify({"logo": stored_logo})
|
| 505 |
|
| 506 |
-
def normalize_phone(phone: Optional[str]) -> Optional[str]:
|
| 507 |
if not phone:
|
| 508 |
return None
|
| 509 |
-
digits = re.sub(r"[^\d+]", "", phone)
|
| 510 |
-
return digits or None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
|
| 512 |
|
| 513 |
def validate_client(payload: Dict[str, Any]) -> Dict[str, str]:
|
|
@@ -750,7 +758,7 @@ def api_invoices() -> Any:
|
|
| 750 |
"address_line": row.get("client_address"),
|
| 751 |
"postal_code": row.get("client_postal_code"),
|
| 752 |
"city": row.get("client_city"),
|
| 753 |
-
"tax_id": row.get("client_tax_id"),
|
| 754 |
"phone": row.get("client_phone"),
|
| 755 |
}
|
| 756 |
invoices.append(
|
|
|
|
| 31 |
INVOICE_HISTORY_LIMIT = 200
|
| 32 |
MAX_LOGO_SIZE = 512 * 1024 # 512 KB
|
| 33 |
TOKEN_TTL = timedelta(hours=12)
|
| 34 |
+
ALLOWED_LOGO_MIME_TYPES = {"image/png", "image/jpeg"}
|
| 35 |
+
PRIVATE_TAX_ID_PREFIX = "__PRIVATE__:"
|
| 36 |
|
| 37 |
DATABASE_AVAILABLE = bool(os.environ.get("NEON_DATABASE_URL"))
|
| 38 |
|
|
|
|
| 504 |
save_store(data)
|
| 505 |
return jsonify({"logo": stored_logo})
|
| 506 |
|
| 507 |
+
def normalize_phone(phone: Optional[str]) -> Optional[str]:
|
| 508 |
if not phone:
|
| 509 |
return None
|
| 510 |
+
digits = re.sub(r"[^\d+]", "", phone)
|
| 511 |
+
return digits or None
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def public_tax_id(value: Optional[str]) -> str:
|
| 515 |
+
tax_id = (value or "").strip()
|
| 516 |
+
if tax_id.startswith(PRIVATE_TAX_ID_PREFIX):
|
| 517 |
+
return ""
|
| 518 |
+
return tax_id
|
| 519 |
|
| 520 |
|
| 521 |
def validate_client(payload: Dict[str, Any]) -> Dict[str, str]:
|
|
|
|
| 758 |
"address_line": row.get("client_address"),
|
| 759 |
"postal_code": row.get("client_postal_code"),
|
| 760 |
"city": row.get("client_city"),
|
| 761 |
+
"tax_id": public_tax_id(row.get("client_tax_id")),
|
| 762 |
"phone": row.get("client_phone"),
|
| 763 |
}
|
| 764 |
invoices.append(
|