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>
291 lines
9.3 KiB
Python
Executable File
291 lines
9.3 KiB
Python
Executable File
"""
|
||
Base Validator Class
|
||
|
||
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."""
|
||
|
||
# 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."""
|
||
rules_path = self.project_root / self.rules_dir
|
||
if not rules_path.exists():
|
||
print(f"Rules directory not found: {rules_path}")
|
||
return
|
||
|
||
for rule_file in rules_path.glob("*.yaml"):
|
||
if rule_file.name.startswith("_"):
|
||
continue # Skip main config
|
||
|
||
with open(rule_file) as f:
|
||
data = yaml.safe_load(f)
|
||
if data and "rules" in data:
|
||
self.rules.extend(data["rules"])
|
||
|
||
def validate(self) -> bool:
|
||
"""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
|
||
) -> None:
|
||
"""Add an error."""
|
||
self.errors.append(
|
||
{
|
||
"rule_id": rule_id,
|
||
"message": message,
|
||
"file": file,
|
||
"line": line,
|
||
"severity": "error",
|
||
}
|
||
)
|
||
|
||
def add_warning(
|
||
self, rule_id: str, message: str, file: str = "", line: int = 0
|
||
) -> None:
|
||
"""Add a warning."""
|
||
self.warnings.append(
|
||
{
|
||
"rule_id": rule_id,
|
||
"message": message,
|
||
"file": file,
|
||
"line": line,
|
||
"severity": "warning",
|
||
}
|
||
)
|
||
|
||
def add_info(
|
||
self, rule_id: str, message: str, file: str = "", line: int = 0
|
||
) -> None:
|
||
"""Add an informational note."""
|
||
self.warnings.append(
|
||
{
|
||
"rule_id": rule_id,
|
||
"message": message,
|
||
"file": file,
|
||
"line": line,
|
||
"severity": "info",
|
||
}
|
||
)
|
||
|
||
def print_results(self) -> None:
|
||
"""Print validation results."""
|
||
if not self.errors and not self.warnings:
|
||
print(f"✅ All {self.rules_dir} rules passed!")
|
||
return
|
||
|
||
if self.errors:
|
||
print(f"\n❌ {len(self.errors)} errors found:")
|
||
for error in self.errors:
|
||
print(f" [{error['rule_id']}] {error['message']}")
|
||
if error["file"]:
|
||
print(f" File: {error['file']}:{error['line']}")
|
||
|
||
if self.warnings:
|
||
print(f"\n⚠️ {len(self.warnings)} warnings:")
|
||
for warning in self.warnings:
|
||
print(f" [{warning['rule_id']}] {warning['message']}")
|
||
if warning["file"]:
|
||
print(f" File: {warning['file']}:{warning['line']}")
|
||
|
||
def run(self) -> int:
|
||
"""Run validation and return exit code."""
|
||
self.load_rules()
|
||
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
|