[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
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:
0
src/core/__init__.py
Normal file
0
src/core/__init__.py
Normal file
0
src/core/application/__init__.py
Normal file
0
src/core/application/__init__.py
Normal file
0
src/core/application/dto/__init__.py
Normal file
0
src/core/application/dto/__init__.py
Normal file
0
src/core/application/interfaces/__init__.py
Normal file
0
src/core/application/interfaces/__init__.py
Normal file
0
src/core/application/use_cases/__init__.py
Normal file
0
src/core/application/use_cases/__init__.py
Normal file
0
src/core/application/use_cases/auth/__init__.py
Normal file
0
src/core/application/use_cases/auth/__init__.py
Normal file
0
src/core/application/use_cases/billing/__init__.py
Normal file
0
src/core/application/use_cases/billing/__init__.py
Normal file
0
src/core/application/use_cases/servers/__init__.py
Normal file
0
src/core/application/use_cases/servers/__init__.py
Normal file
0
src/core/application/use_cases/services/__init__.py
Normal file
0
src/core/application/use_cases/services/__init__.py
Normal file
0
src/core/application/validators/__init__.py
Normal file
0
src/core/application/validators/__init__.py
Normal file
0
src/core/domain/__init__.py
Normal file
0
src/core/domain/__init__.py
Normal file
0
src/core/domain/entities/__init__.py
Normal file
0
src/core/domain/entities/__init__.py
Normal file
67
src/core/domain/entities/base.py
Normal file
67
src/core/domain/entities/base.py
Normal 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})>"
|
||||
|
||||
0
src/core/domain/enums/__init__.py
Normal file
0
src/core/domain/enums/__init__.py
Normal file
0
src/core/domain/exceptions/__init__.py
Normal file
0
src/core/domain/exceptions/__init__.py
Normal file
59
src/core/domain/exceptions/base.py
Normal file
59
src/core/domain/exceptions/base.py
Normal 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")
|
||||
|
||||
104
src/core/domain/exceptions/user_exceptions.py
Normal file
104
src/core/domain/exceptions/user_exceptions.py
Normal 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"
|
||||
)
|
||||
|
||||
0
src/core/domain/value_objects/__init__.py
Normal file
0
src/core/domain/value_objects/__init__.py
Normal file
94
src/core/domain/value_objects/email.py
Normal file
94
src/core/domain/value_objects/email.py
Normal 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)
|
||||
|
||||
202
src/core/domain/value_objects/money.py
Normal file
202
src/core/domain/value_objects/money.py
Normal 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))
|
||||
|
||||
140
src/core/domain/value_objects/phone.py
Normal file
140
src/core/domain/value_objects/phone.py
Normal 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)
|
||||
|
||||
0
src/core/utils/__init__.py
Normal file
0
src/core/utils/__init__.py
Normal file
Reference in New Issue
Block a user