feat(5.16+5.17): tipografie/antet branded + tipuri cont, planuri si enforcement
PRD 5.16 — propagare design finalizata (system font stack, fara IBM Plex self-hostat): - US-001/002/008: tokeni --font-ui/--font-mono (system stack) + scala --fs-*; zero @font-face si zero /static/fonts/; landing aliniat la acelasi stack - US-003: RAR online = dot compact in antet + meniu burger; banda rosie DOAR pe blocat (invariant zero-silent-failures pastrat) - US-010: antet "ROMFAST AUTOPASS" + nume service + /login brandeit 2 coloane + badge plan; meniu burger cu separatoare; gate strict pe is_authenticated - US-011: selector tema pill icon+eticheta (reuse THEMES) - US-004/005/006/007: bug-fix editor prestatii (picker cod+denumire, add_extra in mod operatii, cod ales se salveaza fara "+", Renunta inchide via closest) - US-012/013: landing Autentificare->/login; wizard import colapsat + 4 pasi pe tokeni - fix VERIFY E2E: contoare duplicate pe 390px (inline display:flex batea @media) -> CSS + test-lock PRD 5.17 — tipuri de cont + trial Pro 30z + enforcement DUR: - US-001/002/008: accounts.tier + trial_until (migrare aditiva defensiva); app/plans.py sursa unica (PLANS, FREE_MONTHLY_LIMIT=60, effective_tier(now injectabil), monthly_usage, CONSUMED_STATUSES); create_account trial Pro 30z; CLI set-tier (protejat id=1, audit) - US-003/004/005: enforce volum 60/luna INAINTE de build_key pe ambele canale (PLAN_LIMITA_LUNARA, 3 niveluri + log_event); gate API Pro+ (PLAN_FARA_API 403 actionabil); valideaza/nomenclator raman permise; downgrade lazy; flag AUTOPASS_ENFORCE_PLANS (kill-switch) - US-006: badge plan antet + linie burger + consum N/60 + warn>=80% + 6 stari + copy RO pluralizat + banner one-time trial->Gratuit + pagina Cont Regresie: 1380 passed, 0 failed, 1 deselected (live). E2E browser pe 390/1280 confirmat. Backend trimitere (worker/masina stari/idempotenta/contract RAR) NEATINS. Lucrul 5.18 (corpus kNN) ramane separat, necomis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,8 +29,10 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from ... import errors
|
||||
from ...auth import resolve_account_id
|
||||
from ...auth import require_api_access, resolve_account_id
|
||||
from ...crypto import decrypt_creds, encrypt_creds
|
||||
from ...db import get_connection
|
||||
from ...idempotency import build_key, canonicalize_row
|
||||
@@ -413,7 +415,7 @@ def _already_sent_lookup(conn, account_id: int, keys: list[str]) -> dict[str, di
|
||||
async def upload_import(
|
||||
file: UploadFile,
|
||||
sheet_name: str | None = None,
|
||||
account_id: int = Depends(resolve_account_id),
|
||||
account_id: int = Depends(require_api_access),
|
||||
) -> dict:
|
||||
"""Upload fisier xlsx/csv -> staging in import_batches/import_rows.
|
||||
|
||||
@@ -934,7 +936,7 @@ class CommitIn(BaseModel):
|
||||
def commit_import(
|
||||
import_id: int,
|
||||
req: CommitIn,
|
||||
account_id: int = Depends(resolve_account_id),
|
||||
account_id: int = Depends(require_api_access),
|
||||
) -> dict:
|
||||
"""Gate HARD confirmare + enqueue randuri ok + log atestare.
|
||||
|
||||
@@ -1022,6 +1024,48 @@ def commit_import(
|
||||
if n_total_ok == 0:
|
||||
raise HTTPException(status_code=422, detail="Niciun rand ok de confirmat.")
|
||||
|
||||
# T3 (PRD 5.17): enforce volum plan — INAINTE de enqueue (invariant idempotenta).
|
||||
# Decizie #21: respingere TOTALA a lotului (nu enqueue partial tacut).
|
||||
from ...config import get_settings as _get_settings
|
||||
from ...plans import PLANS, effective_tier, monthly_usage
|
||||
from ...observ import log_event as _log_event_plan
|
||||
_settings = _get_settings()
|
||||
if _settings.enforce_plans:
|
||||
_acct_row = conn.execute(
|
||||
"SELECT tier, trial_until FROM accounts WHERE id=?", (acct,)
|
||||
).fetchone()
|
||||
_now = datetime.now(timezone.utc)
|
||||
_et = effective_tier(_acct_row, _now)
|
||||
_plan_limit = PLANS[_et].get("monthly_limit")
|
||||
if _plan_limit is not None:
|
||||
_usage = monthly_usage(conn, acct, _now)
|
||||
if _usage + n_total_ok > _plan_limit:
|
||||
_remaining = max(0, _plan_limit - _usage)
|
||||
_log_event_plan(
|
||||
"plan_limita_lunara_atinsa",
|
||||
account_id=acct,
|
||||
nivel="WARNING",
|
||||
mesaj=f"Import de {n_total_ok} respins (usage={_usage}, limita={_plan_limit})",
|
||||
context={
|
||||
"n_to_enqueue": n_total_ok, "usage": _usage,
|
||||
"plan_limit": _plan_limit, "tier": _et,
|
||||
},
|
||||
conn=conn,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail={
|
||||
"error": "plan_limita_lunara",
|
||||
**errors.eroare(
|
||||
"PLAN_LIMITA_LUNARA",
|
||||
cauza=(
|
||||
f"Ai trimis {_usage}/{_plan_limit} prezentari luna aceasta;"
|
||||
f" mai poti trimite {_remaining}."
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
# Incarca maparea de coloane pentru a construi payload-ul
|
||||
first_row_db = conn.execute(
|
||||
"SELECT raw_json FROM import_rows WHERE batch_id=? ORDER BY row_index LIMIT 1",
|
||||
|
||||
@@ -13,11 +13,13 @@ import csv
|
||||
import io
|
||||
import json
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ...auth import resolve_account_id
|
||||
from ...auth import require_api_access, resolve_account_id
|
||||
from ...crypto import encrypt_creds
|
||||
from ...db import get_connection
|
||||
from ...errors import eroare as err_eroare
|
||||
@@ -135,7 +137,7 @@ def _rezultat_respins(submission_id: int | None, cl: dict) -> SubmissionResult:
|
||||
@router.post("/prezentari", response_model=PrezentariResponse)
|
||||
def create_prezentari(
|
||||
req: PrezentareRequest,
|
||||
account_id: int = Depends(resolve_account_id),
|
||||
account_id: int = Depends(require_api_access),
|
||||
) -> PrezentariResponse:
|
||||
"""Enqueue una/mai multe prezentari. Idempotent: continut identic -> acelasi submission.
|
||||
|
||||
@@ -165,6 +167,46 @@ def create_prezentari(
|
||||
# Reguli text incarcate o data per cerere (seam partajat cu dry-run).
|
||||
text_rules = load_text_rules(conn, acct)
|
||||
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
|
||||
|
||||
# T3 (PRD 5.17): enforce volum plan — INAINTE de build_key/enqueue (invariant idempotenta).
|
||||
# Decizie #21: respingere TOTALA a lotului (nu enqueue partial tacut).
|
||||
from ...config import get_settings as _get_settings
|
||||
from ...plans import PLANS, effective_tier, monthly_usage
|
||||
_settings = _get_settings()
|
||||
if _settings.enforce_plans:
|
||||
_acct_row = conn.execute(
|
||||
"SELECT tier, trial_until FROM accounts WHERE id=?", (acct,)
|
||||
).fetchone()
|
||||
_now = datetime.now(timezone.utc)
|
||||
_et = effective_tier(_acct_row, _now)
|
||||
_plan_limit = PLANS[_et].get("monthly_limit")
|
||||
if _plan_limit is not None:
|
||||
_usage = monthly_usage(conn, acct, _now)
|
||||
_nr_cerut = len(req.prezentari)
|
||||
if _usage + _nr_cerut > _plan_limit:
|
||||
_remaining = max(0, _plan_limit - _usage)
|
||||
log_event(
|
||||
"plan_limita_lunara_atinsa",
|
||||
account_id=acct,
|
||||
nivel="WARNING",
|
||||
mesaj=f"Lot de {_nr_cerut} respins (usage={_usage}, limita={_plan_limit})",
|
||||
context={
|
||||
"nr_cerut": _nr_cerut, "usage": _usage,
|
||||
"plan_limit": _plan_limit, "tier": _et,
|
||||
},
|
||||
conn=conn,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=err_eroare(
|
||||
"PLAN_LIMITA_LUNARA",
|
||||
cauza=(
|
||||
f"Ai trimis {_usage}/{_plan_limit} prezentari luna aceasta;"
|
||||
f" mai poti trimite {_remaining}."
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
for prez in req.prezentari:
|
||||
content = prez.model_dump()
|
||||
# canonicalize_row inaintea build_key (odometru strip ".0", VIN upper).
|
||||
|
||||
Reference in New Issue
Block a user