Skip to content
This repository has been archived by the owner on Sep 15, 2023. It is now read-only.

Commit

Permalink
Refatoração das configurações de invoice (#16)
Browse files Browse the repository at this point in the history
* refatoração do novo padrão de verificações e novo formato

* refatora Configuration

* adiciona safetynet no _run_validators

* adiciona validador de regex

* refatoração de Configurations

* refatoração na base!

* refatoração em transaction

* fmt

* adiciona bdd's

* fmt

* complementa testes

* valida datas

* altera testes

* fmt

* altera todo's

* string regex

* altera nomenclatura

* adiciona validação de datas

* completa bdd

* altera validações de data

* altera validações

* testa validators da situação especial

* docstrings

* Update imopay_wrapper/validators.py

Co-authored-by: PedroRegisPOAR <[email protected]>

Co-authored-by: PedroRegisPOAR <[email protected]>
  • Loading branch information
rodrigondec and PedroRegisPOAR authored Sep 24, 2020
1 parent 8635adc commit 6e31c5e
Show file tree
Hide file tree
Showing 20 changed files with 1,381 additions and 222 deletions.
46 changes: 46 additions & 0 deletions imopay_wrapper/field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from dataclasses import Field, MISSING


class ImopayField(Field):
def __init__(
self, opitional, default, default_factory, init, repr, hash, compare, metadata
):

self.optional = opitional

super().__init__(default, default_factory, init, repr, hash, compare, metadata)


# This function is used instead of exposing Field creation directly,
# so that a type checker can be told (via overloads) that this is a
# function whose type depends on its parameters.
def field(
*,
optional=False,
default=MISSING,
default_factory=MISSING,
init=True,
repr=True,
hash=None,
compare=True,
metadata=None,
):
"""Return an object to identify dataclass fields.
default is the default value of the field. default_factory is a
0-argument function called to initialize a field's value. If init
is True, the field will be a parameter to the class's __init__()
function. If repr is True, the field will be included in the
object's repr(). If hash is True, the field will be included in
the object's hash(). If compare is True, the field will be used
in comparison functions. metadata, if specified, must be a
mapping which is stored but not otherwise examined by dataclass.
It is an error to specify both default and default_factory.
"""

if default is not MISSING and default_factory is not MISSING:
raise ValueError("cannot specify both default and default_factory")
return ImopayField(
optional, default, default_factory, init, repr, hash, compare, metadata
)
84 changes: 66 additions & 18 deletions imopay_wrapper/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

@dataclass
class BaseImopayObj:
VALIDATION_METHOD_PATTERN = "_validate"

def __post_init__(self):
self.__run_validators()
self._init_nested_fields()
Expand All @@ -15,23 +17,31 @@ def _init_nested_fields(self):
pass

@classmethod
def get_fields(cls):
def __get_fields(cls):
"""
Método para retornar todos os campos!
"""
# noinspection PyUnresolvedReferences
return cls.__dataclass_fields__

def to_dict(self):
data = {}
for field_name, field in self.get_fields().items():
value = getattr(self, field_name)
def __get_field(self, name):
"""
Método para retornar um campo com base no nome passado!
"""
try:
# noinspection PyUnresolvedReferences
return self.__get_fields()[name]
except KeyError as e:
raise AttributeError(f"Não existe o campo {name} em {self}") from e

if self.is_empty_value(value):
continue
@staticmethod
def __is_field_optional(field):
"""
Método para verificar se um campo é opcional ou não!
if isinstance(value, BaseImopayObj):
data[field_name] = value.to_dict()
else:
data[field_name] = field.type(value)
return data
Influência na validação!
"""
return getattr(field, "optional", False)

def __get_validation_methods(self):
"""
Expand All @@ -40,10 +50,24 @@ def __get_validation_methods(self):
"""
data = inspect.getmembers(self, predicate=inspect.ismethod)

validation_methods = [item[1] for item in data if "_validate" in item[0]]
validation_methods = [
item[1] for item in data if self.VALIDATION_METHOD_PATTERN in item[0]
]

return validation_methods

@staticmethod
def __get_attr_name_from_method(method):
"""
Método para retornar o nome do atributo/campo a partir de
um método de validação (que siga o padrão `_validate_attr`).
"""
name = method.__name__

initial_index = len(BaseImopayObj.VALIDATION_METHOD_PATTERN) + 1

return name[initial_index:]

def __run_validators(self):
"""
Método que executa todos os métodos de validação.
Expand All @@ -53,20 +77,34 @@ def __run_validators(self):
errors = []

for method in validation_methods:
error = None
attr_name = self.__get_attr_name_from_method(method)
field = self.__get_field(attr_name)
try:
method()
except FieldError as e:
errors.append(e)
error = e
except Exception as e:
error = FieldError(method.__name__, str(e))
finally:
if error is not None and not self.__is_field_optional(field):
errors.append(error)

if errors:
raise ValidationError(self, errors)

@staticmethod
def __is_empty_value(value):
return value == "" or value is None

@classmethod
def from_dict(cls, data: Union[dict, Any]):
if data is None:
data = {}

missing_fields = {
field_name
for field_name in cls.get_fields().keys()
for field_name in cls.__get_fields().keys()
if field_name not in data.keys()
}

Expand All @@ -76,6 +114,16 @@ def from_dict(cls, data: Union[dict, Any]):
# noinspection PyArgumentList
return cls(**data)

@staticmethod
def is_empty_value(value):
return value == "" or value is None
def to_dict(self):
data = {}
for field_name, field in self.__get_fields().items():
value = getattr(self, field_name)

if self.__is_empty_value(value):
continue

if isinstance(value, BaseImopayObj):
data[field_name] = value.to_dict()
else:
data[field_name] = field.type(value)
return data
115 changes: 93 additions & 22 deletions imopay_wrapper/models/transaction.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
from typing import List
from dataclasses import dataclass, field
from dataclasses import dataclass

from .base import BaseImopayObj
from ..exceptions import FieldError
from ..validators import (
validate_obj_attr_type,
validate_obj_attr_in_collection,
validate_date_isoformat,
validate_date_1_gt_date_2,
)
from ..field import field


@dataclass
Expand All @@ -14,28 +22,77 @@ class BaseTransaction(BaseImopayObj):


@dataclass
class Configuration(BaseImopayObj):
value: str
type: str
class BaseConfiguration(BaseImopayObj):
value: int
charge_type: str
days: str

PERCENTAGE = "p"
FIXED = "f"
DAILY_FIXED = "df"
DAILY_PERCENTAGE = "dp"
MONTHLY_PERCENTAGE = "mp"

VALID_CHARGE_TYPES = {}

def _validate_value(self):
self.value = int(self.value)
if self.value <= 0:
raise FieldError("value", "O valor é menor do que 1!")

def _validate_charge_type(self):
validate_obj_attr_in_collection(self, "charge_type", self.VALID_CHARGE_TYPES)


@dataclass
class InterestConfiguration(BaseConfiguration):
VALID_CHARGE_TYPES = {
BaseConfiguration.DAILY_FIXED,
BaseConfiguration.DAILY_PERCENTAGE,
BaseConfiguration.MONTHLY_PERCENTAGE,
}


@dataclass
class FineConfiguration(BaseConfiguration):
VALID_CHARGE_TYPES = {BaseConfiguration.FIXED, BaseConfiguration.PERCENTAGE}


@dataclass
class DiscountConfiguration(FineConfiguration):
date: str

def _validate_date(self):
validate_date_isoformat(self, "date", past=True, allow_today=True)


@dataclass
class InvoiceConfigurations(BaseImopayObj):
fine: Configuration
interest: Configuration
discounts: List[Configuration] = field(default=list)

def __post_init__(self):
if isinstance(self.fine, dict):
self.fine = Configuration.from_dict(self.fine)
if isinstance(self.interest, dict):
self.interest = Configuration.from_dict(self.interest)
fine: FineConfiguration = field(optional=True)
interest: InterestConfiguration = field(optional=True)
discounts: List[DiscountConfiguration] = field(optional=True, default=list)

def _validate_fine(self):
validate_obj_attr_type(self, "fine", dict)

def _validate_interest(self):
validate_obj_attr_type(self, "interest", dict)

def _validate_discounts(self):
validate_obj_attr_type(self, "discounts", list)

for discount in self.discounts:
validate_obj_attr_type(self, "discounts", dict, value=discount)

def _init_nested_fields(self):
if self.fine is not None:
self.fine = FineConfiguration.from_dict(self.fine)

if self.interest is not None:
self.interest = InterestConfiguration.from_dict(self.interest)

if self.discounts:
for i, discount in enumerate(self.discounts):
if isinstance(discount, dict):
self.discounts[i] = Configuration.from_dict(discount)
self.discounts[i] = DiscountConfiguration.from_dict(discount)

def to_dict(self):
"""
Expand All @@ -60,17 +117,31 @@ def to_dict(self):
class Invoice(BaseImopayObj):
expiration_date: str
limit_date: str
configurations: InvoiceConfigurations = field(default_factory=dict)
configurations: InvoiceConfigurations = field(optional=True, default_factory=dict)

def __post_init__(self):
if isinstance(self.configurations, dict):
def _init_nested_fields(self):
if self.configurations is not None:
self.configurations = InvoiceConfigurations.from_dict(self.configurations)

def _validate_configurations(self):
validate_obj_attr_type(self, "configurations", dict)

def _validate_expiration_date(self):
validate_date_isoformat(self, "expiration_date", future=True, allow_today=True)

def _validate_limit_date(self):
validate_date_isoformat(self, "limit_date", future=True, allow_today=True)
validate_date_1_gt_date_2(
"limit_date", self.limit_date, self.expiration_date, allow_equal=True
)


@dataclass
class InvoiceTransaction(BaseTransaction):
payment_method: Invoice

def __post_init__(self):
if isinstance(self.payment_method, dict):
self.payment_method = Invoice(**self.payment_method)
def _init_nested_fields(self):
self.payment_method = Invoice.from_dict(self.payment_method)

def _validate_payment_method(self):
validate_obj_attr_type(self, "payment_method", dict)
1 change: 1 addition & 0 deletions imopay_wrapper/regex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
date_regex = r"^\d{4}-\d{2}-\d{2}$"
Loading

0 comments on commit 6e31c5e

Please sign in to comment.