add domain entity
parent
691acffcfa
commit
e5b36a27c3
|
@ -0,0 +1,16 @@
|
|||
[
|
||||
{
|
||||
"id": -1,
|
||||
"type": "Income",
|
||||
"date": "2000-11-10T00:00:00",
|
||||
"amount": 100.1,
|
||||
"description": "sdfsdfsgdhfgh"
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "Income",
|
||||
"date": "2000-11-10T00:00:00",
|
||||
"amount": 100.1,
|
||||
"description": "d"
|
||||
}
|
||||
]
|
|
@ -1,7 +1,7 @@
|
|||
from abc import abstractmethod
|
||||
from typing import Protocol
|
||||
|
||||
from clifinance.application.dto.expense import ExpenseDTO
|
||||
from clifinance.domain.expense.model import Expense
|
||||
|
||||
|
||||
class FileDriver(Protocol):
|
||||
|
@ -10,5 +10,5 @@ class FileDriver(Protocol):
|
|||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def write(self, data: list[ExpenseDTO]):
|
||||
def write(self, data: list[Expense]):
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -2,10 +2,10 @@ from abc import abstractmethod
|
|||
from datetime import datetime
|
||||
from typing import Protocol
|
||||
|
||||
from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO
|
||||
from clifinance.domain.expense.model import Expense, ExpenseType
|
||||
|
||||
|
||||
class FileSession(Protocol):
|
||||
class Session(Protocol):
|
||||
@abstractmethod
|
||||
def commit(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
@ -15,27 +15,27 @@ class FileSession(Protocol):
|
|||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def add(self, expense: ExpenseDTO) -> None:
|
||||
def add(self, expense: Expense) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def update(self, expense: ExpenseDTO) -> None:
|
||||
def update(self, expense: Expense) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, expense: ExpenseDTO) -> None:
|
||||
def delete(self, expense: Expense) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get(
|
||||
self,
|
||||
expense_id: int | None,
|
||||
type: ExpenseTypeDTO | None,
|
||||
type: ExpenseType | None,
|
||||
date: datetime | None,
|
||||
amount: int | None,
|
||||
) -> list[ExpenseDTO]:
|
||||
) -> list[Expense]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_all(self) -> list[ExpenseDTO]:
|
||||
def get_all(self) -> list[Expense]:
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -1,36 +1,11 @@
|
|||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ExpenseTypeDTO(Enum):
|
||||
INCOME = "Income"
|
||||
EXPENSE = "Expense"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExpenseDTO:
|
||||
id: int | None
|
||||
type: ExpenseTypeDTO
|
||||
type: str
|
||||
date: datetime
|
||||
amount: float
|
||||
description: str
|
||||
|
||||
def to_json(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"type": self.type.value,
|
||||
"date": self.date.isoformat(),
|
||||
"amount": self.amount,
|
||||
"description": self.description,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_json(data: dict) -> "ExpenseDTO":
|
||||
return ExpenseDTO(
|
||||
id=data["id"],
|
||||
type=(ExpenseTypeDTO.INCOME if data["type"] == "Income" else ExpenseTypeDTO.EXPENSE),
|
||||
date=datetime.fromisoformat(data["date"]),
|
||||
amount=data["amount"],
|
||||
description=data["description"],
|
||||
)
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import argparse
|
||||
|
||||
from clifinance.application.abstractions.protocols.gateway import DataGateway
|
||||
from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO
|
||||
from clifinance.application.dto.expense import ExpenseDTO
|
||||
from clifinance.domain.expense.gateway import ExpenseGateway
|
||||
from clifinance.domain.expense.model import Expense
|
||||
|
||||
|
||||
class AddExpense:
|
||||
def __init__(
|
||||
self,
|
||||
gateway: DataGateway,
|
||||
gateway: ExpenseGateway,
|
||||
) -> None:
|
||||
self.__gateway = gateway
|
||||
|
||||
def __call__(self, args: argparse.Namespace) -> None:
|
||||
def __call__(self, expense: ExpenseDTO) -> None:
|
||||
self.__gateway.add_expense(
|
||||
exp=ExpenseDTO(
|
||||
id=None,
|
||||
type=(ExpenseTypeDTO.INCOME if args.type == "income" else ExpenseTypeDTO.EXPENSE),
|
||||
date=args.date,
|
||||
amount=args.amount,
|
||||
description=args.description,
|
||||
exp=Expense.create(
|
||||
id=expense.id if expense.id else 1,
|
||||
type=expense.type,
|
||||
date=expense.date,
|
||||
amount=expense.amount,
|
||||
description=expense.description,
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
from clifinance.application.abstractions.protocols.gateway import DataGateway
|
||||
from clifinance.application.dto.balance import Balance
|
||||
|
||||
|
||||
class GetBalance:
|
||||
def __init__(
|
||||
self,
|
||||
gateway: DataGateway,
|
||||
) -> None:
|
||||
self.__gateway = gateway
|
||||
|
||||
def __call__(self) -> Balance:
|
||||
|
||||
return self.__gateway.get_balance()
|
||||
# from clifinance.application.dto.balance import Balance
|
||||
# from clifinance.domain.expense.gateway import ExpenseGateway
|
||||
#
|
||||
#
|
||||
# class GetBalance:
|
||||
# def __init__(
|
||||
# self,
|
||||
# gateway: ExpenseGateway,
|
||||
# ) -> None:
|
||||
# self.__gateway = gateway
|
||||
#
|
||||
# def __call__(self) -> Balance:
|
||||
#
|
||||
# return self.__gateway.get_balance()
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from clifinance.domain.value_obj import DomainValueObject
|
||||
|
||||
EntityId = TypeVar("EntityId", bound=DomainValueObject)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DomainEntity(Generic[EntityId]):
|
||||
id: EntityId
|
|
@ -0,0 +1,8 @@
|
|||
class DomainError(Exception):
|
||||
def __init__(self, message: str, *args: object) -> None:
|
||||
self.message = message
|
||||
super().__init__(*args)
|
||||
|
||||
|
||||
class DomainValidationError(DomainError):
|
||||
pass
|
|
@ -0,0 +1,17 @@
|
|||
from clifinance.domain.error import DomainError, DomainValidationError
|
||||
|
||||
|
||||
class ExpenseValidationError(DomainValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class ExpenseError(DomainError):
|
||||
pass
|
||||
|
||||
|
||||
class ExpenseNotFoundError(ExpenseError):
|
||||
pass
|
||||
|
||||
|
||||
class ExpenseAlreadyExistsError(ExpenseError):
|
||||
pass
|
|
@ -2,35 +2,30 @@ from abc import abstractmethod
|
|||
from datetime import datetime
|
||||
from typing import Protocol
|
||||
|
||||
from clifinance.application.dto.balance import Balance
|
||||
from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO
|
||||
from clifinance.domain.expense.model import Expense, ExpenseType
|
||||
|
||||
|
||||
class DataGateway(Protocol):
|
||||
class ExpenseGateway(Protocol):
|
||||
@abstractmethod
|
||||
def get_balance(self) -> Balance:
|
||||
def get_all_expenses(self) -> list[Expense]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_all_expenses(self) -> list[ExpenseDTO]:
|
||||
def add_expense(self, exp: Expense) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def add_expense(self, exp: ExpenseDTO) -> None:
|
||||
def update_expense(self, expense: Expense) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def update_expense(self, expense: ExpenseDTO) -> None:
|
||||
def get_all_expenses_by_type(self, type: ExpenseType) -> list[Expense]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_all_expenses_by_type(self, type: ExpenseTypeDTO) -> list[ExpenseDTO]:
|
||||
def get_all_expenses_by_date(self, date: datetime) -> list[Expense]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_all_expenses_by_date(self, date: datetime) -> list[ExpenseDTO]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_all_expenses_by_price(self, price: float) -> list[ExpenseDTO]:
|
||||
def get_all_expenses_by_price(self, price: float) -> list[Expense]:
|
||||
raise NotImplementedError
|
|
@ -0,0 +1,74 @@
|
|||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from clifinance.domain.entity import DomainEntity, DomainValueObject
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExpenseId(DomainValueObject):
|
||||
value: int
|
||||
|
||||
|
||||
class ExpenseType(Enum):
|
||||
INCOME = "Income"
|
||||
EXPENSE = "Expense"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExpenseDate(DomainValueObject):
|
||||
value: datetime
|
||||
|
||||
def isoformat(self):
|
||||
return self.value.isoformat()
|
||||
|
||||
@staticmethod
|
||||
def from_isoformat(date: str) -> "ExpenseDate":
|
||||
return ExpenseDate(datetime.fromisoformat(date))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExpenseAmount(DomainValueObject):
|
||||
value: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExpenseDescription(DomainValueObject):
|
||||
value: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Expense(DomainEntity[ExpenseId]):
|
||||
type: ExpenseType
|
||||
date: ExpenseDate
|
||||
amount: ExpenseAmount
|
||||
description: ExpenseDescription
|
||||
|
||||
@staticmethod
|
||||
def create(id: int, type: str, date: datetime, amount: float, description: str) -> "Expense":
|
||||
return Expense(
|
||||
id=ExpenseId(id),
|
||||
type=ExpenseType(type.title()),
|
||||
date=ExpenseDate(date),
|
||||
amount=ExpenseAmount(amount),
|
||||
description=ExpenseDescription(description),
|
||||
)
|
||||
|
||||
def to_json(self) -> dict:
|
||||
return {
|
||||
"id": self.id.value,
|
||||
"type": self.type.value,
|
||||
"date": self.date.isoformat(),
|
||||
"amount": self.amount.value,
|
||||
"description": self.description.value,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_json(data: dict) -> "Expense":
|
||||
return Expense(
|
||||
id=ExpenseId(data["id"]),
|
||||
type=ExpenseType(data["type"]),
|
||||
date=ExpenseDate.from_isoformat(data["date"]),
|
||||
amount=ExpenseAmount(data["amount"]),
|
||||
description=ExpenseDescription(data["description"]),
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DomainValueObject:
|
||||
pass
|
|
@ -1,41 +0,0 @@
|
|||
from datetime import datetime
|
||||
|
||||
from clifinance.application.abstractions.protocols.gateway import DataGateway
|
||||
from clifinance.application.abstractions.protocols.session import FileSession
|
||||
from clifinance.application.dto.balance import Balance
|
||||
from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO
|
||||
|
||||
|
||||
class JsonDataGateway(DataGateway):
|
||||
def __init__(self, session: FileSession) -> None:
|
||||
self.session = session
|
||||
|
||||
def get_balance(self) -> Balance:
|
||||
data = self.session.get_all()
|
||||
balance = sum(x.amount for x in data)
|
||||
income = sum(x.amount for x in data if x.type == ExpenseTypeDTO.INCOME)
|
||||
expense = sum(x.amount for x in data if x.type == ExpenseTypeDTO.EXPENSE)
|
||||
return Balance(
|
||||
balance=round(balance, 2),
|
||||
imcome_balance=round(income, 2),
|
||||
expense_balance=round(expense, 2),
|
||||
)
|
||||
|
||||
def get_all_expenses(self) -> list[ExpenseDTO]:
|
||||
return self.session.get_all()
|
||||
|
||||
def add_expense(self, exp: ExpenseDTO) -> None:
|
||||
self.session.add(exp)
|
||||
self.session.commit()
|
||||
|
||||
def update_expense(self, expense: ExpenseDTO) -> None:
|
||||
self.session.update(expense)
|
||||
|
||||
def get_all_expenses_by_type(self, type: ExpenseTypeDTO) -> list[ExpenseDTO]:
|
||||
return [x for x in self.session.get_all() if x.type == type]
|
||||
|
||||
def get_all_expenses_by_date(self, date: datetime) -> list[ExpenseDTO]:
|
||||
return [x for x in self.session.get_all() if x.date == date]
|
||||
|
||||
def get_all_expenses_by_price(self, price: float) -> list[ExpenseDTO]:
|
||||
return [x for x in self.session.get_all() if x.amount == price]
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
|
||||
from clifinance.application.dto.expense import ExpenseDTO
|
||||
from clifinance.domain.expense.model import Expense
|
||||
|
||||
|
||||
class JsonFileDriver:
|
||||
|
@ -15,12 +15,12 @@ class JsonFileDriver:
|
|||
except json.JSONDecodeError:
|
||||
return list()
|
||||
|
||||
def write(self, data: list[ExpenseDTO]):
|
||||
def write(self, data: list[Expense]):
|
||||
with open("./app_data.json", "w") as f:
|
||||
json.dump(self.__data_to_json(data), f)
|
||||
|
||||
def __data_to_json(self, data: list[ExpenseDTO]):
|
||||
def __data_to_json(self, data: list[Expense]):
|
||||
return [x.to_json() for x in data]
|
||||
|
||||
def __data_from_json(self, data: list[dict]):
|
||||
return [ExpenseDTO.from_json(x) for x in data]
|
||||
return [Expense.from_json(x) for x in data]
|
|
@ -0,0 +1,29 @@
|
|||
from datetime import datetime
|
||||
|
||||
from clifinance.application.abstractions.protocols.session import Session
|
||||
from clifinance.domain.expense.gateway import ExpenseGateway
|
||||
from clifinance.domain.expense.model import Expense, ExpenseType
|
||||
|
||||
|
||||
class JsonExpenseGateway(ExpenseGateway):
|
||||
def __init__(self, session: Session) -> None:
|
||||
self.session = session
|
||||
|
||||
def get_all_expenses(self) -> list[Expense]:
|
||||
return self.session.get_all()
|
||||
|
||||
def add_expense(self, exp: Expense) -> None:
|
||||
self.session.add(exp)
|
||||
self.session.commit()
|
||||
|
||||
def update_expense(self, expense: Expense) -> None:
|
||||
self.session.update(expense)
|
||||
|
||||
def get_all_expenses_by_type(self, type: ExpenseType) -> list[Expense]:
|
||||
return [x for x in self.session.get_all() if x.type == type]
|
||||
|
||||
def get_all_expenses_by_date(self, date: datetime) -> list[Expense]:
|
||||
return [x for x in self.session.get_all() if x.date == date]
|
||||
|
||||
def get_all_expenses_by_price(self, price: float) -> list[Expense]:
|
||||
return [x for x in self.session.get_all() if x.amount == price]
|
|
@ -1,17 +1,17 @@
|
|||
from datetime import datetime
|
||||
|
||||
from clifinance.application.abstractions.protocols.driver import FileDriver
|
||||
from clifinance.application.abstractions.protocols.session import FileSession
|
||||
from clifinance.application.dto.expense import ExpenseDTO, ExpenseTypeDTO
|
||||
from clifinance.application.abstractions.protocols.session import Session
|
||||
from clifinance.domain.expense.model import Expense, ExpenseType
|
||||
|
||||
|
||||
class JsonFileSession(FileSession):
|
||||
class JsonFileSession(Session):
|
||||
def __init__(self, driver: FileDriver) -> None:
|
||||
self.driver = driver
|
||||
self.data: list[ExpenseDTO] = driver.read()
|
||||
self.__dirty_new: list[ExpenseDTO] = list()
|
||||
self.__dirty_updated: list[ExpenseDTO] = list()
|
||||
self.__dirty_deleted: list[ExpenseDTO] = list()
|
||||
self.data: list[Expense] = driver.read()
|
||||
self.__dirty_new: list[Expense] = list()
|
||||
self.__dirty_updated: list[Expense] = list()
|
||||
self.__dirty_deleted: list[Expense] = list()
|
||||
|
||||
def commit(self) -> None:
|
||||
for expense in self.__dirty_new:
|
||||
|
@ -37,36 +37,28 @@ class JsonFileSession(FileSession):
|
|||
def rollback(self) -> None:
|
||||
self.data = self.driver.read()
|
||||
|
||||
def add(self, expense: ExpenseDTO) -> None:
|
||||
if not self.data:
|
||||
last_id = -1
|
||||
else:
|
||||
last_id = max({x.id for x in self.data if x.id is not None})
|
||||
def add(self, expense: Expense) -> None:
|
||||
# if not self.data:
|
||||
# last_id = -1
|
||||
# else:
|
||||
# last_id = max({x.id for x in self.data if x.id is not None})
|
||||
#
|
||||
self.__dirty_new.append(expense)
|
||||
|
||||
self.__dirty_new.append(
|
||||
ExpenseDTO(
|
||||
id=last_id + 1,
|
||||
type=expense.type,
|
||||
date=expense.date,
|
||||
amount=expense.amount,
|
||||
description=expense.description,
|
||||
)
|
||||
)
|
||||
|
||||
def update(self, expense: ExpenseDTO) -> None:
|
||||
def update(self, expense: Expense) -> None:
|
||||
self.__dirty_updated.append(expense)
|
||||
|
||||
def delete(self, expense: ExpenseDTO) -> None:
|
||||
def delete(self, expense: Expense) -> None:
|
||||
self.__dirty_deleted.append(expense)
|
||||
|
||||
def get(
|
||||
self,
|
||||
expense_id: int | None = None,
|
||||
type: ExpenseTypeDTO | None = None,
|
||||
type: ExpenseType | None = None,
|
||||
date: datetime | None = None,
|
||||
amount: int | None = None,
|
||||
) -> list[ExpenseDTO]:
|
||||
result: list[ExpenseDTO] = list()
|
||||
) -> list[Expense]:
|
||||
result: list[Expense] = list()
|
||||
for expense in self.data:
|
||||
if expense_id is not None and expense.id != expense_id:
|
||||
continue
|
||||
|
@ -81,5 +73,5 @@ class JsonFileSession(FileSession):
|
|||
|
||||
return result
|
||||
|
||||
def get_all(self) -> list[ExpenseDTO]:
|
||||
def get_all(self) -> list[Expense]:
|
||||
return self.data
|
|
@ -5,29 +5,30 @@ from inspect import signature
|
|||
from typing import ParamSpec, TypeVar
|
||||
|
||||
from clifinance.application.abstractions.protocols.driver import FileDriver
|
||||
from clifinance.application.abstractions.protocols.gateway import DataGateway
|
||||
from clifinance.application.abstractions.protocols.session import FileSession
|
||||
from clifinance.application.abstractions.protocols.session import Session
|
||||
from clifinance.application.usecases.add_expense import AddExpense
|
||||
from clifinance.application.usecases.get_balance import GetBalance
|
||||
from clifinance.infrastructure.json_driver import JsonFileDriver
|
||||
from clifinance.infrastructure.json_gateway import JsonDataGateway
|
||||
from clifinance.infrastructure.json_session import JsonFileSession
|
||||
|
||||
# from clifinance.application.usecases.get_balance import GetBalance
|
||||
from clifinance.domain.expense.gateway import ExpenseGateway
|
||||
from clifinance.infrastructure.persistence.json_driver import JsonFileDriver
|
||||
from clifinance.infrastructure.persistence.json_gateway import JsonExpenseGateway
|
||||
from clifinance.infrastructure.session import JsonFileSession
|
||||
|
||||
|
||||
class Container:
|
||||
|
||||
@contextmanager
|
||||
def provide_get_balance(self) -> Generator[GetBalance, None, None]:
|
||||
yield GetBalance(gateway=self.json_gateway())
|
||||
# @contextmanager
|
||||
# def provide_get_balance(self) -> Generator[GetBalance, None, None]:
|
||||
# yield GetBalance(gateway=self.json_gateway())
|
||||
|
||||
@contextmanager
|
||||
def provide_add_expense(self) -> Generator[AddExpense, None, None]:
|
||||
yield AddExpense(gateway=self.json_gateway())
|
||||
|
||||
def json_gateway(self) -> DataGateway:
|
||||
return JsonDataGateway(session=self.get_session())
|
||||
def json_gateway(self) -> ExpenseGateway:
|
||||
return JsonExpenseGateway(session=self.get_session())
|
||||
|
||||
def get_session(self) -> FileSession:
|
||||
def get_session(self) -> Session:
|
||||
return JsonFileSession(driver=self.get_driver())
|
||||
|
||||
def get_driver(self) -> FileDriver:
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import argparse
|
||||
|
||||
from clifinance.application.dto.expense import ExpenseDTO
|
||||
from clifinance.main.container import Container
|
||||
|
||||
|
||||
def get_balance(args: argparse.Namespace, ioc: Container) -> None:
|
||||
with ioc.provide_get_balance() as usecase:
|
||||
print(usecase())
|
||||
# with ioc.provide_get_balance() as usecase:
|
||||
# print(usecase())
|
||||
print()
|
||||
|
||||
|
||||
def select_expenses(args: argparse.Namespace, ioc: Container) -> None:
|
||||
|
@ -18,10 +20,14 @@ def add_expense(args: argparse.Namespace, ioc: Container) -> None:
|
|||
argument=None,
|
||||
message="All arguments are required: -t, -d, -a, -D",
|
||||
)
|
||||
if args.type == "income" and args.amount < 0:
|
||||
raise argparse.ArgumentError(argument=None, message="An income cannot have a negative amount.")
|
||||
if args.type == "expense" and args.amount > 0:
|
||||
raise argparse.ArgumentError(argument=None, message="An expense cannot have a positive amount.")
|
||||
|
||||
with ioc.provide_add_expense() as usecase:
|
||||
usecase(args=args)
|
||||
usecase(
|
||||
expense=ExpenseDTO(
|
||||
id=args.id if args.__dict__.get("id") else -1,
|
||||
type=args.type,
|
||||
date=args.date,
|
||||
amount=args.amount,
|
||||
description=args.description,
|
||||
)
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue