From f80331d562acf4ee6cea8d11a41d64757d63f4ae Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Mon, 22 Dec 2025 12:27:13 -0300 Subject: [PATCH 01/27] initial --- code/.gitignore | 115 ++++++++++++++++++ code/core/__init__.py | 0 code/core/asgi.py | 16 +++ code/core/settings.py | 110 +++++++++++++++++ code/core/urls.py | 22 ++++ code/core/wsgi.py | 16 +++ .../layout_modelagem_investiments_api.png | Bin 0 -> 85303 bytes code/docs/index.md | 1 + code/manage.py | 22 ++++ code/requirements.txt | Bin 0 -> 172 bytes 10 files changed, 302 insertions(+) create mode 100644 code/.gitignore create mode 100644 code/core/__init__.py create mode 100644 code/core/asgi.py create mode 100644 code/core/settings.py create mode 100644 code/core/urls.py create mode 100644 code/core/wsgi.py create mode 100644 code/docs/images/layout_modelagem_investiments_api.png create mode 100644 code/docs/index.md create mode 100644 code/manage.py create mode 100644 code/requirements.txt diff --git a/code/.gitignore b/code/.gitignore new file mode 100644 index 000000000..d625ce0c4 --- /dev/null +++ b/code/.gitignore @@ -0,0 +1,115 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ +.vscode +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +.DS_Store +*.sqlite3 +media/ +*.pyc +*.db +*.pid + +# Ignore Django Migrations in Development if you are working on team + +# Only for Development only +# **/migrations/** +# !**/migrations +# !**/migrations/__init__.py \ No newline at end of file diff --git a/code/core/__init__.py b/code/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/code/core/asgi.py b/code/core/asgi.py new file mode 100644 index 000000000..cf099bf89 --- /dev/null +++ b/code/core/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for core project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + +application = get_asgi_application() diff --git a/code/core/settings.py b/code/core/settings.py new file mode 100644 index 000000000..2bb945a04 --- /dev/null +++ b/code/core/settings.py @@ -0,0 +1,110 @@ +import os + +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.getenv('SECRET_KEY') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = os.getenv('DEBUG') + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'core.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'core.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/6.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/6.0/topics/i18n/ + +LANGUAGE_CODE = 'pt-br' + +TIME_ZONE = 'America/Sao_Paulo' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/6.0/howto/static-files/ + +STATIC_URL = 'static/' diff --git a/code/core/urls.py b/code/core/urls.py new file mode 100644 index 000000000..366711211 --- /dev/null +++ b/code/core/urls.py @@ -0,0 +1,22 @@ +""" +URL configuration for core project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/6.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/code/core/wsgi.py b/code/core/wsgi.py new file mode 100644 index 000000000..6d3653019 --- /dev/null +++ b/code/core/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for core project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') + +application = get_wsgi_application() diff --git a/code/docs/images/layout_modelagem_investiments_api.png b/code/docs/images/layout_modelagem_investiments_api.png new file mode 100644 index 0000000000000000000000000000000000000000..b26e0802620a238cc30a5e5c0f28dd504ccb2e8d GIT binary patch literal 85303 zcmdpeWmg>i6D$cH9D-Z$;O_43Zo%DkaRNbty9Emng1ZHG2=4CgEbg#(@_*jkk8sbO zIULyCS(({icUMY(b~Z-qnj^-4o2pUL10?tR|F&XfR%gur-L2Yb zyh%y^Z_5wp?vj=G-}V|Dl=R<$Pq5H5|2_Q{2~O#M$B`f7B4_<~+$=L5PsD#OXvFgW z%?pMuo_Rj=!?Uyx9fSWn<9uZp8_l=McM%)^51eGp%Gn(V)z~L#ZbOpeE3Y9_0w^$1 zh6=N0QGefM$o14G_>(aNE2>}Z2qN^`{l z*_<~LCT;TwGs;}8q-k`gc8??{!W7jv_9b!e5X5WP;h1q6NCCWpRS?XC1Z#M@v=Ny> z*%rCYhh#a$etc5hnPiV6)9lucLX4lFY`P72klbggngQ7J01HQfg?Zgi!?iU}X zOJ5A3F`;QxnYTX8PtWIpk0YaE6Gwh|hF{y8)T}uU%1CGuCh$!{a!-=b9WI3R6Hd*m>MCFtCr4OpiTWpB(ZNiWttbC1iT| z_%2q-y}(?@HBpXMx@pOlzO4M5?Z+fB4o+fD)-Jz`J6_8$UFjJreC#h5)+v{%gwKN- z`YZhOrMB5jG^i&-mo8HnCU|&z9x-Ec4Y!A>BfGlzw0tpeYdi{OoBNHP?(`prSF^U* zz};)-?t9tHHF|=+7GBmewrasuMC|G+26wV362GVi@}T)zqhTcrJI>2*X5)W~7=awb z)jbTqRr!jY%gp9gJ=>c^OzrCCLuGTZGtOzpVof*qU`B!RySsyyY)W!OCCQ`40*(!W zs`%&B+qXK-Q}1FEujq{Ki%+-sVwM*SHNMQvuLq$v=n4Ai)JhdfaHrn**@Ui7hB{_z zWoG1*UMMA>{NMYHk4_OL?R0(76@1A%5}v{dVMtr28&n;y03rF**Xh;env+e|Z&Uem zHWi-X-PQv(1NJpbr>Hd&vuUJS#wfSy?q=9NR}|=!CoGamE!phFA^VAXW0c3RBjYl9)zrb^#NOtlZ<6Pc zcJ3|0GoykNWh{&dYnT-?Ma`my-;Z5=X3~7%@D#O4rd;K--(K-M~fO+z(< z<;B@k_b9?2daB=rL*+uL`nw*=N}LA#^3m(^rmR`iJFbpfs1@Gtc@?OY+@uhW&6l=N z*MSmtKfu2EL7Uy#9%uSdq|tuVQ8VH#g{^ouAp_H-SJv(NIDA5&?>Beo-i%FG*tu$2 z;b4EnmY*mfA9;NCg}ibm=R|`cB@aC?J>zKr=i_$V|MgwCS69t2CBYtPFkqh4=S(2L z5vPdlg9+jvP=Mgr%VieS))4ZMNL4GjNI_U15$N&_3cEMsNVva3OWsxp?oB^1O_(r> zHLLAJ7udOrZMWhLyGD*Nt@+D8HlZK;eyE7~Yxl#Hi|8X&8iG)r0bR<^=gsx(!yG=h z$|`i9FHk!f7#k8)>mMHatWxiZ5FeP+AW2Cncha6>nk>CL@N%W|f^xQ7nLfcL&X8cZ=f8sU4$h=?)6YfatL0N) zjbI==zYg+6*JBA9*Y`s=vA+t6(elx87RzmH+0Jy!k6OicW?!8R&K;#tZ#zYR`oqEU zle0+0=gq9OQWz*cs*uEpW$4gelgb8+%DwI$rNUMz^4dou3gDeu=KC;RT*VCZ19bAB^yEyw~(@oylb$3{uE;p0t-XdCl-PuG(M9$w&LV4(1tCh&+9 z-LBoGJ`w%PUq`LcVN3;`;X_L7s3(k%(5UK+$yFKLfJt4 zo!3MREiEk{q9XZ;^XVZOuW=t}$jJ!~4U}F6r3^ba4Q(TQdXL=CgniUDEXK!2H}~Gs zc3b5gitTz8>t=Ix#hDW3$ztmx)lm{@nOsN>Ce=13t8LH1rfaV@e{TAMTLj&_2J(8P z(<*fKd^y?3x69C02|waJY)$V`QRReN(|?S#QKx2ZF$s{jJ9&!HcWm7oSWLH08)*PR zx`TVJ;{pL3Y;o&`0LE6rU3&RztjaZzz;0&TKiKwG^q^wTZGiqd{1w_;K-<$N9UoduCA~APOXK$S!=!bZJjS&8edf3 z22#z=BS6p3%w}FcA37*ye;z!pw9n43zpaPM|5o_H%FxgV*6WaNQrW zFNx$@TgNgwFs46dIr68bxp=xt_-bx39dEtyRfQn(6)i;)k{<4>v<|6?$BQC zOK?xYpz^x!0&$0?fpf>EbN-cpo4A(erBV+cA2J|R7!`FNXhPgua_`42Vc^R*@Z*NCjNjyw zc3_&!jmiVoTcv#Wu+OzHn2VFUK)+CJt^P2qB#B0ja+`6*^K8A96h4Hv&Wb^gMVmY! zHAS1jsQGtI2`vMM`xMIcz<%s{twZ>f^|UQ#>)k1ckGKP)ryaDBkro&*-RFYWUz|X( zKb{lOv)s}GTGxM>e7)CkXv8jCXlQ6C`c1jvaZs^Vmh=5EO6-%Zy)DWlom&`M66!X%!1iv*j8c)h6U(2h9I&1Pm+zN=t>(7iurTTy z(+6z&g&$-4xo?Nh>_y6wEg}fsTg^rR2l_^Fk28|Xc|`5%LC!psj6ImIvz>{m->Rby0WqnYc>--1Yulcu;KT2V-KAY&QzXhf8X|m z7^L={tgTm@eXH`ykQ&Sz(`#G}neqw>W*qM)LWb$SXIg+baWDK1{F_NM;3BEra#DGqsDinF}t{Fk6w#rVDR+{S2&v|z}`%q{diEn zdmn%1w<%3eWeD+yR_uXq1nhXoADRWo$+sVd)EA?sKKFfzu)!b8(GX^%dBzofCcEpy zE}MTkFe|pUwtt#2dV4F#6%6ex3cSrEZoT`N|5D|T>T!$G`T9zFLP+rPN{(UcbvYdu zcsE-Wa5-{-M$Ai%khaJhN5ubj{v1`4_W0Zh)4qPOmAEJT+Rq&LHtI&NU3GohlXhTO z0qX?0hc3A6Auat$@^+gGvb8-H$hB^}-Kf3#06}`~qC(IIo%(=#snSV_KAgr*fPS4I z@!+?=-9uIaFXB5p!NzqpdJZ`>+FvO8sS26fuZr&OJOd##(NZ&{U!Ls(MDsc!dcC?o zvE7!Yj_!mclAmyZ1neYJn3&ubGMg+B(Ehmh9X4Trj%1#^y-DpYSd?vS4Hv#JR>?mt z4=IC%kIwe|6kjwbsKLX7CrySj5h|OBjt-NVOd_2T2f19egr)1^rhYd0*YcCk2A-du zo_?mj-ejs^W^UYiV9`i;t4m_ zmOcT2j~jR%H{ZBqI!B0nQ1cE(>?0-mdX>;%p*5ET&&bx6sg9pQNDi0q07sIsRJ1SSCJ5l=ypj7i1i|JS+_=pF8 z!{A&f->CQNKUTLI|Dlj;=Zc8vA{zA0VhceF0v&DGADbkyq9;o zzkUHBk#$-g-sux)>PN(;Y2h*U_AUI<@>?E~8GTu?fa@vGy6@F2J|#IAUc`|oAIO0< zAc!b(*ta^Tkgr>eT4kGDqn5!Sld<;DRj9OXdpVZgW3c4rZb43pKnJKDL z{9{Z~osu&)av#bXjiVV(JnvYyRTwrPH7l#>ffBpIqhhRwC!Ak3X33wX{Z+2l-pud; z2Oq1V%n4YvpW*o3xX^ly#XN3r!&Tb{l^g3Dw~E#DwGT>~u!{KjZ*v)*5l6X738Ges z-=usYhzU-WzC@r%Kyl&kPzVDF_ zk0Z)t@kG|^<(cYu?%poSc^xx?yalpOsqPHyKf&MneYO+!DUp-x9t`YKP3wEORioy@VI#`o6Pzg1tTn2@82JDwrToksI8Pjg5#k zXz=_7-kxJd+oGXmHQB!@A>?+teci97Y5xLg-GE#!89_YP2F56)%%1~Ryn~L93F_lr zrDJ5|J}9>^4}!U5F+R%4X8gA2DgRxZ3muR)e|TxDQN+DpP(;~ ztov{78*Uf}5g!8=J)5_OB4fUivoX5fV`ifxm;>+bRIu861+}$Tbz|~tn>j~Dll_XW zmR4*hS{j4+tf;li&BHkY#`~$yET6itUi-J>6I5dsz#jG}X>_HeGl<zFzk=VBT+P!1pO7%KTG`~q z%L()%!n4|D!GA|>#4NV_6q89`T3UltwNj1fB)V-_YY!XDG`%obHvb8BYhrGP%@+tC zbOvsxwJ!#Gkp|(9(jsDs8y$Aa6G$qIICh4H(53I++0Udp5NAK#W^#eoTQ{B?D%Q{L z7?>?FSJ>Z^(7_DGxIMzAPln5}%)0i8?1TOvMUWssgwF;_uxwwYYx5JCBq4)JEi z`op81t$@&zR@(2oE|ZFC6rZG`#-PLB+e0-pc+fADcWQOgUkZh@2k5v?8^63e=o;js z$}j69Lp_OSC{B4TSM)5BWg8}oz3H4>=zBn`fO6u8BDRKl)N7|5lC65V;|_eCp+{Q< z-G`0oPw}LVV8;dSOl`dGFC{1M9v)g(NS$~`X*)QGYbwUbRk*ff@m1i&K#PjVcf|yT zTm9<1Se+@5j%j{q}zsWQri;=OIepu8QqR}=ra`oDfpH-~T zT5~xW!%oq(M$NyBAP{4Gq(gzhr8`5)9ihY=D$ke+eh*z^nVsv$xPygB6s@>jRn>mg z?i6SS2@pYFQ^WsFh^Um1V_@@NJxuX`JArsxPKeWNS^^}whRekNi)4mc6UR}DvLsREH#SoYXRR79*E(EZ+&B*hU6+cfCWL=(0euiL|T#YnBK zPWu4$B}K0NFzm6;QbCgr`wheJi}6>-tzhr#{qX^p+Sa_d7|+qe1gr6VGUmAj?r4I9 znQKqo3|)LY$e}6)dQd#m@Gw1)$8v;0cNfw105a5V7uJlIEo3iJY`(AL$9Cm)-2qU) z)E7}Q%_Ho*Yv$NGoIB59o*0XeZxJ<*ZVuS%;%a8E^fLU@Q!>vuZjwQ51 z>NLt4w3@OJh`=9W1R$Plp!4mD4Qp>$Z#~&Fx5(48RLBfAoW5tutvk5#9f#)11KMll zSy6!Y8yy<&V=!T{&u5*Q8j_7jm(bDqybtfjLr$}D%~-icmNrzRl<{4XMA$Q0Y^%f^ z9BiIXt%^tVQAb@}TgB@OdFtvP=QoF@RUYqc7zTd0R+G0ZpcOWyOeaDWUDD0aajUZ$ zFRrYRbnx0&rc4|$$1PXS!ebFGj+f*fWzST=8i;GY41=9gO`xK(7*ibc5nm`jQg>>( zgoA00l$!uWP`K5q`U6K*lvNEzig<*EhZmyA3%czSeL2g4-_-%b2qu5%PnN}U>Tof! zwmw{Hdr7QSE0hcEuAa@#hSQ+i@8+h|n#syw-yhEiy|xka6WIT79bxuq{(bG!=+O+z zK|(@;bX+cv$CJH*{b|pmo0UR#G`Fc46B83YKNslyR2Sc-7yVblmj4a|Bcu3x=`#~o zt*C})!l^Q38k`2b619McmDQ;|K$W?LLv3etAm&+kdOl)XB`^6j&C{n^G2MD%E_|9i z?iTqCHeuS$9?Z>`eIT7eRk+8~tEr*Y)C39%fhq8g)GAqXBSZ*SDiY`KeH-d8BTzoE zoqfFS_i2aFQH0C^3uc?hKwaH;b8~Al9=+aIK$sIF>goz<)Y47S!O+c3UQR91>A~^c zyLXFR<9mCV3j3wU1mbC|0s^b|i8|!n5xR5<0GZhMyue_MZ0P89JaqreM2S@|G)0bp zhQ`RmICp|#jh?2dArTTzHe|+9ZcuKzp*JuPE|s{pb(8W?iFk_0NK;QSHF;@>zc_Zy z#IKivs(_FgbFZ!GsIb6I(gyH6Uo;GYE4I7xC^R`aZBvf>O4$FlcQm0*{1s6jy-z{) znbK2~m6NbTi}_1282cQC|5q)!m-Itm{_}*<@mw9zw?}#3!#9*70I|=}esnbtbMuB( z94n5)u|K$UxeTkUG^0@>=Obsx(`QP1{BfE~%f}{1mtZW>8&*jECoBvO zH_nuR+%0eGp?8)f9I(C6`68)dE@5vUEmzpX)#-nblPi3^3X~Z#oF59mV=|kBZbuGs zALpFVZK{XJd`jl%3e_Tdmy6CpXRql4R1dv`jaD0j9C&bLh~AjQJakWA`hS`YL=U9K zp%79i$jaWO`3}{Lo3p({4Lwf(l&k0=!<80J8=>_}RH)WV=_7yoSxGziLOkX+3*KJ!s8`KJU?XsoMNQZK zwCLRW?AyotaN+j1q+H=T(_208sh7~rbg@P;YxD+pbnn(`yA^~v@O3*b*N;Fx(z{yE zfsP>)cOF5B`$0QTW7|1ssmBC{J&f}^t9653InqQp%@>@gyF$E-S)I*iBIUP0z z;`!fmmc0oVpsJUnC8N$_Zae|@gF6G;peY}%_@;{KG0VE!tneJ8uV#|QUKt0%88dr* z+s1@F5*N&ozCyGxv;^`YK1n0k9UkGKedg8K#u)FRJTP2 z)@U|xQ)2z)A#9MAo!^Z|De!x62WETsb#D&g{)F#KX51FIM!F+%MnLI+{kM{+{-cpo z`}5^6X9VIU{!y=AFG&1Kv{WGAYBtlUv(@IBtG+3$@P#>1lRIhTu@j?I6tC4M41Kuj9T>o%dwN&Z~orU(uE0Iryuh%H7MxZ4$j+~3#6#}0xM6H*;+#BuKDGC(h?Cq2CKz($c7N)lsWfl3HpA` zp>F9KDXQagv6op0V!wZZj7;=%rm+ch$bZYoSRv@NT&`9cuZJ{&3v%})Kf_H+29${> zq2J9=jh;=LsAwpPva+kK5U5x^0HePxFl|SPq?7k382UrcH#j+oh4A(PRZ$4LjmmOk6S*-T>VOV6Gc#2=;AYg@47?)bns`b5&>lnoaS1K`^zu;?bmCAG;YUp{jqjs|LC9i(C8 z8b%|dzOk~~zOlZc2_6>CmgD&Dj;mlwL;IkaFjtHT?E~_kz2A~m9WUE6%5S^BCAlBY zal$yu=8jsT2O8%aE14$7UfczlI9rC}>xpwa;4+^+*gfUVj5~Uh+ zlcnNh>#bxU~2F+zMrH z0Znt6la0cyTQsc*(r8rRK=N^kanZmT{Xu8;E|1Ur!q2=}w0f8RLx~p?j%5a^_u?)| z1JA6y^Y7;qbjU?DeOnZ%CqT0~jR^!NNaCi%-YelGuHdnJWJALa%=+$+a-mMx59?9y zE|}Uvu(zuD-@kY7sQ5^a5JR%j#PPXXCi%kz_10_7j5s;QPCFhHBb{>Q>@!aE+BXoJhe3@Kuae^`UpcGE&Dyo5)1U9`lvhE{m@nnkrFM zK6H$Iavp4S*SCfU2a<-6FSA^wvvt=2YBsscP;EwUo0_L>f_7h=5xua%w`X6)r4$|3 zq~(P>4(LI@eo<;_X$RK3!%q7GP-vfVMPQ|9B|9mblbptGtR)6c9G9zc`1pq;(tcr4 zA=8&9*3yr@(%u$x?7|Sl_bE;x0K-k#WG|kQziyge;MDGok$5n@M-K#QTo%(-_PtOR zw{Mt1o=%4;XX4yLjah6i&+VJEJhMr6K5OMHK4KCaEv+vYq`06BL6B)GOP3-NE}*sS4}@MLeTZtb zUkPFfo1&(nQ3M#YnSBR<&vy-F;Q8zYpme(hCy7v?XVPYwIEaeG@K11S14UfFjC$=I zH>_Pd@ASZ?w_A`_0SW=+I`{1}2c;LlBowED{jO`;pV3GJ{M_HNbQdo?&({fY2!cO< zM**M23Dn#7+s@7bxi-i4w&$JcSPP05MbG17e&W##wQ>fB%j>{cI{G>r8x6mpAc|Y) zkHJTBTC#T|9iF{XqdK;7Y^F!W7`%X{(r?Wgk%_zZwIAH9gps)MY&^sq@FY%j{&rE+ zDR8stuiMKmq}TW!dewD@Ttm{(Ur>w4GYKd~lFY;4_7f!|e^W<70s>*Z4%6*FF5fTH z3(vhp6rW;)(&tC2-X7Om&+%BvN9qE;1+)0-H-Y;CPCStSa_0JA3=#RlPkyOgaiG87 zugx~wX>D&0*Kwoy8Bi8|JBknchS&*C`^hhRVBH)%pIcBetG@wob$j!rjuaxyuy!u^ z?0np3;x`@Qf^TEv^zzI|l!`l=>2`5}QBIH*3{a8PkD&%pD@2>-s_NRLn7@cKGR6;_ z?|9*q^4EWA`2-~HJ=cqs zsC?=qi&-h>+9)EESw`5B;a^9x?EkBg^G<=f{`Gv06fMXxLWvu z{b%&P!`B;>g0o{bI!(C-JxxQ+I`@7$pm#FW)Jga? zig_Bw^=s0;01cPGPpqVXYNlQ$DBOBGQE@+QH%dnybD$aX;W{p(`?CihBhKpK>!dWp zo!_@e8xsLM{jW@2_N-ONy$; zpcOsnc2p}9tMQJha}U5=jXi*eA)gz~;WQ-EHr6zS0@JW_Wovz- zJ8M>A)ht(^IIHb9{kHyd&UVjL_ns}E?n26*tRupwbXI@eFjvPetdfcH4l29yK;W~` zqcH+l@>8F_%1Xy=lw;IfWMmIH1*s4 zts6ffdeo3HlUaj!?kCxMQ*m-xWInRCDXL!@x{bfUYm>$ihDG=&GGW+QT8A1VR0S{k2DHq~>f(=DVA#Q`5>DHPMx1h`^D#Iz|6;;JF(njabNm&w@)P{mOtN&>0t}-Y;{T3ZApI9E&b)?m9wSz|KscF^Cl_Z84AhR@TSU-17NK)l{cLn4Ei6(xJC#j# zHRm|PV~OmXz!$i;lTppDS1N|-m7ND_DpAFw0|UQBU5ZAT0evXov0>AhM=6KzTZ27E zMl3*!aQtX#l7*n>>RV`f*yuiDTb9(R2fp;to=C>Ybqq6T*`*D+#c>?F*&|@81|r5q z_vChlhs8hi?Rq0QUEZTl;fAE%56xILo-JB%>H7tGw)R4nX#ms2k01Il-3@N{>sUof zfq5^R?tExOe13gy-{w2QH*364?tU!mH67^XDg(MBK)x|C(B0h|VgHT)%MK*8dL4$h z{HULIKlJr(;l6bS%JeR8RJ+)1J(33oI_WaZ@6j)4Xf7<;-yJXW{p!A!q-wo8Z(4bs z`1SY_o6XY<*(3|^Jpy;iIQYLN?$NilM8Cb+iK4HjaNbg3&oM2`z4h(-balzj`nrY& zgV+mB;w8xOPwy)!TlUF&dVz%J-}}P*Ag}QKKFuehe|t80mo|y|^T)`<>DuL%r=aV( ztT{_b7QBokVjO~F=BJREqt8sVcDA;_82};=u_RR?>Vd^|v=~#afQ#R9_3X)3Wi?ZG zX#LXc>NXfMEhZl9)NW%UBU9ZO_`Qe8k}D&jtSYIhyNXRHJgGSC)Kl6mygT4g`4n(- zGVQxI&pWId=t(&XlwUy3giaS^~G-i+Wq6NH9n-`S0ezhZjViLSwYy!8xnK7f z{EndudAjEZFfl;Gtn0V+?O{5m3u{xS5GW3`cDhQ7i|!`44|bcfQbZzg%JF@rE(xgpb0wX_L@Mv_LiTX!WRFeuQ)cda`NKS|8433 zEF-vh#l0ngZ&^!qR*FuJHjmav!+`(e&tf{Dav2$mbS>Xz3T)7|N}3Hu5Bx~>vQ&WE!oePeV5aEj*N!nd$M+a3p~XF z0FThX#b;s;ocnRBLktSsyHhWkNKH*KsvW0^COseww6Z$rT=!|p%Nshwacn;t@OHC$ zH03>p>}f-yZviZF^|S#2M}URD=xWw^zDlL&*)%N`DQ8DFMC+`$n`-v?OsDyVFT2KY8G`%0dF%jEz!R% zMzp!OxM`>;SKStfbDk-EtpKwPqPW(=EYKSomko4G9^?`~tPc1F@_NdVLiR zSATlBp7`etCS1AUxY%4YuU&Tby1mE3!0<Scy~Abg)o|n8UGgu9U!Nq9 z%)=b`J;$=yv3h^L>`QWgTHK8#X$B}@+Y~-V&b>;0CtR4VhoKo6qwhE+L$VuB0~9ea ziU3uPk$^$Uq#O3r<7A9mOG67Do(vn#ezpE~e8nkS7&L#}K&U~dsb>0K@7U0A0MHba ze(;d+(sWjT{v?8nK@yAn?IYQrwW5ivUTc07xDYk-&GPcC5$$rZ&mT}xdqxKb2g${S zydY}`W&koB9~zI2jushJFVO^;qh)!VMu`_XhRkG&rrHLTwY6Vk0HNuo)+5j6p(d?^=l z6AmyVu?T45vm5mu{H6!oB>p^1Tv`RHRk}^@FwQTX1ecfRa{w7NVPwyBQj=mYwZww8A5q=w!rl+I7zC!Bl5kn&px;hj`TlXGhRK>vf9;#Pi`PQ}s z(DD3kOlX{O=$==j8H1gdizrC@aPaVEb51KACwD830f#}pJKLr;Dv;Mr3d*y=j=M>K zRb_MlY+^Yy8>Z8MRg9UQK5*MNZm_@K*b>z@Q1BUGNT^d}w5#;vZi5;40LKCan&9`8 z^%AheC=+v)L|dP0wt&ZztA~(F56=cMPW(ke2G9jH#%~&%-y-e|(zVmBS=K$zGY>fL z9=g~wX8VYXn^AB@s8UFuuD1&Fe8$sPo$!{b7xH)_N`30^?Z6}>ZklBFUY=D6+t%n|Tn+BVt~Q!+b4?9)KI-_JnJFrsT|{%3 z3){<_a63pUy-lHYST*W=_<*F!Tl`_vd;=3k+g-y;M@A*LXsJx35O4C{dY7uF zV;f%h+v^-OJs0}hTk45L;&<_1gc^xxsNX7d@h$PK=pBI8-LVn8e9Gni$@0^;!rzpS z1A#%Yu}U0IfG?riZbB%U$fOt2llHY41r13O-4n5Lgi+q-ht*qx0^raSENLxRGoA!5 zfsBD92Uwk03Dey)^z9XpYjX{l*fSuYG~D*?$=T@^?~%J(!Y1RK6{eO7RW-5qzdPI? zab=u0y^Lw_c2^D{Ho^S=!+e<1cW&^bx3JC#0 zr@?aV3cHNeCR-^pIr)$8LibgNlKr`IAZox8A7nQzQmLq}zWk+=lwrwt z^XHH`W~J9NmAy=Q(&9gLZ)N^nZjM!xOoghnq$GJO-N!4WM%J{M(6*gq|Lu^-P8;snjg^i|xL^plLiL+ z*Zp>So`Kf&(MSkT>b3&6MlQyos9*6%g1d!mZshwgck6-nwiVzsUTz}56S?gA)a|F- zylf>LbmXRhr}Dh{B-Sd|#}y#z-f)G2qMyEj{R!ni;t)Jt9yk2GKsuOs-8VC8b=i6W z1Z6F8HfLzQ$))>A0t1;-vu^kob?m4@wGyl8+?V&LNq7A$6ZT%E$Lj3fi0cbg zDhEIc=kVA^m58PKUwsBXydI zTzuPV8{W`&P?Y!Q^8o4-}7E-Ou)mxrK^ zTaSuVZVWv0e?DettNgl9s-o);t-=(*{Ls_OD7Blnq=jzt!G!VQH0&WJ);2NfV7e-W zZ@^$MR}%>{VT@-h@fe6>vbFQESwzNJ#oX)Ry!{!VTmVUv#o67RW4pV#?M2;ce4d-@ zHzn0mK-TXWTMxdo47W$XWb%MZwPj+08t%3cB>vgP`a+<9ikffPv2J03n}d_PLa(*s z05?L2E`^O;;B@y0phI%|cU8y}q z;b3BxYka;&n#g3__lHN*T$nd-1{s(WH7mr*d^-vY3(G$SJSzNreA-oOUlX3;uUuNu zNCi7=M!05wY0#;bX;2r~?Cd+tH8hykz)%7H4aCbDz{yfkUOGO){5>L4gD&OmK2ieE z>1Zah_%qYuG;l~re*8!bewkmNayh(Eg=qu~a)6Z>4;d~(f;?^HmnotpBD-0QPW4@U zd8C{im&2MPm%fTTo`byn$$+9h;3ZWG;PImx0gak8L=S?uH0^J;#>9GbR< z#)kWsn|JJS6@`Gi*2=`fg0nFmV9l28J^PM;P9*AJNjgfUSoMB&fQ3Zl6|bemFH>Dl z&(_A11RG3l65(f2h=_H>1q z4nE1V^Yc4}E<3I(dMiMDV2EL2VX;kzo1`BW7hhbkl#Tt?_{^r7{9}4@adB~$2m>Q0 z^~VpudBerkQeRF)g|t78pO7^7TGw9uR|aNexy7ewCiH^W;;Qe@;$m&BD`n8(Ve#k| zzD(y9#@4|EE;k3)UMxujPA9mXNwjM>F$pvHTd;}_V7mEetR0 zQxQEqJG6XI^O%fsRBUX$o?PEn$G- z&Kk}=nu~SRd>xN3`@8thM^VS+Sy`Z;iBCf-8Q@!yx1>tfRK)zQ)D;_ou$ zq3l)@9v=9(W4Yl?X8RZm7>;hW?11TjfR~)mYp@2s=ZD?R%ucU>;C7ewE(KSOMG8&fzhdNb8WeX3{)LUMku+$W=Y3bM%b7&af z3(zNj*l5J~IkPpUlh@(EAjHJPOY1UvI3-hEG_g;XYNlz4GSRhv?94MVJR%Mtrh)>g zeRDKr<=#>O0O6I8WYQ0OTRqA2E>VbNp!*Y=z^h9})-5QUe1zqguwvf$2NpNcH8<{s z4sWw_Yxm!@1+>-StF-cQ>*!JjKF`~eJTUMGm$FB%P2nWq3>HM7c_V+~NCQ{)c-(FT zzGQx5jhs9PYBMy@!}zJGh|f(RyJE!Wg?!OETbrjaONbdk$-~3K@;QED>)+HR!R)Hz z#9D+`jic}uca}tZ-@*R|3{l=OiSig3;C%d#ErZWvY`%GQTC3M^+@B)9Ykp4^1L#B256j%IM~Ge8)bjk@_;VD%zAEMN-UD+N)X{`x zDqcB}CQbbFng9>+)FWRN78=HrJudvZ2L_hnduV7su!EzG_s&=oaP50~d;O?z z^*dZ|Yj$XA+1RW64kjA!Vguh!7#rm9v53!@TX3#EeAw_iSz84Pg-{UIsv|euuw$Kv zaK)}(jZVec*_mxhX;tOR>FKH8d1%^=kGPbN-h>MQvf%{e4!it5{qS?(lqAqE|2f^! zG?R+E5tOy4#***TYmnnY2<$?HeyRBkT)Sd!7||}#jek@a2ULAyBQ|!5z{}<VLPZHDqn<_41>pJ}pzyCQ0LqcZu z3;-|l)0lqMV>?$(zy`$GM%t>V6C%%Vq_H}^tf#75$c^_RnY(hBKta+5K1b?#{IrMD ztiQ5+LXa8S?aV(X8V-z--iCepQ+7gN?nZ(6t`%-V4+fw_YqX)vFFYX{e)U2vz$-_(BjNjUF}1_*dB#M`2zbcwoYz%c)?-OkBix;Py=vMJzQ0f?D2W*pU$80Ukm|sij@+eVNj4hIaD5c1-P|?IU2K+s zsiF@bqLi|QqPA+<(n-a&we1z$rKJyk+-8pFi~vrH|J z*Q1X6)8ldl=1LX{t}Zc4?{-Vry%;$te^qvv?4(wt8_3$=(_tZnZYgK+gz{Da&cr2q zfD7aUT!SUX_A!b;19`RzOe+l81J2PN-vx)&5O1D z0G1ra)|MAP+)4))eN%a3Wx($D(}CZ=0WK_ij9vs*uRIlSxR46C!_LaZITizbkJxo#6HC6Dz$eaSN_(K!9*=!jS$@2AmW&J)n- za#Vdp`j1?r;sxK1H6V>6|7MkA z(I^Ait*;qyTA7$62qZ`*AWkLVkrrvuUJd{QAOL|?Dfk>EFBe0NCH8T96;R0jY6EV9 zwPs&5wV9g|^--LWY9knVU!c?k;x*sHv#AxSD{*6pb)!?h@#k@W}GD0VSQ0n;ElN()XUU zUZ(=DqoSPx=y^uxwhKwk>OC`2ZhvySJ1n=M^iE{(noX>~%G~0e0VZ^eO#orA5;{~k zc;W7w_x!1`tlI;I(Etk-in21q_)=}Q+6hDDOmMK%=7j6le4KVUA{r7YH#KOak=XkY zcI~9Tx&L}?e|3qySFE?X>n|i_juV)@kt0lvJZ94$#ltic@_8{=^85|VbX^qeeXlBz zj$LdAkzRTF`|ljFA5k=hal7kE_EZ1Da|WK0B!kbwVoh>MKrX9*rr1XX&?!^5vi z{@Q66M~;Bo<1m32?`LD>my%=}YHA5dDIUYh)KnbsUkLg6M#1bGd_?WnU*O#}|w~=#$G<=>|M$L=XjX4PX)sLuChNaA{ z3JZTLWD8)}7@)6y2sJ3wD&ai-vD^~BxtV?_Gs;4q@ns{_J|jCNlrrl_A|*ytLav)@ zt>svH*iSnacasEGvStk&bo8&bqQ&Ze_b6Oz2X5SY#f0u6x2{R0EW_37})VFcn{QVv$XP@=|E=MWb$cP9rH-aTZU2>5kalR|G1FD2bDw3SQSIM-XkZ&L# z_|!j=`RoT*>t5Ft0EEgF@EAC{c|ra_Hpz{dG3r)GiivtP1h6KaXG;Kw8FsjbAB8JL z)lF*I&o7`B4|+Ppmj9*J^cP}CWJ>pTmSfUaK{cY~SAVkua5xVg675h@N8qcgSS&D( zZ1J25_a{AVro?2i<57`Lr&xQZE-tx8fCS;{M54k)UbKQhhGOmE^ms=Rggh-~A zAw81S?mB)NpXVbFaGP@(%9rec+S+>p45I+SP+D4PV{hMcBVZ>^o@!dL$(T!xny{n+M>2?0wN94NC+sM(%s!9jeyeKsWeg|(k0y> z(hbtm-QC@}f&I;m=bZO_uj~7vKT!5wYpyxRnsbcjxyKM1*r@4j>^jwnWeCWw3qGyRUEeqbt}|ebUg;~1!Dmz3p2A0+}}cAAL1JlA{{2CY^l>A zL#ax|%i{4g$e_zhE)Y}4rA#xc_AFZz$e5(%oMPw&Hi$Qsej*|w4s>>Qc-;;Kf;X1q zT6qAqkQE_|+bS3lMk^UBZHrJDw1{cj`YE3qTd znxx|CX)a;)*d}kr=4)gWq(L2rrZ?M|0C&=0#F$VU+Q5SiQvGMd!UxMWgpr) zA&*1FGUTL!7HZtBg@s?ap~rv4Mc3558qpiKoLsf5QD8RjV6DHNea^5qUpBDiK)NK8 z$o#DIP~Q8>z3yVC6Qp%dHu}A+k1*;B^7FH~osEq@gc2k|*0m12s*UjMD5aQ(1H$@< zfGu;u`@$F1I*uRy@nt}0_?HA`*WpeYb?DlGH7-&>o8M*7;UWLc))^@8r3O_r8WcYW zsr)e4p$bcYKQ1t1)1$%^*n`MM|H-84L6_*XW#6K*V|n zA+WwXBZiupnr_{B9e-1iTDvT*0S7YP+kyp6b#$I{odMfJU9YnNP+dm6GG?*2Q(6XU`ch5kWo#1#d0SXF z5k<7bS4|q~0wD0zxQQRh=}+XvJc(CV_=-!M`u+Q5N1}DnyQU%N9RteM`5x5EW!)mB zDYe*rd4B;J)yNGqm0$4jbGckdM9Qdhd>IQ1*e}6Y1O>nH-L^UlUe;d}`}w(eqYsH1 zDVZ9Xe3jySlS+l3sd`lhN6z;Bo_4c&z2;O$kdRA;dWxJcs74?DRb%e^m^puD)*FkMpAGyF^HN#BVTA?wDC`4$!50faE{ z|8RL~i}O^a!lbqi()ijzomm=`h0ntJe3x581R6T)*>h4K8^?)NMSMgYGJF9$Ov+P71-r7dfD>zaiwxQ<5pdrW)9coZ z-=8#vsiVS)qN7-NWXYD~?!U{y)Pe@{EI@-?i3&{S-UWHsPko~_XqDc)5ET5ZLZaLWYrv4i9D25eezt#7wrKX#yy3mjRu`SorBcA=oO9kh8( zfgq&#ZY{q4gg&Q7PsB42Cio@CD3vQ1GkNQQ0)rh5{aAMjDO4*MY3CL^2h^kVc_5UC z-^$yflKlH0(1+H37RiiCBgU&xMqRtZKG($Nn?xJ>Qrb`?x(CWfZvdEpJ-wkbXdpp4nAU&3EOr+qXKw%S0v7MVB!i7E>~3P!v7=*7IRAo`{=FOhP3Gx66i}gueZ$Wk z<^({Mu^2!5?}>Xo^HETHr>vmz=kGj#T`9~&wq!thPp<%9K(gu-qAkbgSJng=8D{|^ zWRlWyGC?70bG@+34x)$Mf2@&Grd+n{{nJIJtG&F8D?C-%bRS_TR-j(h1}skDA`{j^@BVM{>dVbha)m?-=e@fzO7ynEOqcC?msg?gf<)r_1QB-tdPGm z5{|b*iXxJc@QnB9M|EfRywRC6Ix?U&_@}}PCWn!hi(Oo1=(%5<^ssS--HM}*u(2#J&;$RUTLA-{qdV|VhRjK=pznPd zBGlmPeuRdK12u!2?1xN`Tu+rIAEWyX0G~i{EZU-EEY-n zs6T7Wf3E~t4m>VSCVF0qV30~g{r4MmoQ4xvr`*-r(ufRi|Ko$xnJ~qwX`GNnIq8NE1WYPaVy9APIZmv@uv8W)N;L>nLP#jWzp?Ob z`8um+m|Vfy-?;a8`ZbYbNV-WWeQ=`&({hWnLi-TVTTC&K6b

