[INIT-001] Initial project setup with Clean Architecture (feat)
Some checks failed
CD - Build & Deploy / build-and-push (push) Has been cancelled
CD - Build & Deploy / package-helm (push) Has been cancelled
CD - Build & Deploy / deploy-staging (push) Has been cancelled
CD - Build & Deploy / deploy-production (push) Has been cancelled
CD - Build & Deploy / release (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / security (push) Has been cancelled

- Implemented Clean Architecture with Domain, Application, Infrastructure, Presentation layers
- Added comprehensive project structure following SOLID principles
- Created Kubernetes deployment with Helm charts (HPA, PDB, NetworkPolicy)
- Configured ArgoCD for automated deployment (production + staging)
- Implemented CI/CD pipeline with GitHub Actions
- Added comprehensive documentation (handbook, architecture, coding standards)
- Configured PostgreSQL, Redis, Celery for backend services
- Created modern landing page with Persian fonts (Vazirmatn)
- Added Docker multi-stage build for production
- Configured development tools (pytest, black, flake8, mypy, isort)
- Added pre-commit hooks for code quality
- Implemented Makefile for common operations
This commit is contained in:
Ehsan.Asadi
2025-12-26 15:52:50 +03:30
commit 8a924f6091
135 changed files with 8637 additions and 0 deletions

0
src/core/__init__.py Normal file
View File

View File

View File

View File

View File

View File

@@ -0,0 +1,67 @@
"""Base entity for domain layer.
This module defines the base entity class that all domain entities inherit from.
"""
from datetime import datetime
from typing import Optional
class BaseEntity:
"""Base domain entity.
All domain entities should inherit from this class.
This is NOT a database model, but a pure domain object.
Attributes:
id: Entity identifier
created_at: Creation timestamp
updated_at: Last update timestamp
"""
def __init__(
self,
id: Optional[int] = None,
created_at: Optional[datetime] = None,
updated_at: Optional[datetime] = None
):
"""Initialize base entity.
Args:
id: Entity identifier
created_at: Creation timestamp
updated_at: Last update timestamp
"""
self.id = id
self.created_at = created_at or datetime.utcnow()
self.updated_at = updated_at or datetime.utcnow()
def __eq__(self, other: object) -> bool:
"""Check equality based on ID.
Args:
other: Other object
Returns:
bool: True if same entity
"""
if not isinstance(other, self.__class__):
return False
return self.id == other.id
def __hash__(self) -> int:
"""Hash based on ID.
Returns:
int: Hash value
"""
return hash(self.id)
def __repr__(self) -> str:
"""String representation.
Returns:
str: String representation
"""
return f"<{self.__class__.__name__}(id={self.id})>"

View File

View File

View File

@@ -0,0 +1,59 @@
"""Base exceptions for domain layer.
This module defines the exception hierarchy for the domain layer.
"""
class PeikarbandException(Exception):
"""Base exception for all custom exceptions."""
def __init__(self, message: str, code: str = "INTERNAL_ERROR"):
"""Initialize exception.
Args:
message: Error message
code: Error code
"""
self.message = message
self.code = code
super().__init__(self.message)
class DomainException(PeikarbandException):
"""Base exception for domain layer."""
def __init__(self, message: str, code: str = "DOMAIN_ERROR"):
"""Initialize domain exception.
Args:
message: Error message
code: Error code
"""
super().__init__(message, code)
class ValidationException(DomainException):
"""Validation error in domain."""
def __init__(self, message: str, field: str = ""):
"""Initialize validation exception.
Args:
message: Error message
field: Field that failed validation
"""
self.field = field
super().__init__(message, "VALIDATION_ERROR")
class BusinessRuleException(DomainException):
"""Business rule violation."""
def __init__(self, message: str):
"""Initialize business rule exception.
Args:
message: Error message
"""
super().__init__(message, "BUSINESS_RULE_VIOLATION")

View File

@@ -0,0 +1,104 @@
"""User-related domain exceptions."""
from src.core.domain.exceptions.base import DomainException, ValidationException
class UserNotFoundException(DomainException):
"""User not found exception."""
def __init__(self, user_id: int = None, email: str = None):
"""Initialize exception.
Args:
user_id: User ID
email: User email
"""
if user_id:
message = f"User with ID {user_id} not found"
elif email:
message = f"User with email {email} not found"
else:
message = "User not found"
super().__init__(message, "USER_NOT_FOUND")
class EmailAlreadyExistsException(DomainException):
"""Email already exists exception."""
def __init__(self, email: str):
"""Initialize exception.
Args:
email: Email address
"""
super().__init__(
f"Email {email} is already registered",
"EMAIL_ALREADY_EXISTS"
)
class InvalidEmailException(ValidationException):
"""Invalid email format exception."""
def __init__(self, email: str):
"""Initialize exception.
Args:
email: Invalid email
"""
super().__init__(
f"Invalid email format: {email}",
field="email"
)
class WeakPasswordException(ValidationException):
"""Weak password exception."""
def __init__(self, reason: str = "Password does not meet requirements"):
"""Initialize exception.
Args:
reason: Reason why password is weak
"""
super().__init__(reason, field="password")
class InvalidCredentialsException(DomainException):
"""Invalid login credentials exception."""
def __init__(self):
"""Initialize exception."""
super().__init__(
"Invalid email or password",
"INVALID_CREDENTIALS"
)
class AccountLockedException(DomainException):
"""Account is locked exception."""
def __init__(self, reason: str = "Account is locked"):
"""Initialize exception.
Args:
reason: Reason for lock
"""
super().__init__(reason, "ACCOUNT_LOCKED")
class InsufficientBalanceException(DomainException):
"""Insufficient wallet balance exception."""
def __init__(self, required: str, available: str):
"""Initialize exception.
Args:
required: Required amount
available: Available amount
"""
super().__init__(
f"Insufficient balance. Required: {required}, Available: {available}",
"INSUFFICIENT_BALANCE"
)

View File

@@ -0,0 +1,94 @@
"""Email value object.
Email is an immutable value object that represents a validated email address.
"""
import re
from typing import Any
class Email:
"""Email value object.
Represents a validated email address.
Immutable - once created, cannot be changed.
Attributes:
value: The email address string
"""
EMAIL_REGEX = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
def __init__(self, value: str):
"""Initialize email.
Args:
value: Email address string
Raises:
ValueError: If email format is invalid
"""
if not self._is_valid(value):
raise ValueError(f"Invalid email format: {value}")
self._value = value.lower().strip()
@property
def value(self) -> str:
"""Get email value.
Returns:
str: Email address
"""
return self._value
@staticmethod
def _is_valid(email: str) -> bool:
"""Validate email format.
Args:
email: Email string to validate
Returns:
bool: True if valid
"""
if not email or not isinstance(email, str):
return False
return bool(Email.EMAIL_REGEX.match(email.strip()))
def __str__(self) -> str:
"""String representation.
Returns:
str: Email address
"""
return self._value
def __repr__(self) -> str:
"""Developer representation.
Returns:
str: Email representation
"""
return f"Email('{self._value}')"
def __eq__(self, other: Any) -> bool:
"""Check equality.
Args:
other: Other object
Returns:
bool: True if equal
"""
if not isinstance(other, Email):
return False
return self._value == other._value
def __hash__(self) -> int:
"""Hash value.
Returns:
int: Hash
"""
return hash(self._value)

View File

@@ -0,0 +1,202 @@
"""Money value object.
Money represents a monetary amount with currency.
"""
from decimal import Decimal
from typing import Any, Union
class Money:
"""Money value object.
Represents a monetary amount with currency.
Immutable and provides monetary operations.
Attributes:
amount: Decimal amount
currency: Currency code (e.g., 'IRR', 'USD')
"""
def __init__(self, amount: Union[Decimal, int, float, str], currency: str = "IRR"):
"""Initialize money.
Args:
amount: Monetary amount
currency: Currency code (default: IRR for Iranian Rial)
Raises:
ValueError: If amount is negative or currency is invalid
"""
self._amount = Decimal(str(amount))
if self._amount < 0:
raise ValueError("Amount cannot be negative")
if not currency or len(currency) != 3:
raise ValueError(f"Invalid currency code: {currency}")
self._currency = currency.upper()
@property
def amount(self) -> Decimal:
"""Get amount.
Returns:
Decimal: Amount
"""
return self._amount
@property
def currency(self) -> str:
"""Get currency.
Returns:
str: Currency code
"""
return self._currency
def add(self, other: "Money") -> "Money":
"""Add two money values.
Args:
other: Other money value
Returns:
Money: New money object with sum
Raises:
ValueError: If currencies don't match
"""
self._check_currency(other)
return Money(self._amount + other._amount, self._currency)
def subtract(self, other: "Money") -> "Money":
"""Subtract money value.
Args:
other: Other money value
Returns:
Money: New money object with difference
Raises:
ValueError: If currencies don't match or result is negative
"""
self._check_currency(other)
result = self._amount - other._amount
if result < 0:
raise ValueError("Result cannot be negative")
return Money(result, self._currency)
def multiply(self, multiplier: Union[int, float, Decimal]) -> "Money":
"""Multiply amount by a factor.
Args:
multiplier: Multiplication factor
Returns:
Money: New money object
"""
result = self._amount * Decimal(str(multiplier))
return Money(result, self._currency)
def _check_currency(self, other: "Money") -> None:
"""Check if currencies match.
Args:
other: Other money value
Raises:
ValueError: If currencies don't match
"""
if self._currency != other._currency:
raise ValueError(
f"Currency mismatch: {self._currency} vs {other._currency}"
)
def __str__(self) -> str:
"""String representation.
Returns:
str: Formatted money string
"""
return f"{self._amount:,.2f} {self._currency}"
def __repr__(self) -> str:
"""Developer representation.
Returns:
str: Money representation
"""
return f"Money({self._amount}, '{self._currency}')"
def __eq__(self, other: Any) -> bool:
"""Check equality.
Args:
other: Other object
Returns:
bool: True if equal
"""
if not isinstance(other, Money):
return False
return self._amount == other._amount and self._currency == other._currency
def __lt__(self, other: "Money") -> bool:
"""Less than comparison.
Args:
other: Other money value
Returns:
bool: True if less than
"""
self._check_currency(other)
return self._amount < other._amount
def __le__(self, other: "Money") -> bool:
"""Less than or equal comparison.
Args:
other: Other money value
Returns:
bool: True if less than or equal
"""
self._check_currency(other)
return self._amount <= other._amount
def __gt__(self, other: "Money") -> bool:
"""Greater than comparison.
Args:
other: Other money value
Returns:
bool: True if greater than
"""
self._check_currency(other)
return self._amount > other._amount
def __ge__(self, other: "Money") -> bool:
"""Greater than or equal comparison.
Args:
other: Other money value
Returns:
bool: True if greater than or equal
"""
self._check_currency(other)
return self._amount >= other._amount
def __hash__(self) -> int:
"""Hash value.
Returns:
int: Hash
"""
return hash((self._amount, self._currency))

View File

@@ -0,0 +1,140 @@
"""Phone number value object.
Phone represents a validated phone number.
"""
import re
from typing import Any
class Phone:
"""Phone number value object.
Represents a validated Iranian phone number.
Supports both mobile and landline numbers.
Attributes:
value: Phone number string (normalized format)
"""
# Iranian mobile: starts with 09, 10 digits
MOBILE_REGEX = re.compile(r'^09\d{9}$')
# International format: +989...
INTERNATIONAL_REGEX = re.compile(r'^\+989\d{9}$')
def __init__(self, value: str):
"""Initialize phone number.
Args:
value: Phone number string
Raises:
ValueError: If phone format is invalid
"""
normalized = self._normalize(value)
if not self._is_valid(normalized):
raise ValueError(f"Invalid phone number: {value}")
self._value = normalized
@property
def value(self) -> str:
"""Get phone number.
Returns:
str: Phone number
"""
return self._value
@property
def international_format(self) -> str:
"""Get international format.
Returns:
str: +989XXXXXXXXX format
"""
if self._value.startswith('+'):
return self._value
return f"+98{self._value[1:]}"
@staticmethod
def _normalize(phone: str) -> str:
"""Normalize phone number.
Removes spaces, dashes, parentheses.
Args:
phone: Phone string
Returns:
str: Normalized phone
"""
if not phone:
return ""
# Remove common separators
phone = re.sub(r'[\s\-\(\)]', '', phone.strip())
# Convert international format to local
if phone.startswith('+98'):
phone = '0' + phone[3:]
elif phone.startswith('0098'):
phone = '0' + phone[4:]
elif phone.startswith('98') and len(phone) == 12:
phone = '0' + phone[2:]
return phone
@staticmethod
def _is_valid(phone: str) -> bool:
"""Validate phone number.
Args:
phone: Normalized phone string
Returns:
bool: True if valid
"""
if not phone or not isinstance(phone, str):
return False
# Check if matches mobile pattern
return bool(Phone.MOBILE_REGEX.match(phone))
def __str__(self) -> str:
"""String representation.
Returns:
str: Phone number
"""
return self._value
def __repr__(self) -> str:
"""Developer representation.
Returns:
str: Phone representation
"""
return f"Phone('{self._value}')"
def __eq__(self, other: Any) -> bool:
"""Check equality.
Args:
other: Other object
Returns:
bool: True if equal
"""
if not isinstance(other, Phone):
return False
return self._value == other._value
def __hash__(self) -> int:
"""Hash value.
Returns:
int: Hash
"""
return hash(self._value)

View File