feat: add customer profile, VAT alignment, and fix shop auth
Customer Profile: - Add profile API (GET/PUT /api/v1/shop/profile) - Add password change endpoint (PUT /api/v1/shop/profile/password) - Implement full profile page with preferences and password sections - Add CustomerPasswordChange schema Shop Authentication Fixes: - Add Authorization header to all shop account API calls - Fix orders, order-detail, messages pages authentication - Add proper redirect to login on 401 responses - Fix toast message showing noqa comment in shop-layout.js VAT Calculation: - Add shared VAT utility (app/utils/vat.py) - Add VAT fields to Order model (vat_regime, vat_rate, etc.) - Align order VAT calculation with invoice settings - Add migration for VAT fields on orders Validation Framework: - Fix base_validator.py with missing methods - Add validate_file, output_results, get_exit_code methods - Fix validate_all.py import issues Documentation: - Add launch-readiness.md tracking OMS status - Update to 95% feature complete 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,21 +5,78 @@ Shared functionality for all validators.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class Severity(str, Enum):
|
||||
"""Severity levels for validation findings."""
|
||||
ERROR = "error"
|
||||
WARNING = "warning"
|
||||
INFO = "info"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Violation:
|
||||
"""A single validation violation."""
|
||||
rule_id: str
|
||||
message: str
|
||||
severity: Severity
|
||||
file_path: str = ""
|
||||
line: int = 0
|
||||
suggestion: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
"""Result of a validation run."""
|
||||
violations: list[Violation] = field(default_factory=list)
|
||||
files_checked: int = 0
|
||||
|
||||
def has_errors(self) -> bool:
|
||||
"""Check if there are any error-level violations."""
|
||||
return any(v.severity == Severity.ERROR for v in self.violations)
|
||||
|
||||
def error_count(self) -> int:
|
||||
"""Count error-level violations."""
|
||||
return sum(1 for v in self.violations if v.severity == Severity.ERROR)
|
||||
|
||||
def warning_count(self) -> int:
|
||||
"""Count warning-level violations."""
|
||||
return sum(1 for v in self.violations if v.severity == Severity.WARNING)
|
||||
|
||||
def info_count(self) -> int:
|
||||
"""Count info-level violations."""
|
||||
return sum(1 for v in self.violations if v.severity == Severity.INFO)
|
||||
|
||||
|
||||
class BaseValidator(ABC):
|
||||
"""Base class for architecture, security, and performance validators."""
|
||||
|
||||
def __init__(self, rules_dir: str, project_root: Path | None = None):
|
||||
# Directories/patterns to ignore by default
|
||||
IGNORE_PATTERNS = [
|
||||
".venv", "venv", "node_modules", "__pycache__", ".git",
|
||||
".pytest_cache", ".mypy_cache", "dist", "build", "*.egg-info",
|
||||
"migrations", "alembic/versions", ".tox", "htmlcov",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rules_dir: str = "",
|
||||
project_root: Path | None = None,
|
||||
verbose: bool = False,
|
||||
):
|
||||
self.rules_dir = rules_dir
|
||||
self.project_root = project_root or Path.cwd()
|
||||
self.verbose = verbose
|
||||
self.rules: list[dict[str, Any]] = []
|
||||
self.errors: list[dict[str, Any]] = []
|
||||
self.warnings: list[dict[str, Any]] = []
|
||||
self.result = ValidationResult()
|
||||
|
||||
def load_rules(self) -> None:
|
||||
"""Load rules from YAML files."""
|
||||
@@ -37,9 +94,17 @@ class BaseValidator(ABC):
|
||||
if data and "rules" in data:
|
||||
self.rules.extend(data["rules"])
|
||||
|
||||
@abstractmethod
|
||||
def validate(self) -> bool:
|
||||
"""Run validation. Returns True if passed."""
|
||||
"""Run validation. Returns True if passed.
|
||||
|
||||
Subclasses should implement validate_all() instead.
|
||||
"""
|
||||
result = self.validate_all()
|
||||
return not result.has_errors() if hasattr(result, 'has_errors') else True
|
||||
|
||||
def validate_all(self, target_path: Path | None = None) -> ValidationResult:
|
||||
"""Run all validations. Override in subclasses."""
|
||||
return ValidationResult()
|
||||
|
||||
def add_error(
|
||||
self, rule_id: str, message: str, file: str = "", line: int = 0
|
||||
@@ -109,3 +174,117 @@ class BaseValidator(ABC):
|
||||
passed = self.validate()
|
||||
self.print_results()
|
||||
return 0 if passed else 1
|
||||
|
||||
def _should_ignore_file(self, file_path: Path) -> bool:
|
||||
"""Check if a file should be ignored based on patterns."""
|
||||
path_str = str(file_path)
|
||||
for pattern in self.IGNORE_PATTERNS:
|
||||
if pattern in path_str:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _add_violation(
|
||||
self,
|
||||
rule_id: str,
|
||||
rule_name: str,
|
||||
severity: Severity,
|
||||
file_path: Path,
|
||||
line_number: int,
|
||||
message: str,
|
||||
context: str = "",
|
||||
suggestion: str = "",
|
||||
) -> None:
|
||||
"""Add a violation to the result."""
|
||||
violation = Violation(
|
||||
rule_id=rule_id,
|
||||
message=f"{rule_name}: {message}",
|
||||
severity=severity,
|
||||
file_path=str(file_path),
|
||||
line=line_number,
|
||||
suggestion=suggestion,
|
||||
)
|
||||
self.result.violations.append(violation)
|
||||
|
||||
if self.verbose and context:
|
||||
print(f" [{rule_id}] {file_path}:{line_number}")
|
||||
print(f" {message}")
|
||||
print(f" Context: {context}")
|
||||
|
||||
def validate_file(self, file_path: Path) -> ValidationResult:
|
||||
"""Validate a single file."""
|
||||
if not file_path.exists():
|
||||
print(f"File not found: {file_path}")
|
||||
return self.result
|
||||
|
||||
self.result.files_checked = 1
|
||||
content = file_path.read_text()
|
||||
lines = content.split("\n")
|
||||
self._validate_file_content(file_path, content, lines)
|
||||
return self.result
|
||||
|
||||
def _validate_file_content(self, file_path: Path, content: str, lines: list[str]):
|
||||
"""Validate file content. Override in subclasses."""
|
||||
pass
|
||||
|
||||
def output_results(self, json_output: bool = False, errors_only: bool = False) -> None:
|
||||
"""Output validation results."""
|
||||
if json_output:
|
||||
import json
|
||||
output = {
|
||||
"files_checked": self.result.files_checked,
|
||||
"violations": [
|
||||
{
|
||||
"rule_id": v.rule_id,
|
||||
"message": v.message,
|
||||
"severity": v.severity.value,
|
||||
"file": v.file_path,
|
||||
"line": v.line,
|
||||
"suggestion": v.suggestion,
|
||||
}
|
||||
for v in self.result.violations
|
||||
if not errors_only or v.severity == Severity.ERROR
|
||||
],
|
||||
}
|
||||
print(json.dumps(output, indent=2))
|
||||
else:
|
||||
self._print_violations(errors_only)
|
||||
|
||||
def _print_violations(self, errors_only: bool = False) -> None:
|
||||
"""Print violations in human-readable format."""
|
||||
violations = self.result.violations
|
||||
if errors_only:
|
||||
violations = [v for v in violations if v.severity == Severity.ERROR]
|
||||
|
||||
if not violations:
|
||||
print(f"\n✅ No issues found! ({self.result.files_checked} files checked)")
|
||||
return
|
||||
|
||||
errors = [v for v in violations if v.severity == Severity.ERROR]
|
||||
warnings = [v for v in violations if v.severity == Severity.WARNING]
|
||||
info = [v for v in violations if v.severity == Severity.INFO]
|
||||
|
||||
if errors:
|
||||
print(f"\n❌ {len(errors)} errors:")
|
||||
for v in errors:
|
||||
print(f" [{v.rule_id}] {v.file_path}:{v.line}")
|
||||
print(f" {v.message}")
|
||||
if v.suggestion:
|
||||
print(f" 💡 {v.suggestion}")
|
||||
|
||||
if warnings and not errors_only:
|
||||
print(f"\n⚠️ {len(warnings)} warnings:")
|
||||
for v in warnings:
|
||||
print(f" [{v.rule_id}] {v.file_path}:{v.line}")
|
||||
print(f" {v.message}")
|
||||
|
||||
if info and not errors_only:
|
||||
print(f"\nℹ️ {len(info)} info:")
|
||||
for v in info:
|
||||
print(f" [{v.rule_id}] {v.file_path}:{v.line}")
|
||||
print(f" {v.message}")
|
||||
|
||||
print(f"\n📊 Summary: {len(errors)} errors, {len(warnings)} warnings, {len(info)} info")
|
||||
|
||||
def get_exit_code(self) -> int:
|
||||
"""Get exit code based on validation results."""
|
||||
return 1 if self.result.has_errors() else 0
|
||||
|
||||
@@ -34,8 +34,6 @@ from pathlib import Path
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from base_validator import Severity
|
||||
|
||||
|
||||
def run_architecture_validator(verbose: bool = False) -> tuple[int, dict]:
|
||||
"""Run the architecture validator"""
|
||||
|
||||
Reference in New Issue
Block a user