Lf34#*F9K#&X!v62*En;jFBqnH0U*6wsdP~1q#XAcpZ`3oY|JEaT8EWgsJ2!s+ zXPP&VVRx?~-gGX+z~&3GnN?L67z#qLpkU0yZRB_rE;1InS8wD0or#1nDuiT_O0J63 zze@)rn1h*&mW}HR>@y3MVFOK8>jeaQ)svfT)h7QA&NpeaB>I*G;m5dgL^pNA#M%nX z4$y1g8HhMo1^DTdBOmzDQNQFM{N3*~lT^=bg!BLnQScGd4c>-8fN~)4BuHi5dDSBH zmskxs9tph58QI$0nL$mT21CwKjX=%<-7nmdKbd-;-7KR$!f4V;AlO+#H&ICl|&WM+x>Un?y)*ZmCSP~>E<*$@8 zB1ta8w&oOI9h`i<@qxd^uR-M{_1_K^dZ;~J8h*JeUDM6)hfMS-735DO6Pll^&=!4fq|~% z7#96>yzjR}XtQbss`}G}t0;tA$1C^lJT7l&?BJNO$8`;kF0`|Wi|Zd;jh!Iij=~qR&E!`Q0Aqq78;i~$)GrWjh6p92d-dQo zS^8&jn>vC-P5cC{*+;4W90-J|qeaBUK!y^8LE%qxR^Dd}-p?cC30Pd!9F7nDtLAR+ z0s+z?fzf@bXzXo;Jm0Oh%J5+Y0bA4gg!hMt_@p?d?T_;O6SKphWP3XI+Ur{!ADMY? zre4$Kv3Uu!p{8bSamf%E;sgy1PcZpCsjW1e?AA9I?(QNg9M5n+3QY&?&Pr~*>O7ci znBX_4MFGB`$QgU|MpzhvBRTjy-Irr?jtUFM0J4)8Y#V%3>Y+RG^|LA`oAc-dFjG$1 zUm3r60aCnb>J$4wPX!!aPU|*-tw$H^cf8*FyYL5fI~iHTO^`crz&z3_vl>x9XxiXj z@Sx?Q-VUF!dWMKd7$4;bJRiVm*8(KJKrhN^C9}T+y=+o7A2+-~%vv@_yKTQ|1^wiW50;fpwvvX6-WnQC3alu>g)hlo1wUL}=RK4nogU*BcKe0$*= zAtlvg4G?U{U!=;~wH4o~s_Gb4&4frvwPTcuP;>e^XsV$DC=d=oh6?rLM-B#Qq*=ym z54q+ph@s=)E>ks&fXcb!QaLKB@o2ME-UE8{>C=M+EU?`&A!+91y1Pg;ym8K2P$k{uD6Qg8$xJ+c5Vr_b;kcK~+zMquNgJ*4zC zeMVZ(=0O_iYi6>WaV_kcxqXmrApO`M$3_Lren94t$Yu>~HTMQ&6pddNfJJs>k)z9? z-W5%)svsbsB;*0;V&g=0pvt|D;dj7yGy>)hvu=aIJRwv9ww)|y8Qw_so?qbnG_^r7+QPaCDil z%HguJ(U`l4xWtiE0}fvHZtoHk6H`3imX)9ab#&Ei`C+A&m5i*UTHbfII$jE@qq>Fb z>r>OHC=5(A1*S=&syUDYPFJA@rBCnc)`NjKwwjDGdkwSW)!>yEi2mNwk9~n_t&c5C ziZoX8R#X^$JY;H{QOi;t>U9CsTZO;4nE1pp6d@ttX+5y31BPO4`jZP3YLnGVkc

?!?U7!?aYRYJZyFa(MFar-C=^J;{h50ti13HI4fZ4U`U04d7x)ivBFT zB`Hml`MbGdXxX!}w6;FjT>W?B@V}T`unib%HR*?A(xlq}OXK_H#}sKm3SJ~$u$L!( zQrr7Tu?!T%!C+UzaM%$*#8=lnuraGhr+WLs7i_uHNt+>r{g(#alDyw9R^{Y6X? zm-^s*+69hh!o>uh!?Tn>c-J0U55aHiSbESR>ac)8PRpyPX~@X0Y|P7=)8mrRZvxy? zAFZ!FYxR>&C5!sx)lWwApF_POcMRQMcHWn@+S+V0R+$G_^4b7yD-Q{A@Xe~{SyDG? z#c#zq$`IM2Z#{0_kb)LPQtW~`eO9eqbqKc&AGUvxi9&NXEid`}hK&3T$aOQrs%ejjVG=mfq<5wZ>+?S0H(OBCHwjhiZ>e{f`w48^oWJ zgi@=0(5G@q_O`l*w=;dQ)C}~*tQJl=Lq;`5iy{=G6XQQ#vGJu15Rd->gUfpO%%?89 zn2%h7o0%mUtaN)}9viMAA$R|ZV+D2f46reu_^!3eCLJbwoX;lu*lmq0OIzX1ldRsN zQ{=%wR)b&7Z)!BgJNv|m3F8v9W_D3N+9>F!Rwo^$=M3>E$**r@-<2xWe!r-R{m#%Oc?Sr@*)Ryn(& zWUS6I{cC390L$oeY*BR`lhSv2;{6Ce1N`v~U&uwzW__>0x+A>~Y9(V$W{;?UZE?J! zc$eNdK5uGbHA3^ z-R`n0(ucE22WYjm5-m#zo@Vitg2hXQk2NTQfcgw+FduAb|2#lV z3N>&z+hyPi_r`qnek3ExlfogH+q3uqR7_k9s2;AR+{BC2beJtM&U`dh6>80W{tw6X zMjPyNf?(FDnvJ{j(G6UVEo#&8s;xd9e1B>hdRbE9)2U`U3~(@2RhxL`yM+Jwb6$h6 zHETSGno>$W9*5Q#tZgJlQlgYub@a1mwuL9DpNoAFoqa`&PWE9bnNwFLn_*R@pkQO*8cg02}~P7h3LtTKYR>vD^5Ghd`46$_QsrINL3%QFc~^c~W|?Zr|4TgS5$Eu{-1)&$^8 z4OXA9nj9UTUS}X`#@fGTG_VXk(K9Gk#ZOaMx7u({0ZC{i{Kq=;1nbJFhKrd6ZTrVO z@nU2#r&r4GCSUE`yzjQBNXRF49|zUn{KnCB8`Ed}G4sCkW~riSf6BS}0%L0L98g`G zPXDClo0~Zw%s~N1A~(PItjWo>D>vEgHtm%-^r!?1?gU71a3g?2tZeEgwyELhnjh*v z1n8<`)?OE1=Cz=YQ$>u%p5?Ua(2!~O0LX5pRmT1Pdi6<*&O2p2CB;3vqoG^R2wCr1 zDSWPkqY|~OaccOrl2QiAVuhd*9S!_l&6$~_63w+GnzV;mrFpJrwa6E9o*VKhm-7Q& zm!28j<(?Z>K&iy;x_oKveH%%z-X|+1<8s*odFi_Oc{Ni6YOV`hQN*70$DRC_^kR_J z2(M3X+&8~zi+q=z@_j)DNQXi#a6=0vPvi@~ zUiS~mD9N1g0ix(+(xotY_a3VYuPwSrq;g0^thyTS=~<_Ecq*IIpIsnR(EXTmR|379 z=(1IQEth)Hk$9j{I1cJQvfaJy1yLXAfMx5f$+C+HU)E>wC9j#EP+CxEsK}>6YZD$% zHtQ$(?>p*yhcKZF5#IBkr-ukqAbrwPhLrND?kiw_nM@yx{EF4n=>m zGm)<|UvQT9jM;#1~mw))fN1t317RR#Gmm z)1nUq?u(0FhL}buo84mZ90aIY8=-@dbUSK1kWqOcejXVd+$8SrF=7TF4alosEEg6! zsgWt?(|XY0pl$EHk>!>53wIL>&{N2Q)m%*h&>1N<)nplWW{S5kBhw1I7a>T;U)QGC8?W`7dGXuj*iVade#~ zAg_z`o}S?zoEtOV-P{CbD}uLV8)y6tH#cQXxNpfIVeAW5CKe*~fL*)beKav(p;6%g z&>*O&Jw^^FhCAT>qIiOE2u>L(58a!ubm^la2w3nlZow4zF)b`i zPnfO8*=9iW?Pnfc1;YMcF}W*ynV_YM*z{rODFFD{&VLb*Pdhk>DfHGKerIYVuHUxJsRTm1M z&Fr~N4joJd`$8bh=W)FT!m+CqPk0#6+B=QEMpQ{K(>5qWGs}6R+iQ|4c>@dnIT^)q zi8lg>FW1YN|MK=-iBk_npUF9orP0TZ5v3MHNld-3W;2`Kc3vayXmZMVZIiG!UVGlL zJzksShvZ-AoqA?%Vf%e|pU~5~nzH<$#7Foa^)&jD!Ng(SOJ7aNcUi4VNGOP*A;HHu zXO9aB;$^Z&a#awg+9$emRrBLVvRSLjffR_tLzygO95#N~q4kC0pL|U>8_E!MpWxgdd_PTY2`&xm+MF*YTio*)FHZXoM>Z*o_g*TZC9Bc2c9uPL4X3x*k|2BPb zUaw|-fJVuWPp5oXH*5Zw)84sdX}BYQpH7x6CRCb;iOI6OqzcN4701S-cXfEYv}EcD z9YxL=seo=udd6~cvpl|P)05I`3n;3yv5}VxP33U<-jqwcn3<+To$=ep$1%Uf1#!z- zE_QG*9~E^xFZvepaInHZX=>WkY>#oWA@)pH0L|~Vt}?^$I_b*Os}3Qjr^4%`UvkCX zFr5sRHezjF6jAtBW31^k^!n#xy{}FJ{08C3#@l*!d9<(jUNJNFPK?I}wRcAzWkmh* zC_&LRi3XlFIJl=EX(QtB6c}L`1wL-p!m;y$Q`Z16VR90p#P+QHJ)z$|=VYNIf3Kuj zth8Qp$w*6LWFZwU#JAZb9VyWUnpA921$*dYqU!)KE|V6m@3I^CjGgZqtDY)6iH@+M z)OE55Z7^!c%joZUU%SO$8@o2{t;%u)ZPub=5Ka!Iu_w^q^5BAO>N*tvt!W#XTOA@m_f3Q!ryQUa`*=drcMu=AU}NwS!jpQ!ky_aiaS z5LhytmaT5g>;#lmbpoXF5Rsoz7vwsfZinxH8RZK|03Xp$4)AZ0EqoUF^psM(8$tPj zkal)HM{ls5Cz|E-s?w(ipt>(lE>7P9xf>czff98;HWnwnW1=?f8vO?N)D;+xO5doH zW7kB}SM735-u%pbU!{S|$T>g}qmh*obVzYn`sVShsvGSWjY36g-$$&7u6lE89?jl_ z$D#-zn_ilyYWMDdedT|Sh5&b+Q^Cf*v~bwv9g*o)GT0+&6>ke^8`JxnfrPB?UjdgCMa_lS5T_W;Mf)-Rh)7`I>Lk0A7CI!!am zOg4Lk6Yg$oZTgG7P+xcFbi?i5np;nAp~SxV(P2$(wa@m}`k&a|Ffx*JpxF{LP(>pq ztL+ekf;13itn?cWJwmJ>28+9>H(gMBL7Eq`W2C)JagY72o#0siQ8%KmALM*SCICkD z^#uxO{h7VN5*h^3oAG54!>C^~zD}6-*Ts%{wL~;%9Frk#M7lOU`W72ImWa>mx;ZlH zlG~PRVbJ(s!Cqcq5)CD)j1a)#tR!(f?YiA+p6QSJpqM@!IVqGzrTA?*(5T1CtK zbwIOws@TFPX; zvdJgY2-Fj@Fmp4L6*hU(N`c@baOfvKN+6d*19H~3rcXhkmX)0J|8rcrcs?+H^Si7e zqwHrjD?OK)_CCV9Cf-9ZB5ty1v%i1TKZlSsH($AGFK@K^kgTrZ!1^;tzT|2-6zj8k_QteoS? zfUZ$A-T;rXR9$Y@cm3rLb=8dLy7oM1K4JUm7}eHOp?as)uY#%H+rLK5)K4hB?)JIo zM(~NKp?!#0(aU(2+4~IsGdUY%*}e+ou;2PFUY$Q$o2R5r1T3b1?FFauRaip>Pss@H zcJuUZ0c*g00;Ry(Ck~2B~_p9fGGo zodb9x@Q9@vJnrMCbmr2vNt?Fp9czFs?N;!TWA~wG4Wh~6b|ClZWW2 z>FL}zcOpO?gUEd_6?#3;FL#@`i6#+y|S@n-zS-HXjMY7pUfu^IBBiEhO>l zceAetv)9!s5KWWytTB+E`jCJ|6rh2{g1j)O!+-1Mh3c%o<~Yp)DKmrKr7bUP1pi7C=)+UlL+VKsegc1Fie*DE3KezFGzn5)&j3%;*3Mtaa` zbw^FQd0;`r9f|H)L*;kzmX#%XPp#R}2*}g&%<(rz)_mjB)0H2gf&=?h*zF?Xx7YEh z#N0nm&iEVa-4{Mj*K#4nBy)O9!5{R-zrFRP=()QIjMa5ps=Ia<6B_&y+p`N_?&}xWqmy=0qj|35i3{6XgxC4}?~x zmF5*Scy@g6miIU~njOkG#Qq*f_d#Z@n;6;@`RZ2>H8qpY!er3qBfu51|1hA@-~_|| zx$Ut*!sj4f*C8JlJRnUULfWk^Zgg!g4^M&W6Hr_X@~Al9#Zz@lQfCcYu;E01Ur*(S z-W=$1daP;)8yc#s$2xx(#zxTtQU)fbp(x650NNFc%B!seMl9XiwGurZJ#DiJSF7dx zF*AUKZtoB&fWPvkm?MigZwcn(-ynGzK=t#H8F3te4qI81P z3G}AQ8|-|SHrEMy;a84`b{Po}Ytkzl)3Pcm8q*$9U>rO*foP&T6|tS3-;9617Lj%r z_cUbTUnG2lh1gEVPA^gkvorNxjtI?)kpz{Dn=Ue!Q%`-P}+?JB=xjQ*`2L|QaBh|6lfst5_jdIjJ^vKZh0?-w4!U+JUgD}L(HnqJdNMbeFI|_U9cqe z-yKft0bbqhEe;ij-f7^3?g8`Z3Oo()E8SdhL))odh^v=P(IU_%VX7Bu)_JX9L(wL} z=M210ik$Deyx`2MWmXp7LGNzm8l1greibML>PhH$w!+dwdMith;8BULdKMNK6HDe_ zC~>S$))&k#@E__EUoB=BbKvUUywU09Nzk@ zPEwV@`p^J+1$J*@(U{A=#FEo<>=03a=k@EOqig)kfCk*BhST4pa{7C99Na;J2PSF& z)6AoyB4D$wyPXf@;eVLS{@SmJZC4qzWy!41Pb+=5$dVY~2RcBwH`)a%s8?nvnk0esGcZ+vuoAGFB;h{F16JT4beadFvr)+GR=sWJ-)%HZpK)};S^>{oB|#m;iejr$aJtVmh02|ElC-QZn0ZRMIS*kjk}uaQ zbp+iQh#FOXZ=uFXi35Iu)~)pK`OgoZID7ZWXlX&N(w71DsNnmb@k%EP$BpCXpesve z@QS^7#3`!7UJ|$Zr&DgS*E`EU(ed$pFr5!KIL6MMiM$knbJcmy*4$>0Vx8v|?_Re5 z^Wy1&h|;noAh8bfZVFooZZcV{`ORlXU$S^$eN$I7XPu8=Ut)sO z?W7RBWviHLbnRE|ioGL-X^AG%`$#R)H;&BFLQY5U=V_#*_8$jS^1nZ`Egi{`xjrkC z4(qzYmx=TtmM8|SIWBrwjONPZpr(%^VAsogdTx-d-_RsvcTCneYRavqYA z&x|GO3@f$2w$oCH+A8xDaEvPWLr`AE{KX+gGRvK0c1L%qHd-7*G$es@1#OkXgCnv`t_6s(?13k#Z$PrMG@~usp)1SgU(dPlZ zM-VjQX6kQ*9Kf%F_Gdo`>8vZH!canU8p<$pNV9}KPrZ3Vn-3Z-Ir&V{P|?v82)X^MMMsaJqc2UCKXP~ofZ+htF_=3RhZiyyaE3)l82Uy@MMX(d*PucZG;kUWk7QOgXu=WXXHfBd<- z*<4y)QN&x)Tz2V_As2!?R(4*0HtR&+^rJo<;{+QEchiRJC-NmWF#-8@eqId)L(`Aq zM)WdL^bp0=Nvt6a=J?)R$_lZtix_w89Fzhq0#&fb--t@?_#lH*msl0fit+o%E5EsB=N=`6nrd{vS0x)BjLUuwn6coOEHV;3#40Y9kG5jN z75#M_fpuJj!C@duy{4wx5zzi{*pYGp`G^(BwF5jeqxXIx@TF!faXXKP0=v(qLv1AI z)SFI4cYx0|!CDTgv>#|J>_y7#Tg`le!r)#4yILVZu7;XS)D>*VrBvF#3 z1MdfZOPf3+^)&EK<+bN>Iv1>B(k8WeWcJ~AH4_K@QWqvMPDsZ1VW>kU`hYd!@833l zg2HAXVBBDB}&Wf+^rGLs| z$3~}BSXN9|?oY;}5eKgCgF{+c15NeQQ1}_f7+gcGQZqWuo4&q21agp67U6aQ_HT)I zxSa2WmENZxperCG0pMioFAdu9SK<-C6H0;^6CL~UqcIFvd^q$AKm^dg9pMnY@DEbe zQ7(fvlghvaKfvRlBJ~0s-e^@-j9{!ON>V;AfYg=XBTf0Sj27H((r73IkMh#?`6IR_ z&>q1c2bd)1>JIdkrR~M0e*$z{+ETF+)lh;si|0j(XB@@uN*rGKWvTduwy%pR=n*3q zThLHMbjCtUF@=ML?mqA7&E$huTeUYE7G%z*6;q<^Yftgv!4L(VCGH1;tA|I1fU?(4 zVQiWQYdj=sVb7%_9!@0Qr{5J_FhTl~M1_PDf!<&YSf3?>0~u*v%Wak87m_5-g)3J& z>x#UVOVMqxGW;YL!ZDFHA)m^81)PgT+=6HdqWaJu)4oF5LX7Fd&JTw*?;P zV4T?cLZdx98(r@eXuSpaH;gIH3s4hKbg_V&WE(6OaNu!U7I`o9=+`o+%K$h?hgSU# zD+c<`;!=iP4bl>1GdK!#63Ok|-qs{EQ`F0)w;-5vgkkKBGzsY$p_FA$wN^U%X1-Qs zYUp*!k;GLvJKZfbEuajVm6G6s3W0+K`6WS2g3pvzl|fXynXnPB%chN))EDbHgCk(I z_vyoZ8ybZB;>Bl0(rhyM6dp(Hu^?ZJb98dEdfT(wvp6YB&&QAKK&8R2LDjr89HY`| zCb$n5o@O+3Zb_r^eZ_{eq@!aF;0=IUSC6|!5%8ahtf>Hh<@e~1#sC5A1QZKQpR49? zA4C^U5d5T8R{9n>1~N^h>hFEUWt~d6Y2_r}wV&(MDOOEdmAV>bqV8~_0v_BBn?(m1 zPHfEklYadDRBRntlZmD3Kw)bk3nZsk-dl@I&5t)03kG){hQ|RvdV9_Ge4SM)i+w=T z>9I}ux9(MrM|XQ+?0}`hbG1U;CURl;JVoGNf0ZTU%jh7JMf)1RFMrI+Q&^aXBdfoJ zNPDUu`3DB{r*fOO%uWHlK-SutiDU#}B(HUUFRmxPd3PUu7>dvMuV15)_ZFWT6n_Ld?_6T1xXV|-{A4u ziMWbp%~_owZDft*GBv_N909`>BE#3H=nDP$UEqo*nM5Lp@%h{t)O%>e~)NCBU3G*|!}NEgTc@p_cXVQhu#ekEQjDkzi;mDw$-O?vzo~+ zT`jkYhm-ag1RT`tobMAezFH>{)i(fkNoNf+>h!O3ORg>fBU?PS*GO`G>~ysT!)-@# z*uP8$9MzRH0bi&hGZTpvPZ8drq{m2vwH>RJI@0p1utyX)?}83)0PheN1vd8S4{ci5 zK1l?qa5|48jZ%}jT}UF@;?p6HGB7iy^QrCkZYk3g@K&*%B0m7NFhDdYKM}DSxX9%# zfhUfS3PYE}ZA8BoE%{S*+$a9*ol#x+k#dckKRKK%Z z!!t^n?^Jn#dGe!Kz=FqQ@5f{NUz-80fThxvJ~_(t(xMrmLBqH#TOJTM323p+MYFu6 z?Q6CBii%2ym;eZ|$#m#Zjf}3gHnWq({_IrQ&!5Ji;hXB%Wg@#fTcu6tpNrxUgJ^^F zxPcpw0fv}T$$@y@GC;oUaM!XED375r(rC)17&QYxl#=?K<_B}XpPsDuC5O%B6cy3Y zy?sl|ie+eJZhla+&$G}J6K&|;@vR>Kr}NIegSQg{0r$`#=g?Am@<#5SE4NMX$)rnP z%a!DtRF+-?bpOlA$N9@0^TvqN;$FyQl>3Psp+!FRe67`5+tEs#&ou!)KQ${J{Yi*6 zSvteB(uaZ)MQZGC*+}7j%v{Vc`YVOw!f_~*bd*T)30@qiDUirh+E%xFNPw$+g)x7!12 z7cc!_YP2U6n*LR<_zVhhYh)-pC5)JVZ;kK3FS-kvIg_sh3*A)A+;|ROA>7%fs%Q!6mFYCMm4m6 z-D0hQ=1Hk~XgZ&wX;@faN#prM4#@d|6l5r}@*&c$zw5s1!$+0P%frJ9Ad~*l`xX%5 zH|HOrCw`Pmt%MFPrQR&Zg8niLllftkC!mG#4anhJP1Zj>*|n?9#K^mt)}UQ8KH&J0 zbx7}n#f_h@lO}?slD2Tv9l;_?iTP_2jOr4|0!L_&cfc*u96V!*SZj3|T$-uzuwaAa zOeABLyzgi7N>&TxI!9hlgb!A+sN(jRfn)n%!5BMJl&iP&!2c}<7FH1Q49pDb2rH#5 zmX}w8e$WI+I(6s!=9ZRa+=PuAcX(mhl|YXuUoM4yL-vXc#s}oDzgdPQyFi}vr~?3d z8?T3-gR`%?={r{d?@aN89Mo+TZ-6ez^lano>mj7#@x7D)tz&3k)@~;F3fI(T{gkTY zW}kh4?`*Zfc`n%Lr?*06*ZWd7wzrOsY01_{B6?YWW=yTD8V@U-Bp49X&(4rdMhsKh z-%Bd!lYAo$5HW8AtGvz@HOK=1cv3(BE?{V4G#-8;AGvY9{a!EN=qM}p{2V3pXJ&bC zumym*FZiD~Yz!+zPuokMzbw~8{(#iupXZDh77KZxPKyrzWb+#arh+uX-Srem9SluY zn~)2r{2DSko<)|X{p~~csKVLd{Sh~BllWMGzkfw}xq;*87`y({D=~jK8cIzqt&hM> zqBZkYQyy20^LL771~{An0*0BDsi>UrRnEj*J)pyZbUx^77`S9;XjqDvoDAMft(srgi9;r{^^OE=HK0$+Y}>Py zpK*6M^iOBNW4o+sy4nPVoIP0Q6cDq>J^>-g??0_v;%tu#3rAOtkj^0sck77}o^;ON zp)rb+yREH4sW(TcAal)`r=q5$6yWIGBO3w|o9#KESOj|43FtR}_)IuIG!;D6O6eEY zq0cyN>GYkG2xorxH3xZhMuTt(I|y6PeP*2JnEm|y!79An-g=!19UB3v^NI9yKFDt- zndiYcuUdL9w4GNv!dqfy=7$W*W=anx_fi-d>e7ZmmpzXe3@7B8jT4fm>z&&TwDHCG z!6MAeG_PLszStT1Yf%Quspf754q{BAv&mO_o}g1vT1#?dikjk9&0Cb^5zs{ls1RA* zR)atW8IYyvL5&15cQIixIXXJ)(*#JwH<0@(06E>;r;6mfmXrND?y>ZGQ(jkzAK&w| z2(|zU@pQFmLFWDo9p12C(63Ky@H1t3E7&T_H*&@_qiHE;veLVr&@|KZ6=_L`fSc^MR>JgbeS zKwqM^6e`TuSXkH%w+pC<12_g>{VuCRPYnQ0_g%e14X7=0lDUJ_KXc9QZM(EuiO#}J zH}5J_U>6VQmjb=jw>4BgNqWcfbzU*th12s7dC1i90%|NFr&I5i4R!RNl%0MSxKa+# z$vIvoAt4D^?1I{Srf2FMJH7cYO`vJ58Vrq+KMX;^6u40yVpCIcaoM|cJYA`2Q*XV6 zuTJ?Kfu5{sa#0 z{m5Lk>rNdpEuY6J$8e)HTH$6!r+hu+Zux>w!w1>oG#4H8C!wXMZ%O#Q{Hh@!Zk_FV8HzB9L3-`V~AO_B)tNrlX$53M?cxj#Cy@8 zavC@vf`>=}n*WbUX~o-wEV%ft+pO*SwC3`qt3QFUsHpNgkemU-&TblNa|=^>NvY0=+dP z1XDJo>qJ4n5)Gr$HcT8Qp7kmAmj}D9#hIe!1g&*XaGYdl{kttM;mLle z(mK?>$vf3v*h!?J4eHrC4#TwWJYLpYc%b2t(d`$MuH@QLX*BiaPyRMB{}Hc)`^JPl zI)j+cJQJLt1a;(fBX|2Rlef7Vwy{G&sy`IEO2uk-@Dk%o(tna{BBLM*jX9QZ zldIl@7@WHKB4Qq@A;5KLF^Xmh1y7$4wRjvH!!feQAXvB6xrM{G!pNl;=Au;*BW1~P z3ePk&R1n0n#&dyj$F|txF=vhA$bF zkY*b$KUf%G53JCaB45VfuzVs9E)3^vb6~2vM0GqashWRRuFvk^tsm)?oPJ*M=tDzv4=;tCae1t;Kd7&%0M z_T$9DkawJDeoYGkvY?>71yDh|Gs#@=xM1gZR{Ggl2&G83@W;Q=AwsO7WhO^+FK+Ty zcs7i(S>z8SJABWQ1Wod->&a=HBs>Z)m64Rg&9@)cU2{ai_e4 z-laHr8I)6ScAq?X1#F6+EcBGM_(4}GK!3+;RXV6WIpaI zPox^OUQ#1=20qX4Q9r)wfq(NvE^#hT5u(W$w`bLuwW*b=`2QGt%djZhwGC8|lI{?Y z0cjDWyBnlSS`g{(jsc`Qr5h3HZfOwd66um|q+{RkzH6;-?_c{k=9f4#&)j*%88q^R z?^&CSp7Pqd%bRTsxj24S#$LUwa5>N!{D(!PBbVf>;@ka4k;3MV$yV?9aDA5Ofk0#% z%$mZGgqrpx;7V(FmBerr=c?K2cijVe>FTLg2LQ|hK-)M??q1zf>5j798dpJdJ3O>1kN3EdZD{ad4p-K4FIOtUs(e`mK4pNEc9vAHo7 zsiAGjng?qWujTBhHNPC1>Ae?9ikwYO=g-H zBZvm8^m*!gjYF2^!>v9k??8*%XQfmqWF5C{ceDWD^}fnQS9vgj#y=^`@GkzGb?!`o65U!-3d#Ijva@B<}ipE6dH9(fz8_;fNnv8uMjpTh~In%upQ49UjqP~7;rKOU}U80mfC*) z{PvdO!Ov0u_5^7ApPYNxl-&dE-TN7pwy$5$_*f!K$pe0lL@yndV-^ewm$bC3ax&d6 z1ExWun(E!^{oM&*+>8R+bRFlN7ZA5vc>k;Fe(6B(QU#Q=9(3%0++=Roxk=kZ6-m$w zhCd*X)^}f5;99a=Xq6 zz{%qQh2Y}jb@z1D&s)d!d(M_{@}GWhiQ>O`uPtdJt=V9c3!cyzU3I+765BbtH1pAbh?#oDfxa z#x^0$InZeZ`Oiz*E`38o=>4sz76_z)C9B=U)fPtEeQ{xC^JPzvbO;1oz zQApg~f`JE^92-kZt!BsWWNFr_-nZU2_qTa1ExBL5P_j_yW5Nl+-)vt=QV0X;_@p|*}%;*4vw>(v!CXq8<5 zp0b%^akv}g{y<$eTMysae6`UQ@tMz}tG?*mXO9?|$#|UYN4@=-%=O_eV9UWnZ;tsQ z1U{yR{>2nUOP%r^RG4LO5Qi3PRM8J#*VFT@x;s5t#R8dY>n?vwc5y+i^+aD9IMYic zh)Hn6MI3XLo`b@>b=|D4nr^NtZ6c%oU%9!bXY6pTc*18j6Ppb@$hQX7A@4y^?(Dcd$F0_cyU*7kzeO zf?l{@P6~n&!vhjvXwbshS}*Lw{Bmxu4iu9x%j2lQWQswb5BROz&myo2=T(N_G*aTE z=3}t^c)=>ubEOZV$jOE^srzyWG}wfE1cA87(1L}j=`I@~FcksPnn2Ew&756J3oSxU zD}tJ>%i-g<&aH1nC4>D#Hyb-XU5h8fcOpO>71|>vnJJKJQa4*?wfHx)Zn5eeLk9WI zw&4duU<>!sgi+^>Lg6G(P^I>=U(e_^_X4@BlKp@j%IfMWeX%*o^t#O#1l+1g z)d1=00n}s7^I3T4zqVG;@z5)mM$cN-j++cqQFkG_9tc^~XzgqhkA2GTGrHrDDb!p1 z^((;n9oTfBfp~v-@3mn9&}|+zxB5D79ksPR-a6v;5aHp4)N$5T2X`A*yLj2~0rhC$ zJS;0SVBK^rOsU}LNF>*qw3z}_M=m#H!v^R95kUAgEhMXV_F{{Et^ z_pOC~`Wx%;gGy)0FNpgsJK)I4;P(1@ULp#l4(uCr&=&N=K`jmVVT|tlZB1%SNKCxl z*gIFGd4SwCmGxnI2-2T9VJM^9y$doDF4;A)s^pc6RYJGRd!Q!*C^{jR$1}Iwf&vZ} z79c&s#=$|a20XQ{x@8j&cx_GhcR!ogZ17^{jsoq?w8YqL50r^1c1NNJ0Qs=17~MEV z*3!}f1~t{jw-Rg0VclOxzkr+T$3QVU8E=K_R&TPHRQU#rZn#PJj~Y>lA&q zTzFK(-o(y2%S;aZ2^zvJ0M6BD?Ex`I8hOGXB~aW;)KK-_Yrh6tiy0eurTg_p4gS@w zpQQmmo(Tx<+#DlxIB)?~i(jL_y?i)hfGi>IHjd0&P^g&w^G~4tyV?4Y?LFCBY55=h zL#O8$Fjw z2thb+^6YHjjh3ZVgmIBDwY^Ufj}7e`E7#WJ<@L_a@vFNaBhvyP=3VRL3E6LQJOe#2KS|Vo5=zu z-kEi*4EQ@!y4Qtr&%-_hwms;+5yA~YPr8L+QUGxSy@z-b!3^jYzOzdG`0OcChGifBZ@mI@e&nQ*>nfH6(=R6V^ z`nOxoOLtsmTTKpzMEYvqR1QsP4iAU@0&t__pS$}vHpV8=%m~Qxa75_Mka&$LU@#L# z%Y_Bkzyho=u?*_NX`m1!7H8phH#Oh9Xlrx68IsraajcCoNb*bK!qKk~S5W3oU5?6Q zA{xPs1eJPz)0AiQGo3e07Fn-f`mYREHP{+=Uoqcml6xsSf&SA6dicVzzHo(8Hh3&9MAe3%Y z(!wC@)y#K4Z74Dhg>n{yY^@j^NTs7`ZLU-B3kSZpsbc z4GuXCm^jF4>lyT4*cCA=VbyCfK_Pv#y|1}CMPW_ezBmXATZpi44-tiFLp9AD z#|DMyK`Hx%eA>+7dWX|E%-Yj{xzfj;ZZSH|DTn9aJ;FVFV5u2#`Tm3(U}ecQa+M$PKSQ7c`?wOIwCa;nKoVkwC+*+A*i${uLbO;> z-7pYfup^cK=MnXLEo)OsIEGP`v^w66i9|3b@a9)D(&1tdweH^ZftS!{hQ7I zz7wti`*Agl;3i;`5s={o{l5*`+Ar&#+4GW;s{!wLm2Pn?v3l@Gbf*ekZA@;1Kz z`%%ntitmM_6rV(DpCCL=_q#pxut{nxF4@=r8KxJH)26dyJ2gGUDP51gK`V+WHCKAy z<3PDVvb!lt%xDwvFeiGj3zY(gDSK7F_&xOD7Z_jMT2xg@rrnBhb>>vRaHz)Bc5c(T z{#8)DWR%crwzlN9+|`gK5Xb(KKcS$)87*l38eRKaEf8$|tU}|SR!XFZ9+6z4P|DO*0-+<@{Tcg826<)iD)6#-MmZ1DOf;Ug zbV^U#iD|^uw_{7)B_a7K_cuw4@0p#|ahZA2ETa=*(tvwwY?F=EzK*Ne$zeC4%>)On zm8i|bdRqDPQBy~M{$U0Fcg2W|PXvTlhpZ~o-N^qy$&&kbXMt}j{{~fC%JbKQdDsMT zFLH~T>V|1rp%3>ActsQcyJR8ili*Q8>(L)>u!olp%)h(U-ad_Tr8+Se>wJzm2|HD2 zQ1iRdTax9O4TumDKJ5A9JRq&82sM>9mS$&FLkT?Xg}0Q@Yoo!hi>4GF#T6f4(6xXi zQYskCYdd3WGT%e;%*IL*;=e*W+YiFcUS69EJheKf?}=!mCMA9@KYty(5@SJ%?iih_?X1S8~L+tnW_EVnjKi+EDKm^^B zt~&?LauHpJ8w|2JF~^NZF{x=4qHrF6R`7x7RaZQX7k-n;LGkz5?pz-$=d<9dOeB+c zmYosYTA{C-^h>=gRkUm?HSjx~{=I{!KQkxJv>cRon}^lqhK!BuXiQEvqF z7`OZ^|K0RL@taPu={FpY#{7_R(;!{L1S6ZM2}PO$lM(geYL8c(UL5Hu!Y1kXs^%2@ zaRqd+H#V1j{cqOhFsJPL^56 z5=~Is$Ch!!*nO`6w6HueC3-r{bIXm4+iYbMh+Cv$xtLqU`x?x+f>Ab6HqOw4Hjqr7 zR*e2;kTv~lq>yxQ%kq;xB>pW}-nlaVFW12O=%LM_otof8*sM{Z$-7?pLF=tmR_!E- z9B%+_LM5Kxd7;Yfg1&5oQgKQX+kHQ!9i<52R}_mVS!hQ7vWv(iV8#i()-6e{OGZ%B z)Ocx}{zp4CFUJ$6I;TS8#{f1-w`qOwZouF}Iu&Rxc zeW@|+j6R76PEZz0bG(2hg@jBG# z=KSjKPDr2^mKInim6yNG-A)U5L&G+}9D((3E%JB)V8T^o7v&e_qfIY2bN3Bmrw~&{ zr|PDQP0?jnD?0K7kJz$s_&w*0Il`b!Dpmif`;+!vLne;n2mLamDl9+0iAr|lh(EH_ zk(J5$S6Z3|5@InSbdK)StMzMDj!n9iT9k=Y8?TKGSC9I`ANKz=USV8k2zN8w?ePGU zhqEMeqyb)ApR7}%98@DFJY7N(l1U^AT1`$TBZjk;S?o~e`uW}(7mXR) zkDzqYV|5ie5mQ}KW$_SGmj2F<Dam?ye>tYKAk!;1`Y`{k@%`pF=R zOX2Cfi+(jREn{3YF~y*+5gLuX2rYP~F8$joLzE0^9Gs=dOh4LAd)WK(;U);Blm__k zdHxnG1M=1jqQX$p%i4V;Xw?J-=WD(6e09u#@r{$~w^M94t)BHd_nPKd2Y%p>U>IBf z-TJogc! zQ~t`8$PJeKpY#1O`XLb$wzRggIN0oP;*uo5CT}QsPNlo}!PUk=+;`S@up9CUtpyEz zS;Fer^5XatSQ+6Tcb$U4&HdvaR_g~6 zX$mfyUOV|4r5WP)7&P|dE0ZtevxbefHurNq4^7IIpq%bp@EHtz#r?eb6|=U{Yfj?r z*jl7BvwI#ALGd&uwya6SlUmcJmSE2r?UExI+L<+E>aqI#QT^2ZVh9W2BOw$OrzTdd zC_dw>HfV< zAs+~0DRU`0Kje0R5biMm`xF8RZT9_e2xj?VqF+R(eBo$;dki3RC?_Y_)u%avDa`*3 z6yX~F3I2njzZNE~SU%4!`aT>+4@*oob97>1ausL1=!yia0=R~mdal61k5WKZU{@BK}oj5F_l<8@(t&5f68rzPnxtzR20u6&*AM#2_IR0a4eFSHxN{D zG%@kn(TEKXOMEaxL3FDRo-I@QZ8q+=*_RJr7|!>2ZA?yZuJp10^FzQaY39op=F*bv zAILIj=DRLggql@-_8>FZVETxQY}l@RwM>Jz+OvAhu|UC=)_BkC+(YuaWo11F@8{k* zw7tlKOo>6Or@k2zC5iQ%|A+i%Y0-x8%(o_Q^VU0wDk)n2zUcOksyZV0OaJ%;T(M$E zd`w8N&R~QegA_vG&}pZmWzQE<@_td+2I3EuN|m$cq0Eo{LQfDyx`yHX<5hx@JGB6+ zBmsJmABPDyoP( zIz=yojYKRGk3=HOioDF#VNGLBFHXw>&akrb(1%HQi7p;k*&b$&MS@(Kh4#DP$l}s}lCZaF&EM^sH=Z29 z!@^2Or?l`ZzsEGIlTP2m&V}R{(g{P%zm|vnla3gz2|on+!noZXW1$^m_QFEhE3AY! zQX!DWCNY0OGZySDR$-qWD#adwqB>#^u9m=l+GDHsx@_>hVZP3AqoWV!tO93$!ChGuT`Y4iBj(I;1bQsQ*U^AlVuh0$so8W$r$ zlR9aST02DN6Ua*g-b1=J6W-}j#`QFu6ymN#>I#U0B7Mk+ZDp;@Tus6&x~}}uhfQb$ zGG~yp%X$P+u3(J3#CQf>sNxOX`u~~E_!1X>`&!yG3|ya~tfm-I|KDsz7y?oddwEk! zbrL@}T?0*#;v^cQW0Gt1Z?RJSYq5os&F9O!pmMvEqg-UI*%jNfb?G4Xt8Hndc=bfg63hK6Up z1qg$0^gT|qHN20iw5PxExL!zUx&@A!vS>4q^pbKrH-pB^`f>h?#m7&Eum1Bc{|4h> zFFXuC5)>0x+I+&?Us%xUM;Pxm4}1vGHpLYFSkE$@{4q=sg=d-?Ti3s4qbmp#`wc`$stNdyQfbnl%0^g ze2M&U_ECdCf7IxV1f)5uj?PYPXIH6<_oHDxH#^1T!tMS2(v;GG(q~ID4dflh#%NEyrP*xBChD0!KALXbAjV5)xvbw?~pV zE30Swf_(#Feh*hFCk-sdeYw&G-ao;Xcz?(%vv3hg5&ISfscE<;3$r>{OC0YVnEPZz zZTv;+l~OJGNq$|mEdx&KZJb120d=pK2u1?kz;x~m-*I2O6fomyW#XYuj?%v*P(|ygp&xuN)?~70FC!- zum2}Af*g;1=A6AUPkYRaRN+5Fe#P;Uyem5@YWe*8ea|Xe4p+L4N7QKlvWae|(Rphp ze(`Go4);G3AukFoB$oO+s#2O&o;RWClT72m%bGeF+0-2nkt%1TwGZl8t6JC*;(<9M z6vyobGBfXeZo7W6{l1mWQ&=ukb32ou;;dSN+m=r~Ya7Gl?8e00czzHVPIQ%${t?lG z^#JwVX|7b%xRgKiJ(HLulz)T~h(oOqfrvx!i(O-M*9=xzx32fFR_<(@?6XhViVeFSc72pDWtx zy`<%`p$4k~jF13Y@q3Y{s0_u2B-gmKT#~*9DDs@ma@DDdwvn$1@$$#U_vvU`VCFam zU+<;2NVMqb@xl;+YKjfDHG2CqrmjCd2)5zjkz1Kx@E<}16p;MC_#gtxa>1P^9U%_f zUep#6?({>%Y-c4Ey0C*+s@S}K0<8l=8xC%k*gW5y9SEga)Yn$-A`Uu z&7gWum=FHeRtHOURmWN0Np4Tq<(pcuDo5&`@_pSmEoPS?hI*skq4_=1beOw4fX@CT z%9))_E=$1lyWFm7BN;-ikaE!{dj5*=(&IwcLx9(IGk7-r;|#A&o72@q(e?MOU(am& z36pA_2el|;i**C&>TlhG9uK4zJis3?dstuz~T0Uj=P)D(^so(*vHx!pTn z=e(uH`Jyi<=r5ah6icb5s;)$dM?x|&GpVbtXD7f}*3^XP*`ZlHdG=F9|9*1Vr_pf6 zP|-ch0fgB7md*h%B_db-iX!D2!}<|Y0N?Lq#JJg`NkvT8zV4&ndeVw)w=4BPUDF(KC!bQZ|n=tpVw=Al;H z+=)2l?UB7(^zR7^vMVa;^HABIk&L0H$Hq_m{{4Gu3MQqzvA}t*UNu2RO=Iy%4tk!3 zuu{dfh*0sUFHgFFlg#_~LZTnydMBxD82@67VFmPtsF*O)Gb@)UKSy;(O3R^bkuRLy zF-7h|DH=C#C*>$nXD#o2oBjTjXNHtR%2``d($&<|T}#H^a3xOPgN=?^xN!XWDNo|Z z_QGp>q|XIrNZb^BmVigh&(9Ab;crJ1DpK|H(cEXLmM@#!`HS4W9#kjn8*OpO=g`CE zpV?YhMsWOVbY^B@G+Vv+I4qR!t`$klopGmG+v6$dbtNWb4)XW_ELhqcifwIEA!1j< zct1BuWX=lqp&5sEWu?P+jCbF@0vBgwEOvIzOztGKebRJY2v(JYeN_9F!wi2*%YO7c zS6bvvN5_L}JSSU6_M|z`MM6VG0ynj#l|_*4+ydk+_xCEznGFYFU4N9cuO&9V5OU<) z(X}%KRC~l_U1jx!xjESFTagEqS`^CJEnDB=$Vho9jJ$0TVFL{f&WYTNh1RrY6!l92 z1mb|F&$ztK=C-Odm-Z?@`awP**V8R!Z11syV$EZ0gNpbmo5naVpNeM1Rg3H)H95y} zUe{{4eU2qQfB(gBkQBM*&xx_45X5OmHFg@ZYoXR>))Lds)XK^dyynQX9il%kHrU97?#G~^xBK1b2Y=2=J$SUw5$ z`32g}pvn#F-~J?aPscg3f3p9Xn53i`TMBe#rPLae#cOp;Ih_PC#YD&bciz#egZ+t+ zZatq2kWyt66<5Rik_(E@Nx45i-uefCjyWUJYenI)1P?nZY0%oT_F9?xerrpD5o!Cw z+Ql~Q2&+R>CXH)G#HIS`~# zw}E1d&s6wUpg7gi<`K`6kzaq&(2T?K?+?FczXa6jr=#`PRW0CtKrm=?+d=P3n=!m~ zTx@T=x*94ijpDR<_4<`R3J_U+iR4;78$`?jJX>!&at!18f&Dl}?188J!oqD3f-%_` zkSWjtc9rdt#7&J&CC(lWvafm_*lV0T1ZJmA;%lg$tk_=DG?AMJ7w60Erow-SA|??M*vt{H*+7pqRFXOzE8t?A@FHXlt{y_plkiN=Cswc$;lcH z1^yVAqPC5yTT)V8MHq-aAOvKMxxtvUiDGtzJr-X=)nZqg+Kt-(W2@6;J z+u=NsjxWuY*)^9ZQ`uUgo43KG=r>Ld0Wp+0U*G<^$v(Ni+x1mW=1M?ECfeTXwCg$& zz4>&`MavX59^p&L6`gX|5%cD5-u6zeC0Pzz&ob#1q4Do(I`v}q9naF)?%}NApHgkn z`ij~b7{|aAdCro>-%vU@DpSD9UQX9b+k@8?5V?Hr+k9FsPLdY2&qr6_PsVih{Tds- z`G7Ehoq})oXM<(oYG|h%FHh6mVNzS$#Tb*%(axe@{c(L`{K97kO~Ly~o27}ig&QW* zRJu57f$N`~{!rLqy{)ytO&c71ZC|Q? zwH`!flfvz#<*D;6&f(7n0cc7pWUyh0sSf%^NnIbEpEr5!y&hG{WJ>C;d|=chzSe|e z!Ha)m)azjQB)SH$Hah}w<^0v*iX>ttQq%@)^qY*&eiCJ8XO~ww9(9~n#{;v}A@-~3 zh|KmU;AQa{lUXf{T@;vT)ec`uD?3=ck{ZVwNc7!EHoYAG zff5*qoTyo2xYcp#fD$Hq#Vdb;5{l61-vNld?wO;f%by|u1sOP`(&&(mLciC!I7@tz zWYBtZpE&u7qSLSZiBB8Ak^D_1J$18mJRPG#nf|6@{e9*jni*pb3&DSAQ9(gje>q5g zjs79y?t7QrBFFZeswPEBCZAfa(8Gh0O7`Z@!M5PN_U-Scl%NcwzwW>#jd!+d_CV$4 zjBMKuXS4g&XN|?NIp-*Fp#03#)40FX-(GC~n!zzkKoHp4js&}0j$@n)YRn>GoGd9f zLl_A@*HM-?g&7}iZ@@jYm2^)@1`SO5X0-2??6pkrqkr7&&2{m;+Wk7(*#T9-vUj!yb8V5gr0++!ua!sETsqkG<_vAA7u^|@& zrP0(+vmgwzo!#wEqNY#e()lW~E4D1vosEnFBywSDKCpdu^(!>%V^VvkgM6%zuiA2v zd*E|0V*?M52wcl6pY~+wO$pW)tA3rSGWLxuaX|aq%mBoFnC^GGeQy3n`CRS2!ffiD zWKVQzZ|3#^u*Z&~jZ6>qmp*&ay}i8)UcFXKeE0J}{*8>+Zmi$UeGP%dPUMgN{lzSM zu?FGIX^axqK6&HqtzRtd5A>w!(>YPM`2|Yp1wXj&-awvN!Ta>4F5?cN9zf)vk>-|` zMHZ%tMQYLd>FOTar)zHPyC=aTIH)aL%8@TOwl zm%IkEUiq1L;mCKKAMB@fU%sr-2>Pu5<-C){rbLpeQwZxm7I$gS;smBfX6(Pi?aXd3 zm5V1)Himiau6|3pFr1T+h^{~d6(r_S=_9)f==o#r+=I*~ znH&yROu34)HS%WrMzeJZTEF*UW44U9iMKnzh@L(&fI!Zo7#B;Nn>2Qw8a_x7n;bqc z+JS&B^f++(ZmnE1zkl3xBtJta#(>%EIa=lYIO$wugpMK5(c1Vii`%2Ob<3Gv79Qg!sf!RGg&)jM)0ZI)d8m z-&%cU_Y-2brZvx_q43?^(Z_!h6Kl`T4$I#Rb3=U@jEs$p(ud)x5?#p2RNsq=uJ)^# z0OmhHhcz{>g~Yh(9pY#sjI6>vpXjVJJ$Vy9)0gi>%!n(4uVI*Oo z#s$!PeZ#|tvl`ysy;gO!rF`#5x(w{3=}af?Z`X%gPkwR=(;`6po-*F{Cp%jI`r$;KYtR-%cJ))cN^c;+v{M$Zr(4p z9;2if1Br%gw)P7Bg_lV7bjJq*$Zm|*MqU3@0x5!f5^H~7Q6H@+Oav>8Ous6cG}1zV zZ>;zx_4aMSSkLGQ0V{&wn0aC~k1}R(qdD`NV764s)C44CYT<&f?0NE%z8RX8P(O8w zUbk!;i*T3%b=qJ%oHe=>)#nt{FBsl(up?6icI=nvoN4Mc|6+%Cd1q0D^t-Z78!F4n3>-FqVQR!BPpPAsCd2MKjd?2^TLM_ zp>dk)2Vbd%MA`U!qkBSIX1mvy2mQOxxY#5lctoR<^8gg=uwhjva56|^&~%W&E4=FD z=-BA>RrRYTYk|Kn13jZ%lcS>(+PZ#G8mEi1ky!opp|X%t$-V0d@x2#8?JXk434sTS zxDq`*o!8F1)6(6U$BvB5kGIq(t+$9k%5dIg+RL;;D~U z%8DBD%fGCN2#@|)ADH{>b$ldA3#PMl-EDu|3vsUD!gYKKgh&sGadDD{b_>JkXbix6 z5_4G1R9Gk^6=%9(X65&q7z?oo%#~v``+XXgQrOww_de)E+o*I1V4^?q6LQbOe62ov zy(0)@<{?PV9aIC-%6AnI9u~uaTGG_o{mspI6w~eae!$ZpvzkLKdQJBVv0)SWLpiV1 z4Xa5Nb@;F=Nb`hXGx<|rAKEkf84DzN-OW4gEZ)C=it8w2Pa(FqmZt6EId&iT22^RB zo=z`=ij(h-tsd`!b`WcC4f@7M@#P;iKhrVFVF8H|Y0t#$4%_&ialtxizga&f^>sM| z5tg{M>+t0Tt4wtIwX*|;X9PU?#2oD>iRgpoeYVeQ-0CFg9p*c;6SoMox`=W zPoQ3}@Q!T;mnTH=m)8Kr-^sZt>~`nz;IgfRg))dM$w z#*&%vlub^QLgWi zJNmwGc9wWMhS7`{5`tg2tvtGj|95izXo7mh;!&1xzMj8^tt8QeQ=;n0oC zyQ3CVIg6U-hp6P|9#60Ry&%rPSd^dTi=DVgqP1L%=gn+;;iKH&5G0EfDFf;Yd#O93hwpnQzKpA=l9ep@|##`0yZQ^&^v_4ERZ}*@wI9i+F*8 z?~D8-kizAr@fd~f2rdHhf!I|7{<)(tjCaO|OvCuHPU*IrR_YPSl7{||4k)Kp{%%36-K0(fHE5nG-mdTHY zM;0%d%nIN?euCak+U)mocYx70`#EF4D?+M)#txDnf?{xtPjz6+!c7ieXn{r7Ni$*-Z}(7M6l53UqH_M zm^;uZRnFkB2mZEarxD~^;&Aao^y%vlPh?dqQTRd{1h3{tI;xBF(y<>1wwNP5Nad@# zv9X@7gzfR*d34)R<^BVAnuDrP_AO;S@if4QBBtPa7LrF)S*`elYJ3JgCuZV0CP0jB zDzsrHXOdx{Z(qKsR08fY?|uK7deJ5{*+(ytiQFKM68n-{M6JQKfljH1O%zE!5DJ%# zgsf--Nk`8)gxVelE9x;2UL%zxP7w8_$Yj&%e`6#d$Gm@1St3r~{C)P@F{&tg!tv(J zZ}W-p}tbMKwnk>xJ=T!b=MA#DTR7-_9V&yYyo9-TYtr^9);!E4`g;HPA zi~=HL4)PQKj>)Fw@uhwtZZfj4Rd+)#ef!nj@CnZDBN|WwZKTw~+U%n#3kQA9eJ_N} zFdjvoL^%x7*|oLy+ap~5hTW)sE}Fb1IEm0uYL9lq{L_RC+z0Yr?IJq}7dOkqs{2O- z@19H3Lx&80(4a7?25?0?GdJH%>xJq0K|m|DVVZpB5O zp%^ZdjjBoq1_FW# z5;`~~23iJdGpiGlMTk&n5~!6WXuK=oaHC$1c@he0Vt>|(SV5`8FFNSm^u%Dowvmq} z8#`y)-=W3~JvTIEfbkLdx9vpNmPb)mE{17Kx@UlmPcfWfm{54p8&w^n6yP6Tp%Rke zZ@{d^D=5m3SL=4A_5v`IkNA+VXFr-~U(tW}r=?*6&NB>(1n7mi4f$wy?n7uONG3pQ z4x-A}y8ts`EWZ%w9~321%C4&Q`}dW`b+h}>#ve2G?I%7y!$)|V`};E|Gk1}ZrLtBF zlasfjJ+F1=^(i#l==p~C?i|bNtS<6>rL|{&7h7y|*vyX3dwot24-&RBn@P0FWMgAz zXJgy{eKM02y8UWWI=BCHX5s1Uk`kc!ER#?PJe_#CvDOOU9-+AB5XN)t*}g+03?~%% zrb+jb5-T_&wfgDVfY-vG|JXIc@{~9qLES(unX$S|A#JGQ;9%v|0txklfsw@@yjOZeY%u6n0<%pGM8v3{l>H_{#+TRECAw8t8BNyrf8wHmKf8y#jvrG=9I6~mSGT&V zIxorespvB(Dsf2k)!RQq8G2Yge-B|F;IXKP{|EQ7Rjf(=q5-aHzQSVBAe0gyVU*)m zB-dEI{Af^y&FvM;b^-qQQ1<)uDCAurpRCn^hGvFi{IBSbA#12^iCQ)4aPlMqPa;o} z7}^9*B&uB;oA28{)zp0hEby6fGYcu(@_B3R7W;0X@U^I0=M7A8fIc^0s@GjtKdXg0 zV?8rNlLgmx{z=RH4U z?W!}4-ND^S&dS5XM9ciR6Ju_`?xr`jEXPW8b=0DFzv!}Xu(Pwu#rWpUVfOU_ z9WBG}+7C(X?_Ce?nqGEva@x$pNJoT(eERaTlYe^!i1s}lZ}!~E87kce`3r15kn|?2 zKi|*mx|q-0=Z#Ak90?yZ_Bgh{prNAjz8IAVp6<5`BlwxgW_RO3<_R3>I6w*Dv`D4W z!5)|v-<&m(d7ku_^9Fs$Qo@e?Xf=rFgvF>IEL& zW&2fB@mC96r#2l_+(6Z%u1q(MknG^@`?s1B zw8|NMGUeE~xqY`wc*S_g9s~X>%0K(OQ?)M6t##Hdb3U)6AA0-1@^)jLz`M~!kkWGH z;-|KaCLS&!5&h)4Ti{a~7b;f(at@LW7;?Cr5rJPA><9;-*HsdhJ`77U=ge!P&t# zeyMG}(_Zvsq2@84F7z;wPeq2psG4fY9N+m%0-09g)RY8<>)QkGUh^uvr1-9$9^bDz zE%ukIQ3ALx@DVhQ_{i{x?!J4SClCGsXoqK?iL#ilI_D}*#@A&D7AwuWXLd=V(SS3c zdg|X98!uq4LBeAX)U(lW(fgu$$-L?dY9G2Pc?wQWdzDmSLQXg5=}r)KFjO%qzZ*MN zVvAI$l^9pQFMnrK$ar(-xd2}^b3EsgG5Uoa2s%3RGywG=f!jfv9+SKzHC5wC*{I0S z@X+vPP;3Q~p1dwT95zF5LkonZ@7iqmCub(r7Du>z_AN0rsH=#o=kEN22$<3|p7J#0 z?(hzK;y9wvDqvPX{O6~h3ef^j4rfr^SHi+-aJ!7972dbEXJSYbm_QX>t!gymkdAly%M=@FickF1R0rp`}-3gsRbaM1Gx9X?-ty8wMK$A98Q#DW9^TR zaaKNLUiQQRU*qQy2Nujd-2JrTqX}Y9|P_w-eyJB#*n|3{}_p7=Nt-XL@&Bs5d-T0{IK=S3LJ=JrrY*2 z%#*;t?r2m+7YMqGSEEAvo7=xIaOCx(c_mhRS=?kEn|_+Cg`|RZ$k2#zB(kJL_i0iK zQu&RBZzu1+fUdQ6tUh7gy*odlm~xwDCMc>ao!aZn)hq;sQR|1Y8n(H~@(K}(Z zGJrw$4ch)dojLmn>9ftEWqHZXOhrC5e<<=vQcK2AN-=AGpO(JZ)au~0UXPU$9)7Jm zqG!*Zcn;YgmHm`s^O(3sSVrO*$S!4hB>c@+Tp8);VA$S?t2tTe>M9$;A~k&KDL5Cr zL0S`&G$D{1htOqr-vcrFfmdHk*(5D|lC>gw=D#8GBhc41JM%*ZH`Hpw z$%ZgnZFTMC_k0LvSUf|vR5m1Flmq&EzFYAv+#jMUMeQ0mJ_h)2J z`3XuYTIn1Dj0i(1ZCpp;TKTZ^AFF0g-APSX=TI)G@;_#wf_tY#{S|Gg%hZEXcwi7} zH8uaVPF`*?$gSFZ@YVujBMJCdBk?7Z&ahdB)%3wbW45;CB0Ikj&=y)a3l&Q8T5q zztyj4L}})WbfuD`!OyVh$$ znB$D}xa*Od_}lFWO_0z`-oeMW?ViW;D`!9j}a zJ83@av)R}9mlzmPTI$~coz&00J zK2zycs9qcq6=7g%{%~L{x?O{-bwyjjYkqO~0C?KFUF|3^By(=`Rjd*mqwr7be)jXK z{T54_l;J+TXVG@KHtl`fapEWHc_{sZ0Gb7Fwx&d_VHzQPn1#>6GIr2>I|UBVi>5_4 zgn%4~J}vZ@!>eDhW5?f4xKI8J>9smCuWrXQ#mDDSj!0!0jE#?@x;L<@;Cq)Uib<2; zHY{Wz%8k#=z=x!9e6)8_7zW}vKygHse*G-nW690_x};boHjb~A$&Wzi5*zLU#f{H_ zqqwETVQ?GIVdLkv{^hN#cIN*>)>}qZ8MR&8beD9OAR(c2!=|LWrBsma2I&szklu86 z2+|-W-O?Zk(kUI^!u!3S?~O71=lA(D!VRlIhMk6C}jDI|xJq%q(U{yv~$RE)jwG6sj={>t3z z%{Ewh6)B|sagD=!Nff?R0RP&M$=5O1zq~nX@YoGXA$A!c8516!CE|is{OjCfAt4%V;&*=thBV{B_$-2D(Y{&_P+y3 zWKAURL_y2-U7VQTm7vs+n$g+QnTak@O(($i?3#}!gzgVJPq}HnWN+5v76r!LVL2}6J^x!`}t`U z+^fZSx)_9P)~fT$rYd0kr>8f82%ph`VpT~RhM%96(He~>I_pl-fN)p{;vL`p@$FgPfTCuu5rzi|`bFb^$ot);rv(_0~-c(?9Gr&}21#+8q zGsrqFSi71NL(JnPUAdU__6W3>wIrpH(q8D$dyJHMEy0+Fpr8@t;Zj722e^hv=b>Vu z&g=>$CCAqSaaY@ZEqR)=@^T0)9YqAgx&{W(g+MNZ4$wy3U~lAbt+E_MLfcVZoPe$ zQdzJc&GW!r2{FG8LA9a;c$$=~7h@l{mCQbRddgc_S?B^JWjJV$5ORPcCXs8b{pPv5 z89r5{b$|y z@I1I@$J**>|L|b%fLqU|5n^PCsrdfW@cAH~@f)1~I!b4zJc9M9!~*}5pCifC#AQl7 zWocmkjPgH@&MvCvDhixbv$|2`Ore~H3h&#|W$b0&DleYQI7ED7Wjpp_Ci2faE$1Yk z$@>uz5!Veum&Axeu+Y!dZ$9$gGG-SL{?kK1#-6ORM+P6pTrA?af5VditP+H05YR@+ zdPivZ{MGxh{5O_@<(D)02O5lPbilgEP+za_)i9u8AYDWQi)s`zo%rq^=gnTj$?Ldw zJ_j7M(PzNC&9NZX#V-J81bIys2;1R3$T}q+hw+PadhE~8+~qy*{@H)okMGz4m&z*f z%uJ~lq=3k+8J|CmpUfOATmsB+Gh2)Zl4GVW$|e}xPHcGi9B{becGUNGsuB7G#$`KB zf0`qSFL|s)5;CL}vy>Rz9s^S2X_Ql)Rvd z&&>;A3`|$|6*Nf8?{6cgMJ3ZR!mLr4h!@;YVy z1vbILSgr+=GhuPI+7(utt~%YiZZdU;B*G zN-2<|7Db!|S5P^udS-q8TttC2xI@k3{b!h8@I}ZUj1x>VHRk(sC+ksupKU@cjRHoT z8r12ztdD!8z4;oYE$VuY(RBqoA#LGo_aU7B*@~GW4+##+;PEdj(I1;YWvZ`&K@wm< z5&l0$=ux#j#OhE$FbJQpex1si1^;~xM(W!R{Pu~&Cvmh9e)@DxQqk1ts230u!r$nT z+Kl8_zm|Okt;!^SB71=4AjQhi;|6y02jn1ww|qGAHmtucK9d)Z|8#fkm$rSh^;tsj zoc(m2GEs@;FonF-eN9c}=Mga~C*mzJOVn?s}{oAYly?!O%jf-j^rnY8G)i+N`p)6AQFF z(-G7Z49aGFO3dG=uaI$kU_l^R!Jct|MjSu%;hUX6R3%V#1wuhUNiCBLKe%K7~ zgB7aT{|tv3%f*Mai{uuH!@S}9eIl^+IyFB{uBKGq+9KITTrH>*FZAM6PZ%}C)Z4Pg zEuwnwd#mxg$h+Vo`!C7ES$N>16E;R05g*2GbM%zSkFAOBk;eaxu1AsjI?mE6PC$@b;1o%6k+mOykIMvvf4vn%Q(swIe7N~&!*h=X6$ zCp`D`uk{VQjr9@j$2B=)|G)=9j6d)^TcpxnM4lnG7?f5eb<-9nI^P_I{$%JFscZ`N z*z&UOlT6#*Tu}Gv;IX>2}#MFjTKVXNJPUze#aw6 zP>?J18bek?D~weUS1SI%M$I-EhPk+1Iw%4US300^cKfBZzK6I@)qnS9PAV!N++NN1rflHHI13w`3FDBaK&xoHNPu~?X@;M7R zUmQ}+qHFY$XI7r)W!D+5R2*glZ{K?^354JX)o?6n*ujKdtFjq2DY@2wZ<~WOxXcVB z@A4+SN=)0%aT!Q{^ZP%oMEH*Hj{fYcF%i_LKEwa*CHlNnXKtWu0&P@HFy8NVW(IY{ zhs&a8#R~C^(OF~!YTeA{>=sHH1+MrU=PV6jm&EVEn`d-pSncJrs9k8| zmrtpxI+(G?+8>s_nXa?-sO?Q0KmHv(+cI!{^@4n{ywr|DN|a*9QQ>{;e{4b%=6rJ| zTR262#UBkFpoaV7sxlEU^7UhZKm$H+8Tg{qocuEiig>czm4Q!R9Pc@bxQt)dD)j6> z@d%hz+zh$old0=buE*yQ6TbkWXd>q!Yl93~V^5@Rd3_VaCIFRWh0*p!7YX&J#*cBL zkiFYt#E+J_bbAU=w zIwixdH|x?{9(ddoGk|>pwoM?l0T3z2ttzSpbCU~fztR*@9G%vwKkRGdZo@ox!Se_H4g8df&;Qo9by}+DfH8KzBmA_CInE*3^2At?s-z5$)we^Nt9E6# ztwgaWt8OMXDaiTSMYB|_Xlph=2EGEv$7fO{MMZ$@(H&>C0>gkwDtp6#c{*28HKZMs z7Y%-7^P1iuME7Vrauve1l?*NvTe5`tg&C{v{>kD1r}+6d=bFLHkmL7*#yLi4QiJkV z0{cc?JbB-vj@`qeHq2N#N#TA=Rs459aM|nf^XbL5{(q)aO;x3dKjnA^4h@ji%{#7* z$5jZr&hNYC4b~rZt^?zT7(VbS1%9}=){hYdA;>4dcXih&;C716-|K=V!EJI50Y46{ z8noc|8tip~A8D9$AKmTp99ou;e%Qq3gsdPL_~h}(pg#S;eg4|SWhVD`7+kHy;0X8P zdlMfioPp+>X{l1B_r`4d(&%a)P(HB^S3Yv3XM?8A9&vFIy~|Y!AcGIG``mY9XWbrt z%|3Of534*p49#{Yb)D_FeW9?inmbHCJB~a`3m}4h$^7CrgfpU&) z0W;OKz;lPGkLFAf}Zxy_zZ;9z;DBG1iIF$#F+F$7Ox)^ zFZR5N*(SnIW#s@Vu9DJ9#ju#Vd7HD8%J4Ad7^NR~zpH$azfC*M!;mX&cRb!IZB6rs zZ@ln5dc#ggkaBos5;OAg)_3f&sXTXb1GU)@ywtS}wABoQ>r!dJ@hhcjvBB}K=kOmy zh=l1l)AL?NUU7EM1vuuFGXw-pC%^l_fEp)P0>TtOFMyj5@TfELJb9yDq~Hm5K!k)u zUX>)B;R!{)*L$nVsX`|YUPlY{ru`u!Z?{KNfqNIbvNIk6wlu?-Lh%3As@8=Adj zK(>z`1q(B9#K{l*$WbKj(QfuaJbPe@8r^%N;H=^3sAlS-2!5x#1oFVZmFPy(0qna# zGtJJX;o`#oZU!ddey10C^G+i$qnTp_2sssPd5w*R_`T@Y!Fm-Tg*!o4d>}e<@{c3) z^hj#Mrp=maHV~o7BN~Eq)^lX=ok9(Z(nU;h$IhxbEq-0ia~7Tj03r@havuHygyYpV z8AcDd9K{_fkF!C*6_@ryF749_Dcz{;e0q+`j3oQ6{CW%Khd6SCQQ668kmUb*`d!51 zQCgNq(fRQ5!Ars8o?6y%up1Fy3UKHdzV`!Hm$KHEXY2OnH9GXNY9)!@(g}o5!lZQu*=n()V@h@Uv&{&_H9(UtVz66Lz4U zLuc$iPyC)3#CJDqh41KqmOg|7D!Ve?jOh+2WGjY-`2IFR2j;no3;9YgL5*g}luaA` z`fQ5&DCWqbLDlA;ArhL^66TM|BI>}5$(H2=uTp~20TbVD(oPwlxZM#fZ_Gd&ktUFE zf!ztrWZ+%;-3)5vb8R|y$k?|o_)_^eU|_sH!j!EAKU6QTRIUjH%6rKde=M)34VRHmeE=%{G?kE-Ca3Z_&T9rSuI^ zRLB{MKpZj+>GM?*=Kz@uYon^^Wf#s*uf(T-QTBjmY+daAuRs0SAA9l`Lug(o6q=2g zL7ho0rPd?!lDAls2`sGYOgMbHsO9Ku?g#;bg9~5lJsP;zOieRJuid<)b_f*jEtW*i zYa&5|dpa(V#x)Y@nCKW`cI~f&+7c5Q!m3ddWQy@&~?|_ z=!@B$tjdI-;p0( z8o9i?PO(KsKPu#}_)h8SULd-CPX6?ZM5~P9;jea44^0h=#aH>~e1F3KU0ek+m@yyR z!Slu8`m|#J&r=)%;`SHZjE|8OZ7oM2ss|FL+f&frgIs24WxmT!Sp)~b2sb<75j|66H5?R!Rp)ZpWyJnrw(>q?BW!=_(*)vrqZa{^ zXuK2d8|-6{$#z?+`ofVAvI~QfsJ~wmq*zXYUE*a5J4H6X?V9=?aM81F}awIh6d#tB3TsPd08t0Cqx5Lh-srW2CgZ&C z$~xSvb;l4xW1*8{*#rU$-(yxB3iGE@6aMtvUMcf9oycm~GwQ0+&T64PKBPOX$ykgZ zWFKsHmO6xMXf{`S!xw2K$ZIicEtT5yp)ji8l!43MTKRSWy97?qugTik#zruDuz48; zI^H`UJt)5YDwv@ZGbOd=Z``O;JGPxAl7M)-R@c%J`WT^3k({iJ56-PW6m)S1uAvhq zxae=}dI49xLOH!wa zqCT+szA&k=3d?`}Xa%C-q^4JYBOD@$;tDBeE57MfvBXSGq`kD!N{U2mh8vKH>41HO z)(u`gBYE4b?=+a3MeU<{m`yp^40?yZynLIk>9dkUgv02jU1+9;S2RvNTE!(5+oLqC z2~Y;K+ljknkhloKNd15Z*wal3Cn650hNfk+-Mmv*3Me19+}j1HV#w*=^YZ|CV6~97 zAPE~g%*r}z_VD)Z0~&K*;a2S_n!UA!jc~fl1Nhu{y(wxLxWB*0q7YdC33mZFy0Q^1 zeKpTt)2X??)P7b0%0Ou)Z12yN3D|}B4ZiddEgcOE8o|ubrPjMOXy>;Oq+?W~OOnsc zM5oG`B73QXO0?g;d5xM%L~RQSwp8-56F!Y_ED9gxs|KU6wf`YJVrv4XWc~b5QIani z6!VTeRdSL;8F6!|ziuHyE@IuXRvVITm&XnSd7Y*{PKB3D&nLizkywbuq)^My);|dL zj=7#bKD2CTKYoD6rsji4cN?W$xRVX!mz?hk;^XvQb9NISrJBal-wHMhg>vb z*=KOQ`y^%zGaw%-p~N|M#-NmZ2c=}EmQ z>~%?|SO{?BgXL#Ra1UPPcbk7#kuwphrutAW_YwIC+c$8BEOk!nP=!Iu(o+-!(*|_ zvK4(hKUCS+%_x%=A9#AkkW@*aZjvOA)0LBdGXxy%s4$$;y(&Yqj9K?_9L#C)ipvb& zQi8%GPMqx)4sd%0?o&rFX}9V6J{N7X7NEL(54d}5y8S<-$#5@!n7b>BPjBYfb5gpE!vwV}x8ZSOi3nr`Jhb zGxlD07h-4p?u8q|Mf3o{y^J--2bR?dnmM8}sfBj`vI@e4$xS=B7>YH~`yhA=t`BU> z3oM2MgnZF_S0r;2FX)TZZrvP0FHiJ@2c0H&HiX=Nwk28F;5O*d?j@=uvSa8`BfOt) zNjKB1?H(5HPf3Xw`TVOS?~@8+>KDfZ(NIO{ojcu}D(#4<7b7n$9x3OY`?1mcu}^Z| zow1GoKUX@+|FE1|IN%M&Lf{ z=DK)xQ1Po-UEC?pghw9uP<3mXKIEO9zZPK$sQ>c&=%ZIlRVjlCq5BLsHPfQrB8cU` z>nLkkbUJjjCofWAOk^ojV}2JXaVfz-LqiFe1N^|}?t>E!meqw!a%EF;xd;JxSitxw zshYv%ATGi-tlrm$4KQvaAGP!+{AWb)^xNc@H3@dbdUlCO? z(D6C)-@O}ze<~KH!5gV?ue+Mit+7mDhwiJ{MxMG}4p7+?+7@!JMg(VLyrXU z&#kd^JR-6@4dyY^?cr4J=bRt(SAyYueLJq5K7!O(fkv&e??yOSyep^id=j~e}5|Y<6oIU^jMK)RE`kOm4(!<*0>uP243>UNlip}#B%jh!`V-ipiZoY?!k%ejt2 z&Xcd7x_zZ5mH(?$39i^&S00&RBu}~`KK*BD%-RGe$NW8Y8(&dT5E}W2J~Hl^Il68S zOuzwj^`w|XwSXS4v3XhNK=yRk@Ct1h#`vN zM2|KMzkpE<%Y~|yK$_5U#Z>Y2QOrEDLeHC47V_mvf?$>(p5Ap<|70=%B*A@ExZK60 z+hQEill-*W9nO+3(&5{!=*uFr?hUIirZ6+P6SIf&Gm$;(a8S4>KUa!`#+p%fIIwMh!b6x z9bf+_Zw5@Fi8CwWfeyMjvAUHDOBf z?G86sW0?}Kreos*Ar97-v}^w7bVgEUo%4w}0CDv1Y`r({vS^qGea(P-r_T5jtRf|) zq}`SDVIerDr|6*ZSEwH$XKs&>Jp*)&2=!}k37{zHhdSZ^BFvcSWu8b6z-z|G$%qPY z7Wg#2Pxp-SKa8334SPOuIE*fTUZ0ZSci>tW4I@&I#lRkhp;+>BC^Ca}(d}W!?y+ev zx?zr%FH>oo%&zV2+qqbNw&e{jOM5;KMi$Y`1t%dGReX>vTMi}T-K$T-D;g!|b9TN- z9F$muWh*JOEB;rGKHdMev8a%a6HUrdEkMpilQ(I35M6~Fli%RvM z<@QB>58n?2Y~m#ubtb2Ie|n$WiODqKr0=R!t7*GkmQ~$bfzVj?fuQ~7Pt=e?;E}ON z8)av?0icx?5J;1d_@F0OE1m=FwF?)6mEXU+N8~=0>CVnllHv|KEP>(~on+u%Ra9gJ zUeQnltm4%QJ(Y2h%KC+?ZF*QZ~cYLA8v5%ReuD^h?lp8=Yvo~b$12iyx-T%q& zaOYmkOvyl$Yt>6Ju#>WYjPay(CKQb+WyJEgYJXXNM?@b-Jy?aWWM{Yf?JpHj>2EX4`tE2iK)4-K0RBbm26`*G2^oeO!|>VEk61hE1(dkVf!8u+2R z*#2LKK+R*;!_5Y;L}6i}-7GxE7IHr_@Cliim*%z{>X@7mib(AhEriXGbiu6$!%6uIv3?Lf7Wb+Zlg7<2$g$UU&wgST zkBlt3`rOQmm`{0Su)Y5(sPM6lYt!nz^ZU9*NVCC>tGT76&yIEncpQ$&vHj_EE41RHwO-bh60?$1BPEtKx(DIE{OT;5RcExCcO-C;}bT!zia(EVa9Pwm}EiV zKCsUrd!or?A^=!&+$H(Qqk)0Y)RX`at!QYvuv&yWs);G5h5CEWs@wAFprGTogk%Nf zI5uV3gOYL$ClXTSCfWt^K^{`{8Nm!@s;c3P5jcqXjRg&uw86)37Z#YAnZ0gr&ON<- z`_rZdlajR)leB>WcGpD!g_+?}{3*9$06#F)Db_53#G$6Ij~4T1cXweR%wAMf^muAv zR?BD=rRItRgW3=Dt;zrRAt)d)L&{yIA|^0nnUt^r$W}D-IHUvOT7w_AH!rrIwL;Ka zfOMW;UtUvEl9TgnA39~Ru8A^s_M?8?Z>ujH;ePx^XbMD3*Piiv+csg2hJpgMK&yWvA))@x zA)LHr9v7c@eAOF4_az$>{1LayUur(Hu?#GaAZ<+pZ($V70c*%R2Ol4rFBA&Xb934% zDhLpXmsGgJ`un|z?>4YnOG~Bkzo&8*LaL4LB7Vp*a`=k+x?Q5vHGONuBBvP|r}T?Z z)m}x>4f%#Z0BtEEDz~hg?-7@P;zK#SBdQTl5s?v*J=e~OuT4(YDy}DO*iN8TQD0-A z;~OyH!7gV&Lv+f4suXBga#=PZOs;L46<1a=v9baSQHZlRQVxxViU!6*m+f(*Fffv~ z6fp>~VEm|TXkg`IMB(^Tsr^^wI&&0=J*N@OukiJ;R3*~TBjwgs9{FKl6v<>IAsSGu zvP2wg^pD6d{XYPT5iykl0&KUYzrVjqiP-ClxdZurWuP+<5u^a)n?E=7%L|i_oxg)| zpw@)(@9@p)YKMki=KSdSj8WsqTTP=g=#3%;ukFtfdfu6-si%d7NI^vgh#2$HqipU2 zVhwI@xQds|6tWkVCec(S{2>MbWJRsD9GBv-OGGNzG!6HEf2^4q>ubWHk?z{OQ+@pf z-w{nWFkZlAiv^!*n%5qm9<|$l)8C^D3%&>Y_%blRd;PAtuF4T-UJ-R^O|JnL;6`CD z>8Vny2`7O>4EoYp;|*-pKRX zcfIY;$u?Y8jOg8JjuA>SqgCe1@p8prZ4uC}(@!SH7baW!hqqLbNQ_zia8yx5f~6 zxJ5oJEpV6pde1&3W$|_X>(P?82m|20d2MGY=X4>Dj00AZm8E6Ktf)EsxOOa(42;kn zMC@Btz}DK~*hLThzvLZzePa&n{UI_{Qc@BazPyho`amP2-}cxA<-kZA4DK^Fe=T;# zPG!+W?Cy=>=6x_W`dJ2;?eB1IgAyr`g(i}PlyOHF?6bgQNNu+!&h6rnH+5Ou!Z#Nx zTfZW<;$Y$I17t-V6`gvkF|*9fWkJ_NeI`&S6x178+o!}kSUT!Kv@ zBte4bt4}Sup>OP$v<1)DU}Jw^YlYAXx1$1_i7o&Z_^X=|XjURQGcaPt(&9Y-Bk`5@ zxZ0(Ev~IhWTF^^m8QBpI&c9FiPt7dw0#n)9T3Lm<^V%%0OfFz$wS4mu^YXLxzdIDP zCq^TJ^Y1i8w_8F(4ZOGnHHp+j(4elZ;}>u(03VprYYkgQDd0$cid7Rh?aTm!F$4ek zTx0`8*tBM*zft&aFeo31I_Kw=mDPSj!jh6jphwD`wyqIZ>Uu`&a*H5l2WrsY+B|V0 zj2Iam1p{<1_LqH+$5nAKj6ERu?pdN9H{>XE=_4mQ?eMzWPD`r}eu81iAjk&4zOb?a z+Y6iGk3TlpQhn5!^WfuBjnPET_i1{^*(Zw#+Jc;qkKp*{xW;LI9*QLTv-+7Fc|3xy zQ>gE}CyLou-&*vBB2-BNc5x*qU<+9cNa%epAa@3NJ_z)}pK%U5hG@%c2vY2BU= zG~^;M!%tKF1h31k%H`}Ci50;X*TV-4h0P1pPH;{bz?UGVwpop>WMvsLmg5yENvLC< zzxs(oSJ1yludjEQ%UJdX1p#iUd7Cpf?*22T%tfcCrl#hyQ5E#Ci}U-S0~jrWOmC*7XaaWD9Ex_No#x&tnP>z`-;WT{FcPoBDp$G&_3%vTewS zNczmNg} z+PBJfN=Z_#K(N)BU#(ra@j&iI|FzkQ5hJ$ZJ-i ziLP^1l%L<>=DcL>v#X157yWUfdf5!gjs;dxU3HW5U@juyJo*>8>3SeaJ|m-Oa~|yG zewiZiDw2p*2dQX##NUj%nA9ylr58F^p;u!)KiSKiWVtZSG?c>TuLIgamoXL1*BVYND#X)O8&#PlYDK!@~nw7SshD4nqyjXU%jIYnEGhF<|Cp3NVeB=Xaws zGBV<*A(El^HD0)W<)vofa^A@eF4JZSoP9;d{hDSav){kNy-!jKVynewgeKS0T);|A zy>`)HmCR=NB`Xtm9h*e?XbQxmyo1p`lc*L{ZouVSC%1CjvBIrlbxhS5{Ig zhau#sm3BS=#we&IEH5s_MT8wjRAtHFoipbE-EIL?jG3m9M3wqA&fQnTAbQ7c$mhG8 zC~G|F^BO5A2mx}V&GuG&0>4#dtql*KxZ1LJ(!=TF@83jXiw0e_>I}*Vw|?lY@78hS zsf(1-Q0Ng;b7e|%awaGe6P&u`N#v-M6jQmL>EmN?epr|$hz+}T;wI&mF=C`h(nDlt z7Y&5{RM}?wH$M0b1bTMKgaWN0$D3Qf$5Mrz0E^RDI;{|LrZG3IK%5PPX>s=riMYS$ zulUkaUt^(R?aEnMy_^=4!QBu1r3M%mGA1VOj%x3Hk*$3Vfsn?@#)?^s`p~rxg&%s! zeBj#83XTs?af#nmyfe%*rh9;K7&&Mi!ct+_ECeQrAXTtDN_+yS5shS{dzRFkIAP9? zb$Y&U)ahg~Hate1!M3Gx-vOEWW?UY@mx9PaDrRrY#6Jm1Vp;g%cau^5l9N}|ViY;* z*{LE^nTML#z4el}SD}^>iG`}DS!q@I(OOv@{n_E})>MC?4Dz*d)uPj8fe`GZSM>a- zX@#%+j2^AbN4Rd!;uFC;^u6@v@|wli zi|xtD;gAHtRGbyy`1}H9Ygau=JMfe zdamQ*dmeWC-=n!KkjVtDnFCSh`pWj#{IBCo2^2?0#<~2p^?(b7aj>UsZ5@~3gkHU4 zg%N6ih<)!lw~Y+i>{qx>{pc-^n-D+i3EN$i6J_}x&UD=Q%+1E+(EQ?G51?amZ#$`^ zXLrv!@{KX{g}TI(gR&+vsi$TrM?Bbrg(KnH3L>K`SfSkYYj{VLEY4h%&##-gw+J1_ zo#AiHY8@J|gkx$SUf&(h36CDAt?6QpbGY6709phB1uY>Z6E&cT5fxVMYm zWoO@C+f~gUT;%ww2iAABz*NG4ek(3o8uFcA+A-rkwSwf}#(vZDc)bJSePTA>1YXPV z(_T}-t4d6vMT=$oJs0vZnDbXtGv{K)abXz}w_J}yKZUP__qEV#k7)Y2D9%|=j$@Ku zxb)&1euEyOe@h;WJP#Mo!}^%7??Z65?LzHx5+sJV)3Qc<(Lf1R@+hqt0WOrdE0|Vx zAZ3vZrc&TD;c%r^NtYzF3)HlpUS3s1VM=9_9x0v!U6*rf*%1*5l)p*BMGiUO;eY{(Srng`}wJ|@3owb zx3}*iemv}~v)Sw#X{J@({N<;{qu%VcyQ%C)zzJRN%nd3V%^338`3+ceMIC$|yBMvb zLWekE)P8HB`GHJOLP#a#qWt#wLou$`B}EW|eD>E6*(5b3GUJB%%LCE#Ze^d7D&EzA zFt)dZ_a6Snb+u!lTGQ80jnDbKDZ>#06DScShcYpf>RaH6@)hn2oY2sNCi4JK|5-Jd z%kjR16S19PPIr33(I}?HGF^^m@x!}@MMl29AOR}{v_dctNUOv^VYbK8>zppL0F-Xf z>hTji*GebtUQvfFOsu)~F$NuWBB%Kpkm$Z(q4#38e^cSF#^(VPRsS9CM2CduGI{_I&nqlI~OC+~$DL z(3R2M!OKL@21pE8)csi-AJV`*e&zClg2ekwH0Upq;|;mz{{;{>{`Q3rZ9lIaVuS?F zJwVugekAUbJwZ%HHZ{4RrLK;y%<4|v!|CeIY4Ytme7%p9`N9neMvTa(M=GDQG5|(& zo#?)%q+J3tA>%JeXc$YVi!HyC+v+F>CYaX}f@-6smTTGI@ z?W+6!atTqXcpDuxd_Aw6%!;N30%n|8AI7qH+;r9NqEQ|V<{gZqo)ycP-8 zfKX4A!N9cf#kskeWq3E_@3~W!UY<8ly#Q%MVQh#;t075jVhPh=(WpsqXlU0mLP;j4 zQ{Q9z-Q<|z-Gl`85;{rL=kSMXWy7o9u?J)!?YWsQBag(L-=`cX@wAY}-rhd3ox7V9 zcH#Ta4Bb7#RI(TFQZR!d5s>)l-@6yve$fR?rEPh5j%eJN5!faSTpxXo+sptlQ~L=t z$Y2t^_qg7YU!Du3l0*F_7aAi_CsC^KF>>Z3g-$Bs%2^(P%<6+`c}jB2p!Mj;SO-`~ z_+E{?v?w`6Fu)NEds{3767+$)No0&opr{!HvM#YZ?q{Cp1&c$KmFeh?%-Tv#%`AlO zt(~l%7FRIehlV1Su?kRcRp{9Sl_y6qHP*;}raIsSCLn!&rJV+>&dbS-7fnC^0D8hP zPnxh(NEg(~+B)sE4FFfe386rJM6ZAL&~!n@$NhT}%trmch90)Op*)=Y>3;Rc!JSm( zaoKJxl^c&z0m>N|7yyPQM7!VWW}_q7g16!Qlp)DoSHn^A+xzWV7_TjGOzivWOO7`| zS)6={#{GHlHYevKO$-3pgLx)s7T)|6kX{wjsFer3kB?u6E<0>fd=234jQ<#!s>!@P znuU5a9elRc$tlcm)V-TD1d2BfI*Hb)f#3LbnAapUnT*R~yb1MuAJN$?9Uo7b-LOkT zJ0hIvye+2MX5_uo#+sY!d*=JK4HlHp($n-Dvv)x}U4(&FFav2aqzB>rZ0=H>&}8A3^V+}^vJzz&1bSe{#2(oAw% zIZ>>fAq3Al`YXN`m|cJ;qf4FCPyo2AlV)a80c1oi9;|i0OTGyhy9tqY_mfcPyA=1a zR#1@C`63ttAc#2JT$-Q@oW_RLrUEgz7g%ZmZ_z*KHxZ%c%rx-jL3-R)miXJ++W0-L zEemh~G<9?gP@LlWb-Lv-(*_b=M7hOi1=k}YBl|N^-vMtGXgqk@W>9Bwni2jPBt@yN zEp5%M&8czNI=7&vS(*#T4S5e{OIY+yT~%D5Bk#{zvsT#fH+xW{cS#CVd>q}^eLbN{ zps5)wbxO_&g;!UL@9c&eZ)}LLJ4Dbyh{Z_!OMqnCMJZdkBfCz43~mHHBm)#Ccq_V4 zOf(wHYwhVmaMWF0{hXY1WZWw*mKhx+z)#lD($v8&~mgr$dSPHR-6;<);gU=FEfGBVr-a6ogNOP&)5f6jJT{ z`^JDIs4L{z`F25F9dI%bW1!HZpJHRXtlNt^GGuZ11NKLhrfSAqdT|s zGO;espnCWzoee7MF%+Vx1P#?qiJG-4hHEG@1l>UMsOGfVI$sgFE?a=OJm33-H82P0 zCPO=eEigIr6j;T@^V?2cYk+xb3@bAnwWBWB@{Jl@E(sUS)Oa~0-2vcPa`GYYEgWS* zKj4${b8Er9T?EYkm54T&u;MDzk~Al4umJ&!IJU@)9>s)*OkvP2JVMg=6eF0A<)x)a zaiYE}*5+rMTLeKNNP*+Q>f@*W2x5|89}KJqditCUU3t*LMiCw!QWKheAfbhZ21C|; zatxb);9HslvNWASWqQ(#eVy{aa1bd9svmKWcwkO53U}SNLan?ReNvI1$fYDp`s8ET zBR`as&9r#jwyxGKP&g+@+DMD4mBKGb>zL^{Tl-c!Z_`(}_ zV}+JL_1dV8QBL_qoxCw?UOnsA?<82IT)h2i|8Or-C@B@R-lY}KMNbVjToPxtpIHs) zi2aM9W`JUozewkf;xFa9;&WwKjC%Y|@A|38p$&%~IyE+doO|`J{^+K^j`KMu+mE}y zYd7HSiTW1nR*|*8^DS!PKU__x(G$V1_FXj7UwxtFEJVB&PNcV$I^t-EH8-xoiy=0w zE37hFviT;CpaM;k^cm^NIM-a0v`a)TN`-{^PN@d7Ig#El;W%@7KyV}VAGEPvBSd$` z!8)jHkWy9P8{9CRFr_YLXFXOSg*B>uv4Ki=R%+7a8e$C3tzX7UfFMB{MaP7KuJ?5f zmbB$CDVMq=Rta%v!J0?Kfx38UOjs3xz{Wu!17FdCn-m3Y5=JlI1kPXb8@zubpQ7s8A4)BFS9PW&9BW@n zhy%nhI+M>pL;utEfmKd0roRMj*E7L~ub0kZ5u&WW@;_mN9hl zF-TOcB3m;0cf`;8f5~_%ben}Zm~qePOklfV^X4gBQSkPy$M|jKeQ7$@ z)T`#Yu-u0RTfp`_9Pp&@#QlOG`&k)=3V!6}y}8E3eCS0tHF`G3V+2GwGe4Ztzb*f+ zl&{k-w@1tMGdW)*^gE`BQBjL!M&&JVf5i5A3>VL*U0TW6N;5~Id}_P!4$ zg-do@q-urS(2(Gi>*c{^S4wD;s3r>17K`IV$D7hHsnkJAF^AEK=dEkx;$`CSH5@OU zyHvV1<*4@r_>+fbCBg<0N+SP)Yuhpsn``W^!{RP77?Rg%3YE37q0Lz33^>n=Ng18P zh<@OACk`zqx!e1P0g4~uorJlHz@Y&AER4EDA@%UkEM`*?bO4&%tcK!21HE9@EFUxv z;iK6gjFiHWYEwd<4<*)=OVZ10#4KKfet5h}=~#8$BUS0HR2cByu_w^5xIzwx6nN+w*_yGIlNrd&t(iXKs152-*+ z;w%M3hgTnCoF*JUp2%tM$pSpLS0zH~`d`_Bgiv{?$y^o~m>tS-#kVE6K1R)dOOY-h2S(p4u$n9XXU-C(m*m)a) zI@t{2!K(Jm&*2FNDjDqiKfkl;bq#0rGwQ&>&9}$N%j8yetex|HwgI`P6>s1B$Gc-M zupFfQ@KI3o3hcXHzc3;q@bxU?X zKwQ)UYIXf3+*Vv)77NDUaU_cYqr1J#0Sf1Lfs7CSG$GT5zO4fJgrsUxrb!0EeX-!i zl93k*{{OVKoB%JOWEah0<$Z9r=*+|mQ-yp8Y!?6M+>9J3a_bYZnB*#K*V6x%wsROS zMm0MpXV0hd2J@Mcb`d_IlEDC>rx#spA}|ZnM9^gjxJ8K8#$*d5e8iWOq)Unww^{6L z_$pU}s$rV?hf2)SWplJ*>6%k~0<=beBjs2n^jIAT2E+Al)t9okPRB#^-te?|aU-^Zjtmbq%v; zX73gETC?u8e<8^2%_pfr~^Kd&6B{*t4XFlIRH~d0Y<0m|R zHp9NgLiZWo?BGyQL}4|quwVKN=&O50uv-h@;>aju46JRU015=aJhN2bZJmChIobQY z%ac2~%3d2ojkQnw5%S!cB=XsJJ)PO>qLIE3@Kv}D%y(?E5k+?D{>Tzh8 zS5MD#$x;pQ^Q46K_35)0s>1+ZJoWR?7?2&MV!v^(KadIdPmoV&08dHte&D(I-dcv# z6_9nW2}exhMLK%a>u9Ouva~6)Mxrwx2lyA|mEdQFRnIcrW-^O$Wh(QhBf-Jr-)@?t z+8Y@%1BKDqZz}q=O4^J(j2M5IQ}Y_EW-whbqB1r9bmdy8pvgb?$lIVoJ+oDzLr>a0 zuk;KWXCDt)sw$TRuEaX2Vy@hDYj*h(98_^?sf(m)iY*N)+z;5NQw2QIm!79gNh8E-U2iKTU7)^pRBJ;FERF} zOEu3Q2kN@19ceD`{d6jmeO8`XFk~k5HG!aaOxX_y2_0#aQy{q*hUKO;fk&()!27n# znjswfWdrFLQ1a)H${JNeBMQb-!kja87yau`6h{JO#QjhHF)iyS>hP$NM1I+-D% zwQH8m8i|dSIwE4(Li{viPxz9L9IG>-gG?C@5FZ-zxvq#xlHXg#vh%n}K(1t;qbxeE zkh8z0Mi4=H5>v3_oA#M!e3soGK8zK6M00rb*c>)iw>B}z4(TqL^7^qj+6AJ+O0s)XO z`0{hrkLta5i+62(4>vcQ_cvg>*TBk(b#=h+WI>-myp6eurd}gTRN|>Y14AfHZ-($j zI<8Ppn~`>m{eAfG+#BIns8N|>e($ZKdqah8rQ8MGF6p;AaD%Tl!i+03WIKKjVaLxwF^Pdshsy^`)wAa$6o3#1QL%V8 zI0kl8d~UkIjs+(ZlR?9@?B^3GLk%3T!+mnxRY@~uF;?^<E}h`~Cs&f0BIE%L10FXNg|@@e0t?6#GE4fCV9Z zLpwz0P=O=tH)jhBCk{#P{;{CoY6509HA`=Nz90OWD8wf!T@^iV!fb9PFDfMDyTDr+mshEdeKU7?eEz*y8c zR^^uu&k<(zkLBr_6;LjlH{9q?sc@Ky1qLuj{=;(?e5pWjKMpy8`z2<&hx76+cal|&BuRtd z&&7wx8{(^DMT7kJR^z?gKwGu~m1P94tb@BadMS+9FB&|3lR^} z6Y=W}S4o(9HYXyr11%}n3&ibDmzgutW z^8#+>uZ~&nP3maa*(aw&f^`oJ7#|j){w<*QbF%2i`^VCpLzBZLsuaVqg|OCkSd!%^ z&DNWVSkBCUVgCU2DmB=M!Q+t>T)EEi=tVW$kftQqip73KS0UqfN=&-U z*k#ASd*S~k9Hf;vyz@JqagmLughGXNoeT&ESxDJB$)C4E&8686ok;XU*dJdB5t$wR zuFwQm>v;wU*FUP~70Z(a9qBsI^sDX80z|E_*Qf$>rF?t>+A>K}TN!``OL+3>9$JJd zDdckEuRE*fi|G#TN-2U8)l!xBYTvqz!i_3E5~T98D;;vTWNGa~1`fupG8vk(QQv5I z0BkihJA&?e@TtT{wsZT9B+YW(vvqz5s?Ld@E1lLhH*B`@e73iIM)wb{d}3n>!Lr}k zUDy`>wbFQDk06DFa2$-3kroZ6F`CXdT#mZ_j>DWyJp9QJ8SPRPw-B(zqRLmOZyOsq?3ghfp~Xn0~dVY9%8^hJhP z(L^o{&MRAEy&4+Ngds%Xn{Bk*85-_1;v`vAG^#k2q79@Oc;bS-M|Bv}=&?pMp1&K7 zQQC?)qb-y1`g-fg5R6%5ITFaVroC!O5<-=VrK_a<$)BU-3g=MLhWe^5$EbvSz_~7jki&VG~1GO=gQi-_Dw5)74`>ERYc*D zp!N+w7=!aLX>QEQg~v^JfKKWV#SpAwqsb5~Oxe%3IPr-jTdqBB=7akGVA48N1x5h@ zWp5^4qbG3@hO)~|*DPjA3}O?}y# z>6aj5ysm>7KR7s`+a+ezMTCU&czvDQ3kVy)!T}D*Q+X_LLS8?i-K7g9S7*r4phL0= zz_6Q@7akiHa9*$Tam*mxVkMv3jL~-vAkUoVFV)9RWz}fxFs_j{SuF@mg^N?(*^BlU z{e3A$Cy)&P2p3n81r7CNbXsz6wzv5qzp6^_U>adYbn(Xsd)D&WG$snv@- zyD-pHCg+?8D#~UmM}(gwC1QNy807TeG&vRdbrx19tj;+@Oql*jr?!>k7(DuVZzZ$R_g*kH{H> zENwCxpCMIdY1r5$0LpoqLWnY!6UR!|Mg6qv4)o@E!tMrAUV~e+V;2!2QJO-#5p{8I zP_7ywAAN!t)Y6YMDM<`UX217jT_xe^d_s8!J-a4U*;LS=Ur!)Fcs~L4;9L`{9QL6KZ&-A zrmK?XaZ>x*?29xrKS5@D)sq}8Hl$5cvKjeyJD`pzR*DZQ|CSwsLn%dF?@{8Ygt=MC z(O+EiB-S4dXT}1dN?FJI>AbL*a?4S9D-T~fWA`vWL{YHYJZ%Fo)e-z$cevj&KzO_$ z8CB88n9<$6Z#iXv=#a}o`S6RL4Th<+)4ZxI5=MJMrcL>%qV>iuxK+^ULNwI-hG?NLAT1c+reVKY;Heq; zZ9FXL$u?zux98jl@LOp4JIRskog~x^n=gHh&V~t7Os*US9vxo0!RxXt#`@I z%!+a{1ww1S3VmOt%oK7bbn1aVI)zRa>AMsPza*V^xnOKNyU09EWYwHWot+^2aP1pV zvVtS*hI4p7hiYtd{6yT4*=>XC;jook{~;_O|BJ5>>t?uNjZDVD5*Ia#BeC9B>Tfqu zszv0J^JQOKl5;TDAxRmz~txB*>H9DE`fk*x8*^zAqj zW*>%mPr%<+c(uBUL#uQ%$7yfHQE!@O{Hrw759(Ky{uDMg)_OmEIv?k(xfaue4Efm^ zBh(`asf7PZw=u}8(yRO$5@@P|_q)=#ZCny0gPCNh!U*~Y){?=ZbhLO+91sU-#k-V{ z1hebjHQA@z6`rCfxY9eh{8nA^CE*HJ%m?IRY+OaU=h+qd>C-^UE%Hltcr%XFWVPHZ z5HOk(`%zpa&p?kOZ10a@TDDTB_9M4Es;~&_q!O_?{pcy_CZnkkARt$^vtPzJK4SsY z)@pYZ7DEm5v~-~FN*ig;WN2sr-Imm0P|-`!(moiO4)8te>{O+0my;9X1K6Q=L3y+zT0W-W7esW20b0$g-r_P1^etf-PXpe5u*ADKT-LwIS>+V0 z5nUm(N`DPKTtPBJh$VX>Chhkf$gv&z82PNK*C)l2XF+>kn=`$#!kD!gcoo_og4Z+K zJ38d?Q8Lk&^hqVN(UYq8h+_*>Y21v{wgysfAx=EGdsXe=$H>6Dt@h)eB$u)V*|q`F z8@y+p>mg}B8Ql?I6y3+Z(UmXswA=6mF@pd^cbCqP3WYv+zYmp>{t^+1Mv_Xhdrd#@tEJR3zsK+?f`2V^^{OClh_sZC-l*^XjbQpI}RFP(b z(O9IT^(eFKzw(v?{6SE->W9kavJ=+7sus+Od^x(+5X*0I z)$R@(y6%&F5(iK3$iv~^aw7x?z93#Np2cb7MlIDiAauHUL5zj>{?N=CZ|Gj?1^$~M zRGXc+VD`t#Ea?aGje5q<>jkUog#)F)boVcv8f^`Qf*;b5(r6p)1rOhPVy)1DWr?)X z#^tbOtId`7QVkt8-V8pQ%_vvaF3Ka_o%AtzI7_l%ENZ*?KNI_$ug>5&lQ;roix1qF zMiTlG5fBmGAuT+yfOhW8%}qUY+OdL-{XPOPJ!r()ibaK&Bw1*D3F5v-%ym&(Zdd#E;RJt}~Ry(`gr_lD;c$jh6; zO>M{#Pw??f%#x=^=*p$v>65RSqh>la^5iwyxTz~9QWEa`jLKEP^=ekJ9C>z)(!7U|vN-%x_%cDEU}PP3yN26MMzJF_A_&RA;WA7(aU22lw2WH*(;JVm)m zd)xk>ddY;8h<%bCi!p6Upe(D93gv|c5^bYXJTLxh=L0tr94{pHL?fLBC0tt%)rB7W zxmY?srLQ{K9bqXX=;_Rfd%+XR7sS&3Mr|90@03oI^w%lLX>3DCOL{xY7^RCS-Vx^I zAYX#{7mIXX_u zxS3vl3ylKzbX#ur3nq4AlFgoe+n><3dIaN0!j$N8;^q$3w~)_BQm%8yy00SNmKUTe z_mluxk&(4uiz#Bl>G01GzHeTZFoV=H2O?`xR9ap-I5eQ|F?Dw!3WkFz1K1pK$J|>c zGWxu={D~*+(C4xuLq7wH1KNKQjg>9;=|P;*g_?3hl#O3BNfsVF0NnN z0sB8kBkA9P{o!SU2b_yA^SsnZBHk?J62*+Z*(O`$4nWR5sPaMv$RzsW;)mOd2VVER z&PcrsXJ_Yqe%Gzlfw}AOE+C=Yo*nCRe`>bDZNBVg+F6KGHP+pu5BwEDCw?+dDe~u4~hK$zzLmhL7b{ zVSWaFey{7(L+d=acs;HSRcV?i-^Umo3-wnj*oK>fwgoK);Q{nL?i zNj-Hc=W5y8@5FVYXZDc}jQh)byZe^bwhFJ3wgq5o+AjEEf{EgUfY!oeYza5aO_cC# zu`-;hUn=i3Gm?|_Rdk$zbUCn;A3{cp> zM(p9$r$vAdT-2^>$*;vYoBp;vCWMQGi13}^=8qk}Jloj_Zh^QmpBEK9J+H<3N#5Uh zd#5Xh)-w*EWE0=MK^E_h%O5>@1h#peNzDLFw1A&|jY>yHk1s?#oc~-$AO%F4;n#iB z=C*_O@7f1QXOB-=?``e*4}3`SDCLgyIM1*HEJJ=CIy!cW&iw&uINEpWss`^CDMDMO z@&dx?fZoBu;1DNQlYrF=KzC+vazX#nEPEpU>kDi8{Z0?1PQZz%JM>dglC zUH~X?cxWgE3M)A|A1gT@HIvU`TL(ZVScdX_yxs%Y+fIL!u<*jd;tzI*OK!x~GxOY} z+y$SUT#%6vatjKcM@U5ItZ!~=YUs1LK>KId3W7f)fI;)dpn;2rM+GNjrE*jt=i>rI z8`uyD`}wI))Bs2U=7P_~kLvNvOd+KLg~-E$ZD~b$RFtDuj6a{Ks@C%i?#yRWcn#-T z95H`rF-$tnYy!pq^w-@AY8sl2%~uGq6x`eg?q{MfrC#t@K$vx!a6(7S!@nI~ynEg8 z5BRn0(Y=INpwlPkUt*F#S-qjbJwPAXicNqKI@XK~kng2B_206Jw3mWJB_r>E2MOx& zvCCo$G}XX|fsb#m97w07@E-Dduc!gj4^Lg-xBx7#&@rCCQ|f5z;gXQZs;G&YsLIGx z>KBxj+o7SN9_;PCB?MYNhn_dhm=*(&bV$5m-Hd814tFH-_DceS*~!Td>?vfD^x z+A=-%?RL?2WGK1Zj`BI`*i1)!bPL$D7@H_ejEDR}>8k9MikP7tme^|> zkfC`7uf} z{LHtH;cr!AaUD-iL`D4b71BG0tM$uoHgF9*mRcHBRAYg^YcG9q%f((@`qf4pMsQVC z<4ZhC8##9In9|byEV4FzzeI6di(2j{%Z@cW+1M+52X1X9=I-=AmKRA@pCwMUH}p47ge zG!N_+H6;?XP9DMfvbYb1L%1GIEE)#$7JH7nR}4K%Z<2T$F-)|6ST`{#LsEid?ZNJF zFkrqI^KoSBqKCVpjwO>vbMVE9yKOn<&kbfnDK2;}Bpei@_vWLfDfyxux$34{mZQ$I8(-0EbzKA*Eb^jfUbXlz)xIWN%rg;>6iCoWi3 z*+TMELUGi}CBs?su;k+GCb{T-#HEre?I5>zKOWx+Iz~%!FPt{62@mHxWWn76f#8~5 znP6JHtq5i-huS+|phqakrKLXdK7LrE`W3_JAFTgWVv}3y1XB6xwHi%6m4jVgNxA1; zVkbW50AAqpPlRwMNhUXR8hE^sY5Ie-d&>xeqGh}1P)sN$1{>z)3AsumC%$?rWN_W` z7;Z+Du|Fh^oM2J)30#lKv8(@uw55nWd~&o3mEJwwBRIGWZ7$E-6TMcWpz3zC z%j5Mn=|C(J24+G?=u&lFKb6*tNAsP~%`u3ZYx|MW$+@a%@gc`zeCe&oK7B!t zu{&2xclvx+wH^g8^}h~lbF(MoN+QNr)w7tKOx`a&xGZF#BalxWI10D16fiMwx`A$o z5nOsoD2FIzkn{L-wPxU}XJ?J<>>eXsdy94jinLco%zQs{xMJ$o_E#<4iFo!kGVYo~ zO_3``duC>%h@;hYU)s-inF-5^A9Aws{(Sc(8tRx_X?C%dqf^Az`}2INp&uKIO#ZlV znOgbU6_vT9oGyge5bbE*HcMC5a!zd((OMZxoxUv^Rn;x`EIPulk|jARH^(bh2k6qg zQ|jTy90d6=>ecjSnKkR-eeBrCiq|3U@!5pzKV|Itwf9xv^K6mAGB03)GaURpyu9mtsTU;DRib z&zAQsW^&uRSk-OU4}U|oYp?BouahiEtJ=D&K+iDGG11mLW#9Ict*B(V*A(;c3clkw z&o)ZaR?6I@c5+i{rQD~Umm~@I*~~Cz8*|vr)6l*u8WJe6?yuP$Fr;aAja|oevhL@I zXg_%oyy#4F8Re?@%nSuiXwVRoIy%I3)XVIlZTo0GQC@!1VKygqbK#J9aDlPB?%l$t zlC&?fW2{KmO_KM#%6cXX&Phy4(L4lmo4A;F%w_(Yqx1|$`BYb)=f6xRtW+t-ZXurc z$!mMxK$F|f6}=YEaHYVG=vDNZq-DfTXCLR^yZ?9_&kuQKS9fsx35X?|6(OC@>NLF* zFyi&j$(d+AG+3!x*7dqu+4JCPJu=%In2HnhqFHX}BJrLn`o?PD!|gUv(=v)*WMYPV zy|&9ld3YJGr^PTXYX9J3Qa68K(=x1P-B|L++7decc=J-+BK2mF?6BcwDe?nb#j1|= zO@ctppjYkI;dyUwkWTPU)x%PYmSs`Vr2WStcV{-}fppV%&Sbt_0duH5JRIJTQ=evH zJl-3(WwvKPjU6tr##BY%;?v3UgUq*LW~SCzmw^ZY+Fu=*6p^o>d_z)H;iAa%B=;V- zBo9OG8b?B@k?>S~&#rkn62s%+ z^zQPr{3PNW;Kj%nPfULw)Kw?bNo_8JszsM{uTV@thxVkKpehdXIWr3@0{2SUnL?gx zIncqVnToH+0XHjj8s&?YCDvmkaBx3g6mH%mMwY$qp8FcCiku$e7mm_V`|4bAjI5C> z$yj-H%jjYn+r?W!Lp>zhn1y5cZl~1L&V`OED47syK4&&r>M~84*S?9HpWr=y_vrO= zR*m?O)DPzg38_Ki+0|O{i?=kWV7yxyK=H3_IV{ z*3J>J57%+I56Ruelu4CQk$}~4HTx&{N2@!#p1ftpHEBl+o&)kt);@aoTFT}PWgkZ< zTXvnVB|fjWB>hoEx>29%GCBGA@*v=%`|vL8Dwu?8wNQb&`KFX?rvv$>DMK*f5-Qkv ze^4@Fa(Jszt`5|-yw1xXYOlm(-Q7@Yykb*mJW*?APU12Ij!O^k-|5=6JePR*xir;s zulP71AZ!XAE?RI}1Bq2_$p!wAkNczg%(}kRlxWU*8=sx3!Lm79b=m<*6%HIURD8j@ z1)~yDH80lTU3ImPD6i24_oDUFJO{PA#Z%Kigvx5M#XVvoRJan-mUNIViFGj7`Ik=W zweHS4CzTSYrxbfMt~vD8nPxVsb@LSQsH|DMAS|D z+-}~d=&*6T>8-B2yqg@)bFq`i8QqlKA5z;j;F#TbT3S+eTr_9U^rt{WnKZhAu4czO zE}c=kes=!*?}pQ{!Oeq@8sGXoYZ}Tt_AW!RmY2Qknv!Q{n#EX?J>3t7NZj_0r(-wb zRR$%b{Sx?wF%G}#IN?AN6cP#po;E3(E^+wYxlMZqe z{&CIrB2Ba`lu}(X#@-4gd(vFdtLv;lF`<}+pH`hiz4R^r)Yf4_>A(k{@&bR=JR42P zViU)BvlqoLbDnyHD;mwUlVDjT5a5To%9Wa4Ogx*AN!-yauu-=XJY5Ohd4H6RsejTTboe5i@6a+*2ot*riLTr@|+ZVpvZ#>9N~y(KhO+DW521yHgotHP5dp{f+_qhN8f*L5}Ezh*KE4e6^Hw=W1j*e7R3?2|WQ4vRFytgbZq)`M; zU~qSVSb3DcCh*OmD`9Q*R7(uIOsUFNerN-dCLOqJ1%D3xFnebti}ZSHBU0_^Ty#ym zc3-mK$8F12S=?ZhScW+{v_gt=OoE$Dg8x)eD>3r)(bV+KCrcEmD@G1~iE zKJ=JGEjn#dmrZ-mL=n52+IX&n8C(YB40QY4_L%s58v3IsaNaM+<=I!3M#n|sv-11o zK8ncbZGB2d5^eRlZaa7$&Pq1H(L^f)m;|J*UA zzUAJJY5wkJ2ne%HLH8eeGdw@KTe)AvDc-GTcv0Rt!NCopZY+-nH+Kn9@zL@&-taWI zqbfjtRk$=+I8&$JpKs4l$JtU()26@FodT%^EghYW)vS`L>Yve3f~bs!#|@d8nGNnv zw({p)`RHkSe;gf8rwneE-+fr%=EwbDX|2jh!+`du>9i|xvKHtc)jSXTm7-W}v+(2i zbCj;j4CP!T2Z=#*gUrPC@UXq2-NX3!Cl}4A-jUdqm7qpsxFt{6;`B(@&eW5f2xGII z44x}TtbO`4uG8SoQmy+y-i(o)x@)~t>zbXLadUHltsOn_4Sp~X_oJf?4*pC}Pd9#b z91?QoR1hacVKWEiRrqqzKlPLVxipp`5;rE}c14>3x?kb1{I-aLL zg@;aSZJW1_VnC0)n=H51{UHnYv!0C~m-t0Se~5{oBSsr4+|MG3?yk?066WUZOigDe z)6=eeFg$IT4WqQDgNF*S`FSA^wN#>m&N|AKyxwP3&F2E+P0j z%H2sH?rnp|Y7Ylc&mn!?e#GltBk$%wjt;jOm(v|tURHd2yD8u}^e3bB=CpOH+Q`d7 z^P)D4F1WnwsRN=x0<@%mcaGyg2eTEUd^4T<{=FuV}t9QMuu&95on(|B{eNAZDoDs z7OQ1hI5@~_6~Yg!+YOAoTuz3=q@<$9uEwSgRD)Q(eZOyBv7^j5+@llXg@}!xn$X~> z{3;ym9~$yVM?-pQ{yIjCkBmUylI{(y@;mCPD(pUsB8~80#vrhY*+kniSGi((APvbp zT5WR}eO7!oxc zsZSwkN`x4lW}=3Ad>&ITErJCB7DVea%AnbM7B0+IVh6nlpP9meJUP{b)Gy+WOk6?C zlW)N)JNob|fjEgjZqM+Z?x#=2DNrp9HG60c_ZTiWx3!}}nD~IHNMNXgga3$sv}T=v zggv~Fz-z8^U<@VanqK!XJTekeB>!@9bEV>blVt+3?Z_2uYL%(cQ9T;Q-OZl2gp3S# z^1Q7GvkldhJo%HsjE_AJU!9pME{IFG=teaxc{zoIz$cTXrB!lB=)J6O<3U0Qx5lK{ zo3%XB7NfjsMP5FownZQR$Eu?90 zkR)dt+;yWi`UOB51_vi}f09d-`t|Eqh%vDOEcS5|&B{ta^t~H;Zw}tCmp7w|v$yJ~ zWsOE9>)Z*g#HakVZ3f<0qaq78{GORxsPq*q$Gi}z!7<<48fHw3{f5sRiPA)7B)fQj zFM#uzhUN(Q*4h1qwm2-8{>;K7z%9U%td9iV{*C}EqI^SX+8;gsyZ#PXQ}5!0n38Yj zKZ{oX_pIw~l_hnpH6^KkbMAjyop!xn&J_s4{F{bfdQMKhdC6I3XG*L9PP-)ccM0_z z_|2o`KLZD%QvL&nB?q(JfR=Qu zU1d&r0h(ALPGwMhBYUKfC$0r$Mie&tUVSU6GiY*>jiW^Y#r1_)q}WVa3jZ4bB3~*K z@=8kR)Ex7fM4;al{#LkroDXtdV_jUL<_0^81(Hti_vUZsQ)6=05%Y}p|1H?hWY}7n zZzwfFY(6&?*2qF1Vn0JLh9cz^+`?5#9{pQ+X+`IYvpSb^ccS`yaLlxwePDESWhK^l zV;Ssb^jNA8hKRL0aXZLA1tTudGW5;vOU`el7VT7^yz^9OW0i7Y13cjRuwq=v2ARvN zi;LQ#pW5&I$zrvVKbltVSv3PG;h^ z16F-2NhqLKgQYE9<=TtT=X!;xp!bF4 zf99(vEws18qWS-ZoAmh*?R6E_O_#aXu|@4?3+Uq)B5+YOnU!@uE#!=b?{ zG3>|xpH6_i_9giHeK;Y|V6fMjVD!PBhy9m8gTleR|EGW9s!3t>g@Xe*4eSZlhp?yr m{W^%$|82Veao8YzK5i8=p33%(Qur$`Uv%mkiEa$+ELDvB Date: Mon, 22 Dec 2025 13:01:30 -0300 Subject: [PATCH 02/27] Feat: Users ListCreateView --- code/core/settings.py | 4 ++++ code/requirements.txt | Bin 172 -> 230 bytes code/users/__init__.py | 0 code/users/serializers.py | 17 +++++++++++++++++ code/users/views.py | 9 +++++++++ 5 files changed, 30 insertions(+) create mode 100644 code/users/__init__.py create mode 100644 code/users/serializers.py create mode 100644 code/users/views.py diff --git a/code/core/settings.py b/code/core/settings.py index 2bb945a04..91889d6d0 100644 --- a/code/core/settings.py +++ b/code/core/settings.py @@ -30,6 +30,10 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', + # apps + 'investiments', + 'investor', ] MIDDLEWARE = [ diff --git a/code/requirements.txt b/code/requirements.txt index 94a795c135c62b45a2a77c9b0e7376a141bf2b05..f36355ab2c781a693b8603490398ec18fc027473 100644 GIT binary patch delta 63 zcmZ3(_>6IaooNa~7DFOK9z!}qK0^^hDnl_t2}2r?mB^3_ Date: Mon, 22 Dec 2025 14:08:38 -0300 Subject: [PATCH 03/27] feat: endpoint users/register --- code/core/urls.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/code/core/urls.py b/code/core/urls.py index 366711211..361b2e516 100644 --- a/code/core/urls.py +++ b/code/core/urls.py @@ -16,7 +16,13 @@ """ from django.contrib import admin from django.urls import path +from users.views import UserListCreateView urlpatterns = [ path('admin/', admin.site.urls), + path( + 'api/v1/users/register/', + UserListCreateView.as_view(), + name='user-create-list', + ), ] From 6ca1d07fd3985155d9cba0d4b0f4c098789efb44 Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Mon, 22 Dec 2025 14:21:16 -0300 Subject: [PATCH 04/27] feat:model-admin entidades --- code/investiments/__init__.py | 0 code/investiments/admin.py | 14 +++++ code/investiments/apps.py | 5 ++ code/investiments/migrations/0001_initial.py | 35 ++++++++++++ code/investiments/migrations/__init__.py | 0 code/investiments/models.py | 58 ++++++++++++++++++++ code/investiments/views.py | 3 + code/investors/__init__.py | 0 code/investors/admin.py | 14 +++++ code/investors/apps.py | 5 ++ code/investors/migrations/0001_initial.py | 26 +++++++++ code/investors/migrations/__init__.py | 0 code/investors/models.py | 22 ++++++++ 13 files changed, 182 insertions(+) create mode 100644 code/investiments/__init__.py create mode 100644 code/investiments/admin.py create mode 100644 code/investiments/apps.py create mode 100644 code/investiments/migrations/0001_initial.py create mode 100644 code/investiments/migrations/__init__.py create mode 100644 code/investiments/models.py create mode 100644 code/investiments/views.py create mode 100644 code/investors/__init__.py create mode 100644 code/investors/admin.py create mode 100644 code/investors/apps.py create mode 100644 code/investors/migrations/0001_initial.py create mode 100644 code/investors/migrations/__init__.py create mode 100644 code/investors/models.py diff --git a/code/investiments/__init__.py b/code/investiments/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/code/investiments/admin.py b/code/investiments/admin.py new file mode 100644 index 000000000..e7561bd19 --- /dev/null +++ b/code/investiments/admin.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from .models import Investiment + + +class InvestimentAdmin(admin.ModelAdmin): + list_display = [ + 'id', 'value', 'investor', 'created_at', + ] + search_fields = [ + 'investor', 'cpf', + ] + + +admin.site.register(Investiment, InvestimentAdmin) diff --git a/code/investiments/apps.py b/code/investiments/apps.py new file mode 100644 index 000000000..b3c1b763e --- /dev/null +++ b/code/investiments/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class InvestimentsConfig(AppConfig): + name = 'investiments' diff --git a/code/investiments/migrations/0001_initial.py b/code/investiments/migrations/0001_initial.py new file mode 100644 index 000000000..2eeb4b266 --- /dev/null +++ b/code/investiments/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0 on 2025-12-22 16:52 + +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +import uuid +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('investors', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Investiment', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('value', models.DecimalField(decimal_places=2, help_text='Valor Inicial do investimento.', max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.01'), message='Valor Mínimo = R$ 0,01')], verbose_name='Valor')), + ('was_withdrawn', models.BooleanField(default=False, help_text='Campo bool responsável pela a atividade do investimento.', verbose_name='Foi retirado')), + ('withdrawn_created_at', models.DateTimeField(blank=True, help_text='Data em que a retirada do investimento foi registrada.', null=True, verbose_name='Data da retirada')), + ('created_at', models.DateTimeField(default=django.utils.timezone.now, help_text='Data de criação do investimento (pode ser data passada).', verbose_name='Criado em')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Atualizado em')), + ('investor', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='investors.investor', verbose_name='Investidor')), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/code/investiments/migrations/__init__.py b/code/investiments/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/code/investiments/models.py b/code/investiments/models.py new file mode 100644 index 000000000..f333637bc --- /dev/null +++ b/code/investiments/models.py @@ -0,0 +1,58 @@ +import uuid + +from decimal import Decimal +from django.db import models +from django.core.validators import MinValueValidator +from django.utils import timezone +from investors.models import Investor + + +class Investiment(models.Model): + id = models.UUIDField( + primary_key=True, + default=uuid.uuid4, + editable=False, + ) + value = models.DecimalField( + max_digits=10, + decimal_places=2, + validators=[ + MinValueValidator( # Invariante Estrutural + Decimal('0.01'), + message='Valor Mínimo = R$ 0,01', + ) + ], + verbose_name='Valor', + help_text='Valor Inicial do investimento.', + ) + investor = models.ForeignKey( + Investor, + on_delete=models.PROTECT, + verbose_name='Investidor', + ) + was_withdrawn = models.BooleanField( + default=False, + verbose_name='Foi retirado', + help_text='Campo bool responsável pela a atividade do investimento.', + ) + withdrawn_created_at = models.DateTimeField( + null=True, + blank=True, + verbose_name='Data da retirada', + help_text='Data em que a retirada do investimento foi registrada.', + ) + created_at = models.DateTimeField( + default=timezone.now, + verbose_name='Criado em', + help_text='Data de criação do investimento (pode ser data passada).', + ) + updated_at = models.DateTimeField( + auto_now=True, + verbose_name='Atualizado em', + ) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return str(self.id) diff --git a/code/investiments/views.py b/code/investiments/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/code/investiments/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/code/investors/__init__.py b/code/investors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/code/investors/admin.py b/code/investors/admin.py new file mode 100644 index 000000000..17bac5a45 --- /dev/null +++ b/code/investors/admin.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from .models import Investor + + +class InvestorAdmin(admin.ModelAdmin): + list_display = [ + 'user', 'name', 'cpf' + ] + search_fields = [ + 'cpf', + ] + + +admin.site.register(Investor, InvestorAdmin) diff --git a/code/investors/apps.py b/code/investors/apps.py new file mode 100644 index 000000000..e7949b168 --- /dev/null +++ b/code/investors/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class InvestorsConfig(AppConfig): + name = 'investors' diff --git a/code/investors/migrations/0001_initial.py b/code/investors/migrations/0001_initial.py new file mode 100644 index 000000000..6b867029c --- /dev/null +++ b/code/investors/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 6.0 on 2025-12-22 16:52 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Investor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=150, verbose_name='Nome')), + ('cpf', models.CharField(max_length=15, verbose_name='CPF')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='investors', to=settings.AUTH_USER_MODEL, verbose_name='Investidores')), + ], + ), + ] diff --git a/code/investors/migrations/__init__.py b/code/investors/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/code/investors/models.py b/code/investors/models.py new file mode 100644 index 000000000..f1b76a7c0 --- /dev/null +++ b/code/investors/models.py @@ -0,0 +1,22 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Investor(models.Model): + user = models.OneToOneField( + User, + on_delete=models.PROTECT, + related_name='investors', + verbose_name='Investidores', + ) + name = models.CharField( + max_length=150, + verbose_name='Nome', + ) + cpf = models.CharField( + max_length=15, + verbose_name='CPF', + ) + + def __str__(self): + return str(self.pk) From 8a19a22d7dd1fb28dc3bd21134d088e527723a4d Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Mon, 22 Dec 2025 18:55:37 -0300 Subject: [PATCH 05/27] =?UTF-8?q?feat:=20l=C3=B3gica=20business?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- code/business/__init__.py | 0 code/business/investiments.py | 114 ++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 code/business/__init__.py create mode 100644 code/business/investiments.py diff --git a/code/business/__init__.py b/code/business/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/code/business/investiments.py b/code/business/investiments.py new file mode 100644 index 000000000..43b79abe7 --- /dev/null +++ b/code/business/investiments.py @@ -0,0 +1,114 @@ +import datetime +from decimal import ROUND_HALF_EVEN, Decimal +from typing import Optional + +from django.utils import timezone + + +class InvestimentBusiness: + MONTHLY_RATE = Decimal('0.0052') + + def __init__( + self, + investiment_value: Decimal, + investiment_created_at: datetime.datetime, + withdrawn_created_at: Optional[datetime.datetime] = None, + ): + self.investiment_value = investiment_value + self.investiment_created_at = investiment_created_at + self.withdrawn_created_at = withdrawn_created_at + + def calculate_amount(self) -> Decimal: + """ + Calculo do Montante de um investimento aberto(data atual). + """ + initial_date = self.investiment_created_at.date() + today = timezone.localdate() + + months = self._full_months_between(initial_date, today) + amount = ( + self.investiment_value + * ( # Juros composto + Decimal('1') + self.MONTHLY_RATE + ) + ** months + ) + + return amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) + + def calculate_gains(self) -> Decimal: + """ + Cálculo dos ganhos (montante - investido) em um investimento + aberto(Data atual). + """ + amount = self.calculate_amount() + gains = amount - self.investiment_value + + return gains.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) + + def calculate_amount_withdrawn(self) -> Decimal: + """ + Saldo Montante em um investimento fechado. + """ + initial_date = self.investiment_created_at.date() + withdrawn_date = self.withdrawn_created_at.date() + + months = self._full_months_between(initial_date, withdrawn_date) + + amount = ( + self.investiment_value + * (Decimal('1') + self.MONTHLY_RATE) ** months + ) + + return amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) + + def calculate_gains_withdrawn(self) -> Decimal: + """Calculo de Ganho em um investimento Fechado.""" + amount = self.calculate_amount_withdrawn() + gains = amount - self.investiment_value + + return gains.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) + + def calculate_net_amount_withdrawn(self): + """ + Calculo do montante para saque de um investimento fechado + (A tributar os ganhos). + """ + amount = self.calculate_amount_withdrawn() + gains = amount - self.investiment_value + initial_date = self.investiment_created_at.date() + withdrawn_date = self.withdrawn_created_at.date() + + TAX_UNDER_12 = Decimal('0.225') + TAX_12_TO_24 = Decimal('0.185') + TAX_OVER_24 = Decimal('0.15') + + months = self._full_months_between(initial_date, withdrawn_date) + + if months < 12: + gains_tax = gains * TAX_UNDER_12 + elif months > 24: + gains_tax = gains * TAX_OVER_24 + else: + gains_tax = gains * TAX_12_TO_24 + + return (amount - gains_tax).quantize( + Decimal('0.01'), rounding=ROUND_HALF_EVEN + ) # saldo montante(amount) limpo para retorno. + + def calculate_net_gains_withdrawn(self) -> Decimal: + """ + Calculo dos Ganhos de um investimento fechado e tributado. + """ + amount = self.calculate_net_amount_withdrawn() + initial_investiment = self.investiment_value + + return (amount - initial_investiment).quantize( + Decimal('0.01'), rounding=ROUND_HALF_EVEN + ) + + def _full_months_between(self, start, end): + months = (end.year - start.year) * 12 + (end.month - start.month) + if end.day < start.day: + months -= 1 + return max(months, 0) From e8ce46d6c436a94fdb75fd8ccef6cd4b12e656e5 Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Mon, 22 Dec 2025 19:11:15 -0300 Subject: [PATCH 06/27] Feat:docstring no business --- code/business/investiments.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/code/business/investiments.py b/code/business/investiments.py index 43b79abe7..8022a5bec 100644 --- a/code/business/investiments.py +++ b/code/business/investiments.py @@ -6,6 +6,16 @@ class InvestimentBusiness: + """Lógica de Negócio do Investimento + + O investimento renderá 0,52% todos os meses, + no mesmo dia em que for realizado. + Dado que o ganho é pago mensalmente, deve ser tratado como ganho composto, + o que significa que a cada novo período (mês) + o valor ganho passará a fazer parte do saldo do investimento para + o próximo pagamento. + + """ MONTHLY_RATE = Decimal('0.0052') def __init__( @@ -73,6 +83,10 @@ def calculate_net_amount_withdrawn(self): """ Calculo do montante para saque de um investimento fechado (A tributar os ganhos). + + Se tiver menos de um ano, a percentagem será de **22,5%** (imposto = 45,00). + Se tiver entre um e dois anos, a percentagem será de **18,5%** (imposto = 37,00). + Se tiver mais de dois anos, a percentagem será de **15%** (imposto = 30,00). """ amount = self.calculate_amount_withdrawn() gains = amount - self.investiment_value @@ -108,6 +122,18 @@ def calculate_net_gains_withdrawn(self) -> Decimal: ) def _full_months_between(self, start, end): + ''' + Calcula a quantidade de meses **inteiros** entre duas datas. + + Um mês só é contabilizado se o período completar um ciclo mensal cheio, + isto é, o dia do mês em `end` deve ser maior ou = dia em `start`. + + Regra de negócio: + - Meses parciais não são considerados. + - A contagem só avança quando o mês seguinte é completado. + + exemplo: 12/09 -> 11/10 = 0 meses; + ''' months = (end.year - start.year) * 12 + (end.month - start.month) if end.day < start.day: months -= 1 From 1d04b55532ba5227d65dca3d3dcb32c4f0ceea78 Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Mon, 22 Dec 2025 19:12:53 -0300 Subject: [PATCH 07/27] Feat: docstring no business --- code/business/investiments.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/code/business/investiments.py b/code/business/investiments.py index 8022a5bec..8ca891f97 100644 --- a/code/business/investiments.py +++ b/code/business/investiments.py @@ -6,7 +6,7 @@ class InvestimentBusiness: - """Lógica de Negócio do Investimento + '''Lógica de Negócio do Investimento O investimento renderá 0,52% todos os meses, no mesmo dia em que for realizado. @@ -15,7 +15,7 @@ class InvestimentBusiness: o valor ganho passará a fazer parte do saldo do investimento para o próximo pagamento. - """ + ''' MONTHLY_RATE = Decimal('0.0052') def __init__( @@ -29,9 +29,9 @@ def __init__( self.withdrawn_created_at = withdrawn_created_at def calculate_amount(self) -> Decimal: - """ + ''' Calculo do Montante de um investimento aberto(data atual). - """ + ''' initial_date = self.investiment_created_at.date() today = timezone.localdate() @@ -47,19 +47,19 @@ def calculate_amount(self) -> Decimal: return amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) def calculate_gains(self) -> Decimal: - """ + ''' Cálculo dos ganhos (montante - investido) em um investimento aberto(Data atual). - """ + ''' amount = self.calculate_amount() gains = amount - self.investiment_value return gains.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) def calculate_amount_withdrawn(self) -> Decimal: - """ + ''' Saldo Montante em um investimento fechado. - """ + ''' initial_date = self.investiment_created_at.date() withdrawn_date = self.withdrawn_created_at.date() @@ -73,21 +73,21 @@ def calculate_amount_withdrawn(self) -> Decimal: return amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) def calculate_gains_withdrawn(self) -> Decimal: - """Calculo de Ganho em um investimento Fechado.""" + '''Calculo de Ganho em um investimento Fechado.''' amount = self.calculate_amount_withdrawn() gains = amount - self.investiment_value return gains.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) def calculate_net_amount_withdrawn(self): - """ + ''' Calculo do montante para saque de um investimento fechado (A tributar os ganhos). Se tiver menos de um ano, a percentagem será de **22,5%** (imposto = 45,00). Se tiver entre um e dois anos, a percentagem será de **18,5%** (imposto = 37,00). Se tiver mais de dois anos, a percentagem será de **15%** (imposto = 30,00). - """ + ''' amount = self.calculate_amount_withdrawn() gains = amount - self.investiment_value initial_date = self.investiment_created_at.date() @@ -111,9 +111,9 @@ def calculate_net_amount_withdrawn(self): ) # saldo montante(amount) limpo para retorno. def calculate_net_gains_withdrawn(self) -> Decimal: - """ + ''' Calculo dos Ganhos de um investimento fechado e tributado. - """ + ''' amount = self.calculate_net_amount_withdrawn() initial_investiment = self.investiment_value From 769b03a7298505cd9a42ba3569777ec18ef0317f Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Mon, 22 Dec 2025 19:36:03 -0300 Subject: [PATCH 08/27] Feat: ListCreateView --- code/investiments/views.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/code/investiments/views.py b/code/investiments/views.py index 91ea44a21..558045ca6 100644 --- a/code/investiments/views.py +++ b/code/investiments/views.py @@ -1,3 +1,29 @@ -from django.shortcuts import render +from rest_framework.generics import ( + ListCreateAPIView, + RetrieveAPIView, + UpdateAPIView, +) -# Create your views here. +from .models import Investiment + + +class InvestimentListCreateApiView(ListCreateAPIView): + queryset = Investiment.objects.all().only( + 'id', + 'created_at', + 'investor', + 'value', + ) + + def get_serializer_class(self): + if self.request.method == 'GET': + return None + return None + + +class InvestimentRetrieveApiView(RetrieveAPIView): + pass + + +class InvestimentWithdrawnUpdateApiView(UpdateAPIView): + pass From 3dce97556ccca834cf43dda8d036b303608c6427 Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Mon, 22 Dec 2025 19:42:26 -0300 Subject: [PATCH 09/27] feat: url investiment-list-create --- code/investiments/urls.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 code/investiments/urls.py diff --git a/code/investiments/urls.py b/code/investiments/urls.py new file mode 100644 index 000000000..af36d07cb --- /dev/null +++ b/code/investiments/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from .views import InvestimentListCreateApiView + +urlpatterns = [ + path( + 'api/v1/investiments/', + InvestimentListCreateApiView.as_view(), + name='investiment-list-create', + ), +] From 93555f1af676f68a810a1fff07a4c92aaeb1997f Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Mon, 22 Dec 2025 20:05:35 -0300 Subject: [PATCH 10/27] fix: queryset listcreateview --- code/investiments/serializers.py | 0 code/investiments/views.py | 7 +------ 2 files changed, 1 insertion(+), 6 deletions(-) create mode 100644 code/investiments/serializers.py diff --git a/code/investiments/serializers.py b/code/investiments/serializers.py new file mode 100644 index 000000000..e69de29bb diff --git a/code/investiments/views.py b/code/investiments/views.py index 558045ca6..febcb7699 100644 --- a/code/investiments/views.py +++ b/code/investiments/views.py @@ -8,12 +8,7 @@ class InvestimentListCreateApiView(ListCreateAPIView): - queryset = Investiment.objects.all().only( - 'id', - 'created_at', - 'investor', - 'value', - ) + queryset = Investiment.objects.all() def get_serializer_class(self): if self.request.method == 'GET': From 6f72a5d8c32d60c68884d6404ebdff9bab0e5666 Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 15:02:40 -0300 Subject: [PATCH 11/27] Crud De Investidores(tabela auxiliar) --- code/investors/serializers.py | 8 ++++++++ code/investors/urls.py | 18 ++++++++++++++++++ code/investors/views.py | 17 +++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 code/investors/serializers.py create mode 100644 code/investors/urls.py create mode 100644 code/investors/views.py diff --git a/code/investors/serializers.py b/code/investors/serializers.py new file mode 100644 index 000000000..aa11aa90c --- /dev/null +++ b/code/investors/serializers.py @@ -0,0 +1,8 @@ +from rest_framework.serializers import ModelSerializer +from .models import Investor + + +class InvestorSerializer(ModelSerializer): + class Meta: + model = Investor + fields = '__all__' diff --git a/code/investors/urls.py b/code/investors/urls.py new file mode 100644 index 000000000..1bdd28e4b --- /dev/null +++ b/code/investors/urls.py @@ -0,0 +1,18 @@ +from django.urls import path +from .views import ( + InvestorListCreateApiView, + InvestorRetrieveUpdateDestroyApiView, +) + +urlpatterns = [ + path( + 'api/v1/investors/', + InvestorListCreateApiView.as_view(), + name='investor-list-create', + ), + path( + 'api/v1/investors//', + InvestorRetrieveUpdateDestroyApiView.as_view(), + name='investor-detail-update-delete' + ), +] diff --git a/code/investors/views.py b/code/investors/views.py new file mode 100644 index 000000000..538e3ff76 --- /dev/null +++ b/code/investors/views.py @@ -0,0 +1,17 @@ +from rest_framework.generics import ( + ListCreateAPIView, + RetrieveUpdateDestroyAPIView, +) + +from .models import Investor +from .serializers import InvestorSerializer + + +class InvestorListCreateApiView(ListCreateAPIView): + queryset = Investor.objects.all() + serializer_class = InvestorSerializer + + +class InvestorRetrieveUpdateDestroyApiView(RetrieveUpdateDestroyAPIView): + queryset = Investor.objects.all() + serializer_class = InvestorSerializer From 9178c75bc438ce7c284014891e0e2fc28aa17e7b Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 16:22:31 -0300 Subject: [PATCH 12/27] Feat: ListCreate Investiment; alinhando urls --- code/business/investiments.py | 11 +++-- code/core/settings.py | 2 +- code/core/urls.py | 15 ++++++- code/investiments/serializers.py | 75 ++++++++++++++++++++++++++++++++ code/investiments/views.py | 8 +++- 5 files changed, 103 insertions(+), 8 deletions(-) diff --git a/code/business/investiments.py b/code/business/investiments.py index 8ca891f97..cd497bf66 100644 --- a/code/business/investiments.py +++ b/code/business/investiments.py @@ -84,9 +84,14 @@ def calculate_net_amount_withdrawn(self): Calculo do montante para saque de um investimento fechado (A tributar os ganhos). - Se tiver menos de um ano, a percentagem será de **22,5%** (imposto = 45,00). - Se tiver entre um e dois anos, a percentagem será de **18,5%** (imposto = 37,00). - Se tiver mais de dois anos, a percentagem será de **15%** (imposto = 30,00). + Se tiver menos de um ano, a percentagem será de **22,5%** + (imposto = 45,00). + + Se tiver entre um e dois anos, a percentagem será de **18,5%** + (imposto = 37,00). + + Se tiver mais de dois anos, a percentagem será de **15%** + (imposto = 30,00). ''' amount = self.calculate_amount_withdrawn() gains = amount - self.investiment_value diff --git a/code/core/settings.py b/code/core/settings.py index 91889d6d0..d88e79789 100644 --- a/code/core/settings.py +++ b/code/core/settings.py @@ -33,7 +33,7 @@ 'rest_framework', # apps 'investiments', - 'investor', + 'investors', ] MIDDLEWARE = [ diff --git a/code/core/urls.py b/code/core/urls.py index 361b2e516..f873f02e9 100644 --- a/code/core/urls.py +++ b/code/core/urls.py @@ -15,14 +15,25 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include from users.views import UserListCreateView urlpatterns = [ - path('admin/', admin.site.urls), + path( + 'admin/', + admin.site.urls + ), path( 'api/v1/users/register/', UserListCreateView.as_view(), name='user-create-list', ), + path( + '', + include('investors.urls'), + ), + path( + '', + include('investiments.urls'), + ), ] diff --git a/code/investiments/serializers.py b/code/investiments/serializers.py index e69de29bb..e88a24b6e 100644 --- a/code/investiments/serializers.py +++ b/code/investiments/serializers.py @@ -0,0 +1,75 @@ +from rest_framework import serializers +from django.utils import timezone + +from business.investiments import InvestimentBusiness +from .models import Investiment + + +class InvestimentListSerializer(serializers.ModelSerializer): + balance_amount = serializers.SerializerMethodField() + gains = serializers.SerializerMethodField() + + class Meta: + model = Investiment + fields = [ + 'id', + 'investor', + 'value', + 'gains', + 'created_at', + 'balance_amount', + 'was_withdrawn', + 'withdrawn_created_at', + 'updated_at', + ] + + def _business(self, obj): + ''' + Reaproveita o código Classe das regras de negócio para atribuir + no objeto em instância -> Investimento. + ''' + if not hasattr(obj, '_business_cache'): + obj._business_cache = InvestimentBusiness( + investiment_value=obj.value, + investiment_created_at=obj.created_at, + withdrawn_created_at=obj.withdrawn_created_at, + ) + return obj._business_cache + + def get_gains(self, obj): + business = self._business(obj) + + if obj.withdrawn_created_at: + return business.calculate_gains_withdrawn() + + return business.calculate_gains() + + def get_balance_amount(self, obj): + business = self._business(obj) + + if obj.withdrawn_created_at: + return business.calculate_amount_withdrawn() + + return business.calculate_amount() + + +class InvestimentCreateSerializer(serializers.ModelSerializer): + class Meta: + model = Investiment + fields = [ + 'id', + 'created_at', + 'investor', + 'value', + ] + read_only_fields = [ + 'id', + ] + extra_kwargs = { + 'value': {'write_only': True} + } + + def validate_created_at(self, value): + if value > timezone.now(): + raise serializers.ValidationError({'Data futura não é permitida.'}) + return value diff --git a/code/investiments/views.py b/code/investiments/views.py index febcb7699..78b888e8d 100644 --- a/code/investiments/views.py +++ b/code/investiments/views.py @@ -5,6 +5,10 @@ ) from .models import Investiment +from . serializers import ( + InvestimentListSerializer, + InvestimentCreateSerializer, +) class InvestimentListCreateApiView(ListCreateAPIView): @@ -12,8 +16,8 @@ class InvestimentListCreateApiView(ListCreateAPIView): def get_serializer_class(self): if self.request.method == 'GET': - return None - return None + return InvestimentListSerializer + return InvestimentCreateSerializer class InvestimentRetrieveApiView(RetrieveAPIView): From 4b5ecfc9e2dc2df5b33d3bb3e9e7eb0828a92982 Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 19:23:44 -0300 Subject: [PATCH 13/27] Feat: Investiment UpdateDeleteDetail e View de Saque --- code/investiments/serializers.py | 61 ++++++++++++++++++++++++++++++++ code/investiments/urls.py | 16 ++++++++- code/investiments/views.py | 7 ++-- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/code/investiments/serializers.py b/code/investiments/serializers.py index e88a24b6e..446abad82 100644 --- a/code/investiments/serializers.py +++ b/code/investiments/serializers.py @@ -73,3 +73,64 @@ def validate_created_at(self, value): if value > timezone.now(): raise serializers.ValidationError({'Data futura não é permitida.'}) return value + + +class InvestimentWithdrawnSerializer(serializers.ModelSerializer): + withdrawal_amount = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = Investiment + fields = [ + 'id', + 'created_at', + 'was_withdrawn', + 'withdrawal_amount', + 'withdrawn_created_at', + ] + read_only_fields = [ + 'id', + 'created_at', + 'was_withdrawn', + 'withdrawal_amount', + ] + + def get_withdrawal_amount(self, obj): + investiment = InvestimentBusiness( + obj.value, + obj.created_at, + obj.withdrawn_created_at, + ) + return investiment.calculate_net_amount_withdrawn() + + def validate(self, attrs): + withdrawn_date = attrs.get('withdrawn_created_at') + now = timezone.now() + + if self.instance.was_withdrawn: + raise serializers.ValidationError( + 'Este investimento já foi resgatado.' + ) + + if withdrawn_date: + if withdrawn_date > now: + raise serializers.ValidationError({ + 'withdrawn_created_at': 'Data Futura não permitida.' + }) + + if withdrawn_date < self.instance.created_at: + raise serializers.ValidationError({ + 'withdrawn_created_at': + 'Data não pode ser anterior à criação do investimento.' + }) + + return attrs + + def update(self, instance, validated_data): + now = timezone.now() + withdrawn_date = validated_data.get('withdrawn_created_at') + + instance.withdrawn_created_at = withdrawn_date or now + + instance.was_withdrawn = True + instance.save() + return instance diff --git a/code/investiments/urls.py b/code/investiments/urls.py index af36d07cb..39d6c6028 100644 --- a/code/investiments/urls.py +++ b/code/investiments/urls.py @@ -1,5 +1,9 @@ from django.urls import path -from .views import InvestimentListCreateApiView +from .views import ( + InvestimentListCreateApiView, + InvestimentRetrieveApiView, + InvestimentWithdrawnUpdateApiView +) urlpatterns = [ path( @@ -7,4 +11,14 @@ InvestimentListCreateApiView.as_view(), name='investiment-list-create', ), + path( + 'api/v1/investiments//', + InvestimentRetrieveApiView.as_view(), + name='investiment-detail-update-delete', + ), + path( + 'api/v1/investiments//withdrawn/', + InvestimentWithdrawnUpdateApiView.as_view(), + name='investiment-withdrawn-update', + ), ] diff --git a/code/investiments/views.py b/code/investiments/views.py index 78b888e8d..47b7f5043 100644 --- a/code/investiments/views.py +++ b/code/investiments/views.py @@ -8,6 +8,7 @@ from . serializers import ( InvestimentListSerializer, InvestimentCreateSerializer, + InvestimentWithdrawnSerializer, ) @@ -21,8 +22,10 @@ def get_serializer_class(self): class InvestimentRetrieveApiView(RetrieveAPIView): - pass + queryset = Investiment.objects.all() + serializer_class = InvestimentListSerializer class InvestimentWithdrawnUpdateApiView(UpdateAPIView): - pass + queryset = Investiment.objects.all().defer('investor') + serializer_class = InvestimentWithdrawnSerializer From 10a1f301cc5f6c96fbd39f29ea97915827db8183 Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 19:30:19 -0300 Subject: [PATCH 14/27] Fix: defer incorreto --- code/investiments/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/investiments/views.py b/code/investiments/views.py index 47b7f5043..4945f6911 100644 --- a/code/investiments/views.py +++ b/code/investiments/views.py @@ -27,5 +27,5 @@ class InvestimentRetrieveApiView(RetrieveAPIView): class InvestimentWithdrawnUpdateApiView(UpdateAPIView): - queryset = Investiment.objects.all().defer('investor') + queryset = Investiment.objects.all() serializer_class = InvestimentWithdrawnSerializer From ca4eecd321c97b3599d26c279b22fb4a1d70b481 Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 20:14:50 -0300 Subject: [PATCH 15/27] =?UTF-8?q?Testes=20Regra=20de=20neg=C3=B3cio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- code/business/tests/__init__.py | 0 code/business/tests/test_investiments.py | 82 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 code/business/tests/__init__.py create mode 100644 code/business/tests/test_investiments.py diff --git a/code/business/tests/__init__.py b/code/business/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/code/business/tests/test_investiments.py b/code/business/tests/test_investiments.py new file mode 100644 index 000000000..954dd9a96 --- /dev/null +++ b/code/business/tests/test_investiments.py @@ -0,0 +1,82 @@ +from decimal import Decimal +from django.utils import timezone +from datetime import datetime +from django.test import TestCase +from business.investiments import InvestimentBusiness + + +class InvestimentBusinessOpenTest(TestCase): + def setUp(self): + self.investiment = make_investiment( + value=Decimal('1000'), + created_at=timezone.make_aware( + datetime(2025, 11, 13, 0, 0)), + withdrawn_at=timezone.make_aware( + datetime(2025, 12, 12, 0, 0)) + ) + + def test_calculate_amount_is_equal(self): + self.assertEqual( + self.investiment.calculate_amount(), Decimal('1005.20') + ) + + def test_calculate_gains_is_equal(self): + self.assertEqual( + self.investiment.calculate_gains(), Decimal('5.20') + ) + + +class InvestimentBusinessWithdrawnTest(TestCase): + def setUp(self): + self.investiment = make_investiment( + value=Decimal('1000'), + created_at=timezone.make_aware( + datetime(2025, 11, 13, 0, 0)), + withdrawn_at=timezone.make_aware( + datetime(2025, 12, 12, 0, 0)) + ) + + def test_calculate_amount_withdrawn_is_equal(self): + self.assertEqual( + self.investiment.calculate_amount_withdrawn(), Decimal('1000.00') + ) + + def test_calculate_gains_withdrawn_is_equal(self): + self.assertEqual( + self.investiment.calculate_gains_withdrawn(), Decimal('0') + ) + + +class InvestimentBusinessNetWithdrawnTest(TestCase): + def setUp(self): + self.investiment = make_investiment( + value=Decimal('1000'), + created_at=timezone.make_aware( + datetime(2025, 11, 13, 0, 0)), + withdrawn_at=timezone.make_aware( + datetime(2025, 12, 12, 0, 0)) + ) + + def test_calculate_net_amount_withdrawn_is_equal(self): + self.assertEqual( + self.investiment.calculate_net_amount_withdrawn(), Decimal('1000') + ) + + def test_calculate_net_gains_withdrawn_is_equal(self): + self.assertEqual( + self.investiment.calculate_net_gains_withdrawn(), Decimal('0') + ) + + +def make_investiment( + *, + value="1000.00", + created_at, + withdrawn_at=None +): + '''Top-Level Construtora da classe de testes''' + return InvestimentBusiness( + investiment_value=Decimal(value), + investiment_created_at=created_at, + withdrawn_created_at=withdrawn_at + ) \ No newline at end of file From 109f49f43e82412b24fc088001528aaec75b68cc Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 20:31:20 -0300 Subject: [PATCH 16/27] Feat: Servico de envio Smtp --- code/services/__init__.py | 0 code/services/investiments.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 code/services/__init__.py create mode 100644 code/services/investiments.py diff --git a/code/services/__init__.py b/code/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/code/services/investiments.py b/code/services/investiments.py new file mode 100644 index 000000000..64b23e585 --- /dev/null +++ b/code/services/investiments.py @@ -0,0 +1,30 @@ +import os +from django.core.mail import EmailMessage + + +class InvestimentService(): + + def send_created_investiment_email(self, data): + email = self._investiment_created_build(data=data) + email.send() + + def _investiment_created_build(self, data): + EMAILTO = os.getenv('EMAILTO') + + subject = f"Investimento nº{data.get('investiment')} criado :)" + + body = ( + f"Valor do investimento: {data.get('value')}\n" + f"Investidor: {data.get('investor')}\n" + f"Data: { + data.get('timestamp_created_at') + }" + ) + + email = EmailMessage( + subject=subject, + body=body, + to=[EMAILTO], + ) + + return email From f9829b11759ac68a4a9ff5d2c315e2b20ad9199f Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 20:35:06 -0300 Subject: [PATCH 17/27] COnfig Emai --- code/core/settings.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/code/core/settings.py b/code/core/settings.py index d88e79789..36c1ac397 100644 --- a/code/core/settings.py +++ b/code/core/settings.py @@ -112,3 +112,15 @@ # https://docs.djangoproject.com/en/6.0/howto/static-files/ STATIC_URL = 'static/' + + +if DEBUG: + EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +else: + EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + +EMAIL_HOST = "smtp.gmail.com" +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_HOST_USER = os.getenv("EMAIL_USER") +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PASSWORD") \ No newline at end of file From 1a7526fd9f65d56d4b03e64be8d9b81f6705464f Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 20:36:07 -0300 Subject: [PATCH 18/27] Feat: Pagination --- code/core/settings.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/code/core/settings.py b/code/core/settings.py index 36c1ac397..108efb38f 100644 --- a/code/core/settings.py +++ b/code/core/settings.py @@ -123,4 +123,12 @@ EMAIL_PORT = 587 EMAIL_USE_TLS = True EMAIL_HOST_USER = os.getenv("EMAIL_USER") -EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PASSWORD") \ No newline at end of file +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PASSWORD") + + +# Pag10 +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10, +} +# limit=100&offset=400 \ No newline at end of file From dc58343a5581fe7df12a21b640998e990f3a403c Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 20:38:02 -0300 Subject: [PATCH 19/27] Feat: email Signal config --- code/investiments/apps.py | 3 +++ code/investiments/signals.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 code/investiments/signals.py diff --git a/code/investiments/apps.py b/code/investiments/apps.py index b3c1b763e..1bb485386 100644 --- a/code/investiments/apps.py +++ b/code/investiments/apps.py @@ -3,3 +3,6 @@ class InvestimentsConfig(AppConfig): name = 'investiments' + + def ready(self): + import investiments.signals \ No newline at end of file diff --git a/code/investiments/signals.py b/code/investiments/signals.py new file mode 100644 index 000000000..58879c9de --- /dev/null +++ b/code/investiments/signals.py @@ -0,0 +1,32 @@ +import logging + +from django.dispatch import receiver +from django.db.models.signals import post_save +from django.utils import timezone +from .models import Investiment +from services.investiments import InvestimentService + +logger = logging.getLogger(__name__) +investiment_service = InvestimentService() + + +@receiver(post_save, sender=Investiment) +def send_investiment_created_event(sender, instance, created, **kwargs): + try: + if created: + data = dict( + event_type='create-investiment', + investiment_id=instance.id, + value=instance.value, + investor=instance.investor, + timestamp_created_at=timezone.localtime( + instance.created_at + ).strftime("%Y/%m/%d, %H:%M:%S"), + ) + + investiment_service.send_created_investiment_email(data=data) + logger.info(f'enviando Ok! {data.get('timestamp_created_at')}') + + except Exception as e: + logger.error(f"[ERRO SIGNAL Investiment-create] {e}") + pass From 50773c79078893d51bf1a6d6c74625ad151c65a4 Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 20:38:56 -0300 Subject: [PATCH 20/27] Feat: Log basico --- code/core/settings.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/code/core/settings.py b/code/core/settings.py index 108efb38f..60b3e1603 100644 --- a/code/core/settings.py +++ b/code/core/settings.py @@ -131,4 +131,22 @@ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 10, } -# limit=100&offset=400 \ No newline at end of file +# limit=100&offset=400 + + +# logging +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, +} From 33f678e016dbfc04cef627a6a01008e100c0c18c Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 21:44:07 -0300 Subject: [PATCH 21/27] Feat: Docs Api --- code/docs/index.md | 348 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) diff --git a/code/docs/index.md b/code/docs/index.md index 0417a2326..d18473771 100644 --- a/code/docs/index.md +++ b/code/docs/index.md @@ -1 +1,349 @@ # API de Investimentos + +Esta documentação descreve a API de **Investimentos**, responsável por criar investimentos, calcular ganhos e realizar o resgate (withdrawal) de forma segura e consistente. + + +A API foi desenhada seguindo boas práticas de **Django Rest Framework**, separando claramente: + +- **Model** → persistência e estado +- **Serializer** → validação e contrato da API +- **Business (Service Layer)** → regras de negócio e cálculos financeiros +- **Views** → orquestração HTTP +- **Services** → Conexões externas +- **Signals** → Gatilhos +- **Business Tests** → Teste da Camada de negócio. + +--- + +## 📌 Princípios adotados + +- Cálculos financeiros feitos **exclusivamente com `Decimal`** +- Arredondamento explícito (`ROUND_HALF_EVEN`) +- Uso de **timezone-aware datetimes** (`django.utils.timezone`) +- Regra de negócio fora do serializer (Service Layer) +- Serializers responsáveis apenas por: + - validação + - transformação de dados +- Endpoints idempotentes e previsíveis + +--- + +## 🧩 Modelo: Investiment + +Representa um investimento financeiro único. + +### Campos principais + +| Campo | Tipo | Descrição | +|------|------|-----------| +| `id` | UUID | Identificador único do investimento | +| `value` | Decimal | Valor inicial investido (mínimo R$ 0,01) | +| `investor` | FK (`Investor`) | Investidor associado ao investimento | +| `was_withdrawn` | Boolean | Indica se o investimento já foi resgatado | +| `withdrawn_created_at` | DateTime (nullable) | Data em que o resgate do investimento foi registrado | +| `created_at` | DateTime | Data de criação do investimento (pode ser passada) | +| `updated_at` | DateTime | Data da última atualização do registro | + +--- + +## 💼 Modelo: Investor + +Representa um investidor registrado no sistema. Cada investidor está associado a um usuário do Django e possui CPF para identificação. + +### Campos principais + +| Campo | Tipo | Descrição | +|-------|------|-----------| +| `id` | ID | Identificador único do investidor | +| `user` | OneToOneField (User) | Usuário do Django associado ao investidor. Garante relação 1:1 e protege contra deleção acidental. | +| `name` | CharField | Nome completo do investidor | +| `cpf` | CharField | CPF do investidor, utilizado para identificação fiscal no Brasil | + +--- + +## 🧠 Camada de Negócio (Service Layer) + +Classe responsável **exclusivamente** pelos cálculos financeiros: + +- Juros compostos mensais +- Saldo esperado +- Ganhos líquidos (com tributação) +- Cálculo baseado em delta de meses + +### Taxa utilizada + +```python +MONTHLY_RATE = Decimal('0.0052') # 0.52% a.m. +``` + +### Métodos disponíveis + +| Método | Descrição | +|------|----------| +| `calculate_amount()` | Montante atual (valor + juros) | +| `calculate_gains()` | Ganhos brutos atuais | +| `calculate_amount_withdrawn()` | Montante bruto no momento do saque | +| `calculate_gains_withdrawn()` | Ganhos brutos no saque | +| `calculate_net_amount_withdrawn()` | Montante líquido após imposto | +| `calculate_net_gains_withdrawn()` | Ganhos líquidos após imposto | + +📌 **Observação:** +Todos os retornos são `Decimal` já arredondados para 2 casas decimais. + +--- +## 🔁 Serializers + +### 📄 InvestimentListSerializer + +Usado no **GET /investiments/** + +Responsável por expor uma visão consolidada do investimento. + +#### Campos retornados + +| Campo | Descrição | +|------|----------| +| `value` | Valor investido | +| `gains` | Ganhos (atuais ou no saque) | +| `balance_amount` | Saldo total esperado | +| `created_at` | Data de criação | +| `was_withdrawn` | Status do investimento | +| `withdrawn_created_at` | Data do saque (se existir) | + +📌 **Regra importante:** +- Se o investimento **não foi sacado**, os valores refletem o estado atual +- Se **já foi sacado**, os valores refletem o estado no momento do saque + +--- + +### ➕ InvestimentCreateSerializer + +Usado no **POST /investiments/** + +#### Validações + +- `created_at` não pode ser uma data futura + +```python +if value > timezone.now(): + raise ValidationError('Data futura não é permitida') +``` + +--- + +### 💸 InvestimentWithdrawnSerializer + +Usado no **PATCH /investiments/{id}/withdrawn/** + +Responsável por realizar o resgate do investimento. + +#### Regras de validação + +- O investimento **não pode** já ter sido resgatado +- A data de saque: + - não pode ser futura + - não pode ser anterior à criação +- Caso nenhuma data seja informada, o sistema utiliza `timezone.now()` + +#### Comportamento do update + +- Define `withdrawn_created_at` +- Marca `was_withdrawn = True` +- Retorna o **valor líquido do saque** + +--- + +## 🌐 Endpoints + +### 🔍 Listar investimentos + +```http +GET /api/v1/investiments/ +``` + +#### Exemplo de resposta + +```json +{ + "id": "uuid", + "value": "1213.00", + "gains": 6.31, + "balance_amount": 1219.31, + "was_withdrawn": false +} +``` + +--- + +### ➕ Criar investimento + +```http +POST /api/v1/investiments/ +``` + +```json +{ + "investor": "uuid", + "value": "1200.00", + "created_at": "2025-11-12T01:00:00-03:00" +} +``` + +--- + +### 💸 Resgatar investimento + +```http +PATCH /api/v1/investiments/{id}/withdrawn/ +``` + +```json +{ + "withdrawn_created_at": "2025-12-16T14:54:44-03:00" +} +``` + +📌 O campo é opcional. Se omitido, o backend usa o horário atual. + +#### Resposta + +```json +{ + "id": "uuid", + "was_withdrawn": true, + "withdrawal_amount": 1217.89, + "withdrawn_created_at": "2025-12-16T14:54:44-03:00" +} +``` + +--- + +## 👤 Usuários & Investidores + +### 🧑‍💻 Usuário + +```http +POST /api/v1/users/register/ +``` +Corpo da requisição +```json +{ + "username": "johndoe", + "email": "john@example.com", + "password": "strong_password_123" +} +``` +Resposta de exemplo: +```json +{ + "id": 1, + "username": "johndoe", + "email": "john@example.com" +} + +``` + +```http +GET /api/v1/users/register/ +``` + +Listar usuários Django + +```json +[ + { + "id": 1, + "username": "johndoe", + "email": "john@example.com" + } +] +``` + +### 🧑💸 Investidor + + +```http +GET /api/v1/investors/ +``` + +```json +[ + { + "id": 1, + "user": 1, + "name": "João Silva" + } +] + +``` + +➕ Criar investidor + +```http +POST /api/v1/investors/ +``` +Corpo da Request +```json +{ + "user": 1, + "name": "João Silva" +} +``` + +Exemplo de Resposta +```json +{ + "id": 1, + "user": 1, + "name": "João Silva" +} +``` +Detalhar um Investidor: +```http +GET /api/v1/investors/{id}/ +``` +```json +{ + "id": 1, + "user": 1, + "name": "João Silva" +} +``` +Deletar um Investidor: +```http +Delete /api/v1/investors/{id}/ +``` +**!Banco de Dados integro por padrão, verficiar On_delete no model** +Obs: Coloquei nessa Api, mas normalmente, se não há necessidade, trabalho sem Deletes, +em tabelas de registro isso é obrigatório, tal motivo usei uma flag em investimentos. 👍 + +--- + +## ⏱️ Datas e Fuso Horário + +- Toda a aplicação utiliza **timezone-aware datetimes** +- O Django gerencia conversões automaticamente +- Cálculos mensais usam apenas a **parte da data** (`date()`), evitando erros de fuso + +--- + +--- +*Modelagem De Dados* Desenvolvida para funcionamento da API +![Modelagem Bd](images/layout_modelagem_investiments_api.png) +*wallet foi uma ideia futura para armazenamento desses saldos investidos. +--- + +## 💡 Considerações finais + +- O backend é a **fonte da verdade** para cálculos financeiros +- O frontend consome valores já tratados e arredondados +- Conversão para `float` no JSON **não afeta os cálculos**, pois eles já foram finalizados + +Este design garante: + +✅ Precisão financeira +✅ Clareza de responsabilidades +✅ Facilidade de manutenção +✅ Segurança nas regras de negócio + +Arquitetura baseada na estrutura do Framework que atende ao requisitado. \ No newline at end of file From e7619e067fdbd47568c473019aba59fe346b1f78 Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 21:48:39 -0300 Subject: [PATCH 22/27] Fix:Docs --- code/docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/docs/index.md b/code/docs/index.md index d18473771..c3b0b6464 100644 --- a/code/docs/index.md +++ b/code/docs/index.md @@ -346,4 +346,4 @@ Este design garante: ✅ Facilidade de manutenção ✅ Segurança nas regras de negócio -Arquitetura baseada na estrutura do Framework que atende ao requisitado. \ No newline at end of file +Arquitetura MVT baseada no atendimento aos requisitos pedidos e a proposta do Framework Django. \ No newline at end of file From f43294223fb6a1203a7a9b4b5fc92a9b8bd8c562 Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 21:55:36 -0300 Subject: [PATCH 23/27] Format: Ruff Format Code --- code/business/investiments.py | 31 ++++++++++---------- code/business/tests/test_investiments.py | 35 +++++++---------------- code/core/settings.py | 10 +++---- code/core/urls.py | 6 ++-- code/investiments/admin.py | 8 ++++-- code/investiments/apps.py | 2 +- code/investiments/serializers.py | 11 +++---- code/investiments/signals.py | 6 ++-- code/investiments/urls.py | 2 +- code/investiments/views.py | 2 +- code/investors/admin.py | 4 +-- code/investors/urls.py | 2 +- code/mkdocs.yml | 2 ++ code/pyproject.toml | 23 +++++++++++++++ code/requirements.txt | Bin 230 -> 998 bytes code/services/investiments.py | 13 ++++----- 16 files changed, 81 insertions(+), 76 deletions(-) create mode 100644 code/mkdocs.yml create mode 100644 code/pyproject.toml diff --git a/code/business/investiments.py b/code/business/investiments.py index cd497bf66..66d6838f4 100644 --- a/code/business/investiments.py +++ b/code/business/investiments.py @@ -6,7 +6,7 @@ class InvestimentBusiness: - '''Lógica de Negócio do Investimento + """Lógica de Negócio do Investimento O investimento renderá 0,52% todos os meses, no mesmo dia em que for realizado. @@ -15,7 +15,8 @@ class InvestimentBusiness: o valor ganho passará a fazer parte do saldo do investimento para o próximo pagamento. - ''' + """ + MONTHLY_RATE = Decimal('0.0052') def __init__( @@ -29,9 +30,9 @@ def __init__( self.withdrawn_created_at = withdrawn_created_at def calculate_amount(self) -> Decimal: - ''' + """ Calculo do Montante de um investimento aberto(data atual). - ''' + """ initial_date = self.investiment_created_at.date() today = timezone.localdate() @@ -47,19 +48,19 @@ def calculate_amount(self) -> Decimal: return amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) def calculate_gains(self) -> Decimal: - ''' + """ Cálculo dos ganhos (montante - investido) em um investimento aberto(Data atual). - ''' + """ amount = self.calculate_amount() gains = amount - self.investiment_value return gains.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) def calculate_amount_withdrawn(self) -> Decimal: - ''' + """ Saldo Montante em um investimento fechado. - ''' + """ initial_date = self.investiment_created_at.date() withdrawn_date = self.withdrawn_created_at.date() @@ -73,14 +74,14 @@ def calculate_amount_withdrawn(self) -> Decimal: return amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) def calculate_gains_withdrawn(self) -> Decimal: - '''Calculo de Ganho em um investimento Fechado.''' + """Calculo de Ganho em um investimento Fechado.""" amount = self.calculate_amount_withdrawn() gains = amount - self.investiment_value return gains.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) def calculate_net_amount_withdrawn(self): - ''' + """ Calculo do montante para saque de um investimento fechado (A tributar os ganhos). @@ -92,7 +93,7 @@ def calculate_net_amount_withdrawn(self): Se tiver mais de dois anos, a percentagem será de **15%** (imposto = 30,00). - ''' + """ amount = self.calculate_amount_withdrawn() gains = amount - self.investiment_value initial_date = self.investiment_created_at.date() @@ -116,9 +117,9 @@ def calculate_net_amount_withdrawn(self): ) # saldo montante(amount) limpo para retorno. def calculate_net_gains_withdrawn(self) -> Decimal: - ''' + """ Calculo dos Ganhos de um investimento fechado e tributado. - ''' + """ amount = self.calculate_net_amount_withdrawn() initial_investiment = self.investiment_value @@ -127,7 +128,7 @@ def calculate_net_gains_withdrawn(self) -> Decimal: ) def _full_months_between(self, start, end): - ''' + """ Calcula a quantidade de meses **inteiros** entre duas datas. Um mês só é contabilizado se o período completar um ciclo mensal cheio, @@ -138,7 +139,7 @@ def _full_months_between(self, start, end): - A contagem só avança quando o mês seguinte é completado. exemplo: 12/09 -> 11/10 = 0 meses; - ''' + """ months = (end.year - start.year) * 12 + (end.month - start.month) if end.day < start.day: months -= 1 diff --git a/code/business/tests/test_investiments.py b/code/business/tests/test_investiments.py index 954dd9a96..c6e85c7cb 100644 --- a/code/business/tests/test_investiments.py +++ b/code/business/tests/test_investiments.py @@ -9,10 +9,8 @@ class InvestimentBusinessOpenTest(TestCase): def setUp(self): self.investiment = make_investiment( value=Decimal('1000'), - created_at=timezone.make_aware( - datetime(2025, 11, 13, 0, 0)), - withdrawn_at=timezone.make_aware( - datetime(2025, 12, 12, 0, 0)) + created_at=timezone.make_aware(datetime(2025, 11, 13, 0, 0)), + withdrawn_at=timezone.make_aware(datetime(2025, 12, 12, 0, 0)), ) def test_calculate_amount_is_equal(self): @@ -21,19 +19,15 @@ def test_calculate_amount_is_equal(self): ) def test_calculate_gains_is_equal(self): - self.assertEqual( - self.investiment.calculate_gains(), Decimal('5.20') - ) + self.assertEqual(self.investiment.calculate_gains(), Decimal('5.20')) class InvestimentBusinessWithdrawnTest(TestCase): def setUp(self): self.investiment = make_investiment( value=Decimal('1000'), - created_at=timezone.make_aware( - datetime(2025, 11, 13, 0, 0)), - withdrawn_at=timezone.make_aware( - datetime(2025, 12, 12, 0, 0)) + created_at=timezone.make_aware(datetime(2025, 11, 13, 0, 0)), + withdrawn_at=timezone.make_aware(datetime(2025, 12, 12, 0, 0)), ) def test_calculate_amount_withdrawn_is_equal(self): @@ -51,10 +45,8 @@ class InvestimentBusinessNetWithdrawnTest(TestCase): def setUp(self): self.investiment = make_investiment( value=Decimal('1000'), - created_at=timezone.make_aware( - datetime(2025, 11, 13, 0, 0)), - withdrawn_at=timezone.make_aware( - datetime(2025, 12, 12, 0, 0)) + created_at=timezone.make_aware(datetime(2025, 11, 13, 0, 0)), + withdrawn_at=timezone.make_aware(datetime(2025, 12, 12, 0, 0)), ) def test_calculate_net_amount_withdrawn_is_equal(self): @@ -68,15 +60,10 @@ def test_calculate_net_gains_withdrawn_is_equal(self): ) -def make_investiment( - *, - value="1000.00", - created_at, - withdrawn_at=None -): - '''Top-Level Construtora da classe de testes''' +def make_investiment(*, value='1000.00', created_at, withdrawn_at=None): + """Top-Level Construtora da classe de testes""" return InvestimentBusiness( investiment_value=Decimal(value), investiment_created_at=created_at, - withdrawn_created_at=withdrawn_at - ) \ No newline at end of file + withdrawn_created_at=withdrawn_at, + ) diff --git a/code/core/settings.py b/code/core/settings.py index 60b3e1603..4d5c9d8ae 100644 --- a/code/core/settings.py +++ b/code/core/settings.py @@ -1,4 +1,4 @@ -import os +import os from pathlib import Path from dotenv import load_dotenv @@ -119,11 +119,11 @@ else: EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = "smtp.gmail.com" +EMAIL_HOST = 'smtp.gmail.com' EMAIL_PORT = 587 EMAIL_USE_TLS = True -EMAIL_HOST_USER = os.getenv("EMAIL_USER") -EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PASSWORD") +EMAIL_HOST_USER = os.getenv('EMAIL_USER') +EMAIL_HOST_PASSWORD = os.getenv('EMAIL_PASSWORD') # Pag10 @@ -138,13 +138,11 @@ LOGGING = { 'version': 1, 'disable_existing_loggers': False, - 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, - 'root': { 'handlers': ['console'], 'level': 'INFO', diff --git a/code/core/urls.py b/code/core/urls.py index f873f02e9..194c1c17f 100644 --- a/code/core/urls.py +++ b/code/core/urls.py @@ -14,15 +14,13 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.contrib import admin from django.urls import path, include from users.views import UserListCreateView urlpatterns = [ - path( - 'admin/', - admin.site.urls - ), + path('admin/', admin.site.urls), path( 'api/v1/users/register/', UserListCreateView.as_view(), diff --git a/code/investiments/admin.py b/code/investiments/admin.py index e7561bd19..3235479d4 100644 --- a/code/investiments/admin.py +++ b/code/investiments/admin.py @@ -4,10 +4,14 @@ class InvestimentAdmin(admin.ModelAdmin): list_display = [ - 'id', 'value', 'investor', 'created_at', + 'id', + 'value', + 'investor', + 'created_at', ] search_fields = [ - 'investor', 'cpf', + 'investor', + 'cpf', ] diff --git a/code/investiments/apps.py b/code/investiments/apps.py index 1bb485386..0c7628e30 100644 --- a/code/investiments/apps.py +++ b/code/investiments/apps.py @@ -5,4 +5,4 @@ class InvestimentsConfig(AppConfig): name = 'investiments' def ready(self): - import investiments.signals \ No newline at end of file + import investiments.signals diff --git a/code/investiments/serializers.py b/code/investiments/serializers.py index 446abad82..d6779cac0 100644 --- a/code/investiments/serializers.py +++ b/code/investiments/serializers.py @@ -24,10 +24,10 @@ class Meta: ] def _business(self, obj): - ''' + """ Reaproveita o código Classe das regras de negócio para atribuir no objeto em instância -> Investimento. - ''' + """ if not hasattr(obj, '_business_cache'): obj._business_cache = InvestimentBusiness( investiment_value=obj.value, @@ -65,9 +65,7 @@ class Meta: read_only_fields = [ 'id', ] - extra_kwargs = { - 'value': {'write_only': True} - } + extra_kwargs = {'value': {'write_only': True}} def validate_created_at(self, value): if value > timezone.now(): @@ -119,8 +117,7 @@ def validate(self, attrs): if withdrawn_date < self.instance.created_at: raise serializers.ValidationError({ - 'withdrawn_created_at': - 'Data não pode ser anterior à criação do investimento.' + 'withdrawn_created_at': 'Data não pode ser anterior à criação do investimento.' }) return attrs diff --git a/code/investiments/signals.py b/code/investiments/signals.py index 58879c9de..f03a217a6 100644 --- a/code/investiments/signals.py +++ b/code/investiments/signals.py @@ -21,12 +21,12 @@ def send_investiment_created_event(sender, instance, created, **kwargs): investor=instance.investor, timestamp_created_at=timezone.localtime( instance.created_at - ).strftime("%Y/%m/%d, %H:%M:%S"), + ).strftime('%Y/%m/%d, %H:%M:%S'), ) investiment_service.send_created_investiment_email(data=data) - logger.info(f'enviando Ok! {data.get('timestamp_created_at')}') + logger.info(f'enviando Ok! {data.get("timestamp_created_at")}') except Exception as e: - logger.error(f"[ERRO SIGNAL Investiment-create] {e}") + logger.error(f'[ERRO SIGNAL Investiment-create] {e}') pass diff --git a/code/investiments/urls.py b/code/investiments/urls.py index 39d6c6028..b363e3b22 100644 --- a/code/investiments/urls.py +++ b/code/investiments/urls.py @@ -2,7 +2,7 @@ from .views import ( InvestimentListCreateApiView, InvestimentRetrieveApiView, - InvestimentWithdrawnUpdateApiView + InvestimentWithdrawnUpdateApiView, ) urlpatterns = [ diff --git a/code/investiments/views.py b/code/investiments/views.py index 4945f6911..017d5fbd6 100644 --- a/code/investiments/views.py +++ b/code/investiments/views.py @@ -5,7 +5,7 @@ ) from .models import Investiment -from . serializers import ( +from .serializers import ( InvestimentListSerializer, InvestimentCreateSerializer, InvestimentWithdrawnSerializer, diff --git a/code/investors/admin.py b/code/investors/admin.py index 17bac5a45..cef9c9f84 100644 --- a/code/investors/admin.py +++ b/code/investors/admin.py @@ -3,9 +3,7 @@ class InvestorAdmin(admin.ModelAdmin): - list_display = [ - 'user', 'name', 'cpf' - ] + list_display = ['user', 'name', 'cpf'] search_fields = [ 'cpf', ] diff --git a/code/investors/urls.py b/code/investors/urls.py index 1bdd28e4b..752aedfaa 100644 --- a/code/investors/urls.py +++ b/code/investors/urls.py @@ -13,6 +13,6 @@ path( 'api/v1/investors//', InvestorRetrieveUpdateDestroyApiView.as_view(), - name='investor-detail-update-delete' + name='investor-detail-update-delete', ), ] diff --git a/code/mkdocs.yml b/code/mkdocs.yml new file mode 100644 index 000000000..6dfe8ff67 --- /dev/null +++ b/code/mkdocs.yml @@ -0,0 +1,2 @@ +site_name: Investiments Api +theme: readthedocs \ No newline at end of file diff --git a/code/pyproject.toml b/code/pyproject.toml new file mode 100644 index 000000000..ec3c72b28 --- /dev/null +++ b/code/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "investiments-api" +version = "1.0" +description = "API de investimentos" +requires-python = ">=3.11" + +[tool.ruff] +line-length = 79 +extend-exclude = ['migrations', 'manage.py', '.env'] + +[tool.ruff.lint] +preview = true +select = ['I', 'F', 'E', 'W', 'PL', 'PT'] + +[tool.ruff.format] +preview = true +quote-style = 'single' + +[tool.taskipy.tasks] +lint = 'ruff check' +lint_fix = 'ruff check --fix' +format = 'ruff format' +docs = 'mkdocs serve -a 127.0.0.1:8001' \ No newline at end of file diff --git a/code/requirements.txt b/code/requirements.txt index f36355ab2c781a693b8603490398ec18fc027473..262f2da1979abf681467979006b554b6c9c6d9d4 100644 GIT binary patch literal 998 zcmY*YOHYGf5S+7#Kc!8f5B1=|#FMd!i6>7Dp|nNIL!egt@#@Sjls1Hre6u@`-H*TD z2sO4yQ6a$w_k5-pBj7W}1lPDij6LxfABNuY+$$U_h!*UQknvUjgcUQs3+sNwJI`}! z6oxKNyz4VW>NOfVaHa%DV|7i}Q)ghDTkKF`L{}NVQg4hqw2I#M6Y<OD4_;gG1 zN45)1Hlr8a4)?CTimk7#abRW24l#ME0d0xxIN#h7i8HyVSE@U{a-cox2J}fTdkgE$ z@nt%ipzm6D@C7a2@PH+rPcDWHl|)?!KeQ~VFrWy6y@H7>@ZMuKJXQH%0=cs1N zVdepD%(wJwe3DzU*OdI0s9$rM+YOLf&Ap_oYR^=iUmOk7%VBD$vJG2p@WUNwdj5Zf XbmTkrJ9soK=A?C(J(d4u{k`}H9NCbI delta 21 dcmaFH{)};g%EY9Q$upRACc7}JOkTz;2LN3<2Y~

Date: Fri, 26 Dec 2025 22:00:23 -0300 Subject: [PATCH 24/27] Docs e Reqs --- code/docs/index.md | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/code/docs/index.md b/code/docs/index.md index c3b0b6464..2517308f4 100644 --- a/code/docs/index.md +++ b/code/docs/index.md @@ -346,4 +346,43 @@ Este design garante: ✅ Facilidade de manutenção ✅ Segurança nas regras de negócio -Arquitetura MVT baseada no atendimento aos requisitos pedidos e a proposta do Framework Django. \ No newline at end of file +Arquitetura MVT baseada no atendimento aos requisitos pedidos e a proposta do Framework Django Rest. + +Format: +Tenho um padrão de código bem parecido com o padrão, buscando sempre melhorar, todavia para +facilitar a leitura da equipe e possível manutencao, usei o ruff para format. + +--- + +Requirements: + +```python +asgiref==3.11.0 +click==8.3.1 +colorama==0.4.6 +Django==6.0 +djangorestframework==3.16.1 +ghp-import==2.1.0 +Jinja2==3.1.6 +Markdown==3.10 +MarkupSafe==3.0.3 +mergedeep==1.3.4 +mkdocs==1.6.1 +mkdocs-get-deps==0.2.0 +mslex==1.3.0 +packaging==25.0 +pathspec==0.12.1 +platformdirs==4.5.1 +psutil==6.1.1 +python-dateutil==2.9.0.post0 +python-dotenv==1.2.1 +PyYAML==6.0.3 +pyyaml_env_tag==1.1 +ruff==0.14.10 +six==1.17.0 +sqlparse==0.5.5 +taskipy==1.14.1 +tomli==2.3.0 +tzdata==2025.3 +watchdog==6.0.0 +``` \ No newline at end of file From e03c51b2a94af9427eac7fb143a7b8ab7edea227 Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 22:50:17 -0300 Subject: [PATCH 25/27] =?UTF-8?q?Obs=20e=20Carta=20de=20Apresenta=C3=A7?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- code/docs/obs.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 code/docs/obs.md diff --git a/code/docs/obs.md b/code/docs/obs.md new file mode 100644 index 000000000..f870590c5 --- /dev/null +++ b/code/docs/obs.md @@ -0,0 +1,46 @@ +Olá Pessoal! +Realizei esse teste de Backend. Construí uma api, seguindo os requisitos do sistema/caso. + +Com a Linguagem Python e o framework Django + Drf, realizei o projeto no padrão MVT. + +Motivo de escolha foi a capacidade de implementar um sistema com usuário genérico desaclopado +do Investidor, já que o django fornece um User Default, admin e sua +interface administrativa. + +Além disso, o queryset e Orm do django auxilia bastante no desempenho das queries em funcao +das regras de negócio, caso existisse mais complexidade, era possível otimizar joins entre +tabelas. + +O serializer do django, é o responsável pelos dados com validators e tratamentos que fa- +cilitam a produtividade do dev. + +O Signals/Gatilho do django é essencial para o serviço externo SMTP que indicaram. + +Portanto minha escolha, além da justificativa de conhecimento técnico em aprendizado +constante, deve também, a produtividade do django e sua abstração. + +Dividi as entidades entre: +-Investidores +-Investimentos +-Usuarios + +realizei o crud de investidores e de Investimentos. + +em investimentos priorizei a seguranca de registros e usei uma flag booleana +para indicar o encerramento. + + +Futuramente, eu criaria perms e auth, para proteger o devido uso de cada Endpoint. +Além disso, seria interessante implementar talvez uma entidade Wallet +que assegurasse esses investimentos e métricas/business. + +Utilizei também Libs auxiliares, todas estão nos requirements e na docs. + +Para Format do código usei Ruff, tenho minhas manias e quero deixar padrão a leitura. +Para Task usei taskipy que facilita chamadas de tasks +Para Docs, usei MKdocs que ajuda no visual do Md(index) + +Assegurei as Variáveis de Ambiente no .Env local. + +Gostaria de um feedback, queria a opinião de vocês o que melhorariam e o que gostaram... +Obrigado, Prazer! \ No newline at end of file From 8b3b26d6013126d7642021431474e849221ac746 Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 22:58:42 -0300 Subject: [PATCH 26/27] =?UTF-8?q?Carta=20de=20apresenta=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- carta.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 carta.md diff --git a/carta.md b/carta.md new file mode 100644 index 000000000..f870590c5 --- /dev/null +++ b/carta.md @@ -0,0 +1,46 @@ +Olá Pessoal! +Realizei esse teste de Backend. Construí uma api, seguindo os requisitos do sistema/caso. + +Com a Linguagem Python e o framework Django + Drf, realizei o projeto no padrão MVT. + +Motivo de escolha foi a capacidade de implementar um sistema com usuário genérico desaclopado +do Investidor, já que o django fornece um User Default, admin e sua +interface administrativa. + +Além disso, o queryset e Orm do django auxilia bastante no desempenho das queries em funcao +das regras de negócio, caso existisse mais complexidade, era possível otimizar joins entre +tabelas. + +O serializer do django, é o responsável pelos dados com validators e tratamentos que fa- +cilitam a produtividade do dev. + +O Signals/Gatilho do django é essencial para o serviço externo SMTP que indicaram. + +Portanto minha escolha, além da justificativa de conhecimento técnico em aprendizado +constante, deve também, a produtividade do django e sua abstração. + +Dividi as entidades entre: +-Investidores +-Investimentos +-Usuarios + +realizei o crud de investidores e de Investimentos. + +em investimentos priorizei a seguranca de registros e usei uma flag booleana +para indicar o encerramento. + + +Futuramente, eu criaria perms e auth, para proteger o devido uso de cada Endpoint. +Além disso, seria interessante implementar talvez uma entidade Wallet +que assegurasse esses investimentos e métricas/business. + +Utilizei também Libs auxiliares, todas estão nos requirements e na docs. + +Para Format do código usei Ruff, tenho minhas manias e quero deixar padrão a leitura. +Para Task usei taskipy que facilita chamadas de tasks +Para Docs, usei MKdocs que ajuda no visual do Md(index) + +Assegurei as Variáveis de Ambiente no .Env local. + +Gostaria de um feedback, queria a opinião de vocês o que melhorariam e o que gostaram... +Obrigado, Prazer! \ No newline at end of file From 3dd95bf4a36ea42364b5b0cce3837ac833ee8fcd Mon Sep 17 00:00:00 2001 From: Pablo Alves Neri Date: Fri, 26 Dec 2025 23:07:41 -0300 Subject: [PATCH 27/27] Carta definitiva --- carta.md | 56 +++++++++++++++++++++--------------------------- code/docs/obs.md | 46 --------------------------------------- 2 files changed, 25 insertions(+), 77 deletions(-) delete mode 100644 code/docs/obs.md diff --git a/carta.md b/carta.md index f870590c5..c11d6c360 100644 --- a/carta.md +++ b/carta.md @@ -1,46 +1,40 @@ -Olá Pessoal! -Realizei esse teste de Backend. Construí uma api, seguindo os requisitos do sistema/caso. +Olá, pessoal. -Com a Linguagem Python e o framework Django + Drf, realizei o projeto no padrão MVT. +Realizei este teste de Backend, no qual construí uma API seguindo os requisitos propostos no sistema/caso apresentado. -Motivo de escolha foi a capacidade de implementar um sistema com usuário genérico desaclopado -do Investidor, já que o django fornece um User Default, admin e sua -interface administrativa. +Utilizei a linguagem Python em conjunto com o framework Django e Django Rest Framework (DRF), estruturando o projeto no padrão MVT. -Além disso, o queryset e Orm do django auxilia bastante no desempenho das queries em funcao -das regras de negócio, caso existisse mais complexidade, era possível otimizar joins entre -tabelas. +A escolha do Django se deu principalmente pela possibilidade de implementar um sistema com um usuário genérico desacoplado da entidade Investidor, aproveitando o User padrão fornecido pelo framework, bem como sua interface administrativa nativa. -O serializer do django, é o responsável pelos dados com validators e tratamentos que fa- -cilitam a produtividade do dev. +Além disso, o ORM e os querysets do Django auxiliam significativamente no desempenho das consultas, especialmente considerando as regras de negócio envolvidas. Em um cenário de maior complexidade, seria possível otimizar ainda mais as queries, incluindo joins entre tabelas de forma eficiente. -O Signals/Gatilho do django é essencial para o serviço externo SMTP que indicaram. +Os serializers do Django foram utilizados como responsáveis pelo tratamento e validação dos dados, o que contribui diretamente para a produtividade do desenvolvedor e para a segurança da aplicação. -Portanto minha escolha, além da justificativa de conhecimento técnico em aprendizado -constante, deve também, a produtividade do django e sua abstração. +Os Signals (gatilhos) do Django foram empregados como parte essencial da integração com o serviço externo de SMTP, conforme solicitado nos requisitos. -Dividi as entidades entre: --Investidores --Investimentos --Usuarios +Portanto, a escolha da stack não se deu apenas por afinidade técnica ou aprendizado contínuo, mas também pela produtividade, organização e nível de abstração que o Django oferece. -realizei o crud de investidores e de Investimentos. +As entidades foram divididas em: -em investimentos priorizei a seguranca de registros e usei uma flag booleana -para indicar o encerramento. +- Usuários +- Investidores -Futuramente, eu criaria perms e auth, para proteger o devido uso de cada Endpoint. -Além disso, seria interessante implementar talvez uma entidade Wallet -que assegurasse esses investimentos e métricas/business. +- Investimentos -Utilizei também Libs auxiliares, todas estão nos requirements e na docs. +Foi implementado o **CRUD** completo para Investidores e Investimentos. -Para Format do código usei Ruff, tenho minhas manias e quero deixar padrão a leitura. -Para Task usei taskipy que facilita chamadas de tasks -Para Docs, usei MKdocs que ajuda no visual do Md(index) +No caso dos investimentos, priorizei a segurança e a integridade dos registros, utilizando uma flag booleana para indicar o encerramento, evitando alterações indevidas em registros históricos. -Assegurei as Variáveis de Ambiente no .Env local. +Como melhorias futuras, considero a implementação de permissões e autenticação mais refinadas, garantindo o uso adequado de cada endpoint. Além disso, seria interessante a criação de uma entidade Wallet, responsável por consolidar os investimentos, métricas e regras de negócio relacionadas. -Gostaria de um feedback, queria a opinião de vocês o que melhorariam e o que gostaram... -Obrigado, Prazer! \ No newline at end of file +Também utilizei bibliotecas auxiliares, todas devidamente documentadas no arquivo de requirements e na documentação do projeto. + +Para padronização e formatação do código, utilizei o Ruff, visando manter um padrão de leitura consistente, tento sempre seguir padrão python, mas tenho costumes de aspas e outros toques. +Para automação de tarefas, utilizei o Taskipy, facilitando a execução de comandos recorrentes. Para documentação, utilizei o MkDocs, buscando uma melhor organização e visualização dos arquivos Markdown. + +As variáveis de ambiente foram devidamente protegidas e configuradas por meio de um arquivo .env local. + +Fico à disposição para receber feedbacks. Gostaria muito de ouvir a opinião de vocês sobre pontos de melhoria e aspectos positivos do projeto. + +Agradeço pela oportunidade, Prazer! \ No newline at end of file diff --git a/code/docs/obs.md b/code/docs/obs.md deleted file mode 100644 index f870590c5..000000000 --- a/code/docs/obs.md +++ /dev/null @@ -1,46 +0,0 @@ -Olá Pessoal! -Realizei esse teste de Backend. Construí uma api, seguindo os requisitos do sistema/caso. - -Com a Linguagem Python e o framework Django + Drf, realizei o projeto no padrão MVT. - -Motivo de escolha foi a capacidade de implementar um sistema com usuário genérico desaclopado -do Investidor, já que o django fornece um User Default, admin e sua -interface administrativa. - -Além disso, o queryset e Orm do django auxilia bastante no desempenho das queries em funcao -das regras de negócio, caso existisse mais complexidade, era possível otimizar joins entre -tabelas. - -O serializer do django, é o responsável pelos dados com validators e tratamentos que fa- -cilitam a produtividade do dev. - -O Signals/Gatilho do django é essencial para o serviço externo SMTP que indicaram. - -Portanto minha escolha, além da justificativa de conhecimento técnico em aprendizado -constante, deve também, a produtividade do django e sua abstração. - -Dividi as entidades entre: --Investidores --Investimentos --Usuarios - -realizei o crud de investidores e de Investimentos. - -em investimentos priorizei a seguranca de registros e usei uma flag booleana -para indicar o encerramento. - - -Futuramente, eu criaria perms e auth, para proteger o devido uso de cada Endpoint. -Além disso, seria interessante implementar talvez uma entidade Wallet -que assegurasse esses investimentos e métricas/business. - -Utilizei também Libs auxiliares, todas estão nos requirements e na docs. - -Para Format do código usei Ruff, tenho minhas manias e quero deixar padrão a leitura. -Para Task usei taskipy que facilita chamadas de tasks -Para Docs, usei MKdocs que ajuda no visual do Md(index) - -Assegurei as Variáveis de Ambiente no .Env local. - -Gostaria de um feedback, queria a opinião de vocês o que melhorariam e o que gostaram... -Obrigado, Prazer! \ No newline at end of file